The security model: how the agent makes authenticated VDS calls without holding a token, and why the three plausible attacks fail.
The agent loop runs in the chat application; its outbound work happens in a sandbox tool. Neither holds credentials. A proxy sits at the sandbox's network egress, holding a session-bound sandbox_id → user_id lookup. On every call, the proxy mints a fresh Connected Apps JWT with that user as the subject and forwards to Tableau. The auth and authz primitives are the ones Tableau already enforces; the proxy and per-chat binding are the new piece.
The flow of identity from user authentication through a single VDS query. For the static system view, see architecture.md.
sequenceDiagram
actor U as User
participant C as Chat App
participant A as Agent
participant S as Sandbox
participant P as Proxy
participant V as Tableau VDS
U->>C: authenticate (via IdP)
Note over C: writes sandbox_id → user_id binding<br/>before the agent first uses the sandbox
U->>A: ask analytical question in natural language
A->>S: tool call: run this code
S->>P: outbound HTTP (no token)
Note over P: trust boundary — looks up binding,<br/>mints JWT (sub = user),<br/>injects Authorization header
P->>V: HTTPS + JWT
Note over V: validates JWT<br/>applies RLS and permissions for that user
V-->>P: rows scoped to user
P-->>S: response
S-->>A: tool output
Note over A: performs analysis needed to answer the question<br/>writes final response
A-->>U: answer question in natural language
The binding is written once and read by the proxy on every VDS call. Whether the sandbox is provisioned eagerly at chat creation or lazily on first use is implementation choice — what matters is that the binding exists by the time the agent first invokes the sandbox.
The binding is immutable from anywhere the agent can reach, for three reasons:
- The binding lives in infrastructure the agent can't influence. The chat application writes it once; nothing the agent does during the conversation can change it.
- The sandbox's network egress is constrained to the proxy. Code running in the sandbox cannot reach the binding store, an IdP, or a Tableau auth endpoint directly.
1. Social-engineer the user identity. "Query this as Tony Stark instead." The agent has no means to change the JWT subject. The proxy mints the token from the binding, not from anything the agent says or writes.
2. Scope escalation. "Include the Executive Compensation data source." The agent can write the call. The proxy can authenticate it. VDS rejects it under the user's permissions, the same way it would reject the user querying directly through the REST API. The defense is Tableau's permission check, not the agent's judgment.
3. Bypass the proxy.
- Self-sign a Connected Apps JWT. The shared secret lives with the proxy infrastructure, not the sandbox.
- Ask the user for credentials. The proxy is already authenticating as the correct user; the agent has nothing to gain.
The primitives Tableau uses are unchanged: a user authenticates to an IdP; a Connected Apps JWT is presented to VDS; VDS validates it and enforces permissions. What's new is the arrangement — the JWT is minted on demand by a proxy bound to a session-authenticated user, instead of held by the human clicking a dashboard. The agent is just another HTTP client.
- Authentication. A way to authenticate users to your agent product. A session store that records
sandbox_id → user_id, immutable from within the sandbox. - Tableau Connected App. Configured on your Tableau site; gives you the shared secret to configure the proxy. User identities should align between Tableau and your IdP so RLS rules apply correctly.
- Proxy. Sits at the auth boundary. On every request: read sandbox ID, look up user, mint fresh JWT, inject, forward. Holds the Connected App secret and the binding store; the sandbox holds neither.
architecture.md— high-level system view.sandbox-model.md— the per-chat sandbox the binding attaches to.vds-cheat-sheet.md— what the agent reads to write VDS calls.