ADR-001: VectorFlow's suite identity role and product-native roles
Source of truth:
vectorflow/docs/adr/001-vectorflow-suite-identity-role-and-native-roles.md(synced at build time — edit the original, not this copy). Status: Accepted (2026-07-27). Records the load-bearing decision that this repo’s image serves the Veltro identity role and produces the sessionappsclaim carrying product-native roles — and that the coarseadmin/editor/viewerprojection is an MCP-actor-only concern. Product-local companion to the suite’s canonical identity/role ADR-008, which this ADR references rather than restates. Depends on veltro ADR-003 (docs/adr/003-veltro-standalone-identity-authority.md, Option A) and veltro ADR-006 (docs/adr/006-veltro-mcp-agent-plane.md).
Amended (2026-08-10, veltro#406): the veltro-suite-auth Python package is no
longer published from this repo. Publication moved to veltrosecurity/veltro
(its protected publish-suite-auth.yml), and this repo’s consumer-facing
contract test now verifies against that published wheel (visible artifacts).
The decisions below are unchanged; only the publication ownership of the
cross-language package moved. See CONTEXT.md seam 1.
Amended (2026-09-06, veltro#626): Decision 1 is withdrawn. The fixed-realm
authority cutover made the standalone Veltro identity application
(veltro/apps/identity) the suite’s identity authority, so the
VELTRO_ROLE=identity deployment role and every surface it gated — the
identity-role proxy blocks, the forced-password-reset gate and
/api/veltro/module-access, /password-reset, the /mcp resource server with
its OAuth/PAT authentication, personal access tokens and cross-product tool
catalog, and the identity-role scheduler skip — were deleted from this repo. The
src/lib/env.ts:isVeltroIdentityRole gate no longer exists and its
reintroduction is refused by src/__tests__/identity-role-retired.test.ts.
Decisions 2, 4 and 5 stand unchanged; Decisions 3 and 6 are restated below for
the surviving surfaces. veltro ADR-003 Option A is superseded for this repo.
Amended (2026-09-26, ADR-014 step 4): the product now ships only as the
vectorflow-server-suite image with /vectorflow baked into Next.js. Veltro
Identity owns interactive sign-in. VectorFlow accepts browser request
assertions and reads the live VectorFlow grant on each request; its local
membership rows are a cross-user projection. The standalone image and local
login are retired. Agent URLs must include /vectorflow. Earlier sections
below record the decisions and implementation state at the time they were
written.
Statements are classified [implemented] (with src/…:symbol evidence),
[target] (approved owner decision, not yet built), or [drift].
Context
Section titled “Context”VectorFlow’s repo ships one Next.js image that runs in two deployment roles: the
VectorFlow product (default) and the Veltro identity authority
(VELTRO_ROLE=identity). That means this codebase — not a separate identity
service — is where the suite’s session principal is minted, and where the claim
that CHAD and Warden authorize against is computed. Getting that claim’s shape
wrong silently changes authorization in two other products.
Three forces made the decision load-bearing:
- Two role vocabularies coexist in this repo. A coarse suite role
(
admin/editor/viewer,src/server/services/auth/suite-role.ts:computeSuiteRole) and per-app product-native roles (src/server/services/suite/app-roles.ts). Collapsing every path onto the coarse one destroys Warden’sapproverrole — an editor-mapped actor cannot approve a run. - The docs said one thing and the code did another. The
suite-role.tsmodule docblock still describes asuite_rolesession claim stamped on every mint path; no mint path stamps it.src/server/services/auth/jwt-key.tsreferenced an “ADR-008 rollout” that had no artifact until the suite ADR was published. - Revocation and signing are not where the product needs them. The
appsclaim is computed once, at sign-in, and the suite-service token plane is signed with one shared symmetric secret.
Decision
Section titled “Decision”1. This image is the suite identity authority when
[withdrawn 2026-09-06, veltro#626] Historical record: the role gate was
VELTRO_ROLE=identity.src/lib/env.ts:isVeltroIdentityRole; in that role the process served only the
identity surface (login, /veltro/**, launcher, auth + SCIM APIs, /mcp) and
skipped VF product singleton schedulers because it shared vf-postgres with a
co-located product instance. The Veltro identity application is now that
authority and runs its own process and database; this image runs the VectorFlow
product and its singleton schedulers under plain leader election
(src/instrumentation.node.ts:registerNodeInstrumentation).
2. The interactive session principal carries the apps claim with
product-native roles. [implemented] The jwt callback stamps org_id and
apps (src/auth.ts; SAML mint path
src/server/services/auth/saml.ts:computeAppsClaim call site), computed by
src/server/services/auth/suite-role.ts:computeAppsClaim with a default-deny
“highest role per (app, team) wins” reduction over OrgMember + AppGrant.
The claim vocabulary and types are dependency-free in
src/server/services/suite/app-roles.ts so client components can import them.
CHAD and Warden consume apps.<product> verbatim — including Warden
approver — and 403 by default when the grant is absent
(src/server/services/auth/__tests__/apps-claim.test.ts).
3. The coarse admin/editor/viewer map is actor-token-only and is NOT the
interactive authorization model. [implemented, amended veltro#626]
src/server/services/suite/service-token.ts projects a principal onto a coarse
actor role solely to mint a service token
(:mintSuiteServiceToken) for a cross-product call — the projection that used to
live in the deleted src/server/mcp/product-client.ts:actorRole.
computeSuiteRole survives only for that projection. This map loses Warden
approver by construction; that is a known limitation of the agent path,
tracked as veltro#131, and it must never be applied to an interactive session.
4. Grant revocation must take effect across active sessions within 60s.
[target] — veltro#129. Today apps is computed once at sign-in and fixed
in the signed cookie until re-login or the 24h expiry, so a mid-session grant
removal or role demotion is not reflected: the per-request re-sync in CHAD/Warden
reads a potentially stale claim (see the resolveSuiteRole docblock in
src/server/services/auth/suite-role.ts, which names the staleness budget
explicitly). Immediate invalidation is not implemented.
5. Per-service asymmetric signing is required. [target] — veltro#130.
Today the suite-service token plane is symmetric: encodeAndSign HS256-signs
with env.NEXTAUTH_SECRET, which CHAD trusts as VELTRO_SESSION_SECRET
(src/server/services/suite/service-token.ts:101-103). Every peer that can
verify can therefore also forge. The shared symmetric domain is recorded here as
implementation debt, not as the intended design.
6. VectorFlow stays producer-only on the suite-service-token plane.
[implemented, amended veltro#626] VF mints suite tokens and verifies no
suite-service token inbound. It is no longer an MCP resource server: the
PAT/OIDC-authenticated /mcp endpoint moved to the identity application, and
MCP tool calls reach VF over Product Peer v1
(src/app/api/peer/v1/mcp/route.ts → authorizePeerMcp, per-tool scope and
native roles in src/server/services/peer/mcp-contract.ts). VF-native MCP tools
reuse the same appRouter + withTeamAccess stack the browser hits — no RBAC
bypass.
Known drift recorded by this ADR
Section titled “Known drift recorded by this ADR”suite-role.tsdocblock is stale. Lines 1–13 ofsrc/server/services/auth/suite-role.tsstate that the coarse role is “stamped into the VF session JWT as thesuite_roleclaim on every mint path” and that CHAD mapsadmin→admin, editor→analyst, viewer→viewer. No mint path assignssuite_role; the session carriesapps, and CHAD/Warden use the native role verbatim. The docblock describes the superseded design and should be corrected to scopecomputeSuiteRoleto the MCP actor projection. [drift]- Dangling ADR reference.
src/server/services/auth/jwt-key.tscomments (“tokens minted before the ADR-008 rollout”) pointed at an artifact that did not exist; suite ADR-008 now supplies it. [drift, resolving] /mcpcatalog count. Resolved by veltro#626: this repo no longer publishes a tool catalog, so veltro ADR-006’s count is a statement about the identity application’s catalog (veltro/apps/identity/src/authority/catalog.ts), which pins itself. VF publishes only its own tools over Product Peer v1 (src/server/services/peer/mcp-contract.ts). [resolved]
Consequences
Section titled “Consequences”- Positive. Warden’s
approverand CHAD’sanalystsurvive the session boundary intact, so interactive authorization is the product’s own model, not a lossy suite projection. Default-deny means a user with noAppGrantreaches no product. The claim shape is executable-tested on both sides of the language boundary:scripts/mint-test-session.mjsmints on the TS side; the publishedveltro-suite-authwheel decodes on the Python side (contracts/suite-auth/tests/test_fresh_session_roundtrip.py). - Negative / accepted for now. Two role vocabularies remain in the codebase, and readers must know which path they are on; the stale docblock above is precisely the cost of that. Authorization staleness up to 24h is real and user-visible until veltro#129 lands. A compromised peer can mint tokens the whole suite trusts until veltro#130 lands.
- Obligations. Any new session-mint path (a new provider, a new cookie
builder) MUST call
computeAppsClaim; adding a coarse-role shortcut to an interactive path is a regression against Decision 3. Any change to the claim shape is a cross-repo contract change: theveltro-suite-authPython package is owned and published fromveltrosecurity/veltro, so the package source, its committed vectors, and VectorFlow’s conformance fixtures (contracts/suite-auth/) move in the same wave. - Deployment after ADR-014. The server image and chart require the suite
gateway. Browser authorization uses a live assertion grant; there is no
local password gate or standalone product mode. Existing agents must point
VF_URLat/vectorflowafter the cutover.
See CONTEXT.md for the full aggregate/seam map.