Skip to content

feat(agentex): Linear gateway — invoke agents from Linear (re-land) - #401

Merged
michael-chou359 merged 1 commit into
mainfrom
mc/linear-gateway-reland
Aug 10, 2026
Merged

feat(agentex): Linear gateway — invoke agents from Linear (re-land)#401
michael-chou359 merged 1 commit into
mainfrom
mc/linear-gateway-reland

Conversation

@michael-chou359

@michael-chou359 michael-chou359 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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:

404 One or both branches not found: main, preview/mc/linear-gateway — "This is a bug. Please report it at https://github.com/stainless-api/upload-openapi-spec-action/issues"

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 4718eee passed — 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/events verifies Linear-Signature + webhookTimestamp, dedups on Linear-Delivery, normalizes AgentSessionEvent (created/prompted), runs the Slack-gateway selector-cascade dispatch (task_metadata.channel="linear"), and replies via agentActivityCreate (thought → terminal response/error) using a client_credentials app-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.

  • Registers and documents POST /linear/events.
  • Adds signature freshness checks, delivery deduplication, target selection, bot identity propagation, task dispatch, reply polling, and Linear activity delivery.
  • Adds unit coverage for authentication, normalization, dispatch, identity handling, and token refresh.

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

Filename Overview
agentex/src/domain/use_cases/linear_gateway_use_case.py Implements the complete Linear ingress and dispatch lifecycle, but applies first-turn selector stripping to prompted follow-ups.
agentex/src/api/routes/linear.py Adds the Linear webhook route and preserves the raw request body required for HMAC verification.
agentex/src/api/middleware_utils.py Whitelists Linear ingress so the gateway's webhook-signature authentication can run.
agentex/src/api/app.py Registers the new Linear router while removing a 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 exercise selector-like first words in prompted follow-ups.

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 error
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
agentex/src/domain/use_cases/linear_gateway_use_case.py:329
**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.

Reviews (1): Last reviewed commit: "feat(agentex): Linear gateway — invoke a..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

## 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>
@michael-chou359
michael-chou359 requested a review from a team as a code owner August 10, 2026 16:48
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

✱ Stainless preview builds

This PR will update the agentex-sdk SDKs with the following commit messages.

openapi

feat(api): add events webhook endpoint to linear

python

chore(internal): regenerate SDK with no functional changes

typescript

chore(internal): regenerate SDK with no functional changes
agentex-sdk-openapi studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅

⚠️ agentex-sdk-typescript studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️build ⏭️lint ⏭️test ✅

⚠️ agentex-sdk-python studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️build ⏭️lint ⏭️test ✅


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-08-10 17:00:26 UTC

await self._emit(inbound, "thought", "On it…")

principal, auth_headers = await self._acting_identity()
target, prompt = await self._resolve_target(inbound, auth_headers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Fix in Cursor Fix in Claude Code Fix in Codex

@michael-chou359
michael-chou359 enabled auto-merge (squash) August 10, 2026 16:56
@michael-chou359
michael-chou359 merged commit fc835c0 into main Aug 10, 2026
47 checks passed
@michael-chou359
michael-chou359 deleted the mc/linear-gateway-reland branch August 10, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant