refactor(agentex): Slack gateway reads bot credentials from env, not DB - #394
Merged
Conversation
The gateway acts as ONE fixed identity — a dedicated bot service account, not a per-user proxy — so its credentials (signing secret, bot token, acting API key + account) are static, app-level values. Move them out of the throwaway agent_api_keys DB store into env / k8s-secret, which is where static deployment secrets belong. - Drop _gateway_secret and the DB-store name constants; _fetch_bot_token, _fetch_signing_secret, and _acting_identity now read env directly. - Rename the acting identity user -> bot (SLACK_GATEWAY_ACTING_BOT_API_KEY) to reflect that the bot is its own entity, not the invoking user. - Remove the now-unused get_by_name_and_type repository method. - Update unit tests to the env-based reads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
michael-chou359
enabled auto-merge (squash)
August 4, 2026 22:41
…under authz
Removing the agent_api_keys fallback widened an existing fail-open: with the
credential sourced only from env, a deployment that upgrades without setting
SLACK_GATEWAY_ACTING_BOT_API_KEY dropped to the (None, {}) dev bypass and
_run_turn dispatched with no principal and no auth headers — an unauthenticated
downstream request / authz-boundary bypass.
_acting_identity now fails closed: when the bot key is absent AND authz is
enabled (AGENTEX_AUTH_URL set), it raises instead of returning the empty
identity, so the turn stops before target resolution and dispatch. The empty
(None, {}) bypass is retained only when AGENTEX_AUTH_URL is unset (local,
authz-off) dev. Gated on os.getenv so the check reads env directly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
michael-chou359
disabled auto-merge
August 4, 2026 22:59
michael-chou359
added a commit
that referenced
this pull request
Aug 10, 2026
## 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The Slack gateway acts as one fixed identity — a dedicated bot service account, not a per-user proxy — so its credentials (signing secret, bot token, acting API key + account) are static, app-level values. This moves them out of the throwaway
agent_api_keysDB store and into env / k8s-secret, where static deployment secrets belong.Why
The DB store only ever existed as a bridge toward per-user, per-request secrets that vary by user and can't live in static pod env (the code itself called it a "THROWAWAY store — plaintext … To be replaced by sgp-secrets user-scope"). With the gateway settled as a single bot identity, that justification is gone: these are one-per-deployment secrets, so the environment's secret is the right home. It also gets plaintext secrets out of the application database and removes the DB read path entirely.
Changes
_gateway_secretand the DB-store name constants;_fetch_bot_token,_fetch_signing_secret, and_acting_identityread env directly.SLACK_GATEWAY_ACTING_BOT_API_KEY) to reflect that the bot is its own entity, not the invoking user.get_by_name_and_typerepository method.Deploy note
The gateway now reads four env vars —
SLACK_SIGNING_SECRET,SLACK_BOT_TOKEN,SLACK_GATEWAY_ACTING_BOT_API_KEY,SLACK_GATEWAY_ACCOUNT_ID— from the deployment secret. They must be present in an environment's secret before this rolls out there.Testing
73 passed(gateway + delegation-header unit tests), ruff clean.🤖 Generated with Claude Code
Greptile Summary
The PR moves the Slack gateway’s fixed bot credentials from the database to deployment environment secrets and removes the obsolete repository lookup. It also closes the previously reported missing-identity path by refusing to dispatch without a bot API key when authorization is enabled.
Confidence Score: 5/5
The PR appears safe to merge because the previously reported unauthenticated-dispatch path now aborts before dispatch when authorization is enabled.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Slack turn] --> B[Read bot identity from environment] B --> C{Bot API key present?} C -- Yes --> D[Verify API key and account headers] D --> E[Resolve target and dispatch as bot principal] C -- No --> F{Authorization enabled?} F -- Yes --> G[Abort turn without dispatch] F -- No --> H[Local development bypass]Reviews (2): Last reviewed commit: "fix(agentex): Slack gateway fails closed..." | Re-trigger Greptile