Skip to content

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-attributed chad:* and warden:* scope families for the suite MCP plane. Those scope families require actor_email and actor_suite_role; the original automated settings:*/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) and POST /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_TOKEN were removed in favour of Warden’s analyst-driven GET /api/related-cases (issue #138), so the D-egress SSRF note in Consequences is moot — CHAD does not exempt WARDEN_SERVICE_URL from its outbound guard. CHAD’s IndexPattern is renamed Data Source (/api/data-sources canonical, 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 pinned CHAD_VERSION=0.43.2 carries no backend/app/api/cases.py, no frontend/src/pages/Cases.tsx, and migration backend/alembic/versions/20260807b_drop_legacy_cases.py). See docs/traceability-matrix.md.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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-auth package (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, derived suffix; no tokens) → the CHAD-sink destination dropdown.
  • GET /api/external/index-patterns/{id}/token — that pattern’s ingest auth_token; the read is audited as index_pattern.token_read_external, attributed to the suite-service account.
# 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 vectorflow IndexPattern (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 — a WardenConfig (webhook push, from WARDEN_SERVICE_URL + WARDEN_WEBHOOK_SECRET), and a Warden enrichment webhook (WARDEN_ENRICH_TOKEN).
  • Warden seeds a CHAD (suite) connection (auth: "suite", from CHAD_SERVICE_URL) whose writeback mints suite tokens (C), and (pre-existing) a WebhookSource + 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).

  • One env var (VELTRO_INTEGRATED) + one shared secret (VELTRO_SESSION_SECRET) now express “in the suite” across all three products; the umbrella compose hardcodes VELTRO_INTEGRATED=true on the full-profile services and fans the secrets.
  • New suite-only secret in .env: WARDEN_ENRICH_TOKEN (WARDEN_WEBHOOK_SECRET already existed). All openssl rand-generated.
  • Standalone remains inviolable: every product with VELTRO_INTEGRATED unset 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_AUTH wiring is now the single VELTRO_INTEGRATED). ADR-003’s identity mechanism (the cookie, VELTRO_ROLE, DB-sharing) is unchanged.
  • Amendment (suite-simplification wave): CHAD_SUITE_INGEST_TOKEN removed — CHAD mints the vectorflow pattern’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, CHAD app/services/webhooks.py) exempt exactly the hostnames of the compose-provisioned suite peer URLs (CHAD_SERVICE_URL on Warden; WARDEN_SERVICE_URL on CHAD) when VELTRO_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 and ALLOW_INTERNAL_WEBHOOK_IPS stays a blanket dev-only override.