ADR-004: VELTRO_INTEGRATED, the suite-service token plane, and the product integrations
Source of truth:
docs/adr/004-veltro-integrated-and-service-tokens.md(synced at build time — edit the original, not this copy). Status: Accepted (2026-07-04). Consolidates the per-product suite switch, generalizes the cross-service token plane to all three products, and records the four product↔product integrations (VF→CHAD log sync, CHAD→Warden alert push/escalate, Warden→CHAD writeback, the Warden⇄CHAD enrichment loop). Amends ADR-002 §4/§5 (module contract) and ADR-003 Decision 4 (the delegated-auth flag wiring); everything else in ADR-001/002/003 stands.
Amended by ADR-006 (2026-07-17):
svc="veltro"may mint actor-attributedchad:*andwarden:*scope families for the suite MCP plane. Those scope families requireactor_emailandactor_suite_role; the original automatedsettings:*/external:*flows remain actor-optional and unchanged.
Amended by ADR-008 + the 2026-07-27 owner canonicalisation: integration B (CHAD→Warden) ships as two lanes —
POST /api/webhooks/chad(always-Case, manual escalate) andPOST /api/events/chad(case-optional, automatic). Integration D (Warden⇄CHAD enrichment) is superseded and pull-only: the push endpoint/api/enrich/alert-context+X-Warden-Enrich-Token/WARDEN_ENRICH_TOKENwere removed in favour of Warden’s analyst-drivenGET /api/related-cases(issue #138), so the D-egress SSRF note in Consequences is moot — CHAD does not exemptWARDEN_SERVICE_URLfrom its outbound guard. CHAD’sIndexPatternis renamed Data Source (/api/data-sourcescanonical, alias removed; issue #39). The historical standalone-CHAD case exception below is also superseded: Warden is the sole case authority in every supported mode, and CHAD’s local Cases surface has since been deleted (issue #135, shipped: the pinnedCHAD_VERSION=0.43.2carries nobackend/app/api/cases.py, nofrontend/src/pages/Cases.tsx, and migrationbackend/alembic/versions/20260807b_drop_legacy_cases.py). Seedocs/traceability-matrix.md.
Context
Section titled “Context”ADR-003 made Veltro the suite identity authority and introduced three separate
per-product delegated-auth flags (VF_DELEGATED_AUTH, CHAD_DELEGATED_AUTH,
WARDEN_DELEGATED_AUTH) plus VF_SESSION_SECRET (the fanned-out NEXTAUTH_SECRET)
and a VF-only suite-service token plane gated by VF_SUITE_SERVICE_ENABLED. That got
the suite to single sign-on and a read-only suite audit feed, but left four gaps:
- Four flags for one concept. “Is this product running inside the suite?” was signalled by a different env var per product, plus a fourth for the token plane.
- The token plane was VF-only. Only VF could mint suite-service tokens (to read CHAD’s audit feed). Warden had a byte-identical verifier that nothing used.
- No data-plane integrations. VF and CHAD shared a network but VF never shipped events to CHAD; CHAD never pushed alerts to Warden; Warden’s CHAD writeback used a hand-provisioned API key; the Warden⇄CHAD enrichment loop was configured by hand.
- No documented credential model for the machine-to-machine data planes.
Decision 1 — One generic suite switch: VELTRO_INTEGRATED
Section titled “Decision 1 — One generic suite switch: VELTRO_INTEGRATED”Collapse the three delegated-auth flags and VF_SUITE_SERVICE_ENABLED into one
per-product boolean env var, VELTRO_INTEGRATED, and rename the fanned session
secret VF_SESSION_SECRET → VELTRO_SESSION_SECRET. Clean cutover — no aliases,
no deprecation shims (the umbrella compose is the only real consumer).
VELTRO_INTEGRATED=true on a product means, together:
- delegated auth ON — the product trusts the Veltro-minted Auth.js session cookie
(decoded with
VELTRO_SESSION_SECRET) and hides its own login/identity surface; - the suite-service token plane ON — the product mints and/or verifies short-lived suite-service tokens (Decision 2);
- suite chrome ON and suite auto-seeds armed — each auto-seed additionally requires its own env inputs (Decision 4).
Default (unset/false) ⇒ standalone: byte-for-byte the pre-suite behavior. VELTRO_ROLE
(which surface the VF image serves — ADR-003) is a separate axis and is untouched.
HTTP wire fields are unchanged for backward compatibility: CHAD’s /api/auth/* still
return {"chad_delegated_auth": …} and Warden’s /api/settings/auth-mode still returns
{"delegated_auth": …} — only the server-side config field and env var were renamed.
Decision 2 — One suite-service token convention (all three products)
Section titled “Decision 2 — One suite-service token convention (all three products)”A compact HS256 JWS signed with the shared suite secret (NEXTAUTH_SECRET =
VELTRO_SESSION_SECRET). It is never a cookie, never handed to a browser, minted
per outbound call and consumed within one HTTP round trip.
The canonical mint/verify implementation of this spec now lives in the
veltro-suite-authpackage (see ADR-005); CHAD and Warden consume it instead of carrying byte-identical local copies.
Claims:
| claim | value |
|---|---|
iss |
veltro-suite (renamed from vectorflow-suite) |
aud |
suite-service |
svc |
minting service: vectorflow | warden | chad | veltro (audit attribution; veltro added by ADR-006) |
scope |
a single scope string (below) |
actor_email, actor_suite_role |
OPTIONAL for automated settings:*/external:* calls; REQUIRED together for ADR-006 chad:*/warden:* calls |
jti, iat, exp |
unique id, issued-at, expiry (TTL ≤ 60s) |
This ADR introduced two independent automated-machine families. :write
implies :read only within one family; families never cross. ADR-006 adds five
more isolated, actor-required product families for MCP.
| scope | grants |
|---|---|
settings:read / settings:write |
VF Settings ↔ CHAD settings endpoints (e.g. the suite audit feed) |
external:read / external:write |
CHAD’s /api/external/* — read (GET) / writeback (POST: comment, snooze, exception) |
Minting and verification are both gated on VELTRO_INTEGRATED + the secret being present.
CHAD’s external API accepts either a suite token (scope-authorized) or the existing
X-API-Key (RBAC-authorized); a suite-token write is attributed to a fixed CHAD
suite-service account (not a per-actor JIT — machine caller).
External read plane (external:read, VF → CHAD). The CHAD sink component reads CHAD’s
push-mode index patterns and their ingest tokens over the same token plane, scoped
external:read — a separate scope family from settings:* and weaker than the
external:write writeback in row C:
GET /api/external/index-patterns— push-mode,chad-percolator--prefixed patterns (id, name, derivedsuffix; no tokens) → the CHAD-sink destination dropdown.GET /api/external/index-patterns/{id}/token— that pattern’s ingestauth_token; the read is audited asindex_pattern.token_read_external, attributed to the suite-service account.
Decision 3 — The integration matrix
Section titled “Decision 3 — The integration matrix”| # | Producer → Consumer | Transport | Credential |
|---|---|---|---|
| A | VF → CHAD log sync | first-class CHAD sink component in the pipeline builder → POST {CHAD}/api/logs/{suffix}/queue (JSON-array body), {suffix} = the user-selected push-mode CHAD index pattern |
per-pattern ingest token fetched machine-to-machine at config delivery over the suite-service plane (Decision 2, external:read) via GET {CHAD}/api/external/index-patterns/{id}/token; never stored in the pipeline graph or version snapshot |
| B | CHAD → Warden alert push + escalate | POST {WARDEN}/api/webhooks/chad (generic alert payload) → Warden case + runbook fan-out |
static shared secret X-Warden-Token (WARDEN_WEBHOOK_SECRET) |
| C | Warden → CHAD writeback (comment / snooze / exception) | POST {CHAD}/api/external/* |
suite-service token (Decision 2, external:write) — no hand-provisioned API key in suite mode |
| D | Warden ⇄ CHAD enrichment | CHAD EnrichmentWebhook → POST {WARDEN}/api/enrich/alert-context |
static shared secret X-Warden-Enrich-Token (WARDEN_ENRICH_TOKEN) |
Case-lifecycle boundary (ADR-002): Warden owns the suite case lifecycle. In suite mode
CHAD deep-links alerts/cases into Warden and its “Escalate to Warden” action opens/append s a
Warden case; standalone CHAD cases stay first-class. Warden’s intake response threads back
{case_id (UUID), case_key ("WARDEN-{n}"), created} so CHAD can deep-link the exact case.
Decision 4 — Credential model (compose-fanned static secrets)
Section titled “Decision 4 — Credential model (compose-fanned static secrets)”The machine data planes (A, B, D) use static secrets fanned out by the umbrella compose
(the pattern already used for NEXTAUTH_SECRET). Chosen over dynamic per-org provisioning
because the deployer UX is identical (“up” ⇒ it works) and rotation is a documented
edit .env, restart — no new provisioning API surface this wave. Each product auto-seeds
its side once, on boot, when VELTRO_INTEGRATED + the relevant env inputs are set, then the
row is UI-managed:
- CHAD seeds a
vectorflowIndexPattern (minting its own ingest token at seed time) — now the default option in VF’s CHAD-sink dropdown, one of possibly many selectable push-mode patterns rather than the sole sync target — aWardenConfig(webhook push, fromWARDEN_SERVICE_URL+WARDEN_WEBHOOK_SECRET), and a Warden enrichment webhook (WARDEN_ENRICH_TOKEN). - Warden seeds a
CHAD (suite)connection (auth: "suite", fromCHAD_SERVICE_URL) whose writeback mints suite tokens (C), and (pre-existing) aWebhookSource+ enrich token.
Realized (this wave): the CHAD sink component fetches + caches each selected pattern’s
ingest token at config-delivery time over the external:read plane (row A); CHAD mints that
token when it seeds the pattern (no CHAD_SUITE_INGEST_TOKEN env), so no static ingest token
is exchanged in either direction. Follow-up path (pre-decided, not built):
dynamic provisioning — a CHAD POST /api/index-patterns/suite-provision guarded by
require_suite_service_scope — remains future work if per-org tokens or restartless pattern
creation become requirements. Warden intake auth stays the UI-rotatable X-Warden-Token
(migrating it onto suite tokens would break the standalone CHAD→separate-Warden pairing).
Consequences
Section titled “Consequences”- One env var (
VELTRO_INTEGRATED) + one shared secret (VELTRO_SESSION_SECRET) now express “in the suite” across all three products; the umbrella compose hardcodesVELTRO_INTEGRATED=trueon thefull-profile services and fans the secrets. - New suite-only secret in
.env:WARDEN_ENRICH_TOKEN(WARDEN_WEBHOOK_SECRETalready existed). Allopenssl rand-generated. - Standalone remains inviolable: every product with
VELTRO_INTEGRATEDunset behaves byte-for-byte as before, seeds nothing, and exposes no suite surface. - Amends: ADR-002 §4/§5 (the module contract’s flag/secret names) and ADR-003 Decision 4
(the
*_DELEGATED_AUTHwiring is now the singleVELTRO_INTEGRATED). ADR-003’s identity mechanism (the cookie,VELTRO_ROLE, DB-sharing) is unchanged. - Amendment (suite-simplification wave):
CHAD_SUITE_INGEST_TOKENremoved — CHAD mints thevectorflowpattern’s ingest token at seed time (secrets.token_urlsafe) and VF fetches it over the suite plane, so the static shared token no longer exists on either side. - Amendment (suite egress policy, 2026-07-06): both products’ outbound SSRF guards
(Warden
app/services/notifier.py, CHADapp/services/webhooks.py) exempt exactly the hostnames of the compose-provisioned suite peer URLs (CHAD_SERVICE_URLon Warden;WARDEN_SERVICE_URLon CHAD) whenVELTRO_INTEGRATED=true. The seeded rows C/D target internal docker-network hostnames that the private-IP rejection otherwise blocks; the trust anchor is the operator-set env var, so request/alert-derived URLs remain fully guarded andALLOW_INTERNAL_WEBHOOK_IPSstays a blanket dev-only override.