기능연동

Slack

Slack 연동에서 지금 되는 것과 아직 없는 것

세션이 사람의 결정(도구 권한, 질문 답변, push 승인)을 기다리면 그 스레드에 카드가 올라오고 Slack에서 바로 처리할 수 있다. 승인과 답변을 본다.

지금 있는 것API누가
내 Slack 계정을 웹 계정에 연결POST /v1/linked-accounts/slack/start, …/callback, GET /v1/linked-accounts, DELETE /v1/linked-accounts/{id}로그인한 본인
에이전트에 Slack 앱(bot) 연결POST·GET /v1/agents/{id}/identities, DELETE …/identities/{identity_id}owner·admin·그 에이전트 관리자
guest의 에이전트 사용 허용POST /v1/workspaces/{id}/slack-guestowner·admin

셋 다 웹 로그인 cookie로만 부른다. API key는 403이다. 아래 예제는 만들기의 WEB 헤더와 WS를 쓴다.

guest 사용 설정

Slack guest(멀티·단일 채널 guest)가 에이전트를 쓸 수 있는지 정한다. 기본은 꺼짐이다.

WS=$(curl -sS "$API/v1/auth/me" "${WEB[@]}" | jq -r .workspace.id)
curl -sS "$API/v1/workspaces/$WS/slack-guest" "${WEB[@]}" \
  -d '{"slack_guest_enabled": true}' | jq .
{
  "slack_guest_enabled": true
}

켜도 guest는 자기 Slack 계정을 연결해야 쓴다. Slack Connect로 들어온 다른 회사 사용자는 이 설정과 상관없이 쓸 수 없다.

Slack 계정 연결

Sign in with Slack(OpenID Connect)으로 연결한다. 운영자가 콘솔의 공개 주소 CONSOLE_ORIGIN을 설정하고, 워크스페이스 관리자가 설정 > 워크스페이스 > 연동 > Slack에 App Configuration Token을 넣어 계정 연결용 앱을 만들 수 있게 하며, 그 워크스페이스에 확인된 Slack 설치가 있어야 한다. 로컬 스택에는 CONSOLE_ORIGIN이 없어서 시작 요청이 503이다.

curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$API/v1/linked-accounts/slack/start" "${WEB[@]}"
503

설정된 설치에서는 시작 요청이 authorize_url을 돌려준다. 그 주소에서 Slack에 동의하면 웹의 연결 화면이 code와 state를 callback에 넘기고 연결이 끝난다. 연결 상태는 active·stale·revoked다.

