Skip to content

Commit 655b321

Browse files
bgagentclaude
andcommitted
feat(linear): resolve API token via AgentCore Identity (Phase 2.0a)
Migrates the agent runtime's Linear personal API token resolution from AWS Secrets Manager to AWS Bedrock AgentCore Identity. This is the "validate Identity SDK" step of the v2 plan; Phase 2.0b will swap the API key for OAuth and converge Linear MCP onto AgentCore Gateway in one cutover. Per Alain's guidance: "start by using api key, if it works, switch to oauth. you will setup an outbound auth for your server using agentcore identity. that identity can be (AC identity is like a wrapper around secrets manager) api key or oauth." ## Scope: agent runtime only Lambdas (orchestrator + processor) intentionally keep using Secrets Manager via the existing `LinearApiTokenSecret` for now. The Python `bedrock_agentcore` SDK has no Node.js equivalent — Lambda migration requires `@aws-sdk/client-bedrock-agentcore` raw API calls and folds into 2.0b's bigger refactor. End-state of 2.0a: agent reads from Identity, Lambdas read from Secrets Manager, both pointing at the same underlying token value (admin populates both). ## What changed `agent/src/config.py::resolve_linear_api_token`: - Drops boto3 SecretsManager fetch + `LINEAR_API_TOKEN_SECRET_ARN` env. - Reads new env `LINEAR_API_KEY_PROVIDER_NAME` (provider name in Identity vault). - Calls `IdentityClient.get_api_key()` with the workload access token auto-injected into `BedrockAgentCoreContext` by AgentCore Runtime (verified by reading the SDK's `auth.py` decorator implementation — no manual workload-identity mint needed inside the runtime). - Caches the resolved token in `LINEAR_API_TOKEN` so downstream consumers stay unchanged: `channel_mcp.py`'s `${LINEAR_API_TOKEN}` placeholder in `.mcp.json` and `linear_reactions.py`'s GraphQL Authorization header. Preserves PR #87's nice-to-have improvements: - `ImportError` graceful fallback (now for `bedrock_agentcore` instead of `boto3`) — degrade with WARN, don't crash the agent. - `AccessDeniedException` and `ResourceNotFoundException` logged at ERROR severity (persistent IAM/config bugs that should page). Other ClientErrors stay at WARN (transient throttle/network). `agent/pyproject.toml`: adds `bedrock-agentcore==1.9.1` dep. `cdk/src/stacks/agent.ts`: - On the AgentCore runtime: drops `linearIntegration.apiTokenSecret. grantRead(runtime)` and the `LINEAR_API_TOKEN_SECRET_ARN` env-var override. Adds `LINEAR_API_KEY_PROVIDER_NAME` env (hardcoded `'linear-api-key'` for now; can parametrize later via context if multi-environment naming is needed) and IAM permissions for `bedrock-agentcore:GetResourceApiKey` and `bedrock-agentcore:GetWorkloadAccessToken`. - Lambdas (orchestrator + processor) untouched — they still grant on the Linear secret and read from Secrets Manager. - Resource scope on the new IAM is `*` for now; AgentCore Identity ARN format isn't fully standardized in public docs as of 2026-05-15. Tighten in 2.0b when OAuth migration documents the canonical resource shape. `docs/guides/LINEAR_SETUP_GUIDE.md`: adds Step 4.5 documenting the one-time `agentcore add credential --type api-key --name linear-api-key` admin command users must run alongside the existing `bgagent linear setup` wizard. Notes that Lambdas keep Secrets Manager temporarily and 2.0b will retire the dual-store setup. Starlight mirror synced. ## Tests `agent/tests/test_config.py::TestResolveLinearApiToken` — 10 tests covering: cached env var fast-path; missing provider name; missing region; workload token absent (outside runtime); happy path with env-var side-effect; botocore error swallowed with WARN; SDK returns None defensively; ImportError fallback; AccessDeniedException → ERROR severity; ResourceNotFoundException → ERROR severity. 542 agent / 1271 cdk / 196 cli, all green. Lint + typecheck clean. CDK synth clean. ## Migration notes for reviewer `bedrock_agentcore` SDK confirmed working in our runtime image (verified in `node_modules` post-install). The `BedrockAgentCoreContext` workload token auto-injection is documented behaviour for code running inside AgentCore Runtime — verified by reading the SDK's `@requires_api_key` decorator implementation, which uses the same context lookup we use here. Stacked on PR #87 (`feat/linear-processor-feedback`). Will conflict on `config.py` and `test_config.py` if #87 needs further rework before merge — happy to rebase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 420fc11 commit 655b321

7 files changed

Lines changed: 1041 additions & 706 deletions

File tree

