The identity track, sequenced: what is genuinely first, which decisions are secretly one, and what stays out
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 9
- Forks
- 0
- Avg merge
- 3h 3m
- Merged PRs (30d)
- 509
Description
Entry point for the identity track. The analysis lives in #548, #549, #557, #558, #559 and the owner decisions of 2026-08-14 recorded on each; none of it is restated here. This issue adds the three things the five issues cannot say individually: the ordering across them, the places where they quietly share a decision, and the boundary of the track. #561 and #562 are fixed and merged via #563; #565 is in flight and is slice zero.
Tree state, verified against origin/main at 93aaaee
Worth pinning because two of the five issues are already partially stale in the good direction. maxRSAKeyBits is on main (pkg/flowstate/v1/auth/jwks.go:47), the client secret is held in a Material — #561 and #562 are done. And cmd/flow/routing.go now serves the OIDC discovery document, the JWKS route, and /healthz, so #549's finding that "the issuer computes a JWKSURL() that nothing serves" is no longer true; the remaining slice-1 work there is TLS and the internal listener only. Still absent, confirmed by grep at write time: no tls.Config or ListenAndServeTLS under cmd/ or the server package, no flow login, no credential source, no namespace_map, no PrincipalKind or on_behalf_of in the proto, and flow mcp is stdio-only (cmd/flow/mcp.go:262).
The rule #565 set, which the rest of the plan is audited against below
#565's first attempt aliased identity to workload; review caught that identity.subject on two surfaces is the authenticated caller while workload.subject on the other two is the subject of the assertion being minted — one name, two principals, and a deny rule copied between surfaces silently stops matching. The generalization this roadmap applies everywhere: a unification is of meanings, not spellings, and where one spelling would carry two meanings, two spellings must survive. Sections "same decision twice" and "one thing that is two" below are that audit run over the remaining plan, and it fires three times.
One recorded decision needs amending in its light. #548's decision says workload is "kept as an alias for one edition and retired after". After #565, workload on the minting surfaces is not an alias and must never be retired — it names a principal identity cannot name. What retires is only any use of workload to mean the caller. Recording the delta here so nobody executes the retirement as written.
Sequencing
flowchart TB
S0["S0 — merge #565<br/>identity ≠ workload"]
D1{{"D1 — action/scope vocabulary"}}
D2{{"D2 — on_behalf_of naming"}}
S1["S1 — TLS + internal listener (#549)"]
S2["S2 — credential source package (#559)"]
S3["S3 — PrincipalKind + on_behalf_of (#557)"]
S4["S4 — claim-mapping table (#559 + #557)"]
S5["S5 — flow policy test / explain (#548)"]
S6["S6 — OIDC human provider + flow login --device (#557, #558)"]
S7["S7 — HTTP MCP resource server (#558)"]
S8["S8 — actor_token in the 8693 exchanger (#558)"]
S9["S9 — DPoP (#549)"]
M["policy modules (#548)"]
S0 --> S3
S0 --> S4
S0 --> S5
D2 --> S3
D1 --> S5
D1 --> S7
S1 --> S6
S1 --> S7
S2 --> S6
S3 --> S6
S3 --> S7
S3 --> S8
S4 --> S6
S5 --> M
S7 --> S9
S0 — merge #565. Genuinely first, and the only serial bottleneck: every later slice either reads the policy vocabulary (#557's kind rules, #558's per-principal posture, #559's claim rules, all of #548's remainder) or extends the environments it defines, and every week it waits is more policy written in the divergent spelling. Nothing else in the track merges before it.
S1 — TLS with explicit cert/key, plus the internal listener (#549 decision 1, minus the JWKS part that already landed). Unblocks the client's off-loopback refusal and therefore every interactive surface; also where /healthz and a metrics endpoint move off the public socket. Independent of S0 — start in parallel.
S2 — the credential source, as a Go package (#559 decision 1, Q2). Code-independent of everything else and parallel with S1, though end-to-end use off loopback waits for S1. It must be the package and not a CLI feature, because S6's flow login --device client and the Terraform provider are the same package's other two consumers — coupling 4 below.
S3 — PrincipalKind and on_behalf_of on Principal (#557 decisions 1–2). After S0 merges and after D2 is answered, because this is the track's one nearly-irreversible move: a proto field ships into a schema that buf breaking guards and plugins compile against, so a wrong name here is the #565 defect made permanent.
S4 — the exact claim-mapping table (#559 decision 2). After S0; parallel with S3. Built once for both issues' uses — coupling 3.
S5 — flow policy test and flow policy explain (#548, before modules per its Q2 answer). After S0 and D1. Worth its early slot because it makes every subsequent slice's negative-direction tests writable as policy cases rather than bespoke Go.
S6 — the OIDC human provider plus the device-grant client (#557 decision on first provider; #558 Q4's client-only half). Needs S1 (no session over plaintext), S3 (the kind it stamps), S4 (the mapping is how an IdP's groups claim becomes a namespace), S2 (same acquisition package).
S7 — HTTP MCP as a resource server, its own surface (#558 decisions 1–2). Needs S1, S3, and D1. Deliberately not gated on per-principal egress policy or compute sandboxing: with flowstate_run_local off by default and flowstate_test reaching nothing by construction, the minimal compliant surface has no per-caller execution to confine. Turning run_local on over HTTP is a different shipment — "one thing that is two", item 2.
S8 — actor_token in the existing RFC 8693 exchanger (#558 decision 3). After S3; claims-mapped delegation first, actor_token second, per the decision.
S9 — DPoP (#549 decision 3). After S7 exists to be worth sender-constraining; independent of S8.
Behind these, in their issues' own order: policy modules after S5 (#548); ACME then mTLS (#549); the credential-injecting proxy as its own issue off #548's enforcement seam (#549 decision 4); the local issuer / non-federated service accounts last and never default (#559 decision 3); WebAuthn and the account store behind their own design pass (#557 decision 3); compute/storage enforcement, which is the sandboxing arc and shares only the vocabulary.
Same decision wearing two coats
This is the section that earns the issue. Each of these looks like two independent work items in two issues and is one decision; building them separately reproduces the two-attempt-count-defaults defect at design scale.
1. The action vocabulary and the scope vocabulary are one list. #558's scopes_supported and its 403 insufficient_scope challenges name operations (flowstate:read, flowstate:run); #557's ceremony rules name operations (action: run); #548's policy surfaces gate operations. Three lists means a step-up challenge and the policy that provoked it name the same operation differently, and no claim mapping can bridge a vocabulary split we created ourselves. One proto-owned action list, read by policy, by the protected-resource metadata, and by ceremonies — decided before S5 and S7 each bake in a spelling. This is D1 in the graph.
2. on_behalf_of is currently one spelling headed for two meanings — the #565 shape, in the schema this time. The workload CEL view that #565 lands already carries on_behalf_of and on_behalf_of_issuer: the caller an assertion is being minted for. #557's Principal.on_behalf_of is the human an agent acts for — the delegation edge, a different edge of the same graph. If S3 ships the proto field under that name while the minting view keeps its own, identity.on_behalf_of and workload.on_behalf_of will be two different relationships one keystroke apart, and nothing will warn the rule author. Two ways out, one must be chosen before S3 (D2): establish that after S0 the minting view's on_behalf_of is definitionally the caller — i.e. redundant with identity on those surfaces — and deprecate it toward identity.subject; or rename one edge. What is not acceptable is shipping both under one name.
3. #559's namespace_map and #557's provider claim mapping are the same table. A GitLab project_path mapping to a namespace and an Okta groups claim mapping to a namespace differ in nothing but the issuer; the exact-table, unlisted-values-refused semantics decided on #559 are the semantics both need, and role mapping (and, after D1, any scope mapping) belongs in the same mechanism. One implementation in the trust policy, kind-agnostic, or the two will disagree precisely because nothing imports both.
4. One credential acquisition path. #559's ambient CI source, #557/#558's flow login --device, and the Terraform provider's auth are three consumers of one package: sources that mint or fetch an assertion, a refresh margin, and a fail-closed unknown-source error. S2 builds the package; S6 adds the interactive source to it; nobody builds a second.
One thing that is actually two
1. flow login is a client feature and an AS feature, and only the client half is in the track. Logging in against somebody else's authorization server needs no AS work from us (#558 Q4); serving grants is the AS obligation both #549 and #558 refused. Any future issue titled "login" should say which half it means.
2. HTTP MCP is two shipments. The compliant resource surface with flowstate_test (S7) and remote local execution, which inherits every per-principal posture question #558 lists plus #548's compute gap. Holding the first for the second delays the tractable 80% behind the genuinely hard 20%.
3. PRINCIPAL_KIND_AGENT is a schema field and a protocol. The enum value is cheap and lands in S3 per #557 decision 2 (the field lands before anything populates it); the delegation semantics are S8. Fusing them holds a schema field hostage to a protocol feature.
Load-bearing open decisions
Needs a decision — the answer is a choice, and the cost profile differs by axis:
- D1, the action/scope vocabulary (coupling 1). Answered late: S5 and S7 ship different spellings and one migrates publicly. Answered wrong: scopes real IdPs cannot mint per-resource, so the 403 step-up path degrades to "re-auth, same token" — the cost #558 already names.
- D2, the
on_behalf_ofnaming (coupling 2). Answered late: the proto field ships andbuf breakingmakes the rename a public break forever. Answered wrong: #565-class silent rule misfires on the delegation edge, including deny rules that stop matching — the worst failure this track knows about. - Where
kindcomes from for a federated principal. A GitHub Actions token and an Okta token do not say whether their subject is a human, a workload, or an agent; the trust-policy entry must say it, the wayroleandnamespacealready come from policy rather than assertion. Answered wrong — inferred from token shape — a workload passes anidentity.kind == "human"rule. Answered late is safe by construction: rules that never mentionkindare unaffected, and a rule that reads absent evidence denies. - The bounds on the new per-caller surfaces (#558's list: concurrency, aggregate in-flight work, per-principal result caps). The house rule decides the shape — bound the resource the peer controls — but which bounds exist is a decision, and late means an unbounded aggregate discovered in production.
Needs discovery — the answer exists in the world and should be measured, not chosen:
- What scopes real IdPs can actually mint per-resource (Okta, Auth0, Entra). Determines how fine
scopes_supportedcan usefully be; measure before D1 finalizes granularity. - The HCP Terraform claim shape. #559 flags it as documented-not-verified; pin it with a
ci_federation_test.go-shaped test before S2'sterraform-cloudsource depends on it. - Whether no-IdP deployments are common. This is the trigger the AS/device-endpoint and local-issuer decisions already hang on; it is answered by deployments existing, not by us deciding now.
Invariants, held by every slice
Stated once so each slice's review can point here. Fail closed: absent evidence denies, an unknown credential source is a startup error, no protected-resource config means no HTTP MCP surface rather than an open one. Namespace, kind, and on_behalf_of never come from the request: verified claims through policy-owned mapping, or the trust entry — never assertable by the caller. No credential in history, logs, or tool results: tokens, device codes, PKCE verifiers, DPoP proofs, challenges — with the containment shapes tested and every new secret-bearing type added to leak_test.go's enumeration, which #562 showed goes stale silently. Both drivers agree on anything identity-derived a workflow can observe. Bound every parser below the library, and bound the resource the attacker controls — #561's lesson that a byte bound does not bound the CPU the bytes legally cost applies to AS metadata, device polling, and every fetch S6–S9 add. Test that A cannot reach B: every slice ships its negative direction — wrong-audience refused, cross-tenant refused, the agent refused what its human could not do, a session pinned to the principal that opened it, a replay refused.
Not in this track
Each of these is a decision already recorded, listed so scope creep has to argue with a link rather than with silence: becoming an authorization server (#549/#558 — revisit trigger is discovery item 7, and the device endpoint is the one AS piece worth revisiting); the local account store and WebAuthn (#557 decision 3 — own design pass first); non-federated service accounts before everything else has landed, or ever as a default (#559 decision 3); compute/storage enforcement (the sandboxing arc — it shares #548's vocabulary and nothing else here); the credential-injecting proxy (#549 decision 4 — its own issue off #548's enforcement seam); CIMD/DCR client registration (#558 Q7 — neither, until AS work exists); dedicated Azure, Clerk, or WorkOS integrations (#559 Q5, #557 Q5 — recipes and claim mapping first).
Questions
- D1 now? Recommended yes: settle the single action list in a short design pass on this issue before S5 or S7 write a spelling down, since it is the cheapest of the three decisions to take early and the most public to take late.
- D2 resolution? Recommended:
on_behalf_ofkeeps its name for the delegation edge on the caller's principal, and S0's landing is audited for whether the minting view'son_behalf_ofis now definitionally redundant withidentity— deprecating it towardidentity.subjectif so, renaming the minting edge if not. Either way, decided before S3. kindprovenance? Recommended: set by theTrustedIssuerentry likeroleandnamespace, defaulting toPRINCIPAL_KIND_WORKLOADfor existing entries, never inferred from token shape.- Tracking shape? Recommended: one sub-issue per slice S1–S9 hanging off this issue, so the graph above stays the single ordering and the five design issues stay what they are — analysis, not queues.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with #565 and then read the linked issues #548, #549, #557, #558, and #559, checking the current entry points in cmd/flow/routing.go, cmd/flow/mcp.go, and pkg/flowstate/v1/auth/jwks.go. Review open decisions D1 and D2 and the sequencing graph; done means the identity-track boundaries, dependencies, and naming decisions are resolved and recorded rather than implemented as separate guesses.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication, authorization, backend-api-design, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100