Skip to content

Commit 66ec509

Browse files
committed
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.
1 parent 836da53 commit 66ec509

1 file changed

Lines changed: 24 additions & 14 deletions

File tree

docs/decisions/ADR-016-pluggable-identity-and-auth.md

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
**Status:** proposed
66
**Date:** 2026-06-11 (revised 2026-07-21)
77

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).
99

1010
## Context
1111

@@ -91,23 +91,32 @@ The abstraction is intentionally a contract, not a forklift of credential handli
9191
Three distinct types, deliberately not collapsed into one "token":
9292

9393
- **`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.)
9595
- **`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.
9696

9797
`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.
9898

99-
## Linear MCP is optional, under the same OAuth app
99+
## Linear is fully deterministic — no Linear MCP (decided 2026-07-21)
100100

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`.
102102

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.
107114

108115
**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.
111120

112121
## Identity propagation for per-user MCP is UNRESOLVED
113122

@@ -129,7 +138,7 @@ Per-user `McpCredential` selection requires the Gateway to know *which task-user
129138
- (−) **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.
130139
- (−) **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.
131140
- (!) **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.
133142
- (!) **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.
134143

135144
## Phasing
@@ -140,9 +149,10 @@ Per-user `McpCredential` selection requires the Gateway to know *which task-user
140149
| 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. |
141150
| 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. |
142151
| 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. |
146156

147157
**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.
148158

0 commit comments

Comments
 (0)