Skip to content

feat: OTLP trace receiver, live tracing, and Open WebUI chat module - #6

Merged
SushantGautam merged 67 commits into
mainfrom
dev
Oct 1, 2026
Merged

SushantGautam merged 67 commits into
mainfrom
dev

Conversation

@SushantGautam

Copy link
Copy Markdown
Collaborator

Summary

Adds the OTLP trace receiver (with none/basic/bearer auth), live tracing on multi-repetition runs, and the Open WebUI chat module. The OTLP credential + drift-stats logic now delegates to the core simpleaudit library (merged in SimulaMet/SimpleAudit#91).

What's in this branch

  • OTLP trace receiver — shared receiver with none/basic/bearer auth, span storage consolidated onto the engine SpanStore, bearer-token verification indexed by lookup prefix.
  • Live tracing — worker supports live tracing on multi-repetition runs; SIMPLEAUDIT_OTLP on/off switch and SIMPLEAUDIT_CHAT_OTLP to export Open WebUI spans to Studio.
  • OTLP credential + drift stats delegate to core — model_registry/otlp_services.py and audits/monitors.py now use simpleaudit.tracing.auth and simpleaudit.stats instead of local copies.
  • Open WebUI chat module — optional bundled chat with Studio SSO, model picker, embedded iframe, and lifecycle management.
  • Test runner — migrated to pytest (xdist + testmon).

Notes

SushantGautam and others added 30 commits September 29, 2026 23:10
Following the README recipe verbatim on a fresh clone leaves the stack
half-up: the web container exits 1 during bootstrap with

    DJANGO_SECRET_KEY must be set to a strong unique value.
    CommandError: Refusing to bootstrap with unsafe configuration.

validate_startup_environment() blocks on three values, but the recipe only
told you to change two. Postgres, Hatchet and the worker all report healthy,
so nothing looks wrong until you go read the web logs.

Name DJANGO_SECRET_KEY in the recipe, say what the check enforces, and flag
it in .env.example next to the value with a command to generate one.

Also give web the restart policy the worker already has. Its whole command
is idempotent -- migrate, bootstrap and seed each skip what already exists --
so a restart is safe, and without one a gunicorn crash or a Postgres blip
leaves the UI down permanently. A bad .env now fails repeatedly in the logs
rather than once into a container nobody thinks to look at.
run_due_monitors claimed due monitors with select_for_update(skip_locked=True)
while select_related pulled in last_run and created_by. Both are nullable, so
the ORM joins them with a LEFT OUTER JOIN, and PostgreSQL rejects FOR UPDATE
against the nullable side of one:

    FOR UPDATE cannot be applied to the nullable side of an outer join

Every tick therefore raised NotSupportedError on any PostgreSQL deployment, so
no monitor ever launched a run. The sweeper catches and logs the failure, so the
only symptom was a warning once a minute:

    Monitor tick failed: FOR UPDATE cannot be applied to the nullable side of an
    outer join

Observed on a Container Apps deployment against Azure PostgreSQL 16.

Lock only the monitor rows with of=("self",), which keeps select_related and the
skip_locked guarantee that two workers never claim the same tick.

The test suite runs on SQLite, which drops FOR UPDATE entirely and so cannot
reproduce this. The claim query moves into due_monitors() so the lock target can
be asserted on any backend, and a PostgreSQL-only execution test covers the
regression itself. CI is SQLite-only, so that test is skipped there; running the
suite against PostgreSQL reproduces the failure without this fix.
User-creation paths (admin add-user, self-registration, demo signup) made
accounts with no project membership, so request.project resolved to None and
every project-scoped view 500'd. Centralize the grant in
accounts.services.grant_default_project (viewer on the 'default' slug) and call
it from all four creation paths, matching the existing WorkOS behavior. Add a
middleware backstop so a membership-less user still resolves the Default
workspace instead of crashing.
Adds an off-by-default module that embeds Open WebUI in Studio at /chat/,
signed in as the Studio user. With SIMPLEAUDIT_CHAT unset nothing changes:
the routes 404, the sidebar has no Chat entry and no extra process runs.

Open WebUI serves from the root of an origin only — it has no base-path
setting and its HTML references /static, /api and /ws absolutely — so it
cannot be proxied under Studio's own /chat/ path. It gets its own origin,
which Studio embeds in an iframe.

Sign-on uses Open WebUI's trusted-header mode. A proxy in front of it asks
Studio who the browser is (GET /chat/authz, the standard forward-auth
contract) and injects the answer as X-Studio-Email/-Name/-Role. Django stays
the only authority on identity; nothing outside it reads sessions or user
tables. Superusers and workspace admins map to Open WebUI's admin role,
everyone else to user.

Two deployments, same contract:

  embedded  infra/chat_proxy.py, a stdlib HTTP proxy started by
            `uvx simpleaudit-studio --chat` alongside Open WebUI on loopback
  docker    `docker compose --profile chat up`, where Caddy does forward_auth
            and Open WebUI publishes no port

Open WebUI must be reachable only from the proxy: it believes the identity
headers on any request it receives. Both proxies strip client-supplied
X-Studio-* headers before adding their own. Note that `open-webui serve`
ignores HOST/PORT and defaults to 0.0.0.0, so the bind address is passed as
CLI flags; it also writes its signing key to the working directory, so the
process runs from its own data folder.

WebSockets are not proxied in embedded mode; Socket.IO falls back to HTTP
long-polling and SSE streaming is unaffected.
The chat module now runs by default where it can: `uvx simpleaudit-studio`
starts Open WebUI alongside Studio, and `.env.example` sets both switches the
Compose deployment needs, so a plain `docker compose up -d` brings the chat
containers up without an extra --profile flag.

Opting out:

  uvx simpleaudit-studio --disable-chat      (--no-chat also accepted)
  SIMPLEAUDIT_CHAT=disabled                  (off/false/no/0/none too)
  comment out SIMPLEAUDIT_CHAT + COMPOSE_PROFILES in .env for Compose

SIMPLEAUDIT_CHAT previously recognised only "embedded" and "docker", so any
other spelling — including "disabled" — silently meant off. It now reads a
documented set of disabling values and treats anything else as on, which is
also what makes the CLI default work.

Deployments that set neither the variable nor the Compose profile are
unaffected: the routes still 404 and nothing extra runs.
Open WebUI takes minutes to come up on a first run — it is downloaded, then it
migrates its database — and until now the console said only "first start
downloads it" before the chat's own log poured into the terminal.

The CLI now prints that chat is starting, warns on a first run that this means a
~1 GB download, and names the folders its data and log live in. A background
thread polls until Open WebUI answers and prints either the ready line with the
/chat/ URL, or why it stopped, pointing at the log. Studio and the worker come up
meanwhile, as before.

Open WebUI's own output now goes to openwebui/server.log instead of the console,
which also makes a failure readable after the fact. The readiness poll uses
urllib rather than httpx so it does not log a request line every two seconds.
Chat was started with a bare Popen and stopped with terminate(), which left two
ways to orphan it. `uvx open-webui` is a launcher with the real server as its
child, so terminate() signalled the launcher and left the server holding the
port; and a hard-killed run (SIGKILL, crash, closed terminal) never got to stop
anything at all, so the next start failed to bind.

Open WebUI now follows the same pattern as the embedded engine:

- one instance per process, guarded by a lock; a second start returns the first
- started in its own process group, so stopping it reaches the real server
- stopped by the CLI's shutdown path and by atexit, SIGTERM then SIGKILL
- its PID recorded in openwebui/open-webui.pid, so the next start can stop a
  leftover from a killed run — only when its parent is gone, never one that
  belongs to another running Studio

Verified against the real Open WebUI: explicit stop, exit without stopping, and
SIGKILL of the Studio process followed by a restart all end with no surviving
process and a free port.
It is printed from a background thread minutes after startup, and stdout is
block-buffered when the CLI's output is a file or a pipe rather than a terminal,
so the line could sit unwritten for the rest of the run.
Open WebUI brings its own sidebar, header and settings, so wrapping it in the
Studio shell put two navigations on the same edges and left the chat itself in a
boxed iframe. The page now stands alone: a slim bar with a centred "Back to
Studio" link, and the chat filling everything below it.

The page no longer extends base.html, so it carries its own small stylesheet
rather than pulling in Tailwind, htmx and the sidebar for one link.
The chat now fills the viewport and the "Back to Studio" link sits on top of it
as a small translucent pill, centred at the top. Nothing is reserved for a bar,
so Open WebUI keeps the full height it expects.

Only the pill takes clicks — the strip around it passes them through to the chat
— and it sits at 75% opacity until hovered or focused, so it stays out of the
way of Open WebUI's own header.
…both directions

The module was spread across infra/ (chat.py, chat_proxy.py, chat_api.py), two
management command folders, infra/tests and the shared templates folder, which is
the wrong shape for something that is meant to be optional and removable. It is
now one app:

    chat/
      config.py          what the module is configured to do, and who you are
      views.py urls.py   the iframe page and /chat/authz
      proxy.py           the forward-auth proxy + Open WebUI's lifecycle
      api.py             talking to Open WebUI's API, both directions
      management/        sync_chat_models, chat_knowledge
      templates/ tests/

The app is installed either way so its templates, commands and tests resolve;
SIMPLEAUDIT_CHAT still decides whether anything runs. Removing the feature is now
deleting a folder and four one-line references.

chat/api.py is new, and is the scaffolding for tighter integration. It
authenticates the way the proxy makes the browser authenticate — the trusted
headers to /api/v1/auths/signin, then the token it returns — so there is no API
key to provision and every call runs as a real Open WebUI user.

Push: a Studio model connection is a base URL plus a key, which is exactly Open
WebUI's OpenAI-compatible provider config, so `sync_chat_models` writes
connections into OPENAI_API_BASE_URLS/KEYS/CONFIGS. Those lists are hand-editable
in Open WebUI, so each pushed entry carries a simpleaudit_connection_id marker: a
sync replaces the marked entries and keeps the rest, which the merge is tested
for in both directions.

Pull: `chat_knowledge` lists knowledge bases and their files, normalised to plain
dicts so Studio code never sees Open WebUI's schema.

Nothing syncs automatically yet — both directions are explicit commands until the
shape settles.
…rowser console

Connections now reach Open WebUI on their own: chat/signals.py follows
ModelConnection and RegisteredModel, and the CLI syncs once as soon as chat
answers, which covers whatever changed while it was off. The management command
stays for a manual run and for --dry-run, and now shares the same code path.

The push deliberately stays off the request's path — on_commit so a rolled-back
row is never pushed, in a background thread so a save does not wait on a second
service, debounced so an edit that writes a connection and its models is one
push, and best-effort so a chat that is down is logged and forgotten. Studio's
data is the source of truth; the next push catches up.

A connection's registered models are pushed as that provider's model_ids, so chat
offers what Studio registered rather than everything the provider lists. No
registered models means no restriction.

Two things the browser console complained about:

- WebSocket upgrades were answered with 501, so Socket.IO retried and fell back
  to polling. The proxy now tunnels them: forward the handshake with the identity
  headers, and once Open WebUI answers 101, pipe the two sockets together.
- Open WebUI polled Ollama on every page load (a 500 each time) and showed an
  empty Ollama section in its settings. Nothing in a Studio deployment serves
  Ollama, so it is off in the environment for a fresh instance and turned off
  through the API on every sync for one that already had it on.

Verified against the running instance: a rename reached chat within the debounce
window, a real handshake through the proxy returned 101 followed by socket.io's
OPEN frame, and ENABLE_OLLAMA_API is now false.
Opening Studio on 127.0.0.1 left /chat/ blank. The iframe always pointed at
localhost:8801 while the session cookie was on 127.0.0.1 — cookies are per host,
not per port — so the proxy saw no cookie, sent the frame back to Studio, and
Studio embedded the frame again. A loop that renders as nothing.

SIMPLEAUDIT_CHAT_URL still wins, for a deployment that knows its own hostname.
With it unset, the origin is now derived from the request: the host the browser
is already on, plus the proxy's port. 127.0.0.1 embeds 127.0.0.1, localhost
embeds localhost, and a LAN address works without configuring anything.

The proxy's signed-out reply also breaks out of the frame to Studio's login page
instead of redirecting inside it, so a session that really is missing shows a
login page rather than a blank frame.
The proxy held a single httpx.Client for the life of the process. httpx clients
keep a cookie jar, so the Set-Cookie headers coming back from Studio and from
Open WebUI were stored and sent again on the next request — whoever it came
from. A cookieless request to the proxy was answered as the last signed-in user:
/chat/authz returned 200 with that user's identity headers, and the chat opened
as them.

Clients are now built per request over a shared connection pool, so nothing is
carried between requests. Every identity the proxy forwards comes from the
request it is handling.

Also adds chat/tests/test_proxy.py, covering identity forwarding, dropping a
client-supplied identity, the signed-out page, and that neither Studio's nor Open
WebUI's cookies survive into a later request.
…ngo per asset

The session-leak tests passed against a shared client, so they were documenting
the fix rather than guarding it. The stub Studio compared the whole Cookie header
for equality, and a leaked jar carries three cookies, so the leak it was supposed
to catch came back as a 401 and looked like correct behaviour. It now matches the
session cookie within the header, and a control test puts the shared client back
and asserts the leak reappears — if that control ever passes silently, the guard
below it has stopped working.

Five mutations of the proxy now fail the suite: a shared client, a cache that
ignores the cookie, dropping the trusted-header stripping, keeping
X-Frame-Options, and serving the chat to a signed-out browser. The third needed a
new test: forged headers were only checked in our own casing, which dict.update
happens to overwrite, while a lowercase one would be sent alongside ours and read
first by the upstream.

Identity is also cached for a few seconds now, keyed on the exact cookie header.
Open WebUI's page pulls dozens of assets and each one asked Django who the
browser was: 20 requests cost 20 round trips, now 2. A sign-out takes effect
within the TTL (SIMPLEAUDIT_CHAT_IDENTITY_TTL, 0 disables it), and an unreachable
Studio is never cached, so a blip does not sign everyone out.
Three cases said nothing the tests around them did not already say, and one
missed the case it was named for:

- the proxy's same-casing forgery test is now one case of the casing test that
  replaced it, which also counts the identity headers that arrived — a duplicate
  is what a casing mismatch produces, and a plain lookup never sees it;
- "a different cookie is a different answer" repeated the session-leak test
  above it, which fails the same way if the cache stops keying on the cookie;
- the view test for the iframe origin was written before ChatOriginTests, which
  covers the same thing per host;
- the cache's TTL test only turned the cache off, so a cache that never expired
  passed it. It now waits for an entry to age out, and turning the cache off is
  its own case.

Checked by mutation: a shared client, a cache that ignores the cookie, a cache
that never expires, dropping the header stripping, keeping X-Frame-Options and
serving the chat to a signed-out browser all fail the suite.
Open WebUI has no embed mode, but its app shell loads /static/custom.css
on every page. Answer that one request with chat/embed.css instead of
forwarding it, so the iframe renders without the sidebar (collapsed rail,
expanded panel, and resizer). The rule lives in this repo and survives
Open WebUI upgrades; both modes share the file — the proxy reads it
directly, Caddy mounts it read-only.
The panel was hidden but the #sidebar-toggle-button in the top bar
survived — with the panel gone it only opened a blank gap, so it goes
with the rest.
…hats

The embedded chat is a throwaway, single-model surface, so:
- pin the model via a ?model= URL param (chat.config.MODEL)
- hide the "..." (Chat actions) menu button in embed.css
- force every chat to be temporary so nothing piles up in Open WebUI
  history: USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED seeds a fresh
  instance, and ChatAPI.enforce_temporary_chats() updates the stored
  user.permissions config on each sync (the stored value otherwise wins
  over the env var)
The temporary_enforced permission is dead for Studio users: the
Open WebUI frontend skips it for the admin role, and Studio
superusers/workspace-admins map to admin. Instead the iframe URL
now always carries ?temporary-chat=true, which the frontend checks
unconditionally on load. Since the New Chat button is hidden, a
fresh chat only ever starts from a full page load, so the param
covers every case.

Also hide the two escape hatches in the top bar so the embed
cannot be switched back to a persisted chat:
- #temporary-chat-button (toggle temporary mode off)
- #save-temporary-chat-button (persist a temporary chat)

Drops the now-redundant enforce_temporary_chats() permission push
from api/sync/proxy and its tests.
The top-bar model picker now:
- closes when clicking outside (a transparent backdrop sits above the
  iframe, which otherwise swallows all clicks so the document handler
  could never fire) or pressing Escape
- has a search box that filters models by name/id, hiding empty groups
- flips to the right edge when it would overflow the viewport, and caps
  its width to the window so it never overflows on narrow screens
- dedupes selected model ids (the same model can be registered under
  more than one connection)
The dropdown was anchored to the centered toggle with a fixed 256px
width, so on narrow windows it overflowed the right edge (and flipping
to the right edge overflowed the left instead). Make the menu
position:fixed and compute its width and position in JS: cap the width
to the window, shift it left when it would overflow the right edge, and
open upward when it would overflow the bottom. Re-runs on resize so a
live window resize keeps it on screen.
Horizontal centering and the width cap are now pure CSS
(left: 50% + translateX(-50%), width capped to the viewport), so the
menu can never overflow the left or right edge at any window size —
no JS measurement to race a live resize. JS only sets the vertical
position, flipping the menu above the toggle when it would overflow
the bottom.
Each model row on /connections/ now offers a chat icon (before the
edit/delete actions) that links to /chat/?model=<id>, opening the
embedded chat already pinned to that model. The icon only renders when
the chat module is enabled (SIMPLEAUDIT_CHAT), and stays visible in
read-only mode since chatting is a read-only action.

ChatView reads a ?model= param from the Studio page URL and pins the
iframe to it, overriding the configured default.
A hand-typed /chat/?model=o3 could pin the chat to any model id. The
/connections chat icon now links to /chat/with/<model_id>, a view that
validates the model is visible and enabled for the user, stashes it in
the session, and redirects to /chat/. ChatView consumes that session
value exactly once (pop), so the pin survives the redirect but not a
refresh, and a query param can never pin the chat.
Open WebUI identifies a model by model_id alone, so the same id registered
under two connections collides in its model list. Push a prefix_id per
connection (keyed on the connection id) so every pushed id is globally
unique; Open WebUI strips it before the upstream call. The handoff, picker,
iframe ?models= param and saved preference all carry the prefixed id, and
the /chat/with/ handoff now takes connection + model.

The default model is no longer a config value: the user's saved chat_model
preference wins (stale ids dropped), else the first model they can see, else
nothing. The picker persists its selection to /me/preferences/.
The proxy piped the upstream's handshake reply straight through, so a
refused upgrade (a 401 when trusted-header auth is not applied to the
Socket.IO path, a 500, ...) reached the browser as raw bytes dressed up
as WebSocket frames and failed opaquely. The handshake reply is now read
first: only a 101 is piped, anything else is forwarded as a plain HTTP
response (the upstream's own status and safe headers when well-formed,
a clean 502 otherwise. Bytes the upstream sent alongside the 101 are
replayed before the pipe starts so nothing is lost.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EOF
)
The ?models= pin only takes effect when the id exactly matches a model
Open WebUI knows; otherwise the chat silently falls back to its default
model. OpenAI-compatible providers register models by the id their
/v1/models endpoint returns, which can differ from Studio's model_id, so
a mismatch is easy to create and invisible at chat time. push_now now
lists the registered models and logs a warning naming the connection and
the missing ids. Reconciliation is a diagnostic: it never raises, and a
models-endpoint failure just skips it.

plan_openai_config now pairs the parallel URL/key/config lists strictly
by index over the union of indices present in any of the three, instead
of padding keys to the URL length. A hand edit or a partially failed
write can leave the lists out of sync; the old padding could attach one
provider's key to another provider's URL. Entries without a base URL are
dropped, since a URL is what makes a provider usable.

Also split NoAdminError out of ChatAPIError: a missing superuser is a
misconfiguration, not a transient state, so the background worker logs
it loudly with an actionable message instead of a quiet skip.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A user with no project at all got an empty model picker and a dead
iframe. The chat page now renders a centered "No models available" state
with a link to /connections/ instead, and skips the picker and iframe
entirely. A user who has a project but no models yet still gets the
normal page: the picker shows its own "No models yet" hint and the
iframe loads.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SushantGautam and others added 17 commits October 1, 2026 22:47
Merges 'Generate' and 'Rotate secret' into one button. The button
creates a credential if none exists, or rotates (with confirm) if one
does. For 'None' auth mode it just sets up the open endpoint — no
secret, no rotate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merged 'Generate' and 'Rotate secret' into one button
- Backend create_credential now upserts: reuses existing target_id,
  updates auth mode and regenerates secret in place
- Switching between None/Basic/Bearer works without orphaned creds
- JS simplified: always calls create, no rotate logic in UI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The demo server binds 0.0.0.0, so the credential-less /auto-login/
endpoint let any machine on the LAN sign in as the bootstrap user.

- CLI generates a secrets.token_urlsafe(32) token at startup and
  prints the full one-time sign-in link in the banner
- auto_login_view checks the ?token=... param in constant time
  (hmac.compare_digest) and consumes it on first use, so the URL
  cannot be replayed
- browser readiness polling now hits /healthz instead of the token
  URL so the probe does not consume the one-time token
- if no browser opens, the printed link is the manual fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts:
#	simpleaudit_studio/cli.py
Brings in the full Open WebUI chat integration (41 commits):
- Chat module with model sync, proxy, and per-connection pinning
- Port conflict detection with force-kill option
- One-time /auto-login/?token=... (cherry-picked + merged)
- sr-only label scroll fix, favicon branding, start.sh entrypoint

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Switch CI from manage.py test to pytest --testmon -n auto with a
  .testmondata cache for affected-test selection
- Update README test section to document pytest commands (fast, full,
  affected-only, targeting)
- Fix 21 pre-existing ruff errors (unused imports, unsorted imports,
  Optional[X] -> X | None, f-strings, startswith tuple, nested with)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The engine multi-rep path does not forward trace correlation, so a
trace_config on a repeated run was silently ignored. Log a warning so
users are not misled into thinking trace evidence was collected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
verify_bearer hashed the presented token against every enabled bearer
credential on each request — O(N) HMAC work per span export. Add a
short, indexed token_prefix (first 16 chars of the token) to
OTLPCredential so the candidate set is narrowed by an indexed query
before the constant-time hash compare.

- Add OTLPCredential.token_prefix (db_index) + migration 0006
- Set the prefix in create_credential and rotate_credential (bearer)
- verify_bearer filters by prefix first, falls back to a full scan for
  legacy rows with an empty prefix so verification never regresses
- Tests: prefix is stored and used; a same-prefix/different-hash token
  is rejected; legacy empty-prefix rows still verify
The studio kept its own hand-rolled in-memory span store (_SPAN_STORE,
a dict of raw span lists) in otlp_views.py, duplicating the engine's
SpanStore and storing un-normalized spans. Route /otlp/v1/traces through
the engine's parse_otlp_json + SpanStore so ingested spans use the same
normalized schema the run path and the judge's evidence selection use.

- Replace _SPAN_STORE dict-of-lists with a dict of engine SpanStore
- Ingestion now normalizes via SpanStore.add_many (single source of truth)
- Drop the dead _store_spans helper
- get_spans_for_target / clear_target_spans now operate on SpanStore

Full fast loop: 543 passed, 1 skipped (unrelated DEMO_MODE).
The multi-rep path previously warned and silently dropped trace_config.
Now run_scenario_repeated accepts trace_config, builds a provider, and
shares a per-rep TraceCorrelation (minted at each rep boundary) so each
rep's turn->trace links are attributable. After the run, evidence spans
are fetched per rep and attached under judgment["evidence_spans"],
matching the single-rep path.

- engine.run_scenario_repeated: accept trace_config, forward
  audit_run_id + a per-rep correlation callable to the engine
- worker: pass trace_config to the multi-rep path; drop the warning
- Tests: forwarding contract (with/without trace_config)

Full studio fast loop: 545 passed, 1 skipped.
- model_registry/otlp_services.py: credential logic (hash, verify,
  header parsing, secret/token generation) now delegates to
  simpleaudit.tracing.auth; studio keeps only the persistence layer.
- audits/monitors.py: wilson/two_proportion_z delegate to
  simpleaudit.stats.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The OTLP ingestion endpoint and credential-management API were wired unconditionally, so a deployment that does not want the listener had no way to turn it off. Gate the /otlp/* and /api/otlp/* routes, the credential issue/rotate endpoints, and the per-connection OTLP button behind a new SIMPLEAUDIT_OTLP flag (on by default, preserving current behavior).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When enabled, Studio starts Open WebUI with OpenTelemetry tracing on and pointed at its own OTLP listener (POST /otlp/v1/traces), so the chat module's spans land in the same place as any other target's. Off by default. Wired for both embedded (proxy) and docker modes, with docs and tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…aders

- CLI now defaults DJANGO_DEBUG=false so local dev exercises real
  production behavior (host checks, static serving, no debug pages).
  Override with DJANGO_DEBUG=true to get the debug tooling back.
- Add SECURE_SSL_REDIRECT, HSTS, secure cookies, and nosniff for real
  HTTPS deployments; gated off for local/minimal/demo mode.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Brings in the .github/agents/* role definitions (Researcher, Verifier,
Test Reviewer, Regression Reviewer, Orchestrator) and the
parallel-orchestrate prompt recovered from the archived
vscode-custom-endpoint-optimization session branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 22:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitguardian

gitguardian Bot commented Oct 1, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 4 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
37641729 Triggered Generic Password 581f420 infra/tests/test_user_project_assignment.py View secret
37641729 Triggered Generic Password 8338945 infra/tests/test_user_project_assignment.py View secret
37641729 Triggered Generic Password 8338945 infra/tests/test_user_project_assignment.py View secret
37641729 Triggered Generic Password 8338945 infra/tests/test_user_project_assignment.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

1 similar comment
@gitguardian

gitguardian Bot commented Oct 1, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 4 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
37641729 Triggered Generic Password 581f420 infra/tests/test_user_project_assignment.py View secret
37641729 Triggered Generic Password 8338945 infra/tests/test_user_project_assignment.py View secret
37641729 Triggered Generic Password 8338945 infra/tests/test_user_project_assignment.py View secret
37641729 Triggered Generic Password 8338945 infra/tests/test_user_project_assignment.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

SushantGautam and others added 7 commits October 2, 2026 00:10
The pyproject.toml points simpleaudit at ../SimpleAudit (editable) so the
studio can use the tracing layer not yet in the published wheel. CI was
failing at uv sync because that path did not exist. Clone the core repo
into ../SimpleAudit before syncing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
actions/checkout cannot write outside the workspace. Use git clone to
place the core repo at ../SimpleAudit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The delegation commit removed parse_basic_header/parse_bearer_header from
otlp_services but the views and tests still called them. Import the core
parsers (simpleaudit.tracing.auth) in the views and tests. Also fix the
unused TraceCorrelation import in engine.py and sort imports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SECURE_SSL_REDIRECT was enabled in CI (DEBUG=false), causing 301 redirects
that broke chat and audit tests. Add a _TESTING gate (ci-secret-key,
PYTEST_CURRENT_TEST, or SIMPLEAUDIT_TESTING) so the headers stay off in the
test suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CsrfCookieMiddleware called the private _add_new_csrf_cookie(), which does
not populate request.META["CSRF_COOKIE"]; CsrfViewMiddleware.process_response
then raised KeyError: 'CSRF_COOKIE'. Switch to the public get_token() API,
which sets the META key and flags the cookie for update. Keeps the existing
"only set if missing" guard to avoid token/cookie drift.

Absorbs the sentry[bot] fix from the seer/fix/csrf-keyerror branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Dockerfiles run uv sync which fails because pyproject.toml points
simpleaudit at ../SimpleAudit (editable) for the tracing layer. Clone the
core repo into /SimpleAudit (which is ../SimpleAudit relative to /app)
before uv sync. Add git to the base image.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
OtlpEnvTests called proxy._spawn which reads the real pid file
(~/.simpleaudit-studio/openwebui/open-webui.pid) that does not exist in
CI. Patch pid_file to point at the temp dir, matching the other test
class.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SushantGautam
SushantGautam merged commit c4958f0 into main Oct 1, 2026
2 of 3 checks passed
@SushantGautam
SushantGautam deleted the dev branch October 1, 2026 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants