feat(agentex): Linear gateway — invoke agents from Linear (re-land) - #401
Conversation
## What A platform-side ingress that fronts a Linear **agent app** and routes each @mention / assignment to the resolved agent runtime — the Linear analog of the Slack gateway (#388 / #392 / #394). ## How it works - **`POST /linear/events`** — verifies the `Linear-Signature` (HMAC-SHA256 over the raw body) + a `webhookTimestamp` freshness guard, dedups on the `Linear-Delivery` id, acks fast (Linear's ~10s window), and runs the turn in the background. Auth-whitelisted like `/slack` — Linear can't present an SGP principal, so the signature is the auth, verified in the use case. - **Normalize `AgentSessionEvent`** (`created` / `prompted`) → the same selector-cascade + `task/create`-or-`event/send` dispatch as the Slack gateway, keyed on the agent session (`task_metadata.channel = "linear"`). - **Reply via `agentActivityCreate`** — a `thought` immediately (a session is marked unresponsive without an activity within ~10s), then a terminal `response` / `error`. The Linear API token is minted via the OAuth **`client_credentials`** grant (inherently app-actor) and re-minted reactively on a 401 — no perishable token is stored, only the static client id/secret in env. - Removes a duplicate `slack.router` registration in `app.py`. ## Identity Runs as a **dedicated bot service account** (its own SGP identity), not a proxy for the invoking Linear user — consistent with the Slack gateway's identity model. Its comments/activities render as the app (`actor=app`). ## Config (env / k8s-secret) `LINEAR_CLIENT_ID`, `LINEAR_CLIENT_SECRET`, `LINEAR_WEBHOOK_SIGNING_SECRET`, `LINEAR_GATEWAY_ACTING_BOT_API_KEY`, `LINEAR_GATEWAY_ACCOUNT_ID`. ## Testing - 20 unit tests: signature verify, normalize (created / prompted / ignored), event control-flow (dev-skip / drop / dedup / ack), acting-identity fail-closed, dispatch metadata, and `agentActivityCreate` token-mint + 401 re-mint. All pass, ruff clean. - Signed ingress round-trip verified over HTTP; `client_credentials` token mint verified against the live Linear API (app-actor confirmed via `viewer`). - The full agent round-trip (posting an activity to a **real** session) validates post-deploy with a real @mention — a fabricated session can't be tested locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- greptile_comment --> <h3>Greptile Summary</h3> The PR adds a signed Linear webhook gateway that normalizes agent-session events, dispatches them through AgentEx under a bot identity, and posts activities back through Linear’s API. - Registers and documents `POST /linear/events`. - Adds signature freshness checks, delivery deduplication, target resolution, task dispatch, and background response collection. - Adds OAuth token minting and Linear activity delivery with one refresh attempt after a 401. - Adds unit coverage for ingress, normalization, identity handling, dispatch, and activity delivery. <details><summary><h3>Confidence Score: 3/5</h3></summary> The PR is not yet safe to merge because concurrent turns can receive each other’s replies and Linear activity-delivery failures can silently discard terminal results. Concurrent turns for one session share an uncorrelated message stream, while failed token minting or activity posting returns normally without delivering a response or error; both previously reported failures remain in the current code. **Files Needing Attention:** agentex/src/domain/use_cases/linear_gateway_use_case.py </details> <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | agentex/src/domain/use_cases/linear_gateway_use_case.py | Implements the Linear gateway end to end; previously reported reply-correlation and silent activity-delivery failures remain. | | agentex/src/api/routes/linear.py | Adds the thin `/linear/events` ingress that preserves the raw body for signature verification. | | agentex/src/api/middleware_utils.py | Whitelists the Linear route so webhook authentication can be enforced by signature verification in the use case. | | agentex/src/api/app.py | Registers the new Linear router while removing the duplicate Slack-router registration. | | agentex/openapi.yaml | Documents the new Linear webhook endpoint and its successful response. | | agentex/tests/unit/use_cases/test_linear_gateway_use_case.py | Covers core gateway behavior but does not eliminate the blocking failures that remain in concurrent reply attribution and activity delivery. | </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant L as Linear participant R as POST /linear/events participant G as LinearGatewayUseCase participant A as AgentEx ACP participant API as Linear API L->>R: Signed AgentSessionEvent R->>G: Raw body, headers, payload G->>G: Verify signature and deduplicate G-->>L: 200 acknowledgement G->>API: Create thought activity G->>A: Resolve target and send event A-->>G: Task messages G->>API: Create response or error activity ``` </details> <sub>Reviews (2): Last reviewed commit: ["fix(agentex): address Linear gateway rev..."](88283a5) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=51719191)</sub> <!-- /greptile_comment --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
✱ Stainless preview buildsThis PR will update the openapi python typescript
|
| await self._emit(inbound, "thought", "On it…") | ||
|
|
||
| principal, auth_headers = await self._acting_identity() | ||
| target, prompt = await self._resolve_target(inbound, auth_headers) |
There was a problem hiding this comment.
Follow-up selector strips content
When a prompted follow-up starts with a registered agent or configuration name, _resolve_target treats that word as a selector and removes it before dispatching to the existing session task, causing the agent to receive a truncated user instruction even though selectors are intended to apply only on the first turn.
Prompt To Fix With AI
This is a comment left during a code review.
Path: agentex/src/domain/use_cases/linear_gateway_use_case.py
Line: 329
Comment:
**Follow-up selector strips content**
When a prompted follow-up starts with a registered agent or configuration name, `_resolve_target` treats that word as a selector and removes it before dispatching to the existing session task, causing the agent to receive a truncated user instruction even though selectors are intended to apply only on the first turn.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Re-lands the Linear gateway from #399, which was merged (
4718eee) then reverted by #400 (bcf5a79).Why it was reverted — and why that wasn't a code problem
#399's revert was triggered by the "Stainless build" check, which failed in its merge step with:
That's a Stainless SDK-generation / CI-infra failure (the action couldn't find a preview branch and flags it as its own bug), not a defect in the gateway. All 42 code checks on
4718eeepassed — the full unit + sync/async integration matrix. So no code change was needed to re-land; this is the same commit (including the Greptile review fixes).If the Stainless build fails again the same way, it's an infra issue to re-run / raise with the Stainless integration owners, not a blocker on this code.
What it is (unchanged from #399)
Platform-side ingress fronting a Linear agent app;
POST /linear/eventsverifiesLinear-Signature+webhookTimestamp, dedups onLinear-Delivery, normalizesAgentSessionEvent(created/prompted), runs the Slack-gateway selector-cascade dispatch (task_metadata.channel="linear"), and replies viaagentActivityCreate(thought → terminal response/error) using aclient_credentialsapp-actor token (re-minted on 401). Runs as a dedicated bot service account. Includes the #399 review fixes: fail-closed on empty signing secret, and no response-body in logs on token failure.Testing
21 unit tests pass, ruff clean. Signed ingress + client_credentials token mint validated against live Linear earlier.
🤖 Generated with Claude Code
Greptile Summary
The PR adds a signed Linear webhook gateway that deduplicates deliveries, dispatches Linear agent-session turns through AgentEx, and returns activities using an app-actor OAuth token.
POST /linear/events.Confidence Score: 4/5
The follow-up selector handling should be fixed before merging because it can silently alter user prompts in existing Linear sessions.
Prompted events pass through the same selector-resolution path as initial events, so a leading word matching an agent or configuration is removed despite the session contract requiring follow-up content to remain intact.
Files Needing Attention: agentex/src/domain/use_cases/linear_gateway_use_case.py
Important Files Changed
Sequence Diagram
sequenceDiagram participant L as Linear participant API as POST /linear/events participant G as LinearGatewayUseCase participant R as Redis participant ACP as AgentEx ACP participant LA as Linear API L->>API: Signed AgentSessionEvent API->>G: Raw body, headers, payload G->>G: Verify signature and timestamp G->>R: SET delivery ID NX with TTL G-->>L: "200 {ok: true}" G->>LA: Emit thought activity G->>ACP: Resolve target and create/resume task ACP-->>G: Agent reply G->>LA: Emit terminal response or errorPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(agentex): Linear gateway — invoke a..." | Re-trigger Greptile