# Forge Slack Agent

Forge has a dedicated Slack app and connector. It is not part of AIEBot: AIEBot
belongs to the neighboring `aietools` repository and serves AI Engineer
operations, while the Forge app exposes repo-scoped Forge coding agents.

## Authority and isolation

The connector's dedicated D1 database is authoritative for Slack event dedupe,
immutable Slack-thread-to-repository bindings, pending delivery, lifecycle
events, and repair state. Forge's main D1 is authoritative for installations,
encrypted bot tokens, Forge/Slack user links, explicit repository grants,
service-principal generations, rotation and revocation ledgers, messages, exact
source SHA, budgets, receipts, and run state.

The Slack Worker resolves installation and user authority through the named
`SlackIntegrationControl` Worker RPC entrypoint on `cloudforge-api`. The RPC
entrypoint is not mounted on the public HTTP router, and the connector never
receives direct database access. For an authorized turn it receives only that
workspace's bot token and that user/owner boundary's least-privilege Forge API
key. The subsequent Repository Agent call uses the same public API contract
available to customers.

The Worker acknowledges a signed Slack event only after inserting a durable job.
A Cloudflare Workflow owns each runnable turn, while a one-minute cron repairs
lost dispatches and expired leases. Turns within one Slack thread are serialized;
later turns remain queued without consuming their processing deadline. Pending
prompt text is cleared as soon as Forge durably accepts the message. Prompts and
secret values are never logged.

### Service-principal credential lifecycle

Each linked Forge user gets a least-privilege service principal for each
personal or organization owner boundary they explicitly grant. Its encrypted
connector key expires after 30 days and is scheduled for rotation after 25
days. The main Forge API—not the Slack Worker—owns rotation authority.

The minute maintenance loop uses a leased D1 rotation outbox and records key
version plus predecessor/successor lineage. It creates one successor, updates
the encrypted link credential with a compare-and-swap on the predecessor ID,
then decrypts the current consumer value and authenticates it against the
service-principal store. Only after that proof receives an immutable receipt
does Forge revoke the predecessor. A crash in any phase is resumed from the
outbox; an unprovable successor leaves the predecessor active. Overlapping
maintainers cannot claim the same link generation.

Uninstall and unlink requests first fence Slack credential resolution and
enqueue every affected principal in a separate revocation outbox. That outbox
uses bounded leases and backoff, treats an already-revoked principal as success,
and records an immutable terminal failure after 20 attempts. Therefore a
request crash cannot silently leave a still-resolvable connector credential,
and eventual authority revocation remains auditable.

Disconnect also enqueues a private connector-cleanup operation in the same D1
transaction. The Slack Worker claims cleanup jobs through its existing private
`SlackIntegrationControl` RPC, deletes the exact link's jobs and immutable
thread bindings locally, then acknowledges completion. The minute repair loop
retries any crash between the authority fence and connector deletion. Workspace
uninstall deletes the complete installation transport partition. Keeping the
binding one-way also avoids a first-deploy dependency cycle between the Workers.

## Slack contract

- Direct messages only.
- Scopes: `chat:write` and `im:history`.
- Root message: `OWNER/REPO your question`.
- Replies in that Slack thread are plain multi-turn follow-ups.
- One Slack thread is permanently scoped to one repository and one readable
  `agent_thread_...` Forge thread.
- The service principal must have only `agent:read`, `agent:message`, and
  `agent:run` on explicitly granted repositories.
- The final response includes the accepted exact SHA, Forge thread ID, model,
  tokens, and receipt digest.
- Every installation, user link, and repository grant is independent. A Slack
  user in one workspace cannot inherit another user or workspace's authority.

From Forge Settings, a signed-in user can install Forge into a Slack workspace,
link the installing Slack identity, and explicitly grant repositories they own
or administer. Another Slack user who messages an installed bot receives a
single-use Forge identity-link URL before any repository agent can run. A
linked user who names an ungranted repository receives grant guidance rather
than another identity challenge.

The manifest subscribes only to direct messages and Slack credential lifecycle
events. `app_uninstalled`, `tokens_revoked`, and `team_access_revoked` are
persisted in the connector before acknowledgement, fenced in Forge, and retried
by the one-minute repair loop. Each lifecycle job captures both Slack's event
time and the current Forge installation generation. A delayed event cannot
revoke a newer reinstall, while a retry for the same generation is idempotent.
Terminal jobs otherwise expire
after 90 days. Binding rows cannot be updated in place, but explicit
privacy/uninstall deletion is supported.

Slack's own OAuth token rotation remains disabled in this release. Forge still
rotates the per-user service-principal keys described above; Slack bot-token
rotation needs a separate refresh-token lifecycle and is not implied by that
Forge credential policy.

## Local development

Create a separate D1 database and replace the placeholder ID in
`packages/slack-agent/wrangler.toml`, then apply its package-local migration.
The Slack Worker requires only these encrypted secrets:

- `SLACK_APP_ID`
- `SLACK_SIGNING_SECRET`

The main API requires:

- `SLACK_APP_ID`
- `SLACK_CLIENT_ID`
- `SLACK_CLIENT_SECRET`
- `SLACK_OAUTH_STATE_SECRET`
- the existing `SECRET_ENCRYPTION_KEY`

Never add any of those values to source control. Install the checked-in Slack
manifest at `config/slack/forge-agent.yaml`. Its OAuth callback is
`https://forge.smol.ai/api/integrations/slack/oauth/callback`; its Events API
request URL is `https://slack.forge.smol.ai/slack/events`. Bot tokens and Forge
service-principal keys are encrypted control-plane data, not Worker secrets.

The only cross-Worker binding is Slack Worker `FORGE_CONTROL` → API
`SlackIntegrationControl`. Cleanup uses claim/ack methods on that same private
entrypoint; no API → Slack binding or public cleanup endpoint exists.
