API surface check (main, 2026-05-14) — no material drift from #212.
Confirmed endpoints: POST /run_sse · POST /apps/{app}/users/{user}/sessions · GET /ada/healthz · GET /ada/version
SSE event shape: content.parts[] → text | functionCall {id,name,args} | functionResponse {id,name,response}; top-level usageMetadata, partial flag.
Auth: bearer token checked against AI_IDENTITY_API_KEY/ADA_ADMIN_KEY when ADA_REQUIRE_AUTH=1. Everything below is safe to build against.
1. Hosting
Ada already runs on GKE in the ai-identity namespace. Add a dedicated ada-slack Deployment + ClusterIP Service; reuse the existing GKE Ingress with a new /slack/* path rule. A second Ingress for one path is unnecessary toil; path-based routing on the existing one keeps cert management in one place.
GKE manifests (new files k8s/ada-slack-deployment.yaml, k8s/ada-slack-service.yaml):
# Deployment (namespace: ai-identity)
apiVersion: apps/v1
kind: Deployment
metadata:
name: ada-slack
namespace: ai-identity
spec:
replicas: 2
selector:
matchLabels: {app: ada-slack}
template:
metadata:
labels: {app: ada-slack}
spec:
serviceAccountName: ada-gke-sa # Workload Identity → Vertex AI
containers:
- name: ada-slack
image: gcr.io/ai-identity/ada-slack:$(IMAGE_TAG)
env:
- {name: ADA_REQUIRE_AUTH, value: "1"}
- name: ADA_ADMIN_KEY
valueFrom: {secretKeyRef: {name: ada-secrets, key: ada-admin-key}}
- name: SLACK_SIGNING_SECRET
valueFrom: {secretKeyRef: {name: ada-secrets, key: slack-signing-secret}}
- name: SLACK_BOT_TOKEN
valueFrom: {secretKeyRef: {name: ada-secrets, key: slack-bot-token}}
- name: ADA_INTERNAL_URL
value: "http://ada-svc.ai-identity.svc.cluster.local:8000"
ports:
- containerPort: 9000
---
apiVersion: v1
kind: Service
metadata:
name: ada-slack-svc
namespace: ai-identity
spec:
selector: {app: ada-slack}
ports:
- {port: 80, targetPort: 9000}
Secrets (GCP Secret Manager → External Secrets Operator):
slack-signing-secret — from Slack app config
slack-bot-token — xoxb-… for chat.postMessage/chat.update
ada-admin-key — bearer token for internal calls to Ada /run_sse
Image: New docker/ada-slack/Dockerfile (Python 3.12, ~200 LOC FastAPI app). Built in existing Cloud Build pipeline, tagged with git SHA.
Ingress patch (add to existing k8s/ingress.yaml):
- path: /slack/*
backend:
service: {name: ada-slack-svc, port: {number: 80}}
2. Authentication
Slack request verification: Middleware verifies X-Slack-Signature via HMAC-SHA256 over v0:{X-Slack-Request-Timestamp}:{raw_body} with SLACK_SIGNING_SECRET. Reject if |now − timestamp| > 300 s (replay window). This runs before any handler touches the payload.
User attribution: Map Slack user_id → AI Identity user at call time (query /api/v1/users?slack_id= or a lookup table in the ada-slack config). Use the resolved AI Identity user_id as the /run_sse user_id field so audit logs reflect real identities. If no mapping exists, fall back to slack:{team_id}:{user_id} as a synthetic handle — better than collapsing all activity to a shared bot account.
3. Session Strategy
Recommendation: one Slack thread = one Ada session.
Session ID: slack:{team_id}:{channel_id}:{thread_ts} (use the root message ts as thread_ts; follow-up messages in the thread carry that same ts). This preserves context across follow-up questions — the primary value of an agentic assistant. Ada can re-read files across a multi-turn debug session without re-establishing state.
The alternative (ephemeral per-command) is simpler but destroys context on every message, wasting tokens and breaking multi-turn flow. Not worth it.
Expire sessions after 24 h of inactivity via ADK's session TTL to prevent stale context buildup.
4. Response Format
Slack requires an HTTP 200 within 3 seconds. Ada's SSE stream can run 10–60 s.
Flow:
- < 100 ms: Return
HTTP 200 (empty body). Immediately POST _Ada is thinking…_ via response_url (5-min window, no auth).
- Background task (
asyncio.create_task): Open ADK SSE stream. Buffer text; accumulate tool events.
- First content chunk:
chat.update to replace the thinking message.
- Subsequent updates:
chat.update on the same message ts, at most once per second (Slack throttle: ~1 req/s per channel per bot). Batch partial chunks within the 1 s window.
- Stream close: Final
chat.update with complete response; post tool-call summaries as threaded replies (see §5).
For MVP, asyncio.create_task in the FastAPI process is sufficient. Migrate to a task queue (e.g. Cloud Tasks) if pod restarts mid-stream become a problem.
5. Tool-Call Surfacing
Post tool calls as threaded replies on the main Ada message — one reply per unique tool name, collapsing multiple calls to the same tool. Use Block Kit.
Mock Block Kit JSON for a read_file call:
{
"blocks": [
{
"type": "context",
"elements": [{"type": "mrkdwn", "text": ":wrench: *read_file* · `path: agent/ada/agent.py`"}]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Args*\n```{\n \"path\": \"agent/ada/agent.py\"\n}```"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Response* (truncated)\n```from google.adk import Agent\n…```"
}
}
]
}
Truncate response to 500 chars with … [truncated] suffix (Slack block text cap is 3 000 chars). For responses worth preserving in full, upload via files.upload and link in the block.
6. Minimal Infra Changes
| Area |
Change |
| CORS |
None — ada-slack calls Ada internally over the cluster network |
| serve.py / auth |
None — ada-slack calls ada-svc with Authorization: Bearer $ADA_ADMIN_KEY; existing middleware handles it |
| Slack app manifest |
New (see below) |
| GKE |
ada-slack Deployment + Service + Ingress path rule |
| CI/CD |
Add ada-slack image build step to cloudbuild.yaml |
Slack app manifest (slack-manifest.yaml):
display_information:
name: Ada
description: AI Identity senior engineer
background_color: "#b45309"
features:
slash_commands:
- command: /ada
url: https://ada.ai-identity.example.com/slack/slash
description: Ask Ada about the codebase
usage_hint: "[your question]"
should_escape: false
bot_user:
display_name: Ada
always_online: true
oauth_config:
scopes:
bot:
- chat:write
- chat:write.public
- commands
- files:write
settings:
socket_mode_enabled: false
token_rotation_enabled: false
New files:
docker/ada-slack/Dockerfile
docker/ada-slack/main.py (~200 LOC FastAPI: /slack/slash handler + HMAC middleware + SSE bridge)
k8s/ada-slack-deployment.yaml
k8s/ada-slack-service.yaml
- Patch
k8s/ingress.yaml (add /slack/* rule)
slack-manifest.yaml
References: #212 (serve.py + chat UI shipped on main)
1. Hosting
Ada already runs on GKE in the
ai-identitynamespace. Add a dedicatedada-slackDeployment + ClusterIP Service; reuse the existing GKE Ingress with a new/slack/*path rule. A second Ingress for one path is unnecessary toil; path-based routing on the existing one keeps cert management in one place.GKE manifests (new files
k8s/ada-slack-deployment.yaml,k8s/ada-slack-service.yaml):Secrets (GCP Secret Manager → External Secrets Operator):
slack-signing-secret— from Slack app configslack-bot-token—xoxb-…forchat.postMessage/chat.updateada-admin-key— bearer token for internal calls to Ada/run_sseImage: New
docker/ada-slack/Dockerfile(Python 3.12, ~200 LOC FastAPI app). Built in existing Cloud Build pipeline, tagged with git SHA.Ingress patch (add to existing
k8s/ingress.yaml):2. Authentication
Slack request verification: Middleware verifies
X-Slack-Signaturevia HMAC-SHA256 overv0:{X-Slack-Request-Timestamp}:{raw_body}withSLACK_SIGNING_SECRET. Reject if|now − timestamp| > 300 s(replay window). This runs before any handler touches the payload.User attribution: Map Slack
user_id→ AI Identity user at call time (query/api/v1/users?slack_id=or a lookup table in the ada-slack config). Use the resolved AI Identityuser_idas the/run_sseuser_idfield so audit logs reflect real identities. If no mapping exists, fall back toslack:{team_id}:{user_id}as a synthetic handle — better than collapsing all activity to a shared bot account.3. Session Strategy
Recommendation: one Slack thread = one Ada session.
Session ID:
slack:{team_id}:{channel_id}:{thread_ts}(use the root messagetsasthread_ts; follow-up messages in the thread carry that samets). This preserves context across follow-up questions — the primary value of an agentic assistant. Ada can re-read files across a multi-turn debug session without re-establishing state.The alternative (ephemeral per-command) is simpler but destroys context on every message, wasting tokens and breaking multi-turn flow. Not worth it.
Expire sessions after 24 h of inactivity via ADK's session TTL to prevent stale context buildup.
4. Response Format
Slack requires an HTTP 200 within 3 seconds. Ada's SSE stream can run 10–60 s.
Flow:
HTTP 200(empty body). Immediately POST_Ada is thinking…_viaresponse_url(5-min window, no auth).asyncio.create_task): Open ADK SSE stream. Buffer text; accumulate tool events.chat.updateto replace the thinking message.chat.updateon the same messagets, at most once per second (Slack throttle: ~1 req/s per channel per bot). Batch partial chunks within the 1 s window.chat.updatewith complete response; post tool-call summaries as threaded replies (see §5).For MVP,
asyncio.create_taskin the FastAPI process is sufficient. Migrate to a task queue (e.g. Cloud Tasks) if pod restarts mid-stream become a problem.5. Tool-Call Surfacing
Post tool calls as threaded replies on the main Ada message — one reply per unique tool name, collapsing multiple calls to the same tool. Use Block Kit.
Mock Block Kit JSON for a
read_filecall:{ "blocks": [ { "type": "context", "elements": [{"type": "mrkdwn", "text": ":wrench: *read_file* · `path: agent/ada/agent.py`"}] }, { "type": "section", "text": { "type": "mrkdwn", "text": "*Args*\n```{\n \"path\": \"agent/ada/agent.py\"\n}```" } }, { "type": "section", "text": { "type": "mrkdwn", "text": "*Response* (truncated)\n```from google.adk import Agent\n…```" } } ] }Truncate
responseto 500 chars with… [truncated]suffix (Slack block text cap is 3 000 chars). For responses worth preserving in full, upload viafiles.uploadand link in the block.6. Minimal Infra Changes
ada-svcwithAuthorization: Bearer $ADA_ADMIN_KEY; existing middleware handles itada-slackDeployment + Service + Ingress path ruleada-slackimage build step tocloudbuild.yamlSlack app manifest (
slack-manifest.yaml):New files:
docker/ada-slack/Dockerfiledocker/ada-slack/main.py(~200 LOC FastAPI:/slack/slashhandler + HMAC middleware + SSE bridge)k8s/ada-slack-deployment.yamlk8s/ada-slack-service.yamlk8s/ingress.yaml(add/slack/*rule)slack-manifest.yamlReferences: #212 (serve.py + chat UI shipped on main)