Skip to content

refactor(agentex): Slack gateway reads bot credentials from env, not DB - #394

Merged
michael-chou359 merged 2 commits into
mainfrom
mc/slack-gateway-bot-identity
Aug 4, 2026
Merged

refactor(agentex): Slack gateway reads bot credentials from env, not DB#394
michael-chou359 merged 2 commits into
mainfrom
mc/slack-gateway-bot-identity

Conversation

@michael-chou359

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

Copy link
Copy Markdown
Contributor

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_keys DB 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

  • Drop _gateway_secret and the DB-store name constants; _fetch_bot_token, _fetch_signing_secret, and _acting_identity 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.

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.

  • Reads the signing secret, bot token, bot API key, and account ID from environment-backed deployment secrets.
  • Fails closed when the bot API key is absent in an authorization-enabled environment.
  • Removes the unused agent-agnostic API-key repository method and updates unit tests for environment-based credential loading.

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

Filename Overview
agentex/src/domain/use_cases/slack_gateway_use_case.py Replaces database-backed gateway secret reads with environment-backed bot credentials and adds the required fail-closed identity guard.
agentex/src/domain/repositories/agent_api_key_repository.py Removes the now-unused agent-agnostic credential lookup method.
agentex/tests/unit/use_cases/test_slack_gateway_use_case.py Updates credential tests for direct environment reads and covers both fail-closed and local-development missing-key behavior.

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

Reviews (2): Last reviewed commit: "fix(agentex): Slack gateway fails closed..." | Re-trigger Greptile

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
michael-chou359 requested a review from a team as a code owner August 4, 2026 22:39
@michael-chou359
michael-chou359 enabled auto-merge (squash) August 4, 2026 22:41
Comment thread agentex/src/domain/use_cases/slack_gateway_use_case.py Outdated
…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
michael-chou359 merged commit 3d1817c into main Aug 4, 2026
46 checks passed
@michael-chou359
michael-chou359 deleted the mc/slack-gateway-bot-identity branch 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>
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