agent/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description = "Background coding agent — runs tasks in isolated cloud environm
55
requires-python = ">=3.13"
66
dependencies = [
77
"boto3==1.43.6", #https://pypi.org/project/boto3/
8+
"bedrock-agentcore==1.9.1", #https://pypi.org/project/bedrock-agentcore/
89
"claude-agent-sdk==0.1.81", #https://github.com/anthropics/claude-agent-sdk-python
910
"requests==2.34.0", #https://pypi.org/project/requests/
1011
"fastapi==0.136.1", #https://pypi.org/project/fastapi/

agent/src/config.py

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,54 +39,101 @@ def resolve_github_token() -> str:
3939

4040

4141
def resolve_linear_api_token() -> str:
42-
"""Resolve the Linear personal API token from Secrets Manager or env.
42+
"""Resolve the Linear personal API token via AgentCore Identity.
4343
44-
Mirrors ``resolve_github_token``: in deployed mode
45-
``LINEAR_API_TOKEN_SECRET_ARN`` is set and the token is fetched once
46-
and cached in ``LINEAR_API_TOKEN``. For local development, falls back
47-
to ``LINEAR_API_TOKEN`` directly.
44+
In deployed mode, ``LINEAR_API_KEY_PROVIDER_NAME`` names a credential
45+
provider in AgentCore Identity (the token vault). The agent runtime
46+
auto-injects a workload access token into ``BedrockAgentCoreContext``;
47+
we exchange that for the API key value and cache it in
48+
``LINEAR_API_TOKEN`` so downstream consumers (the Linear MCP's
49+
``${LINEAR_API_TOKEN}`` placeholder in ``.mcp.json`` and
50+
``linear_reactions.py``'s GraphQL Authorization header) keep working
51+
unchanged.
4852
49-
Returns an empty string if the secret is absent or empty — the agent-side
53+
For local development, falls back to a pre-set ``LINEAR_API_TOKEN``
54+
env var so the agent can run outside AgentCore Runtime.
55+
56+
Returns an empty string if the credential is absent — the agent-side
5057
MCP config then renders with an unresolved ``${LINEAR_API_TOKEN}`` env
5158
placeholder, and the Linear MCP will reject the request (fail-closed).
5259
This function is only called when ``channel_source == 'linear'``.
60+
61+
Phase 2.0a: replaces the prior Secrets Manager path. Phase 2.0b will
62+
swap this function entirely for the ``@requires_access_token`` OAuth
63+
decorator pattern; this imperative shape exists because API keys
64+
don't need refresh and the MCP config expects a static token.
5365
"""
5466
cached = os.environ.get("LINEAR_API_TOKEN", "")
5567
if cached:
5668
return cached
57-
secret_arn = os.environ.get("LINEAR_API_TOKEN_SECRET_ARN")
58-
if not secret_arn:
69+
70+
provider_name = os.environ.get("LINEAR_API_KEY_PROVIDER_NAME")
71+
if not provider_name:
5972
return ""
73+
74+
region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION")
75+
if not region:
76+
log("WARN", "resolve_linear_api_token: AWS_REGION not set; cannot resolve API key")
77+
return ""
78+
6079
try:
61-
import boto3
80+
import asyncio
81+
82+
from bedrock_agentcore.runtime import BedrockAgentCoreContext
83+
from bedrock_agentcore.services.identity import IdentityClient
6284
from botocore.exceptions import BotoCoreError, ClientError
6385
except ImportError as e:
64-
# boto3 missing from the container image — degrade gracefully rather
65-
# than hard-crashing the agent. The Linear MCP will fail on first
66-
# call with a clear auth error.
67-
log("WARN", f"resolve_linear_api_token: boto3 unavailable ({e}); skipping")
86+
# bedrock_agentcore SDK missing from the container image — degrade
87+
# gracefully rather than hard-crashing the agent. The Linear MCP
88+
# will fail on first call with a clear auth error.
89+
log("WARN", f"resolve_linear_api_token: bedrock_agentcore unavailable ({e}); skipping")
6890
return ""
6991

7092
try:
71-
region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION")
72-
client = boto3.client("secretsmanager", region_name=region)
73-
resp = client.get_secret_value(SecretId=secret_arn)
74-
token = resp.get("SecretString", "") or ""
93+
workload_token = BedrockAgentCoreContext.get_workload_access_token()
94+
if workload_token is None:
95+
# Outside the AgentCore Runtime container (e.g. local dev). The
96+
# SDK's `_set_up_local_auth` fallback writes `.agentcore.json`
97+
# which doesn't fit our flow; bail out and let the caller see
98+
# an empty token so the MCP config fails closed.
99+
log(
100+
"WARN",
101+
"resolve_linear_api_token: workload access token not in context "
102+
"(agent must run inside AgentCore Runtime, or set LINEAR_API_TOKEN "
103+
"directly for local dev)",
104+
)
105+
return ""
106+
107+
client = IdentityClient(region=region)
108+
token = (
109+
asyncio.run(
110+
client.get_api_key(
111+
provider_name=provider_name,
112+
agent_identity_token=workload_token,
113+
)
114+
)
115+
or ""
116+
)
75117
if token:
76118
os.environ["LINEAR_API_TOKEN"] = token
77119
return token
78120
except ClientError as e:
79-
# Narrowed from a broader `except` per #63 review — broader catches
80-
# hid genuine bugs in the Secrets Manager call shape. AccessDenied
81-
# is logged at ERROR because it's a persistent IAM misconfig that
82-
# should page someone, not a transient blip.
121+
# Narrowed from a broader `except` per #63 review. AccessDenied is
122+
# logged at ERROR because it's a persistent IAM misconfig (likely
123+
# the runtime role missing bedrock-agentcore:GetResourceApiKey or
124+
# GetWorkloadAccessToken) that should page someone, not a transient
125+
# blip. ResourceNotFound (provider name unknown) is also persistent
126+
# — same severity. Other ClientErrors are likely transient (throttle,
127+
# network blip) and stay at WARN.
83128
code = e.response.get("Error", {}).get("Code", "")
84-
severity = "ERROR" if code == "AccessDeniedException" else "WARN"
129+
severity = (
130+
"ERROR" if code in ("AccessDeniedException", "ResourceNotFoundException") else "WARN"
131+
)
85132
log(severity, f"resolve_linear_api_token failed: {type(e).__name__}: {e}")
86133
return ""
87134
except BotoCoreError as e:
88-
# Never let a Secrets Manager outage crash the agent. The Linear MCP
89-
# will simply fail on first call with a clear auth error.
135+
# Never let an Identity outage crash the agent. The Linear MCP will
136+
# fail on first call with a clear auth error.
90137
log("WARN", f"resolve_linear_api_token failed: {type(e).__name__}: {e}")
91138
return ""
92139

0 commit comments

Comments
 (0)