16 KiB
G16 Spike — Does an Agentforce Employee-Agent-in-Slack callout forward the invoking user's identity?
Status: Spike design, ready to run (2026-07-07). Unblocked by the 2026-07-07 resolution of the
Agentforce org-wide execution wall (Salesforce backend misconfig, fixed). This is the critical-path
experiment the /sh-plan-review gate (GPT-4.1) demanded be run before committing to the M2M pivot
in docs/agentforce-auth-decision.md. Throwaway kit — not production, not gated by the IAM/auth
cross-review.
0. Why this settles the whole auth-model question
The auth-model decision (docs/agentforce-auth-decision.md) hinges on one unproven fact (G16):
when a Slack user talks to an Agentforce Employee Agent and the agent fires an External Service
callout, does the outbound request carry that human's identity, or a single service identity?
- The 0a spike already proved the per-user OAuth chain (Named Credential → Per-User Browser-Flow
External Credential → Cognito → endpoint delivers a valid per-user access JWT). But every 0a probe
hit was a direct External Service test as one user (logs show the same
sub34f8a4e8-…and thesfdc_stack_depthheader) — never an actual agent invocation, because the execution wall threw before any agent callout fired. - So the only open question is the agent-invocation hop: per-user or service. That is now
directly testable. Its answer decides the architecture (see §7):
- PASS (per-user forwarded) → keep the simpler, already-proven per-user D4; shelve the M2M pivot and its Apex-signer machinery.
- FAIL (service identity) → execute the M2M + tool-band plan.
Run this before deleting any per-user substrate.
1. Hypotheses (pre-registered)
- H-PASS — an agent callout invoked by user U arrives at the probe with
sub== U's own consented identity; ≥2 distinct users → ≥2 distinct correct subs. - H-FAIL — all users' agent callouts arrive with the same principal (a service/Named identity), i.e. user identity is lost inside the agent orchestration.
- H-HEADER — identity is not in the bearer token but is in a Salesforce-added request header (a per-user id / signed assertion). Would open a third architecture option (service transport + a trustworthy SF user header, simpler than the custom Apex-signer).
- H-PARTIAL — per-user, but
subis a Salesforce/Cognito-internal id, not the Google Workspacesub(the CR-2 concern) — per-user still viable, add asub↔Workspace validation step.
2. Decision matrix (decide the action BEFORE running, to avoid post-hoc rationalizing)
| Observed at the probe across ≥2 distinct invoking users | Verdict | Architecture decision |
|---|---|---|
Distinct, correct per-user sub each time |
G16 PASS | Keep per-user D4 (proven + simpler). Shelve agentforce-auth-decision.md; finance audit falls out for free (real sub). |
Same sub / same principal for every user |
G16 FAIL | Execute the M2M + tool-band plan; the Apex-signer becomes the finance-audit mechanism. |
| Per-user identity present, but in a header not the token | PASS-via-header | New option: service transport + verified SF user header. Re-evaluate both docs. |
Per-user sub, but not the Google Workspace sub |
PARTIAL | Per-user viable; add sub↔Workspace mapping/validation (CR-2) before build. |
| Principal reverts to "Configured" / 401 after ~1h | (orthogonal) | Not a G16 verdict — token-survival bug; fixed by offline_access (controlled pre-spike, §4). |
3. The kit (reuse the live 0a kit — verified present 2026-07-07)
| Piece | Value |
|---|---|
| Cognito pool | us-east-1_GsDbGe0pa (sh-mcp-0a-spike, ESSENTIALS), IdP = Cognito-native |
| Hosted-UI domain / token endpoint | sh-mcp-0a-spike-19568 → https://sh-mcp-0a-spike-19568.auth.us-east-1.amazoncognito.com |
| App client | 7kavkhm97etk00bfk7sjua2rfq — flow code; scopes openid email sh-mcp-ops/ops:read sh-mcp-ops/ops:tasks; no offline_access (fix in §4) |
| Probe API | ex1v1x1hqc → https://ex1v1x1hqc.execute-api.us-east-1.amazonaws.com/probe |
| Probe Lambda | sh-mcp-0a-spike-probe (py3.12) — already logs PROBE_RESULT {claims_of_interest, all_claims, seen_header_keys} |
| Salesforce | Prod speed-speed-957 (Adam's choice 2026-07-07 — no data to lose there, and it's the real Enterprise Grid, so the result is definitive; see venue note below). Objects sh_mcp_0a: Auth Provider / External Credential (Per-User) / Named Credential; External Service shMcp0aProbe → operation identityProbe. The prod callback URL is already on the app client. |
Venue = prod speed-speed-957 (definitive). The Gemini-Q caveat (plan §0.3) was that a free Slack
workspace may forward identity differently than production Enterprise Grid + real licenses, so a sandbox
result would need a Grid re-confirm. Running in the prod org on the real Grid removes that caveat — a
prod PASS or FAIL is definitive. Requirement to keep it clean: wire the agent action to the External
Service operation identityProbe directly (or via Flow), NOT an Apex @InvocableMethod, so the "no Apex
authoring on an active prod org" gotcha never applies. If an Apex wrapper is unavoidable, deploy it via
metadata/change set (don't author in the prod Setup UI).
4. Pre-spike changes — AWS side DONE 2026-07-07
- Token survival past 1h — resolved at the AWS layer (no
offline_accessneeded). Cognito rejectsoffline_accessas a scope (ScopeDoesNotExistException); it issues a refresh token automatically for the authorization-code grant. The app client already hasRefreshTokenValidity=30d,ALLOW_REFRESH_TOKEN_AUTH, and token revocation on — so the refresh token IS returned to Salesforce. The 0a "reverts after 1h" symptom is therefore Salesforce-side (the Per-User External Credential must store + use that refresh token), not an AWS gap. Verified by test E3; if it still reverts, the fix is in the SF External Credential config, not here. - Probe enhanced + deployed.
sh-mcp-0a-spike-probenow logs full header names+values (seen_headers, with the bearer redacted to length only) and echoes anX-Probe-Tagheader — so a Salesforce-injected identity header (H-HEADER) can't hide and every invocation is correlatable to a user. Smoke-tested green (custom headers captured, bearer shown asBearer <redacted len=N>). - Two labelled test users ready. Both Cognito-native users have known permanent passwords, and
because each username == its
sub, the mapping is self-checking (sign in as X → probe must showsubX). Credentials handed to Adam separately (§5 / the summary). - (Pass-2, optional, deferred) Re-federate the pool to Google to confirm the arriving
subis the real Workspacesub(settles H-PARTIAL / CR-2). Not needed for the primary PASS/FAIL — two distinct Cognito-native users already discriminate per-user vs service.
4b. Salesforce build from scratch (prod speed-speed-957)
Assumes no sh_mcp_0a objects exist yet. Values to paste:
| Field | Value |
|---|---|
| Consumer Key (Cognito app client) | 2oqnqep996kvrh9u12jr774til (confidential client sh-mcp-0a-spike-sf) |
| Consumer Secret | t2vhiu7nsjvrecm65v12hhmfqiblac03rrv2rbggb680ktuqvu9 |
| Authorize URL | https://sh-mcp-0a-spike-19568.auth.us-east-1.amazoncognito.com/oauth2/authorize |
| Token URL | https://sh-mcp-0a-spike-19568.auth.us-east-1.amazoncognito.com/oauth2/token |
| User Info URL | https://sh-mcp-0a-spike-19568.auth.us-east-1.amazoncognito.com/oauth2/userInfo |
| Token Issuer (for validation) | https://cognito-idp.us-east-1.amazonaws.com/us-east-1_GsDbGe0pa |
| Scopes | openid email sh-mcp-ops/ops:read sh-mcp-ops/ops:tasks |
| MCP base URL (Named Credential) | https://ex1v1x1hqc.execute-api.us-east-1.amazonaws.com |
| OpenAPI schema for the External Service | docs/g16-probe.openapi.json (path /probe, operation identityProbe) |
Cognito wrinkle: the OAuth endpoints live on the hosted-UI domain (
…-19568.auth…), but the token issuer is thecognito-idp…/<poolId>URL. Enter the explicit authorize/token/userinfo URLs above (don't rely on OIDC discovery, which points at the issuer host, not the domain host).
A. Auth. Provider — Setup → Auth. Providers → New. Type OpenID Connect; Name/URL-Suffix sh_mcp_0a;
Consumer Key/Secret + Authorize/Token/UserInfo URLs + Default Scopes from the table; Token Issuer as above.
Save, then copy the generated Callback URL — it should be
https://speed-speed-957.my.salesforce.com/services/authcallback/sh_mcp_0a (already registered on the
Cognito client). If it differs, send it to me and I'll add it to Cognito.
B. External Credential — Setup → Named Credentials → External Credentials → New. Label/Name
sh_mcp_0a; Authentication Protocol OAuth 2.0; Authentication Flow Type Browser Flow (reference the
sh_mcp_0a Auth Provider); add the scopes. Add a Principal with Identity Type = Per User.
C. Named Credential — Setup → Named Credentials → Named Credentials → New. Label/Name sh_mcp_0a;
URL https://ex1v1x1hqc.execute-api.us-east-1.amazonaws.com; External Credential sh_mcp_0a; enable
Generate Authorization Header.
D. Permission Set — create/choose a PS → External Credential Principal Access → add
sh_mcp_0a — Per User. Assign it to both test users. (Without this, users can't use the credential.)
E. External Service — Setup → External Services → New → From API Specification. Named Credential
sh_mcp_0a; paste docs/g16-probe.openapi.json. Confirm it registers operation identityProbe. This
creates the invocable action for Agentforce/Flow.
F. Agent — Agent Builder → open/create an Employee Agent (Slack). Add a Topic + Action that
calls the identityProbe External Service operation (keep the default "Agent Router" subagent).
Activate and connect to Slack.
G. Per-user consent — each user: personal Settings → Authentication Settings for External Systems (or the External Credential principal → Authenticate) → at the Cognito hosted-UI login, sign in as User A (user 1) / User B (user 2). Principal must read Authenticated.
5. Procedure (ordered; each step has a check)
Setup
- S1. Apply §4.1–§4.2. Check:
curl -H "Authorization: Bearer <hand-minted token>"the/probeendpoint →PROBE_RESULTshows the decoded claims + full headers + yourX-Probe-Tag. - S2. Ensure two distinct test identities, each able to (a) log into the pool as a distinct
Cognito user (distinct
sub) and (b) invoke the Slack agent as a distinct Slack/Salesforce user: create Cognito user B (user A = the existing34f8a4e8-…); a 2nd sandbox Salesforce user mapped to a 2nd Slack user. Check: each can independently complete the per-user Browser-Flow consent for thesh_mcp_0aExternal Credential (principal shows Authenticated). - S3. In the sandbox, wire
shMcp0aProbe.identityProbeas an Agentforce action on a deployed Employee Agent in Slack; keep the default "Agent Router" subagent (deleting it breaks compile — memory gotcha). Check: the agent is live in the Slack workspace and lists the probe action.
Experiment
- E1 (User A). In Slack, prompt the agent so it invokes the probe action (e.g. "run the identity
probe"). Capture: the
PROBE_RESULTlog line + the agent's returned body. - E2 (User B). Same prompt, as User B. Capture: same.
- E3 (token survival). Re-run E1 >1h after A's consent → confirm the callout still succeeds
(validates
offline_access). - E4 (autonomous, if feasible). Trigger the same action from a non-interactive context (scheduled/automated) → record whether the arriving principal is distinguishable from a human (review QUESTION-6: a service context must not masquerade as a human).
Read-out — pull the results in one shot:
aws logs tail /aws/lambda/sh-mcp-0a-spike-probe --region us-east-1 --since 1h --format short \
| grep PROBE_RESULT
Tabulate per invocation: invoking user, sub, client_id, token_use, scope, any user-identifying
header, principal-still-Authenticated-after-1h. Compare against §2.
6. What we're measuring (the observables the probe already emits)
sub (the identity signal — access token has no aud, no email, memory-confirmed), client_id,
token_use, scope, iss, iat/exp; all request headers (post-§4.2) to catch header-borne
identity; whether the per-user principal survived >1h.
7a. Run outcome (2026-07-09) — programmatic Agent-API attempt, INCONCLUSIVE (leans FAIL)
Drove this entirely via SF CLI + AWS CLI (no browser, no 2nd human), using the Agent API instead of Slack:
- Provisioning works (
/agentforce/bootstrap/nameduser→sfap_apiJWT →einstein/ai-agent/v1). Built an activated agent (sh_mcp_g16c, id0XxhR0000000UnhSAE) purely via Metadata API because CLIagent publish/agent createandaiAuthoringBundledeploy are all broken on this trial org. - Wall: the planner returns
result: []— narrates running the probe but never executes the Apex (Task-DML sink + unauth beacon + per-user callout all confirm zero execution). The fix —GenAiFunctioninput/output/schema.json— server-errors on deploy (ErrorId 44568769-691223). Same degraded-runtime signature as the broken publish/create APIs. - Direct callout still carries the real per-user
sub(baseline good), and found a live token-refresh bug: the Per-User External Credential served a 44h-expired token (test E3, live). - Verdict: LEANS FAIL (service identity) on architecture (agent mandates
default_agent_user; human rides as context vars only), but the decisiveUserInfo/Task.CreatedByIdobservable was never captured because the action wouldn't fire. Committed M2M plan already assumes FAIL. Definitive confirm still needs the Slack path (§4b F–G, 2 real users) OR a SF-support fix for GenAiFunction schema deploy on this org. Instrumentation is deployed and ready (IdentityProbeActionlogsPROBE_RUNAS, insertsG16RUNASTask, beacons UserInfo).
7. Outcome → next action (closes the loop with the plan)
- PASS → update
project_sh_mcpmemory + markagentforce-auth-decision.mdsuperseded/shelved; proceed on per-user D4; the M2M pivot is unnecessary. - FAIL → that doc becomes the committed plan; run its Phase A–F (which the
/sh-plan-reviewFIXes still apply to: provisioning order, rollback branch, cross-review gating, docs). - PASS-via-header / PARTIAL → record the third option / CR-2 follow-up; re-decide.
- Either way: write the result + the exact probe evidence into
project_sh_mcpmemory the same day.
8. Time-box, ownership, teardown
- Time-box: 1–2 days; most cost is the Salesforce agent + 2nd-user setup, not AWS.
- I can do: the §4 AWS-side changes (add
offline_access, enhance the probe) and the read-out. Adam/hands-on: the Salesforce sandbox agent wiring, the two real Slack test users, the consents, and the Enterprise-Grid re-confirm on a PASS. - Teardown: re-authored as
docs/g16-spike-teardown.sh(bash docs/g16-spike-teardown.sh --yes) — tears down the Cognito pool/domain/users, the HTTP API, and the probe Lambda/role/log group; the Salesforcesh_mcp_0aobjects are a manual delete in both orgs. Keep the kit live until the verdict is recorded inproject_sh_mcpmemory.