# Channel adapters Channel integrations authenticate external events, resolve an owned Agent and environment through a source binding, and submit input to a Session. They share the same validation and native Codex harness as API-created Sessions. The bound Agent and environment define the execution capabilities. Offline tests inject deterministic execution ports; deployed Sessions use the configured model credentials and can incur model usage. The adapters intentionally separate: - the **source identity** authenticated by the inbound request; - the **destination identity** selected for a terminal response; and - the **credential identity** used for repository access or outbound provider APIs. A delivery route is an opaque local name or provider channel ID. It is never a webhook URL, token, owner ID, or Secrets Manager ARN supplied by an untrusted user. Use `terraform -chdir=infra output -json webhook_urls` to obtain enabled endpoints. The paths are `POST /webhooks/github`, `/webhooks/gitlab`, `/webhooks/teams`, and `/webhooks/slack`; a route exists only when its ingress secret ARN is configured. The API hostname changes per stack, so do not copy a URL from another environment. ## Secret formats Secrets Manager values may be raw strings or the following JSON shapes: | Setting | Accepted JSON keys | | --- | --- | | GitHub webhook secret | `secret`, `webhook_secret` | | GitHub clone/comment token | `token`, `access_token` (`password` is also accepted by clone only) | | GitLab signing/legacy token | `signing_token`, `token`, `secret`, `webhook_secret` | | GitLab clone/note token | `token`, `access_token` (`password` is also accepted by clone only) | | Teams outgoing-webhook HMAC secret | `secret`, `hmac_secret` | | Teams Workflow URL | `url`, `webhook_url` | | Teams threaded-reply gateway URL | `url`, `webhook_url` | | Slack signing secret | `secret`, `signing_secret` | | Slack bot token | `token`, `bot_token` | Keep ingress, clone, and outbound API credentials in different secrets. The module exposes separate clone and notification ARNs so the worker never receives a provider-write credential. Pin IAM policies to exact secret ARNs. ## GitHub ### Current behavior The GitHub Lambda validates `X-Hub-Signature-256` over the exact raw request body using HMAC-SHA256 before parsing JSON. `X-GitHub-Delivery` is the provider event identity and idempotency source. Accepted events are: | GitHub event | Accepted actions | Result destination | | --- | --- | --- | | `pull_request` | `opened`, `reopened`, `synchronize`, `ready_for_review` | PR issue comment | | `issue_comment` | `created`, only when the issue is a PR and body contains the configured trigger | PR issue comment | Other validly signed events return `202` with `{"accepted":false,"ignored":true}`. The review request checks out the head SHA read-only and fetches the base ref for diff context. Comment requests are case-insensitively gated by `github_comment_trigger` (injected as `GITHUB_COMMENT_TRIGGER`; `@rat-things` by default). This is a cost and noise filter, not authorization: anyone allowed to comment can include the trigger. Retain event, installation, repository, rate, and budget controls. The normalizer requires the configured trigger to be non-empty. Provider result replies include the hidden `rat-things:result` marker, and ingress ignores marked replies and comments whose GitHub user type is `Bot`. These controls prevent the runtime's ordinary result replies from starting another run even when generated text repeats the trigger. They do not authorize human authors; keep repository, actor, budget, and rate policy separate. ### Configure GitHub Configure the signed webhook and its owned Agent/environment binding as described in [Connect a GitHub webhook](github-webhook-onboarding.md). 1. Create a high-entropy webhook secret, a clone-only credential, and a separate comment-only credential in Secrets Manager. The current implementation consumes a static token; `installationId` is retained as source metadata but the runtime does not yet mint short-lived GitHub App installation tokens. 2. Set `github_webhook_secret_arn`, `github_clone_token_secret_arn`, and `github_notify_token_secret_arn`. Limit each token independently to the intended repositories and its one operation. 3. In the GitHub App or repository webhook, set the payload URL to the stack's GitHub webhook output, content type to `application/json`, and secret to the same ingress secret value. 4. Set a distinct command-like `github_comment_trigger`, and subscribe only to pull-request and issue-comment events. Use a separate App/webhook per environment so delivery IDs, permissions, URLs, and secrets do not cross dev/prod boundaries. 5. Create an owner-authenticated source binding for the repository, selecting an Agent and environment. A signed event without a matching binding returns `source_not_bound`. Each accepted occurrence creates a Session; saved terminal root Turns drive comment delivery. For `issue_comment`, the adapter checks out `refs/pull//head` so follow-up questions inspect the pull request rather than the repository's default branch. That synthetic ref is mutable; a future adapter should resolve it to a commit SHA at authenticated ingress when exact replay reproducibility is required. GitHub signs webhook deliveries as documented in [Validating webhook deliveries](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries). Use a GitHub App with short-lived installation tokens as the production credential design; the static token adapter is an explicit maturity gap. For GitHub Enterprise Server, add its clone hostname to `allowed_repository_hosts` and set the validated `github_api_base_url` input to that installation's REST `/api/v3` endpoint. The public default is `https://api.github.com`. Keep both values environment-specific and integration-test clone and `source` delivery together. ## GitLab ### Current behavior For GitLab 19 Standard Webhooks, the Lambda requires `webhook-id`, a `webhook-timestamp` within five minutes, and at least one matching `v1,` entry in `webhook-signature`. It strips the `whsec_` prefix from the configured signing token, strictly decodes its 32-byte key, and verifies HMAC-SHA256 over `webhook-id.webhook-timestamp.raw-body` with a timing-safe comparison before parsing JSON. A present but invalid Standard signature is rejected; it never downgrades to legacy auth. When no `webhook-signature` header is present, the handler retains `X-Gitlab-Token` timing-safe comparison for older GitLab installations. This legacy token authenticates a shared value but does not sign the body or provide timestamp replay protection; do not choose it for a new GitLab 19+ webhook. Idempotency prefers `webhook-id`, then `Idempotency-Key`, `X-Gitlab-Webhook-UUID`, `X-Request-ID`, and finally a SHA-256 hash of the raw body. Accepted payloads are: | GitLab object kind | Accepted actions | Result destination | | --- | --- | --- | | `merge_request` | `open`, `reopen`, `update`, `approved` (or omitted action) | Merge-request note | | `note` | Note attached to a merge request whose body contains the configured trigger | Merge-request note | Other authenticated payloads return an accepted/ignored response. GitLab note requests are case-insensitively gated by `gitlab_comment_trigger` (injected as `GITLAB_COMMENT_TRIGGER`; `@rat-things` by default). The trigger controls noise/cost but is not proof that the author is authorized for a particular repository or destination. The normalizer requires the configured trigger to be non-empty. Provider result notes include the hidden `rat-things:result` marker, and ingress ignores marked replies and payload users whose GitLab `bot` field is true. These controls prevent ordinary self-trigger loops even when generated text repeats the trigger. They do not authorize human authors; keep project, actor, budget, and rate policy separate. ### Configure GitLab 1. On GitLab 19+, generate a signing token (`whsec_...`) for the webhook and store it separately from a least-privileged project/group access token in Secrets Manager. Use a legacy secret token only for an older installation that cannot emit Standard Webhooks headers. 2. Set `gitlab_webhook_secret_arn`, `gitlab_clone_token_secret_arn`, and `gitlab_notify_token_secret_arn`. 3. Set a distinct command-like `gitlab_comment_trigger`. Add a project or group webhook using the Terraform-reported GitLab webhook URL and the matching secret token. Enable merge-request events and comments/notes only. 4. Restrict the API credential to read repository and create merge-request notes for the intended projects. Do not embed it in the clone URL. 5. Test in a non-production project. Confirm a redelivery maps to the same run ID and does not create a duplicate note. GitLab documents the signing-token format, Standard Webhooks headers, multi-signature verification, timestamp, idempotency header, and legacy-token warning under [Webhook configuration](https://docs.gitlab.com/user/project/integrations/webhooks/). For GitLab Self-Managed, add its clone hostname to `allowed_repository_hosts` and set the validated `gitlab_api_base_url` input to that installation's REST `/api/v4` endpoint. The public default is `https://gitlab.com/api/v4`. Keep both values environment-specific and integration-test clone and `source` delivery together. ## Microsoft Teams: primary channel, bridge implementation Teams is the preferred chat surface for this subsystem, but the repository currently implements two temporary adapters: For deployment instructions, secret formats, and a live-tenant verification checklist, see [Connect Rat Things to Microsoft Teams](microsoft-teams.md). ```text Teams @mention -> Teams outgoing webhook (HMAC) -> API Gateway + webhook Lambda -> durable Session input, then acknowledgement (five-second provider deadline) saved terminal root Turn -> independent provider delivery -> Teams Workflow incoming URL -> Adaptive Card in the Workflow's configured destination ``` ### Current outgoing-webhook ingress Create an outgoing webhook for the target team, store the base64 HMAC secret Teams returns in Secrets Manager, and configure its callback URL from the Terraform output. The handler validates the `Authorization: HMAC ...` signature, removes the bot mention/HTML, durably writes S3/DynamoDB/SQS, and then returns `Rat Things request received. I'll reply when session completes a turn.` in the original reply chain. Completion is asynchronous through the configured Workflow or threaded gateway. The normalizer requires both the provider tenant ID and sender ID; a signed activity missing either identity is rejected. The owned source binding determines Session ownership; tenant, sender and thread determine provider continuity. The handler is configured with a five-second timeout, so the synchronous persistence path does not guarantee an acknowledgement under cold-start or AWS-service latency; this is another reason to replace the bridge with a production gateway/ingest design. This adapter inherits the documented Teams outgoing-webhook constraints: - it is team-scoped and works only in public channels; - it is reactive to an `@mention`, not a general bot conversation; - it must return synchronously within five seconds; - it cannot access Teams APIs such as the roster or channel list; and - card actions are limited. See Microsoft's [Create an outgoing webhook](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-outgoing-webhook). ### Current Workflow egress Create a Teams Workflow using an incoming-webhook trigger, grant it only the intended channel, and store its default URL through `teams_workflow_url_secret_arn`. For named destinations, map opaque route names to Workflow URL secret ARNs with `teams_route_secret_arns`; Terraform injects the map as `TEAMS_ROUTES_JSON`. Callers receive route names, never URLs or ARNs. An unknown named route is rejected rather than falling back to the default. The notifier sends an Adaptive Card containing terminal status, a truncated body, and run ID. It does not use the inbound `conversationId` to post a proactive reply. Consequently, a `source` destination means “the configured Teams Workflow destination,” which may not be the exact originating thread. Microsoft recommends Workflows as the successor path while legacy Microsoft 365 connectors approach retirement, but Workflows still have operational constraints: flows are owned by specific users and can become orphaned without co-owners, private-channel support is limited, and webhook-trigger/card features do not equal a full bot. Review [Create incoming webhooks with Workflows](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using) and the [Teams connector reference](https://learn.microsoft.com/en-us/connectors/teams/). Treat the Workflow URL as a bearer credential. Rotate it on exposure, assign co-owners, monitor flow failures/throttling, and keep dev/prod flows separate. ### Threaded reply gateway contract Set `teams_delivery_mode = "threaded-gateway"` and configure `teams_reply_gateway_url_secret_arn` to route source replies through a trusted gateway instead of a Workflow. The notifier posts a versioned envelope containing the original `conversationId`, the inbound activity ID as `replyToActivityId`, a Bot Activity-shaped message with `replyToId`, and the Turn ID as an idempotency key. Named Workflow routes are rejected in this mode. The original conversation/activity reference is retained through Session ingress and terminal Turn delivery. The URL is still a credential and must point only at infrastructure controlled by the deployment. This repository does not yet implement the gateway's Microsoft Entra token exchange, Bot Connector authentication, or live tenant installation. ### Recommended production Teams gateway An AWS-hosted Teams app/bot gateway can replace the bridge while retaining the Session contract: 1. Register a Microsoft Entra/Bot identity and Teams app. Point its HTTPS messaging endpoint at an API Gateway/Lambda adapter in this repository. 2. Validate Bot Framework service JWTs (issuer, audience, lifetime, signing keys) and enforce tenant allowlists before creating a trusted Teams source. Do not treat activity JSON fields as proof of identity. 3. Use the [Teams SDK](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-sdk/) for the Teams-facing gateway. The JavaScript and C# SDKs are GA; verify language status before choosing an implementation. 4. Store an authorized conversation reference when the app is installed or messaged. Map it to a server-side destination ID; never accept a raw callback URL or arbitrary conversation ID as a public run destination. 5. Submit through a privileged internal adapter that preserves the authenticated Teams source. The general control API deliberately overwrites `source` and is not a substitute for this trust boundary. 6. On terminal events, obtain an app/service token and send a proactive message to the stored conversation. The app must already be installed in the destination. Follow Microsoft's [proactive messaging rules](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) and [Bot Connector authentication](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-4.0). This keeps AWS as the compute/data plane while using the Microsoft identity and messaging plane that Teams requires. It enables tenant policy, installation lifecycle, exact conversation references, proactive completion, and future ordinary-input/cancel actions without coupling runs to a Power Automate owner. It must not add a mid-Turn authority-widening path. ## Slack: self-hosted channel adapter Slack is an opt-in provider channel in each self-hosted deployment. The ingress validates Slack v0 signatures and rejects timestamps with more than five minutes of skew. It answers URL-verification challenges and accepts only `app_mention` events. Mentions carrying `bot_id`, `bot_profile`, or the `bot_message` subtype are ignored so the adapter does not consume its own bot output. Results use `chat.postMessage`, preserving the source thread when one exists. For the user-facing workflows this enables—workspace research, durable follow-ups, controlled posting/reactions, scheduled briefings, and broader MicroVM work launched from a mention—start with [Use Rat Things from Slack](slack.md). Like the Teams bridge, Slack currently performs secret resolution plus durable S3/DynamoDB/SQS submission before acknowledging the event, inside a five-second Lambda timeout. Slack requires an HTTP 2xx within **three seconds** and recommends acknowledging before processing; this synchronous path can therefore time out at Slack and cause provider retries even when AWS later accepted the run. Idempotency limits duplicate runs, but not latency or retry noise. Treat an acknowledge-first durable ingress queue/worker split as a production requirement and validate cold starts against Slack's [Events API response contract](https://docs.slack.dev/apis/events-api/). To enable it: 1. Create a Slack app, register Terraform's `oauth_callback_url`, set the Slack event request URL to the `webhook_urls.slack` output, and subscribe to `app_mention`. 2. Request bot scopes `app_mentions:read`, `chat:write`, and `reactions:write`, plus user scope `search:read`. Reinstall the app after changing scopes. 3. Put the OAuth application's `client_id`/`client_secret` and the separate Slack signing secret in Secrets Manager. Configure their ARNs and enable the Slack webhook route; do not store an issued bot or user token in Terraform. 4. Complete the Connections-page flow or `rat-things connect slack --oauth --wait --access read-write --alias slack-work`. 5. Run `rat-things slack-events slack-work --agent-id agent_example --json`. It derives the team selector from the verified Connection, creates one owner Connection Set/binding, and rejects a competing Connection for the same workspace. The service Connection remains write-capable for trusted threaded delivery while the source agent receives the named fixed profile. 6. Test URL verification, a real mention, a same-thread continuation, replayed event, stale signature, read-only write denial, delegated search, and both bot/user token refresh paths. Slack event IDs are the idempotency source. Accepted mentions require both `team_id` and the event's user ID, deriving ownership as `slack::`; missing identity is ignored. Destination channel/thread metadata does not establish the run owner and the bot token does not establish the inbound sender. Delegated search visibility is exactly the installing Slack user's visibility; it is not bot-wide or workspace-administrator search authority. For dated live-provider coverage and remaining validation work, see [Status and roadmap](status-and-roadmap.md).