Skip to content

Commit 04b4d85

Browse files
authored
fix(webapp): allow JWT auth on POST /api/v1/sessions (#3474)
## Summary `POST /api/v1/sessions` was secret-key-only because the customer browser flow runs through `chat.createStartSessionAction` (server-side, holds the secret key). But the `cli-v3` MCP `start_agent_chat` tool is itself a server-side surface — developer's CLI/IDE acting as their own server — and only holds a JWT minted from the user's PAT. Without JWT support on this route the entire MCP agent toolkit (`start_agent_chat`, `send_agent_message`, `close_agent_chat`) is blocked at session creation. Add `allowJWT: true` plus an `authorization` block requiring the `write:sessions` (or `admin`) super-scope. ## Why a wildcard `sessions` resource Resource scoping by `taskIdentifier` isn't possible at auth-resolve time — action routes don't pass `body` to the `resource` callback, and the task name only lives in the body. So the resource is `sessions: "*"` and the super-scope does the actual gating. The JWT-issuer (cli-v3 MCP, customer servers wrapping their own auth helpers, etc.) decides which scopes to mint, which is where per-task narrowing lives. ## Test plan - [x] Verified end-to-end against local: `mcp__trigger__start_agent_chat` → `send_agent_message("pong")` → `send_agent_message("echo")` → `close_agent_chat` all succeed. Two assistant turns reuse the same runId (continuation in the idle window). - [ ] Browser-mediated `chat.createStartSessionAction` flow continues to work unchanged (still uses secret-key path under the hood). - [ ] Loader (GET) and other session routes — unchanged, no scope drift. ## Notes This unblocks T17 in the [ai-chat e2e smoke catalog](https://github.com/triggerdotdev/trigger.dev/blob/feature/tri-7532-ai-sdk-chat-transport-and-chat-task-system/.claude/skills/ai-chat-e2e/SMOKE-TESTS.md) (which lives in the feature branch's skill catalog, not this repo). Pairs with the cli-v3 MCP fix on the feature branch (`feat: AI SDK custom useChat transport & chat.task harness`, PR #3173) — that PR's `agentChat.ts` change makes the call shape correct (`taskIdentifier` + `triggerConfig`); this PR opens the door for the JWT to actually pass.
1 parent f173659 commit 04b4d85

1 file changed

Lines changed: 27 additions & 1 deletion

File tree

apps/webapp/app/routes/api.v1.sessions.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,39 @@ const { action } = createActionApiRoute(
9696
body: CreateSessionRequestBody,
9797
method: "POST",
9898
maxContentLength: 1024 * 32, // 32KB — metadata is the only thing that grows
99-
// Secret-key only. Customer's server (typically wrapping
99+
// Customer's server (typically wrapping
100100
// `chat.createStartSessionAction`) owns session creation so any
101101
// authorization decision (per-user/plan/quota) sits server-side
102102
// alongside whatever DB write the customer pairs with the create.
103103
// The session-scoped PAT returned in the response body is what the
104104
// browser uses thereafter against `.in/append`, `.out` SSE,
105105
// `end-and-continue`, etc.
106+
//
107+
// JWT is allowed when the caller holds an explicit `write:sessions` /
108+
// `admin` super-scope plus a `tasks:<taskIdentifier>` scope — gates
109+
// server-side surfaces like the cli-v3 MCP from creating sessions on
110+
// behalf of the developer without weakening the browser model.
111+
allowJWT: true,
112+
authorization: {
113+
// Per-task scoping via `body.taskIdentifier` (action-route resource
114+
// callbacks receive the parsed body as the 4th arg — see
115+
// `apiBuilder.server.ts:710`). A JWT scoped only to `write:tasks:foo`
116+
// can only create sessions whose `taskIdentifier` is `"foo"`. Broad
117+
// callers (cli-v3 MCP, customer servers wrapping their own auth)
118+
// hold the `write:sessions` super-scope and bypass the per-task
119+
// check entirely.
120+
//
121+
// Note: the auth check is OR across resource types, so listing both
122+
// `sessions` and `tasks` here would let a `write:sessions`-scoped
123+
// JWT pass for *any* task — defeating the per-task narrowing. Keep
124+
// it task-only and let the super-scope path handle session-level
125+
// wildcard access.
126+
action: "write",
127+
resource: (_params, _searchParams, _headers, body) => ({
128+
tasks: body.taskIdentifier,
129+
}),
130+
superScopes: ["write:sessions", "admin"],
131+
},
106132
corsStrategy: "all",
107133
},
108134
async ({ authentication, body }) => {

0 commit comments

Comments
 (0)