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.
- 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;
installationIdis retained as source metadata but the runtime does not yet mint short-lived GitHub App installation tokens. - Set
github_webhook_secret_arn,github_clone_token_secret_arn, andgithub_notify_token_secret_arn. Limit each token independently to the intended repositories and its one operation. - 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. - 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. - 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/<number>/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. 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,<base64> 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#
- 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. - Set
gitlab_webhook_secret_arn,gitlab_clone_token_secret_arn, andgitlab_notify_token_secret_arn. - 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. - Restrict the API credential to read repository and create merge-request notes for the intended projects. Do not embed it in the clone URL.
- 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.
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.
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 <id> 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.
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 and the Teams connector reference.
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:
- Register a Microsoft Entra/Bot identity and Teams app. Point its HTTPS messaging endpoint at an API Gateway/Lambda adapter in this repository.
- 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.
- Use the Teams SDK for the Teams-facing gateway. The JavaScript and C# SDKs are GA; verify language status before choosing an implementation.
- 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.
- Submit through a privileged internal adapter that preserves the authenticated Teams source. The
general control API deliberately overwrites
sourceand is not a substitute for this trust boundary. - 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 and Bot Connector authentication.
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.
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.
To enable it:
- Create a Slack app, register Terraform's
oauth_callback_url, set the Slack event request URL to thewebhook_urls.slackoutput, and subscribe toapp_mention. - Request bot scopes
app_mentions:read,chat:write, andreactions:write, plus user scopesearch:read. Reinstall the app after changing scopes. - Put the OAuth application's
client_id/client_secretand 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. - Complete the Connections-page flow or
rat-things connect slack --oauth --wait --access read-write --alias slack-work. - 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. - 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:<team>:<user>; 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.