Finish the Cairn OBS rename through services, docs, and assets

The rename commit before this one covered module paths and the obvious
user-facing strings; this is the rest of it -- the places where "sentry"
was a default value, a filename, or a picture rather than a word in a
sentence.

Defaults that changed: CLICKHOUSE_DATABASE (sentry -> cairnobs),
POSTGRES_DATABASE (sentry_metadata -> cairnobs_metadata), and
POSTGRES_USERNAME (sentry -> cairnobs), across api/alerting/ingest and
the enterprise binaries, plus the compose files and migrate scripts that
create those objects. These are *defaults*, so a deployment that sets
them explicitly is unaffected -- but any deployment relying on the old
defaults must have its environment updated before it picks this up, or
it will come up pointing at a database that doesn't exist.

Also: the light-mode logo variants (the dark ones existed alone, so the
landing page and sidebar rendered a dark mark on a light background),
regenerated favicons, and the docs/README/threat-model prose that still
said Sentry.
This commit is contained in:
2026-08-22 16:12:08 -07:00
parent 13cf9a30cb
commit c920e0f2c4
68 changed files with 242 additions and 186 deletions
+6 -6
View File
@@ -135,7 +135,7 @@ already configured for.
## Data model
`metadata/migrations/0037_create_agents.sql`: one `agents` table, one
row per `(tenant_id, host)`, in the same `sentry_metadata` Postgres
row per `(tenant_id, host)`, in the same `cairnobs_metadata` Postgres
dashboards/alert_rules already live in — not a new database, matching
this project's established "shared schema, different services own
different tables" shape. `tenant_id` defaults to `'default'` for
@@ -257,18 +257,18 @@ the process exits cleanly -- `pending_command` confirmed cleared and
## CLI surface (punch-list item 3)
`sentryctl agents` (`cli/cmd/sentryctl/cmd_agents.go`), same list/get
`cairnobsctl agents` (`cli/cmd/cairnobsctl/cmd_agents.go`), same list/get
shape as `dashboards`/`alerts`, plus a `config` sub-subcommand
(mirroring `dashboards permissions`) since an override has its own
get/set/clear lifecycle distinct from the agent resource itself:
```
sentryctl agents list|get <host>
sentryctl agents config get <host>|clear <host>
sentryctl agents config set <host> [--batch-max-size N] [--batch-flush-interval-ms N]
cairnobsctl agents list|get <host>
cairnobsctl agents config get <host>|clear <host>
cairnobsctl agents config set <host> [--batch-max-size N] [--batch-flush-interval-ms N]
[--heartbeat-enabled true|false] [--heartbeat-interval-ms N]
[--journald-unit UNIT]
sentryctl agents restart <host> [--yes]
cairnobsctl agents restart <host> [--yes]
```
`config set` is the one command with real logic beyond a thin HTTP
+2 -2
View File
@@ -81,7 +81,7 @@ This split is not to be changed without discussion — see CLAUDE.md.
| `alerting` (Go, Phase 3) | Evaluates alert rules on an interval, calls `api`'s `POST /query` (via a `RoleService` credential once Phase 4 auth is configured — see `/docs/phase-4-isolation-design.md`'s alerting↔api gap), delivers firing/resolved notifications (webhook/Slack/PagerDuty). |
| `enterprise` (Go, AGPLv3 — see "Licensing boundary" below, Phase 4) | OIDC login (`internal/loginhandler`'s `/auth/oidc/login`+`/auth/oidc/callback`) and SAML login (`/auth/saml/login`+`/auth/saml/acs`, via `internal/saml`'s `crewjam/saml` wiring) — both a real IdP round trip, each verified with a real fake IdP (`coreos/go-oidc`'s `oidctest`, `crewjam/saml`'s `samlidp`) but not a real external one, RBAC storage (`internal/rbacstore`), session/service-token issuance (`internal/session`), the append-only audit log (`internal/audit`), `enterprise-auth`'s HTTP surface (`/internal/authorize`, `/auth/features`), per-tenant ClickHouse provisioning (`internal/tenantprovision`) and query routing (`internal/chrunner`), and `cmd/enterprise-api` — a second binary combining core's `api/queryapi`/`api/dashboards` handlers with these tenant-aware implementations. Never imported by core — see "Licensing boundary" below. Also `internal/searchclient` (per-tenant Tantivy routing, wired the same way into `search`). |
| `web` (SvelteKit, static build) | Query bar, dashboards, alerts, and (Phase 4) a settings page that renders SSO status via a runtime capability check (`GET /auth/features`) rather than bundling `enterprise/`'s components directly — an architectural choice (core builds and runs standalone) that predates and doesn't depend on Phase 6's relicensing. |
| `cli` (`sentryctl`) | `ping`, `query`, `dashboards` (list/get/apply), `alerts` (list/get/apply). `$SENTRYCTL_TOKEN`, if set, is forwarded as a Bearer credential (Phase 4). |
| `cli` (`cairnobsctl`) | `ping`, `query`, `dashboards` (list/get/apply), `alerts` (list/get/apply). `$CAIRNOBSCTL_TOKEN`, if set, is forwarded as a Bearer credential (Phase 4). |
| `deploy` | A Helm chart covering every `docker-compose.yml` service, plus (Phase 4) a small Go Operator managing one CRD (`Tenant`) that provisions a per-tenant ClickHouse credential Secret. Never applied to a live cluster in the environment this was built in — see `/deploy/README.md`'s verification section before trusting it. |
## Tenant isolation model (Phase 4)
@@ -199,7 +199,7 @@ escape hatch is opaque to any compiler-injected filter.
new credentials and still never touches ClickHouse/Postgres.
**The deployment-topology gap is closed for both Helm and
docker-compose**: `deploy/helm/sentry/templates/api.yaml`/
docker-compose**: `deploy/helm/cairnobs/templates/api.yaml`/
`enterprise-api.yaml` are mutually exclusive on `enterprise.enabled`,
rendering to the same Service name and port either way, so a
Helm-deployed cluster can't accidentally run the wrong binary — the same
+1 -1
View File
@@ -8510,7 +8510,7 @@
"direct_or_transitive": "direct",
"ecosystem": "docker-image",
"flagged": "True",
"flag_reason": "Category (c): BSL is explicitly source-available, not OSI-approved open source, per the audit's own classification framework. Verified against the actual licenses/bsl.md at the v24.2.7 tag, not assumed. No AGPL linking-compatibility issue (Redpanda is consumed only over the Kafka wire protocol, never linked into Sentry's own code -- same relationship as ClickHouse/Postgres). Real open question: whether a third party self-hosting Sentry (now fully AGPLv3 per task 6) 'as a service' would trip BSL's Streaming-or-Queuing-Service restriction on the bundled Redpanda image -- Sentry's ingest pipeline creates fixed internal topics, not per-end-user topics, so this is very likely NOT a Streaming-or-Queuing-Service under BSL's own definition, but this is a business/redistribution judgment call, not a pure technical one -- flagged for your decision, not resolved unilaterally. See license-audit-report.md's Redpanda section for the three remediation options (accept as-is / swap to Apache Kafka / flag only). RESOLVED 2026-08-16 (business decision): accept as-is. Sentry's own use (internal Kafka-protocol transport, no resale of broker access) is within BSL's Additional Use Grant; the third-party self-hosting-as-a-service question was judged unlikely to trip BSL's Streaming-or-Queuing-Service restriction, and was accepted as a known, disclosed risk rather than swapping to a heavier broker or dropping the bundled image. See license-audit-report.md's Redpanda section.",
"flag_reason": "Category (c): BSL is explicitly source-available, not OSI-approved open source, per the audit's own classification framework. Verified against the actual licenses/bsl.md at the v24.2.7 tag, not assumed. No AGPL linking-compatibility issue (Redpanda is consumed only over the Kafka wire protocol, never linked into Cairn OBS's own code -- same relationship as ClickHouse/Postgres). Real open question: whether a third party self-hosting Cairn OBS (now fully AGPLv3 per task 6) 'as a service' would trip BSL's Streaming-or-Queuing-Service restriction on the bundled Redpanda image -- Cairn OBS's ingest pipeline creates fixed internal topics, not per-end-user topics, so this is very likely NOT a Streaming-or-Queuing-Service under BSL's own definition, but this is a business/redistribution judgment call, not a pure technical one -- flagged for your decision, not resolved unilaterally. See license-audit-report.md's Redpanda section for the three remediation options (accept as-is / swap to Apache Kafka / flag only). RESOLVED 2026-08-16 (business decision): accept as-is. Cairn OBS's own use (internal Kafka-protocol transport, no resale of broker access) is within BSL's Additional Use Grant; the third-party self-hosting-as-a-service question was judged unlikely to trip BSL's Streaming-or-Queuing-Service restriction, and was accepted as a known, disclosed risk rather than swapping to a heavier broker or dropping the bundled image. See license-audit-report.md's Redpanda section.",
"classification": "c"
},
{
+6 -6
View File
@@ -49,7 +49,7 @@ docker compose up -d --build
```
This builds and starts, in dependency order: `redpanda``redpanda-provision`
(creates the `sentry.logs.raw` topic, then exits) → `clickhouse`
(creates the `cairnobs.logs.raw` topic, then exits) → `clickhouse`
`clickhouse-migrate` (applies `/storage/migrations`, then exits) →
`ingest` and `api``web`.
@@ -133,7 +133,7 @@ In another terminal, **after** the agent is running and connected
started won't be picked up):
```sh
logger "hello from sentry phase 0"
logger "hello from cairnobs phase 0"
```
`logger` (part of util-linux, present on virtually every Linux distro)
@@ -150,7 +150,7 @@ interval by default, so the line won't hit ingest instantly.
starts every service in the file). Open `http://localhost:3000`, run the
default query (`SELECT * FROM logs
ORDER BY timestamp DESC LIMIT 100`), and look for a row with
`message = "hello from sentry phase 0"`.
`message = "hello from cairnobs phase 0"`.
**Or via curl, if you want to skip the browser:**
@@ -160,11 +160,11 @@ curl -X POST http://localhost:8080/query \
-d '{"sql": "SELECT * FROM logs ORDER BY timestamp DESC LIMIT 10"}'
```
**Or via sentryctl, just to confirm api is up (doesn't check the data
**Or via cairnobsctl, just to confirm api is up (doesn't check the data
itself):**
```sh
cd cli && go run ./cmd/sentryctl ping
cd cli && go run ./cmd/cairnobsctl ping
```
If you see the row: that's Phase 0 done, end to end. If you don't, see
@@ -199,7 +199,7 @@ Check each hop in order rather than guessing:
at-least-once design (see `/ingest/README.md`), so this may just need
more time rather than intervention.
3. `docker compose exec redpanda rpk topic list` — confirm
`sentry.logs.raw` exists (if `redpanda-provision` failed, it won't).
`cairnobs.logs.raw` exists (if `redpanda-provision` failed, it won't).
**`docker compose up` fails on `service_completed_successfully`.**
You're likely on Compose v1 (`docker-compose`, hyphenated) rather than v2
+1 -1
View File
@@ -54,7 +54,7 @@ docker compose logs search
```
You should see "search gRPC server listening" and rskafka connecting to
all of `sentry.logs.raw`'s partitions. If you see nothing at all, check
all of `cairnobs.logs.raw`'s partitions. If you see nothing at all, check
`RUST_LOG=info` is set on the `search` service in `docker-compose.yml`.
### A2. Generate a log line and confirm both query paths agree
+10 -10
View File
@@ -43,10 +43,10 @@ go run . --count 500000
Create a dashboard and a couple of panels, either through the web UI
(`http://localhost:3000/dashboards` → "+ Create" → "+ Add panel") or via
`sentryctl`:
`cairnobsctl`:
```sh
sentryctl dashboards apply my-dashboard.json # shape = GET /dashboards/{id}/export
cairnobsctl dashboards apply my-dashboard.json # shape = GET /dashboards/{id}/export
```
**Verified live**: a table panel (`severity=INFO | head 10`) and a bar
@@ -95,7 +95,7 @@ Bring up a local webhook receiver for testing (no real Slack/PagerDuty
needed):
```sh
docker run -d --name sentry-webhook-sink --network sentry_default \
docker run -d --name cairnobs-webhook-sink --network sentry_default \
-p 9099:9099 -v $(pwd)/hack/webhook-sink:/src -w /src golang:1.25-alpine go run .
```
@@ -104,7 +104,7 @@ Create a notification target and a rule, either via the web UI
```sh
curl -X POST http://localhost:8081/targets -H 'Content-Type: application/json' -d '{
"name": "local sink", "kind": "webhook", "webhook_url": "http://sentry-webhook-sink:9099/"
"name": "local sink", "kind": "webhook", "webhook_url": "http://cairnobs-webhook-sink:9099/"
}'
curl -X POST http://localhost:8081/rules -H 'Content-Type: application/json' -d '{
@@ -119,7 +119,7 @@ curl -X POST http://localhost:8081/rules -H 'Content-Type: application/json' -d
rule via the `/alerts/new` form, watched it transition in the browser):
the rule transitions `ok``firing` on its first evaluation
(`for_minutes: 0`), the delivery log shows `firing / sent / 200`, and
`docker logs sentry-webhook-sink` shows the real received payload.
`docker logs cairnobs-webhook-sink` shows the real received payload.
Also verified live: a threshold rule whose query returns **zero rows**
records `last_eval_status: "error"` with the exact expected message
@@ -205,13 +205,13 @@ already claimed. Moving off a single-process ticker to a distributed
scheduler, and materially larger rule counts (10,000+), are both
explicitly out of scope for this phase.
## 5. Confirm `sentryctl`
## 5. Confirm `cairnobsctl`
```sh
sentryctl dashboards list
sentryctl dashboards apply exported-dashboard.json
sentryctl alerts list
sentryctl alerts apply rule.json
cairnobsctl dashboards list
cairnobsctl dashboards apply exported-dashboard.json
cairnobsctl alerts list
cairnobsctl alerts apply rule.json
```
Both `dashboards` and `alerts` hit the exact same REST endpoints the web
+1 -1
View File
@@ -108,7 +108,7 @@ yet. Stated plainly so this doesn't read as more built than it is.
## Schema
Lives in `/metadata` (`sentry_metadata`), alongside everything else from
Lives in `/metadata` (`cairnobs_metadata`), alongside everything else from
Phase 3, per `/docs/phase-4-isolation-design.md`'s existing schema
additions (`tenants`, the `tenant_id` backfill on `alert_state`/
`delivery_log`). New tables, continuing that migration sequence:
+4 -4
View File
@@ -265,7 +265,7 @@ with three levels (`ok`/`warn`/`reject`) and human-readable reasons:
query"), decided here rather than left ambiguous: hand-written queries
get the identical assessment an AI-generated one would, so there's
real parity, but retroactively hard-blocking existing dashboard/
`sentryctl` query patterns that happen to have no time bound is a
`cairnobsctl` query patterns that happen to have no time bound is a
behavioral change this phase didn't set out to make and could break
real existing usage. `warnings` is `omitempty` -- a client that
doesn't look for it sees no shape change at all. All existing
@@ -472,7 +472,7 @@ renders them is structurally the same conditional-message pattern
already live-verified repeatedly for Explain/Fix/Optimize's own
"unavailable" states, not new untested UI shape.
CLI (task 11): `sentryctl query --nl "..."` in `cli/cmd/sentryctl/cmd_query.go`.
CLI (task 11): `cairnobsctl query --nl "..."` in `cli/cmd/cairnobsctl/cmd_query.go`.
Same posture as the UI, enforced identically regardless of how the
result was produced: a low-confidence, non-compiling, or cost-guard-blocked
translation is never run, even with `--execute` -- confirmed by
@@ -559,7 +559,7 @@ query at the moment of acceptance, not tracked keystroke-by-keystroke.
**Genuinely verified against a live Postgres**, not just unit-tested
against a fake `InteractionLogger`: `metadata/migrations/0036` was
applied to the running dev stack's `sentry-metadata-postgres`
applied to the running dev stack's `cairnobs-metadata-postgres`
(`docker compose up -d --build metadata-migrate`, confirmed via `\d+
audit_log` before/after showing `ai_interaction` added to the
`event_type` CHECK constraint), and two new tests in
@@ -622,7 +622,7 @@ ends up with:
handler/routing logic via `fakeProvider` -- fast, deterministic, no
network, all run in CI today.
2. **Integration tests** (this task, new): the mock-Ollama-server suite
above, plus `cli/cmd/sentryctl/cmd_query_test.go`'s existing
above, plus `cli/cmd/cairnobsctl/cmd_query_test.go`'s existing
`httptest.Server`-backed coverage of `--nl`/`--execute` (already
written during Track B, task 11) -- proves the plumbing (HTTP routing,
JSON contracts, `planner.Compile`/`costguard.Assess` integration,
+6 -6
View File
@@ -12,7 +12,7 @@ document is verification only.
Every AI operation (`complete`, `explain`, `fix`, `optimize`,
`translate`, and the audit-logging endpoint behind it) has been run
end-to-end against a real `docker compose` stack — real HTTP requests
into the real `sentry-api` container, through the real
into the real `cairnobs-api` container, through the real
`api/ai/provider/ollama.Client`, over a real network call, into a real
process answering Ollama's actual `/api/chat` wire contract. **No real
model weights are used anywhere in this verification** — see
@@ -74,7 +74,7 @@ Run it as a container on the compose network with a network alias of
`OLLAMA_BASE_URL` at it via a throwaway compose override:
```sh
docker run -d --rm --name sentry-mock-ollama --network sentry_default --network-alias ollama \
docker run -d --rm --name cairnobs-mock-ollama --network sentry_default --network-alias ollama \
-v "$(pwd)/hack/mock-ollama:/src" -w /src golang:1.25-alpine \
sh -c "go build -o /tmp/mock-ollama . && /tmp/mock-ollama"
@@ -106,7 +106,7 @@ wired into a stack anyone else might reach:
```sh
docker compose up -d api # drops back to the plain env, no -f override
docker rm -f sentry-mock-ollama
docker rm -f cairnobs-mock-ollama
rm /tmp/docker-compose.ai-verify.yml
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/ai/translate -d '{}'
# 404 -- confirms AI routes are unregistered again
@@ -118,7 +118,7 @@ With AI routes enabled (step 2) and the web dev server running against
`localhost:8080`, open the Search page's query bar:
- Type a partial query and pause — ghost text should appear inline
after ~300ms; Tab accepts it. Stop `sentry-mock-ollama` and confirm
after ~300ms; Tab accepts it. Stop `cairnobs-mock-ollama` and confirm
ghost text just silently stops appearing (no error toast, no
console noise) — this is the "graceful degradation" requirement,
not incidental behavior.
@@ -149,7 +149,7 @@ separately click "Run query".
CLI:
```sh
cd cli && go run ./cmd/sentryctl query --nl "errors in the last hour" --api http://localhost:8080
cd cli && go run ./cmd/cairnobsctl query --nl "errors in the last hour" --api http://localhost:8080
# prints the translated query and, in an interactive terminal, prompts y/N before running
```
@@ -164,7 +164,7 @@ pattern applied to that service instead:
1. Accept or dismiss a Fix/Optimize/Translate suggestion in the web UI.
2. Confirm a row landed in `audit_log`:
```sh
docker exec sentry-metadata-postgres psql -U sentry -d sentry_metadata \
docker exec cairnobs-metadata-postgres psql -U cairnobs -d cairnobs_metadata \
-c "SELECT event_type, query_text, detail FROM audit_log WHERE event_type='ai_interaction' ORDER BY id DESC LIMIT 5;"
```
`detail` should show `operation`/`accepted`/`edited` matching what you
+8 -1
View File
@@ -23,8 +23,15 @@ new cross-backend join strategy from scratch.
Pipe syntax, SPL-inspired, EBNF-ish:
```
query := base_search ("|" pipe_stage)*
query := (base_search | pipe_stage) ("|" pipe_stage)*
base_search := bool_expr // implicit filter/search, SPL convention
// omitted entirely when the query starts
// directly with a pipe-stage keyword (e.g.
// `stats count by host`, no leading filter,
// no leading "|") -- means match-everything.
// A field genuinely named "where"/"stats"/etc
// still parses as a filter (`where=foo`),
// disambiguated by comparator lookahead.
pipe_stage := "where" bool_expr
| "stats" agg_call ("," agg_call)* ["by" field ("," field)*]
| "sort" sort_field ("," sort_field)*
+8 -8
View File
@@ -97,13 +97,13 @@ wire-level `SearchRequest` carries the right `tenant_id`. All pass, for
real, no disclaimer needed for this specific claim.
**Both Helm and docker-compose now close this.**
`deploy/helm/sentry/templates/api.yaml` and `enterprise-api.yaml` are
`deploy/helm/cairnobs/templates/api.yaml` and `enterprise-api.yaml` are
mutually exclusive, gated on opposite sides of the same
`enterprise.enabled` flag, rendering to the same Service name/port — so
a Helm-deployed cluster runs exactly one of the two binaries, chosen by
the same flag that turns on RBAC/audit/SSO, not a second
independently-forgettable decision. Verified by parsing (not
eyeballing) the rendered YAML under both values: exactly one `sentry-api`
eyeballing) the rendered YAML under both values: exactly one `cairnobs-api`
Deployment either way, with the right image. `docker-compose.yml`'s
`api`/`enterprise-api` services are now the analogous mutually-exclusive
choice, gated behind `COMPOSE_PROFILES` (`.env` checks in
@@ -233,7 +233,7 @@ Browser ──▶ enterprise-auth (GET /auth/oidc/login, /auth/oidc/callback)
└─▶ external IdP (OIDC authorization code flow)
└─▶ Postgres (rbacstore: users, tenant_memberships)
sentryctl ──▶ api, alerting (Bearer token when SENTRYCTL_TOKEN is set)
cairnobsctl ──▶ api, alerting (Bearer token when CAIRNOBSCTL_TOKEN is set)
```
Ingest path (agent → Redpanda → ingest → ClickHouse, and Redpanda →
@@ -442,12 +442,12 @@ layer":
1. A dedicated `audit_writer` Postgres role with only `INSERT`+`SELECT`
grants (`metadata/migrations/0012-0014`), via its **own**
`pgxpool.Pool` — never the shared `sentry` role/pool every other
`pgxpool.Pool` — never the shared `cairnobs` role/pool every other
store uses.
2. A `BEFORE UPDATE OR DELETE ... RAISE EXCEPTION` trigger
(`metadata/migrations/0015-0016`) that rejects the operation for
*any* role, including the table owner — confirmed live: even the
`sentry` role cannot `UPDATE` a row without first disabling the
`cairnobs` role cannot `UPDATE` a row without first disabling the
trigger, a privileged operation distinct from ordinary application
access.
@@ -547,10 +547,10 @@ terms:
cluster/trusted network (`api`/`alerting`/`web`), never exposed
publicly. Nothing in this codebase enforces that at the network layer;
it's a deployment responsibility (NetworkPolicy, or equivalent) not
yet codified in `/deploy/helm/sentry`.
yet codified in `/deploy/helm/cairnobs`.
- `ENTERPRISE_SESSION_SIGNING_KEY`, ClickHouse/Postgres passwords, and
(once minted) the `alerting` service token are all K8s `Secret`
objects in the Helm chart (`/deploy/helm/sentry/templates/
objects in the Helm chart (`/deploy/helm/cairnobs/templates/
secrets.yaml`) — standard K8s `Secret` semantics apply (base64, not
encrypted at rest without a cluster-level `EncryptionConfiguration`).
No secrets-manager integration (Vault, cloud KMS) exists; the chart
@@ -571,7 +571,7 @@ terms:
| Ingest tenant *identity* (credential validation, tagging) | **Built and tested** — fail-closed `TenantResolver`, `tenant_id` Kafka header attached per record |
| Ingest tenant *write-routing*, ClickHouse | **Enforced, verified live**`enterprise-ingest`/`chwriter.Registry` route each tagged batch to its tenant's own database, fail-closed on an untagged/unprovisioned tenant; both Docker-free and live-ClickHouse tests pass. Active-tenant snapshot refreshes every minute (`Registry.StartRefreshing`) — a deprovisioned tenant loses write access within a minute, not "until the next restart" |
| Ingest tenant *write-routing*, Tantivy | **Built and genuinely verified**`search/src/consumer.rs` routes each record into its own tenant's index via `IndexRegistry`, same registry the (already-verified) read side uses; no Docker needed, real tests pass. Active-tenant-gated too: `tenants::ActiveTenantTracker` polls `enterprise-auth` every 60s (off unless configured), refusing any tenant not in the polled allowlist — same one-minute staleness bound as ClickHouse's now-refreshing snapshot, no more asymmetry between the two |
| Deployment actually routing traffic to `enterprise-api` (Helm) | **Enforced, verified live**`api`/`enterprise-api` are mutually exclusive, same flag as RBAC/audit/SSO; a real `helm install` against a real `kind` cluster confirmed the `sentry-api` Deployment runs `sentry-enterprise-api:latest` with `enterprise.enabled=true`, real endpoints behind the `sentry-api` Service, not just `helm template`'s rendered YAML |
| Deployment actually routing traffic to `enterprise-api` (Helm) | **Enforced, verified live**`api`/`enterprise-api` are mutually exclusive, same flag as RBAC/audit/SSO; a real `helm install` against a real `kind` cluster confirmed the `cairnobs-api` Deployment runs `cairnobs-enterprise-api:latest` with `enterprise.enabled=true`, real endpoints behind the `cairnobs-api` Service, not just `helm template`'s rendered YAML |
| Deployment actually routing traffic to `enterprise-api` (docker-compose) | **Enforced, verified live**`api`/`enterprise-api` are mutually exclusive via `COMPOSE_PROFILES`, same flag choice as Helm's `enterprise.enabled`; a real `docker compose up` of `enterprise-api` was run in this environment (and caught/fixed a startup-crashing duplicate `GET /healthz` route registration bug in the process), not just `docker compose config` |
| Human SSO login — OIDC | **Enforced, verified live** — real login against a real Auth0 developer tenant, full browser round trip; correctly failed closed on an identity with no `tenant_memberships` row, then succeeded and issued a real session after `-grant-membership-*`, with `POST /internal/authorize` returning exactly the granted tenant/role |
| Human SSO login — SAML | **Enforced, verified live** — real login against Auth0's SAML2 Web App addon acting as a real SAML IdP, over real (self-signed, dev-only) TLS; a real signed assertion validated (audience, destination, signature), landed on `/select-tenant` with real memberships, and `POST /internal/authorize` confirmed the selected tenant/role. Found and fixed a real bug in the process: `loginhandler.go`'s cookies decided `Secure` from `r.TLS != nil` alone, which is wrong behind any TLS-terminating reverse proxy (the deployment shape this handler actually runs in) — `enterprise-auth` never terminates TLS itself, so `r.TLS` was nil even over a genuinely HTTPS connection, silently dropping `Secure` and breaking SAML's `SameSite=None` cookie |