You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(adr-016): remove Linear MCP; Linear is 100% deterministic
Live validation (2026-07-21) proved Linear MCP via Gateway is non-functional on
one OAuth app: a fresh actor=user Gateway target reached READY but all data
reads errored ("An internal error occurred"), and actor=app can't consent on an
already-installed app ("already installed" dead-end). With a second Linear app
rejected (one @bgagent identity), there is no viable Linear-MCP-via-Gateway
path. Decision: retire Linear MCP entirely; Linear runs 100% deterministically
on the one @bgagent ChannelCredential.
- Keep the Gateway as the GENERAL MCP control plane (§4) for other registered
servers; this is Linear-specific.
- The 9 mcp__linear-server__* tools collapse to deterministic homes: writes
already in Lambda (linear-feedback.ts); reads pre-hydrated (aws-samples#176
context-hydration.ts); attachments fetched authenticated (aws-samples#176
resolve-url-attachments.ts + @bgagent bearer for uploads.linear.app, which
aws-samples#176 skips today only because its resolver is unauthenticated).
- Phasing: P4 now "remove Linear MCP + make deterministic" (incl. rewriting the
prompt_builder.py system-prompt guidance); the general MCP control plane +
task-user identity propagation shift to P5/P6.
- Updates McpCredential (no longer Linear-per-user example), the Consequences
bullets, and the header revision note accordingly.
Copy file name to clipboardExpand all lines: docs/decisions/ADR-016-pluggable-identity-and-auth.md
+24-14Lines changed: 24 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,7 +5,7 @@
5
5
**Status:** proposed
6
6
**Date:** 2026-06-11 (revised 2026-07-21)
7
7
8
-
> **2026-07-21 revision.** The outbound design was restructured to separate **execution semantics** (deterministic REST/GraphQL from Lambda) from **credential transport** (AgentCore Identity vault), and to make **MCP a first-class control plane** (registration + Gateway execution, fail-closed, no direct fallback) rather than a per-vendor transport toggle. Introduces three credential types (`ChannelCredential`, `McpCredential`, `McpRegistration`). This **rejects** the earlier two-flag (`lifecycleViaGateway`/`gatewayOAuthOk`) "derived transport" framing and the Linear Hybrid/two-app framing. See the corrections noted inline. Rationale grounding: Engram observation #140 (not re-fetchable in the authoring session; the reviewing session should cross-check this revision against it).
8
+
> **2026-07-21 revision.** The outbound design was restructured to separate **execution semantics** (deterministic REST/GraphQL from Lambda) from **credential transport** (AgentCore Identity vault), and to make **MCP a first-class control plane** (registration + Gateway execution, fail-closed, no direct fallback) rather than a per-vendor transport toggle. Introduces three credential types (`ChannelCredential`, `McpCredential`, `McpRegistration`). This **rejects** the earlier two-flag (`lifecycleViaGateway`/`gatewayOAuthOk`) "derived transport" framing and the Linear Hybrid/two-app framing. **Further (later 2026-07-21, after live validation):** Linear MCP via Gateway was proven non-functional on one OAuth app (`actor=user` reads error; `actor=app` can't consent on an installed app), so **Linear MCP is removed entirely** — Linear becomes 100% deterministic on the one `@bgagent` `ChannelCredential`, and the Gateway is kept as the general MCP control plane for *other* registered servers only. See *Linear is fully deterministic* below. Rationale grounding: Engram observation #140 (not re-fetchable in the authoring session; the reviewing session should cross-check this revision against it).
9
9
10
10
## Context
11
11
@@ -91,23 +91,32 @@ The abstraction is intentionally a contract, not a forklift of credential handli
91
91
Three distinct types, deliberately not collapsed into one "token":
92
92
93
93
-**`ChannelCredential`** — a **platform/workspace** credential for the **deterministic** vendor APIs (§2, §3). Subject = `linear-workspace:<workspace_id>` (workspace-scoped, not per-user). Backed by AgentCore Identity under the `abca-<surface>-channel` shared workload identity. This is what the Lambda tier + the agent's `linear_reactions.py` use for direct GraphQL. For Linear it wraps ABCA's existing `actor=app` OAuth application.
94
-
-**`McpCredential`** — a **user/workspace** grant for **Gateway tools** (§4). This is the credential the Gateway presents outbound to a registered MCP server. Preferred subject is per-user (`actor=user` for Linear) so tool-use is attributable to the triggering user; workspace-scoped is a fallback where per-user is not meaningful.
94
+
-**`McpCredential`** — a **user/workspace** grant for **Gateway tools** (§4). This is the credential the Gateway presents outbound to a registered MCP server. Preferred subject is per-user so tool-use is attributable to the triggering user; workspace-scoped is a fallback where per-user is not meaningful. (Not used for Linear — Linear has no MCP leg; see *Linear is fully deterministic* below.)
95
95
-**`McpRegistration`** — the registration record for a Gateway-fronted MCP: `endpoint`, exposed `tools`, `scopes`, the credential `subject` (which `McpCredential` binds), and the `agent`/`workflow` bindings that say which agents may use which tools.
96
96
97
97
`ChannelCredential` and `McpCredential` are **separate credentials even for the same vendor** — different subjects (workspace vs user), different consumers (Lambda GraphQL vs Gateway), different lifecycles. They are not two views of one token.
98
98
99
-
## Linear MCP is optional, under the same OAuth app
99
+
## Linear is fully deterministic — no Linear MCP (decided 2026-07-21)
100
100
101
-
Linear MCP tool-use (distinct from the deterministic `ChannelCredential`, which is settled) is an **optional** capability with a specific, still-unvalidated composition:
101
+
The Gateway remains ABCA's general MCP control plane (§4) for any MCP server a user/workspace registers. **Linear specifically is removed from the MCP path entirely** and done **100% deterministically** on the one `@bgagent``ChannelCredential`.
102
102
103
-
-**Keep the existing `actor=app` credential for Lambda** (the `ChannelCredential`) untouched.
104
-
- For the Linear **MCP** leg, prefer a **per-user `actor=user` Gateway grant under the *same* OAuth application** — a second grant/subject on the one app, obtained via the Gateway's authorization-code flow, vaulted as the `McpCredential`.
105
-
-**This exact Gateway composition still needs a clean live validation** (an `actor=user` Gateway target on the same app, reaching `READY` and serving `tools/list`/`tools/call` for a real user). It is **not** yet proven end-to-end.
106
-
-**If that validation fails, Linear MCP is simply unavailable** — it is **not** routed direct (no direct-MCP fallback, §4) and **not** given a second Linear OAuth app. Deterministic Linear (comments/reactions/status via the `ChannelCredential`) is unaffected either way, since it never depended on MCP.
103
+
**Why (live-validated 2026-07-21):** Linear MCP through the Gateway does not work on one OAuth app. A fresh `actor=user` Gateway target reached `READY` but **every data read failed** ("An internal error occurred" — `get_issue`/`list_issues`/`list_documents`, retried); an `actor=app` Gateway target **cannot even consent** (Linear's "already installed" dead-ends the authorization-code flow for an installed app). With a second Linear app rejected (identity fragmentation — the agent must present one Linear face, `@bgagent`), there is no viable Linear-MCP-via-Gateway path. Rather than carry an optional-but-broken leg, **retire Linear MCP.**
104
+
105
+
**What replaces the 9 `mcp__linear-server__*` tools** — nothing is lost; each collapses to a deterministic home:
106
+
107
+
| Removed MCP tool | Deterministic replacement |
108
+
|---|---|
109
+
|`save_comment`, `save_issue`, `list_issue_statuses`| Already deterministic in the Lambda tier (`linear-feedback.ts`: comment create/update, `SetIssueState`, `IssueTeamStates`). The agent's MCP writes were redundant best-effort — dropped; Lambda owns writes. |
110
+
|`get_issue`, `list_comments`, `list_documents`| Pre-hydrated at task-creation (extend #176`context-hydration.ts` with Linear issue text/comments; Lambda already fetches `IssueText`/`IssueContext`). Agent receives context in-payload, no live MCP. |
111
+
|`get_attachment`, `extract_images`, `get_document`| Authenticated fetch on the `ChannelCredential` path — extend #176`resolve-url-attachments.ts` to attach the `@bgagent` bearer for `uploads.linear.app` URLs (which #176 skips today precisely because its resolver is unauthenticated). One app, one identity, screened like every other attachment. |
112
+
113
+
**Consequences of removing Linear MCP:** remove `_build_linear_entry`/`_linear_server_entry` + the `"linear"` entry in `CHANNEL_MCP_BUILDERS` (`agent/src/channel_mcp.py`); strip the `mcp__linear-server__*` guidance from `prompt_builder.py`; the agent no longer needs any Linear token for MCP (the per-thread `LINEAR_API_TOKEN` for MCP retires — `linear_reactions.py`'s direct GraphQL keeps its own `ChannelCredential`). The Gateway substrate (`bgagent-linear-gw-*`, the M2M inbound, `gateway_auth.py`) is **not** wired for Linear; it stays available for the general MCP control plane.
107
114
108
115
**Rejected (corrections to earlier drafts):**
109
-
-**No two Linear OAuth apps by default.** One app; the MCP grant is a different *actor/subject* on that app, not a second app.
110
-
-**No claim that two `actor=app` authorization-code prompts can coexist.** Linear's docs are explicit that non-`client_credentials` app tokens cannot exist in parallel — an `actor=app` authorization-code token is not held twice. The deterministic path keeps the single `actor=app` token; the MCP path uses `actor=user`, a *different* actor, which is why it does not collide (and why, if `actor=user` MCP proves unworkable, there is no fallback that adds a second app-actor token).
116
+
-**No Linear MCP at all** (supersedes the earlier "optional Linear MCP via `actor=user` Gateway grant" — that path is live-proven non-functional on one app).
117
+
-**No two Linear OAuth apps.** One app, `@bgagent`, deterministic.
118
+
-**No claim that two `actor=app` authorization-code prompts can coexist** — Linear's docs are explicit that non-`client_credentials` app tokens cannot exist in parallel; moot now that Linear has no MCP leg needing a second grant.
119
+
- The **general MCP Gateway control plane (§4) stands** — this decision is Linear-specific, not a retreat from Gateway-fronted MCP for other registered servers.
111
120
112
121
## Identity propagation for per-user MCP is UNRESOLVED
113
122
@@ -129,7 +138,7 @@ Per-user `McpCredential` selection requires the Gateway to know *which task-user
129
138
- (−) **A new abstraction plus an adapter registry to maintain.** Two seams and their adapter sets are added platform surface; mitigated by keeping the inbound descriptor a parameterization of a single verification path (see the risk below) and the outbound contract a thin selector over the existing flows.
130
139
- (−) **AgentCore Identity adds a managed dependency and token-vault cost.** Vault fetches (`GetResourceOauth2Token` / `GetResourceApiKey`) bill at `$0.010/1,000`. The ECS in-process resolver path remains available where that dependency is unwanted.
131
140
- (!) **Per-user MCP is not yet achievable — task-user identity propagation is unresolved.** The Gateway inbound is an M2M JWT that identifies the workload, not the user, so it cannot select a per-user `McpCredential`. This must be designed + validated (P5) before per-user MCP is claimed; until then MCP credentials are workspace-scoped at best.
132
-
- (!) **Optional Linear MCP is unproven and fail-closed.** The `actor=user`-on-the-same-app Gateway grant still needs a clean live validation (P4). If it fails, Linear MCP is unavailable — there is no direct-MCP fallback and no second Linear app. Deterministic Linear is unaffected.
141
+
- (−) **Linear loses MCP tool-use — replaced by deterministic paths.** The 9 `mcp__linear-server__*` tools retire: writes were already deterministic in Lambda; reads become pre-hydrated context (#176`context-hydration.ts`) + authenticated attachment fetch (#176`resolve-url-attachments.ts` + `@bgagent` bearer). Cost: the agent can no longer make *arbitrary* live Linear queries mid-task — it works from pre-hydrated context. Accepted: live-proven that Linear MCP via Gateway doesn't work on one app, and a second app is rejected. The general MCP Gateway control plane (§4) is unaffected — this is Linear-specific.
133
142
- (!) **The inbound seam must not become a second auth code path.** A descriptor that grew its own verification logic would drift from the shipped Cognito/HMAC path and create two implementations to keep in sync. That is the exact cedar-parity drift hazard ADR-014 calls out. Mitigation: exactly **one** inbound verification implementation; the descriptor only parameterizes it (discovery URL, audience, client list, claim gates), never reimplements it.
134
143
135
144
## Phasing
@@ -140,9 +149,10 @@ Per-user `McpCredential` selection requires the Gateway to know *which task-user
140
149
| P1 |**`ChannelCredential` for Linear (deterministic path first).** Move the Lambda tier + agent `linear_reactions.py` off Secrets Manager onto the vault: one `abca-linear-channel` workload identity, credential keyed `linear-workspace:<id>`, the 6 Lambda roles + runtime role granted `GetResourceOauth2Token` on that domain. Deterministic ops stay direct GraphQL — only the token source changes. | Flag-gated; SM fallback retained until green; per-workspace isolation preserved. |
141
150
| P2 |**Retire SM refresh/write-back** once P1 is green: drop `PutSecretValue` from the 5 Lambda roles and delete `tryRefreshOnce` write-back in `linear-oauth-resolver.ts`; the vault owns refresh. | No SM writes remain on the Linear path. |
142
151
| P3 |**MCP control plane (registration + Gateway execution).** Build `McpRegistration`/`McpCredential`: users/workspaces register MCP servers + bind tools to agents/workflows; every registered MCP runs through the Gateway; unsupported auth fails closed; no direct-MCP fallback. | Registration API + Gateway execution; fail-closed verified. |
143
-
| P4 |**Validate optional Linear MCP** — an `actor=user` Gateway grant on the *same* Linear OAuth app, reaching `READY` + serving `tools/list`/`tools/call` for a real user. |**GATE: clean live validation.** If it fails → Linear MCP unavailable (no direct route, no 2nd app). |
144
-
| P5 |**Trusted task-user identity propagation** (prerequisite for per-user MCP): specify + validate a user-scoped inbound identity the Gateway authorizer trusts, replacing the M2M JWT for per-user credential selection. |**Blocks per-user `McpCredential`.** Until done, MCP credentials are workspace-scoped at best. |
145
-
| P6 | Jira + Slack `ChannelCredential` (same shape as P1); GitHub `GithubOauth2` behind a flag, retire the shared PAT; OBO `act`-claim delegation feeding #237. | Flag-gated; per-surface. |
152
+
| P4 |**Remove Linear MCP; make Linear fully deterministic.** Delete the `"linear"` MCP builder in `channel_mcp.py` (`_build_linear_entry`/`_linear_server_entry`); rewrite `prompt_builder.py` to drop all `mcp__linear-server__*` guidance and instead consume pre-hydrated context + rely on Lambda for writes. Pre-hydrate Linear issue text/comments in #176`context-hydration.ts`; fetch `uploads.linear.app` attachments authenticated via `resolve-url-attachments.ts` + the `@bgagent``ChannelCredential` bearer. Retire the per-thread `LINEAR_API_TOKEN` MCP env. | No `linear-server` MCP entry written; agent runs from hydrated context; attachments fetched authenticated; tests updated. |
153
+
| P5 |**General MCP control plane (registration + Gateway execution)** — NOT Linear. Build `McpRegistration`/`McpCredential`: users/workspaces register arbitrary MCP servers + bind tools to agents/workflows; every registered MCP runs through the Gateway; unsupported auth fails closed; no direct-MCP fallback. | Registration API + Gateway execution; fail-closed verified. |
154
+
| P6 |**Trusted task-user identity propagation** (prerequisite for per-user MCP on the general plane, P5): specify + validate a user-scoped inbound identity the Gateway authorizer trusts, replacing the M2M JWT for per-user credential selection. |**Blocks per-user `McpCredential`.** Until done, MCP credentials are workspace-scoped at best. |
155
+
| P7 | Jira + Slack `ChannelCredential` (same shape as P1); GitHub `GithubOauth2` behind a flag, retire the shared PAT; OBO `act`-claim delegation feeding #237. | Flag-gated; per-surface. |
146
156
147
157
**Substrate independence (verified 2026-07-21, both proven live):** the vault path works on any compute. AgentCore Runtime injects the Workload Access Token as the `WorkloadAccessToken` header; ECS/Fargate/Lambda bootstrap it via `GetWorkloadAccessTokenForJWT(workloadName, userToken=<Cognito M2M JWT>)` against a **standalone** (non-service-linked) workload identity, then call `GetResourceOauth2Token`. Runtime-managed (service-linked) workload identities cannot self-vend, so the ECS path needs a manually-created workload identity. The runtime execution role today has `GetWorkloadAccessToken*` but **not**`GetResourceOauth2Token` — P1 adds it (+ `GetSecretValue` on `bedrock-agentcore-identity!*`), mirroring the gateway service role.
0 commit comments