연결 응답과 GET /v1/linked-accounts의 각 항목에는 Slack이 연결 때 알려 준 이름도 있다. external_user_name은 Slack 표시 이름(id_token의 name), external_tenant_name은 Slack 워크스페이스 이름(https://slack.com/team_name)이다. 다시 연결하면 새 이름으로 바뀐다. 이름을 저장하기 전에 만든 연결이나 Slack이 이름을 주지 않은 연결은 null이고, 웹 화면은 그때 ID를 보인다. Slack 토큰은 저장하지 않는다. 그래서 provider 토큰의 만료 시각인 expires_at은 Slack 연결에서 늘 null이다. 시작 응답의 expires_at은 연결 요청이 유효한 시각이라 이 값과 다르다.

응답뜻
409 SLACK_NOT_INSTALLED이 워크스페이스에 확인된 Slack 설치가 없다
409 LINK_TEAM_MISMATCH설치되지 않은 Slack 팀의 계정이다
409 LINK_TAKEN그 Slack 계정이 다른 사람에게 연결돼 있다
409 LINK_EXISTS같은 팀에 내 연결이 이미 있다

알림 언어

플랫폼이 Slack에 쓰는 안내 문구(거절 안내, 실패·답변 대기 안내, 웹에서 열기 링크, 웹·API로 보낸 지시의 (웹 콘솔) 표시)는 사용자가 웹 콘솔의 언어·테마에서 고른 언어(한국어·日本語·English)를 따른다. 콘솔이 고른 언어를 서버에 저장한다(PATCH /v1/auth/me). 저장한 언어가 없는데 콘솔이 영어가 아닌 언어로 열리면(브라우저 언어 등) 그 언어를 저장해, 콘솔과 Slack의 언어를 맞춘다.

  • 한 사람에게만 보이는 거절 안내는 그 Slack 계정에 연결된 사용자의 언어로 쓴다.
  • 스레드에 올라가는 안내와 지시 표시는 세션을 시작한 사용자의 언어로 쓴다. 스레드는 여러 사람이 함께 보므로 한 언어로 맞춘다.
  • 언어를 저장한 적 없는 사용자와 연결되지 않은 Slack 계정은 영어로 받는다.

모델이 쓴 답변과 사용자가 입력한 내용은 번역하지 않는다. 배포 환영 메시지는 받는 사람이 정해지지 않아 한국어로 보낸다.

승인과 답변

Slack 스레드에서 시작한 세션이 사람을 기다리면, 기다리는 요청마다 그 스레드에 카드가 하나 올라온다. 세션을 시작한 사람을 멘션하고, 웹에서 같은 요청을 여는 링크와 만료 시각을 함께 보인다. 카드는 게시할 때 세션을 시작한 사람의 언어(알림 언어)로 쓰고, 처리한 뒤에도 그 언어를 유지한다.

도구 권한 카드는 무엇을 하려는지 한 줄로 요약해 보인다. 예를 들어 명령 실행: bun test, 파일 수정: src/app.ts, PR 열기: 제목 (브랜치) 같은 식이다. 파일을 쓰거나 고치는 도구는 요약 아래에 쓸 내용의 앞 세 줄을 함께 보인다. 모델이 넘긴 입력 원본은 입력 원본 아래에 접혀 있어, 펼쳐야 보인다. 원본은 필드마다 이름: 값으로 보이고, 값 안의 줄바꿈은 \n이 아니라 실제 줄바꿈으로 보인다. 긴 원본은 잘리므로 전체는 웹에서 열기로 본다. push 승인 카드도 저장소·브랜치·커밋을 먼저 보이고, 나머지는 자세히 아래에 접어 둔다. 모델이 쓴 글은 어디에서도 멘션이나 링크가 되지 않는다.

요청카드에서 하는 일
도구 권한허용·거부. 엔진이 되풀이해서 묻는 규칙을 제안했으면 이 세션 동안 허용이 더해진다. 규칙은 카드와 확인 창에 보인다. 리다이렉션·파이프·복합 명령처럼 제안된 규칙이 그 호출을 덮지 못할 수 있는 Bash 호출에는 버튼을 내지 않는다. 누르면 이 세션이 끝날 때까지 그 규칙에 맞는 호출을 묻지 않는다. 처리된 카드는 이 세션 동안 허용됨이 된다. push 승인과 서버 허용 목록은 이 버튼으로 바뀌지 않는다
질문질문 하나에 선택지가 5개 이하면 선택지 버튼, 직접 입력할 수 있으면 직접 입력. 그 밖에는 답하기를 눌러 열리는 창에서 답한다. 질문이 10개를 넘거나 한 질문의 선택지가 100개를 넘으면 Slack 창에 담을 수 없어, 카드는 "이 요청은 웹에서 답해 주세요"와 웹에서 열기 버튼만 보인다
push 승인저장소·브랜치·커밋을 보고 승인(한 번 더 확인)·거절

누른 사람은 Slack 계정이 웹 계정에 연결돼 있어야 하고(Slack 계정 연결), 웹에서 그 세션의 요청에 답할 수 있는 사람이어야 한다. 웹과 같은 규칙으로 판단한다. 연결되지 않은 계정에는 연결 페이지 링크를, 권한이 없는 사람에게는 거절 안내를 그 사람에게만 보이고 아무것도 처리하지 않는다.

처리되면 카드에서 버튼이 사라지고 결과가 남는다. Slack에서 누르든 웹에서 답하든 같다. 웹에서 답했거나, 요청이 만료됐거나, 세션이 끝나 요청이 닫혔으면 control-host 안의 action-broker가 잠시 뒤 Slack 카드를 같은 모양으로 고친다. 첫 줄은 결과와 처리한 사람이다. Slack에서 처리했으면 허용됨 · @이름, 웹에서 처리했으면 웹에서 허용됨 · @이름처럼 어디서 처리했는지가 함께 보인다. 처리한 사람의 Slack 계정이 연결되지 않았으면 이름 없이 결과만 보인다. 도구 권한과 push 카드는 이 결과에 요청 요약과 웹 링크를 붙인 한 줄이 된다. 질문 카드는 질문 전문을 그대로 두고, 그 아래에 고른 답을 <선택지> 선택됨(웹에서 답했으면 웹에서 <선택지> 선택됨)으로 보인다. 직접 입력한 글은 내용 없이 직접 입력으로만 표시한다. 스레드는 여러 사람이 보기 때문이다. 만료됐거나 세션이 끝나 닫힌 요청은 아무도 멘션하지 않는다. 질문 카드는 질문을 남긴 채, 만료됐으면 만료됨을, 세션이 끝나 닫혔으면 더 이상 답할 수 없어요를 보인다. Slack이 카드를 고치지 못하면 잠시 뒤 다시 시도하고, 메시지가 지워졌거나 채널이 보관됐으면 그대로 둔다. 카드는 누른 메시지가 아니라 저장된 요청에서 다시 그린다. 웹이나 다른 사람이 먼저 처리한 요청을 누르면 다시 처리하지 않고 지금 결과로 카드를 바꾼다. push를 Slack에서 승인·거절하면 그 결과를 세션에 알리는 지시가 이어져, 세션이 PR을 열거나 거절을 알린다. 승인할 때 세션의 턴이 돌고 있으면 그 지시는 그 턴이 끝날 때까지 기다리고, 그 턴이 PR을 이미 열었으면 넣지 않는다. 그 브랜치에 PR이 이미 있어도 넣지 않는다. 그래서 같은 PR을 두 번 열거나 같은 답을 두 번 하지 않는다.

버튼과 창은 에이전트 앱의 Interactivity 주소 /v1/slack/interactions/{agent_id}로 온다. 요청은 이벤트와 같이 그 앱의 signing secret 서명과 300초 안의 시각을 확인한다. 설치 finalize가 Event Subscriptions 주소와 함께 manifest에 넣는다. 이 기능 전에 만든 앱은 다시 확인(POST /v1/agents/{id}/slack/app/events/recheck)으로 manifest를 다시 보내면 등록된다.

원래 Slack 메시지 링크

Slack 멘션·DM·스레드 답글로 시작하거나 보낸 지시는 그 Slack 메시지의 위치(팀·채널·ts, 답글이면 thread_ts)와 Slack이 준 링크를 함께 남긴다. 링크는 chat.getPermalink로 받고, 이 호출에는 scope가 더 필요 없다. 웹 세션 화면은 머리의 시작한 사람·경로 옆과 대화의 Slack 지시 말풍선에 Slack에서 보기 링크를 보이고, 누르면 새 탭에서 열린다. API에서는 세션의 slack_permalink(세션을 시작한 메시지)와 turn의 slack_permalink(그 지시를 보낸 메시지)로 읽는다.

비공개 채널과 DM도 링크만 남기고 메시지 본문을 더 저장하지 않는다(지시로 받은 글은 원래대로 turn의 message에 있다). 링크는 Slack에서 그 메시지를 읽을 수 있는 사람에게만 열린다. 링크를 남기기 전에 만든 세션·지시, Slack이 링크를 주지 않은 지시는 null이고 화면에 링크가 없다.

Slack 설치 등록

운영자가 터미널에서 쓰는 경로다. Slack 워크스페이스에 설치된 Slack 앱이 있으면, 그 앱의 bot token으로 설치를 등록한다. 콘솔에서 Slack 앱을 자동으로 만들고 설치하는 흐름은 다음 버전에 들어온다. CLI가 Slack의 auth.test·bots.info로 팀, bot 사용자, 앱을 확인하고 확인된(verified) 설치를 남긴다. 앱에는 bot scope users:read가 있어야 한다. token은 환경 변수 SLACK_BOT_TOKEN으로만 넘긴다. CLI는 token을 인자로 받지 않고 출력하지도 않는다.

read -rs SLACK_BOT_TOKEN && export SLACK_BOT_TOKEN
bun run slack-install register --workspace "$WS" --app-id A0123ABCD
unset SLACK_BOT_TOKEN
{"outcome":"verified","registration":"registered","installation_id":"…","workspace_id":"…","app_id":"A0123ABCD","team_id":"T0123ABCD","bot_user_id":"U0123ABCD"}

같은 명령을 다시 실행하면 같은 설치를 다시 확인한다(registration: existing). 실패하면 stderr에 한 줄을 쓰고 아래 종료 코드로 끝난다.

종료 코드뜻
0확인된 설치가 있다
2인자나 환경 변수가 잘못됐다
3Slack이 token을 거절했거나 Slack에 닿지 않는다
4그 앱과 팀이 다른 워크스페이스의 것이다(workspace_conflict). 거기서 해제됐어도 그 워크스페이스만 다시 등록할 수 있다
5등록하는 동안 설치가 해제됐다(revoked). 다시 실행하면 다시 연다
6워크스페이스가 없다(workspace_not_found)
7token이 --app-id와 다른 앱의 것이다(app_mismatch)
1그 밖의 오류

에이전트의 Slack 앱

앱 설정 읽기

에이전트 관리자(owner·admin 포함)가 웹 세션으로 호출한다. GET /v1/agents/{id}/slack/app은 앱과 함께 event_url과 events를 반환한다. event_url은 서버가 콘솔의 공개 주소 CONSOLE_ORIGIN에서 계산한 https 주소에 /v1/slack/events/{agent_id}를 붙인 절대 주소이며, 이벤트 입구를 콘솔과 따로 둔 서버는 그 입구 주소(SLACK_EVENTS_PUBLIC_URL)를 쓴다. 계산할 수 없으면 null이다. events.state는 pending·verified·failed다. 설치 finalize가 manifest에 이 주소를 Request URL로 넣으면 pending이 되고 events.url에 보낸 주소가 남는다(보내기 전에는 null). 이미 verified인 앱에 같은 주소를 다시 넣으면 verified를 유지하고, 주소가 바뀌면 다시 pending이 된다. 그 앱의 signing secret으로 서명된 요청(url_verification 또는 이벤트)이 처음 도착하면 verified가 된다. url_verification에는 서명과 시각을 확인한 뒤 challenge 값만 돌려준다. manifest 등록이 실패하면 failed가 되고 비밀 값 없는 error 코드가 온다. checked_at은 상태가 마지막으로 바뀐 시각(아직 없으면 null)이다.

finalize 뒤에도 pending이면 Slack이 아직 이 주소를 확인하지 못한 것이다. 공개 DNS가 주소를 아직 풀지 못했을 수도 있고, Slack이 manifest로 넣은 URL을 확인하지 않았을 수도 있다. POST /v1/agents/{id}/slack/app/events/recheck(콘솔 Slack 탭 Slack 앱에 넣을 주소의 다시 확인)는 manifest를 Request URL과 함께 다시 보낸다. 그래도 pending이면 Slack 앱 설정의 Event Subscriptions 화면에서 Request URL의 Verify(또는 Retry)를 누른다. 콘솔의 Slack 탭은 주소를 보낸 뒤 pending인 동안 같은 안내와 그 화면 링크를 보여 준다. 로컬 스택에서 scripts/slack-funnel.sh on으로 주소를 열었다면, 마지막 줄의 공개 DNS 점검(dns.google·cloudflare-dns.com)에서 경고가 사라질 때까지 기다린 뒤 Slack 앱을 만든다.

GET /v1/agents/{id}/slack/app/manifest는 현재 에이전트와 서버 설정으로 만든 manifest를 manifest_yaml로 반환한다. 비밀 값은 포함하지 않는다.

GET /v1/agents/{id}/slack-channels?ids=C1,C2는 최대 50개 ID의 이름을 조회한다. items에는 id·name·state가 있고 Slack이 없거나 접근할 수 없다고 답한 ID만 이름과 상태가 unknown이다. 통신 실패·상위 서버 오류는 캐시하지 않고 요청 전체가 503이며, 회수됐거나 유효하지 않은 bot token은 409 SLACK_NOT_INSTALLED다. cursor와 전체 스캔은 없다. 앱·채널별 60초 캐시를 쓰며 Slack 429는 Retry-After를 포함한 503이다.

GET·PUT /v1/agents/{id}/slack/app/settings는 { "mention_only": true }를 읽고 저장한다. true면 @멘션했을 때만, false면 멘션 없이 채널에 쓴 글에도 반응한다. 어느 값이든 에이전트가 이미 대화 중인 스레드(세션이 연결된 스레드)의 답글은 멘션 없이도 그 세션으로 이어진다. 이미 연결된 채널을 포함해 사용 중인 모든 채널에 적용되고, DM에는 적용되지 않는다. 초기값은 true이며 앱 생성 전에도 저장할 수 있다. 앱 전체 켜기·끄기는 제공하지 않는다.

binding의 mode는 off(이 채널에서 쓰지 않음)·on(사용)이다. mention_only가 false면 사용 중인 채널에서 사람이 멘션 없이 채널에 바로 쓴 글에도 세션을 연다. 에이전트가 참여하지 않은 스레드의 답글에는 반응하지 않는다.

에이전트마다 자기 Slack 앱을 갖는다. 연결에는 앱 id, 팀 id, bot token이 든 자격 참조의 이름(credential_ref, 값이 아니다)을 보낸다.

curl -sS "$API/v1/agents/$AID/identities" "${WEB[@]}" \
  -d '{"provider": "slack_bot", "app_id": "A0APP", "team_id": "T0TEAM", "credential_ref": "slack/research-bot-token"}'

확인된 Slack 설치가 없으면 409 SLACK_NOT_INSTALLED다. 설치는 운영자가 위의 Slack 설치 등록 CLI로 만든다.

Slack에서 앱을 제거하면 연결이 해제된다. app_uninstalled나 bot token이 든 tokens_revoked 이벤트가 오면 설치가 revoked가 되고, 그 설치의 활성 연결도 같이 revoked가 된다(revoked_at이 채워진다). 그 연결의 채널 binding과 자격 참조 이름도 풀린다. 콘솔의 Slack 탭은 "연결됨" 대신 "재설치 필요"를 보인다. 같은 이벤트가 다시 오거나 재설치 뒤에 늦게 도착해도 상태는 그대로다. 다시 설치하면 새 활성 연결이 만들어지고, 이전 연결은 해제된 기록으로 남는다.

이벤트가 오지 않는 제거도 있다. 채널 목록·이름 조회·채널 binding처럼 bot token으로 Slack을 부른 요청이 token_revoked나 account_inactive로 거절되면 같은 해제를 한다. 이때도 응답은 409 SLACK_NOT_INSTALLED다. invalid_auth·not_authed·token_expired처럼 제거 말고도 나올 수 있는 오류, 429·5xx·통신 실패는 상태를 바꾸지 않는다.

채널 참가와 배포 환영

에이전트의 채널(PUT /v1/agents/{id}/slack-bindings)을 저장하면 서버가 bot을 그 채널에 넣는다. 공개 채널은 bot이 스스로 들어간다. 비공개 채널은 Slack이 막으니 채널에서 /invite @<bot>으로 초대한다. 초대되면 서버가 알아채고 이어 간다. 결과는 binding의 membership.state로 보인다. 저장한 적 없는 채널에 bot을 초대해도 서버가 그 채널을 사용(on)으로 연결한다. 이미 저장한 채널은 설정을 바꾸지 않는다(꺼 둔 채널은 꺼진 채로 둔다).

에이전트가 채널에 쓴 답은 그 채널의 모든 사람이 읽는다. 담당 팀 밖 사람도, 워크스페이스 설정으로 그 세션을 콘솔에서 못 보는 사람도 포함된다. 콘솔의 채널 칸도 이 점을 적는다.

membership.state뜻
joinedbot이 채널에 있다
awaiting_invite비공개 채널이라 초대를 기다린다
config_errorSlack이 token이나 scope를 거절했다(error에 Slack 오류 코드). 앱을 다시 설치한다
join_failed보관된 채널이거나 워크스페이스 정책이 막았다(error)

release가 처음 배포되면 home 채널에 안녕하세요, <에이전트 이름>입니다. 이 채널에 연결됐어요. 멘션하면 답할게요가 간다. 이 인사는 에이전트마다 채널당 한 번이다. 새 release를 배포하거나, 같은 정의를 다시 제출하거나, 이전 release로 되돌리거나, Slack 연결을 끊었다가 다시 이어도 이미 인사한 채널에는 다시 보내지 않는다. home을 아직 인사하지 않은 채널로 옮기면 그 채널에 한 번 간다. bot이 home에 없으면 들어올 때까지 기다린다. 환영이나 참가가 실패해도 배포는 실패하지 않는다.

다음 단계

이 페이지