Skip to content

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 session apps claim carrying product-native roles — and that the coarse admin/editor/viewer projection 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].

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:

  1. 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’s approver role — an editor-mapped actor cannot approve a run.
  2. The docs said one thing and the code did another. The suite-role.ts module docblock still describes a suite_role session claim stamped on every mint path; no mint path stamps it. src/server/services/auth/jwt-key.ts referenced an “ADR-008 rollout” that had no artifact until the suite ADR was published.
  3. Revocation and signing are not where the product needs them. The apps claim is computed once, at sign-in, and the suite-service token plane is signed with one shared symmetric secret.

1. This image is the suite identity authority when VELTRO_ROLE=identity. [withdrawn 2026-09-06, veltro#626] Historical record: the role gate was 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.

  • suite-role.ts docblock is stale. Lines 1–13 of src/server/services/auth/suite-role.ts state that the coarse role is “stamped into the VF session JWT as the suite_role claim on every mint path” and that CHAD maps admin→admin, editor→analyst, viewer→viewer. No mint path assigns suite_role; the session carries apps, and CHAD/Warden use the native role verbatim. The docblock describes the superseded design and should be corrected to scope computeSuiteRole to the MCP actor projection. [drift]
  • Dangling ADR reference. src/server/services/auth/jwt-key.ts comments (“tokens minted before the ADR-008 rollout”) pointed at an artifact that did not exist; suite ADR-008 now supplies it. [drift, resolving]
  • /mcp catalog 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]
  • Positive. Warden’s approver and CHAD’s analyst survive the session boundary intact, so interactive authorization is the product’s own model, not a lossy suite projection. Default-deny means a user with no AppGrant reaches no product. The claim shape is executable-tested on both sides of the language boundary: scripts/mint-test-session.mjs mints on the TS side; the published veltro-suite-auth wheel 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: the veltro-suite-auth Python package is owned and published from veltrosecurity/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_URL at /vectorflow after the cutover.

See CONTEXT.md for the full aggregate/seam map.