aws-samples / aws-samples/sample-autonomous-cloud-coding-agents
RFC: Identity propagation — composable credential delegation via AgentCore Identity Token Vault
- Ngôn ngữ chính
- TypeScript
- Star
- 143
- Fork
- 46
- Merge trung bình
- 3 ngày 10 giờ
- Pull request đã merge (30 ngày)
- 24
Mô tả
# RFC #249: Identity propagation, composable credential delegation via AgentCore Identity Token Vault
Status: Draft for discussion. Drafted by Bonk, reviewed and filed by Laith Al-Saadoon.
Relates to: #245 (attribution data plane), #209 (SessionRole isolation), #215 (Bedrock cost attribution), #237 (governance planes), ADR-016 (pluggable identity/auth), PR #315.
Grounding: AgentCore Identity/Runtime/Gateway deep-dive v1.2 (2026-05-13); AWS API + boto3 references re-verified 2026-06-04; Phase 0 spike executed live 2026-06-14 (`us-east-1`).
## TL;DR
- **What:** Give ABCA's outbound calls (GitHub, Linear, and future targets) an enforceable per-task, per-user, short-lived credential issued by the AgentCore Identity Token Vault, replacing the shared Secrets-Manager PAT. Attribution becomes cryptographic instead of advisory.
- **Design stance:** Identity is **composable**, because ABCA is a reference architecture adopters deploy into their own environments, each with their own IdP and ops/IT stack. Two independent, swappable seams: a pluggable **IdP-inbound** seam (Cognito, Okta, Entra, PingOne, Auth0, raw OIDC) and a pluggable **target-outbound** seam (GitHub, GitLab, Jira, Linear, Slack, internal services). Cognito → GitHub is the first worked example, not the architecture.
- **The blocker is cleared.** Phase 2.0a parked on a `USER_FEDERATION` PAR/`request_uri` bug. A live Phase 0 spike on 2026-06-14 (`us-east-1`) shows that bug **does not reproduce** in the current service build: AgentCore keeps PAR on its own front-channel and hands GitHub plain authorization-code + PKCE. Verdict: **GO-LIKELY**.
- **Ask:** Approve proceeding to Phase 1. Stand up a vault provider behind a flag, keep the PAT fallback, bind on the inbound JWT's `(iss, sub)`. The one remaining unproven step is a human OAuth-consent click (can't run headless); the flow is proven end-to-end up to GitHub's login wall.
- **Decisions needed (Open Questions):** per-target scoping model (OAuth App vs GitHub App, generalized per target); consent UX for webhook-triggered tasks with no live user.
---
## 1. Problem
ABCA propagates *who* as data but not as a credential. The identity story has two halves.
**#245 owns the data half.** It propagates `{task_id, user_id, repo, trace_id}` through orchestrator logs, agent OTel baggage, and `TaskEvents` so any action is traceable. In flight.
**This RFC owns the credential half.** Outbound calls to GitHub, Linear, and future Jira/Slack/internal targets should carry an enforceable per-task, per-user, short-lived identity, so attribution becomes cryptographic rather than advisory. This is the delegation chain #245 lists as out-of-scope follow-up, and it covers ROADMAP's "Per-repo GitHub credentials" and "Layered credential derivation" items.
Current credential posture is the gap:
| Surface | Today | Risk |
|---|---|---|
| GitHub | Shared PAT in Secrets Manager (`agent/src/config.py:resolve_github_token`). | One token for all repos. No per-user or per-repo scoping. Tracked in SECURITY.md. |
| Linear | Per-workspace OAuth token in Secrets Manager, manual refresh (`linear-oauth-resolver.ts`). | Workspace-scoped, not user-scoped. No `act`-claim audit chain. |
| AgentCore Identity | Parked at Phase 2.0a; fell back to direct Secrets Manager. | Outbound identity collapses to a shared service credential. |
## 2. What is already wired (dormant, not missing)
The propagation path exists end-to-end on the AgentCore Runtime substrate today. Three pieces are in place:
- Orchestrator sets `runtimeUserId` on `InvokeAgentRuntimeCommand` (`agentcore-strategy.ts:56`).
- Orchestrator role holds `bedrock-agentcore:InvokeAgentRuntimeForUser` (`task-orchestrator.ts:288-289`).
- Agent reads the `WorkloadAccessToken` header and re-injects it across the pipeline thread; the per-thread ContextVar workaround is already solved (`server.py:283-310, 387-413`).
`bedrock-agentcore==1.9.1` stays pinned in `agent/pyproject.toml` so the path imports cleanly on resume. This RFC resumes and completes that path; it does not build from scratch.
## 3. Design
### 3.1 Composability is the design (load-bearing)
ABCA is a **reference architecture adopters deploy into their own environments**. The team does not operate one central instance; each adopter brings their own IdP and their own ops/IT stack. The identity plane therefore composes across **multiple IdPs on ingress and multiple integration targets on egress**, treating any single vendor pair as one worked example. Cognito → GitHub is that example; the design hard-wires neither.
Two independent, swappable seams:
- **Inbound (IdP) seam.** Pluggable across Cognito, Okta, Entra ID, PingOne, Auth0, or any raw OIDC provider the adopter runs. ABCA binds on the inbound JWT's `(iss, sub)`, which makes the binding key **IdP-agnostic by construction**: `iss` namespaces the subject, so two adopters on different IdPs never collide and the design privileges none of them.
- **Outbound (target) seam.** Pluggable across GitHub, GitLab, Jira, Linear, Slack, or an internal service. The Token Vault is the substrate that makes this composable: its `credentialProviderVendor` enum carries 25 built-in vendors (`GithubOauth2`, `SlackOauth2`, `AtlassianOauth2`, `MicrosoftOauth2`, `OktaOauth2`, and more) plus a `CustomOauth2` escape hatch for any target without a built-in vendor, including an adopter's internal OIDC/OAuth service.
Implementation keeps an **OIDC-inbound descriptor** and an **OAuth2-outbound resolver** as separate, registry-driven seams (the ADR-016 / PR #315 shape), so an adopter swaps their IdP or adds a target by configuration instead of forking the credential plane. `resolve__token()` stays one vendor-neutral seam.
### 3.2 Shared binding key
AgentCore Identity binds tokens to `(workload_identity, user_id)`. The `user_id` value must match the one #245 standardizes: the **IdP-namespaced** form derived from the inbound JWT's `(iss, sub)`, for example `cognito+`, `okta+`, or `entra+`. That form avoids `sub` collisions across IdPs and keeps the key identical no matter which IdP an adopter deploys. One identity, two planes: #245 stamps it in logs and spans, this RFC stamps it in tokens. Bind on the verified JWT (`get_workload_access_token_for_jwt`), not the opaque-userId path, so the namespace comes from the authenticated `iss` rather than a caller-supplied string.
### 3.3 Flow selection
The deep-dive's decision tree maps each integration to a flow:
| Integration | Inbound | Downstream | Flow |
|---|---|---|---|
| Write to a target as the user (commit/PR, issue update) | any IdP | different ecosystem (github.com, gitlab.com, linear.app) | `USER_FEDERATION` 3LO |
| Headless or webhook with no user | none | n/a | `M2M` client-credentials under a manual workload identity |
| Future AWS-native downstream | any IdP | same ecosystem | OBO (`ON_BEHALF_OF_TOKEN_EXCHANGE`) with `act`-claim delegation |
A webhook authenticates the tenant, not the user; that is the honest caveat. The real consent moment is when the agent calls back out. Read-only triage can use an M2M service account; write-back gates on fresh per-user consent.
### 3.4 Token Vault providers
Vault-managed providers replace direct Secrets Manager reads. A `GithubOauth2` provider issues short-lived tokens with auto-refresh, retiring the shared PAT. `AtlassianOauth2`, `SlackOauth2`, and others follow the same pattern as those integrations land; anything without a built-in vendor uses `CustomOauth2`. The agent calls `GetResourceOauth2Token`; on a first 3LO call with no cached token it surfaces the returned authorization URL and gates write-back until consent.
### 3.5 Delegation chain
Where the downstream can consume it, use OBO delegation with `actorTokenContent=M2M`. The issued token then carries an `act` claim recording `user → orchestrator → agent`, the signed actor chain in ROADMAP's "Delegation chain propagation" item. It gives per-action accountability and feeds #237's `abca.audit.v1` `correlation` block.
### 3.6 Substrate-independence (raised by @krokoko on the original thread)
The Token Vault operations are plain boto3 (`create_oauth2_credential_provider` on `bedrock-agentcore-control`; `get_workload_access_token_for_*` and `get_resource_oauth2_token` on `bedrock-agentcore`) with no dependency on AgentCore Runtime. The one substrate-specific seam is how the agent obtains the workload identity token before the vault call:
- **AgentCore Runtime.** Setting `runtimeUserId` makes the platform inject the `WorkloadAccessToken` request header (`agentcore-strategy.ts:47-59`); the agent reads the header.
- **ECS Fargate.** No runtime injects a token (`ecs-strategy.ts:44-46` accepts `userId` but marks it unused); the agent mints the token in-process via `get_workload_access_token_for_jwt(workloadName, userToken)`, then makes the identical vault call.
So the downstream call shape is one seam regardless of substrate. Binding on the Cognito/OIDC JWT keeps the key derived from `(iss, sub)` and identical across substrates, which also keeps it aligned with #245. The Phase 0 spike (§4) verified this rather than assuming it.
## 4. Phase 0 result (executed 2026-06-14, `us-east-1`, sandbox account, throwaway)
**Verdict: GO-LIKELY.** The PAR/`request_uri` incompatibility that parked Phase 2.0a (the primary no-go gate, `aws/bedrock-agentcore-sdk-python#111`) **does not reproduce in the current service build.** Run against a throwaway `GithubOauth2` provider with a real Cognito pool/user and workload identity. Everything testable without a human GitHub-consent click passed.
| Go/no-go criterion | Result | Evidence |
|---|---|---|
| PAR `ValidationException` no-go (the parked bug) | **PASS, does not occur** | First `get_resource_oauth2_token(USER_FEDERATION, forceAuthentication=True)` returned a clean `authorizationUrl`. The PAR `request_uri` sits on AgentCore's **own** front-channel (`bedrock-agentcore.us-east-1.amazonaws.com/identities/oauth2/authorize`); AgentCore then 302-redirects to `github.com/login/oauth/authorize` with plain auth-code + PKCE (`client_id`, `redirect_uri`=AgentCore callback, `response_type=code`, `scope=repo`, `state`, `code_challenge`, `code_challenge_method=S256`). **No `request_uri` reaches GitHub.** |
| GitHub accepts the request shape | **PASS** | GitHub routed the AgentCore-built authorize URL through its normal sign-in (`return_to` echoes the full OAuth params). It validates `client_id` only post-login, so a placeholder client reached that wall. |
| Production JWT binding to `(iss, sub)` | **PASS** | `get_workload_access_token_for_jwt` minted a token from a real Cognito IdToken; `iss=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_…`. The opaque-userId dev path also works. |
| Substrate independence (the @krokoko point) | **PASS** | Every control- and data-plane call ran from a plain boto3 session with **no AgentCore Runtime** in the loop, which is direct evidence the vault is an independent primitive, so the ECS path holds. |
| OBO reachable post-GA in `us-east-1` | **PASS (reachable)** | `oauth2Flow="ON_BEHALF_OF_TOKEN_EXCHANGE"` returns a *provider-type* constraint (`not supported for OAuth2 credential provider type: GithubOauth2`, since GitHub OAuth has no token exchange), not a "flow unavailable" error. The flow is live and validated. |
**Remaining for a full GO** (the one leg that can't run headless): a real GitHub OAuth App + one human authorize click within the 10-minute TTL → then `forceAuthentication=False` returns a usable `accessToken` → push a commit + open a PR. The shape is proven end-to-end up to GitHub's login wall, so this is expected to pass.
**Confirmed live, correcting earlier assumptions:** `GithubOauth2` is a first-class vendor (25-vendor enum); `githubOauth2ProviderConfig` needs only `clientId` + `clientSecret`; `create_oauth2_credential_provider` returns `status=READY` immediately and does **not** validate creds against GitHub (it stores them in Secrets Manager), which is why the PAR gate is testable with placeholder creds. All throwaway resources were torn down; nothing touched any ABCA deployment account.
## 5. Phasing
- **Phase 0, re-validate.** ✅ Done 2026-06-14 (`us-east-1`): GO-LIKELY, above.
- **Phase 1, first target via the vault, behind a flag.** Stand up the `GithubOauth2` provider; switch `resolve_github_token()` to the vault path behind a flag, keeping the PAT fallback; retire the shared PAT once green. Keep the inbound descriptor and outbound resolver vendor-neutral so a second IdP or target is config, not a fork.
- **Phase 2, Linear onto the vault.** Move per-workspace OAuth to the vault provider; delete the manual-refresh code in `linear-oauth-resolver.ts`.
- **Phase 3, delegation chain.** Add the OBO `act` claim where the downstream supports it; emit the actor chain into #237's `correlation` block.
## 6. Acceptance criteria
- [x] Phase 0 spike records whether AgentCore Identity flows work post-GA, with date + region. (GO-LIKELY, 2026-06-14, `us-east-1`.)
- [ ] The `user_id` binding key matches #245's IdP-namespaced value; log attribution and token binding do not diverge.
- [ ] Outbound to the first target uses a vault-issued, short-lived token; the shared PAT leaves the hot path (a flagged fallback may remain).
- [ ] A first-call 3LO surfaces an authorization URL and gates write-back on consent; no silent shared-credential fallback for user-attributed actions.
- [ ] The inbound IdP and outbound target are independently swappable by configuration, proven by standing up the worked path plus one alternate provider (or a documented `CustomOauth2` path) without editing the credential-plane code.
- [ ] No regression to #209 SessionRole tenant isolation; the `WorkloadAccessToken` bridge degrades gracefully if the dependency is absent (already holds).
- [ ] The Phase 3 `act` chain is joinable to #245's `trace_id` and feeds #237's `correlation`.
## 7. Open questions (decisions needed)
1. **Per-target scoping model** (worked through GitHub first). GitHub App vs per-user 3LO OAuth App: an App gives repo-scoped installation tokens matching the per-repo ROADMAP item, but installs per-org rather than per-user, and the `GithubOauth2` vault vendor cannot express it (that vendor only drives OAuth-App 3LO), so it needs a `CustomOauth2` / side-channel installation-token path. A 3LO App gives per-user attribution but coarser repo scope. **Generalize the decision:** each egress target has its own scoping primitive (GitHub App vs OAuth App, GitLab project access tokens, Atlassian scopes), so this is a per-target fork the outbound resolver must accommodate, not a one-time GitHub choice.
2. **Consent UX for webhook-triggered tasks with no live user.** Confirm the split: M2M service account for reads, consent gate for writes.
3. **Token Vault cost.** Roughly $0.010 per 1,000 `GetResourceOauth2Token` fetches; negligible at current volume. Noted for #215 forecasting.
## 8. Out of scope
- Trace/log attribution mechanics (owned by #245; this RFC consumes its envelope).
- Bedrock billing attribution (owned by #215).
- Workload-anchored credential binding via MicroVM attestation (separate ROADMAP item; complementary).
- Cedar authorization decisions (orthogonal; the delegation chain may inform them later).
---
*This issue body was consolidated 2026-06-15 to carry the full, current position in one place: the original RFC, the Phase 0 spike plan + executed GO-LIKELY result, and the composability stance. The comment thread below preserves the history. Drafted by Bonk; edited via Laith's account.*
Hướng dẫn đóng góp
Hướng nghiên cứu
Bắt đầu bằng cách đọc các entry point được tham chiếu: agentcore-strategy.ts, task-orchestrator.ts, server.py và ecs-strategy.ts, sau đó xem lại các kết quả của Phase 0, các issue liên quan và ADR-016. Công việc chỉ hoàn tất sau khi các quyết định thiết kế còn lại được giải quyết, provider path được triển khai phía sau một flag với PAT fallback, và flow OAuth consent cùng write-back được ghi lại trong tài liệu đã được xác minh.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- aws, github, python, typescript
- Lĩnh vực
- authentication, backend-api-design, cloud, security
- Loại issue
- Tính năng
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức độ hoạt động
- Ít trao đổi
- Độ rõ ràng
- Cần làm rõ
- Mức phù hợp với người mới
- 30/100