- 공급자: `anthropic`(API 키 또는 OAuth Bearer) · `openai`(OpenAI 호환, `baseUrl` 로 다른 호환 공급자) · `local-cli`(서버에 설치·로그인된 Claude Code 또는 Codex CLI 세션, 대화·분석 전용 — 0.3.1 자동 탐색, §6).
- "확인"(`POST /ops/api/ai/connections/:id/test`): 실제 공급자 `GET /models`(CLI 는 `--version`)로 사용 가능 모델을 받아 둔다. 목록에 없는 모델로 바꾸면 `model_unavailable`.
- **인증 키 변경은 고위험**: 키 등록(`POST /connections` with apiKey)·교체(`POST /:id/secret`)·OAuth client secret·연결 삭제는 최근 10분 재인증(`reauth_required`)이 필요하다.
- OAuth: `POST /:id/oauth/start` → `authorizeUrl`(state·PKCE S256) → 공급자 로그인 → `GET /ops/ai/oauth/callback?code&state` → 토큰 교환(`code_verifier`, client secret 은 서버→토큰 끝점에만) → Vault 저장 → `/ops/ai?oauth=ok`. state 는 해시 저장·10분·1회용, 시작한 운영자와 같은 세션만 허용. 만료 토큰은 `refresh_token` 으로 서버가 갱신하고 실패하면 `reauth_required`.
- 공개 객체의 `secret` 은 `{ hasKey, hint }` / `{ hasToken, expiresAt, hasRefresh }` / `{ cli: true }` 뿐이다.
## 3. 방과 규칙
-`GET /ops/api/ai/rooms`: `[통합(fixed), 개별…]` — 통합이 항상 먼저. 개별 방 `title` 은 비우면 연결 이름(`PATCH /rooms/:id {title}` 로 수정, 통합은 `room_fixed`).
-`POST /rooms/:id/messages {text, context?}`:
- 개별 방 → 그 모델만(연결이 `active` 가 아니면 409 `connection_inactive`).
- 통합 방 → 활성 모델 모두. `@이름`(어디든) 또는 글머리 `이름:`/`이름,` → 그 모델만. 모르는 `@이름` → 400 `unknown_addressee`(참여자 목록 포함).
- 응답은 비동기 턴. `GET /rooms/:id/messages?after=<seq>` 가 새 메시지·`pending`(응답 중 턴 수)·`actions`(제안)를 준다.
- 모델→모델 전달: 모델 답에 `@다른이름` 이 있으면 그 모델이 **자동 1회** 답한다(`relayDepth` 1, 화면에 "전달 응답"). depth 1 의 답이 또 부르면 HAL 안내만 남기고 멈춘다 — 다음 반응은 운영자 지시로. 자기 자신·비활성 모델은 전달 대상이 아니다.
- 메시지 역할: `user`(운영자) · `assistant`(모델, `connectionId`·`usage`) · `tool`(도구 결과 블록) · `system`(HAL 안내). 공급자 오류는 `status: failed` + `error`(코드·메시지) 로 남는다.
## 4. 도구와 위험 등급
| 등급 | 실행 | 도구 |
|---|---|---|
| 읽기(read) | 모델이 부르면 즉시 | `hal_skill``hal_health``hal_overview``hal_servers_list``hal_server_get``hal_server_metrics``hal_releases_list``hal_jobs_list``hal_job_get``hal_incidents_list``hal_audit_list``hal_mail_services_list``hal_emergency_list``hal_dr_status``hal_changelog` |
- 인자는 `validateArgs`(모르는 키·필수·형식·enum·길이)로 다시 검사한다. 모르는 도구·인자 오류는 실행하지 않고 모델에게 오류 결과로 돌려준다. 임의 셸 명령·경로·네트워크 도구는 없다.
- 제안(`ai_actions`): `proposed` → `executing` → `executed` | `failed`, 또는 `rejected` / `cancelled`. `POST /ops/api/ai/actions/:id/approve`(write: `requireRecent`, high: `requireRecent` + `threeStep(confirmImpact, confirmIdentifiers, confirmExecute)`), `POST …/reject {reason}`. 승인은 한 번만 실행된다(상태를 먼저 `executing` 으로 바꿔 동시 클릭을 막음). 실행 결과는 도구 결과로 대화에 들어가고 모델이 이어서 답한다. 거부는 모델을 자동으로 부르지 않는다.
- 감사: `ai.connection.*`, `ai.action.propose/execute/reject`, `ai.turn.failed`. detail 은 redact 를 거친다(비밀값 없음).
HAL 서비스와 같은 SQLite 를 WAL 로 함께 읽는다. 읽기 도구만 실행하고, 쓰기·고위험 도구는 `isError` 결과로 "운영자 콘솔 AI 채팅룸에서 승인을 받으라"고 안내한다(MCP 경로에는 운영자 세션·재인증·3단계 확인이 없기 때문). 로그는 stderr.
## 6. 운영 메모
- 턴은 HAL 프로세스 안에서 돈다. HAL 재시작 뒤 진행 중이던 턴은 사라지며 "응답 중" 표시도 사라진다(거짓 진행 표시 없음). 운영자가 다시 지시하면 된다.
- 비용·토큰: 공급자가 준 입력·출력 토큰만 메시지 `usage` 에 기록한다. 주지 않으면 "제공되지 않음".
- **[0.3.1] 로컬 CLI 자동 탐색**: HAL 서비스(systemd)의 PATH 에는 사용자 설치 위치가 없어 0.3.0 은 bear 의 `~/.local/bin/claude`·`codex` 를 찾지 못했다. 0.3.1 은 `GET /ops/api/ai/cli` 가 CLAUDE_BIN/CODEX_BIN → HAL_CLI_DIRS → `~/.local/bin`·`~/.npm-global/bin`·`~/.claude/local`·`~/.codex/packages/standalone/current/bin`·`~/node_modules/.bin` → `/usr/local/bin`·`/usr/bin` → PATH 순으로 탐색해 버전·로그인 상태(`claude auth status` / `codex login status`)·기본 모델(codex: `~/.codex/config.toml`, 모델 목록 `~/.codex/models_cache.json`)을 AI 연동 화면 "서버에서 찾은 CLI" 카드에 보인다. 연결은 `cli.tool`(claude|codex)만 주면 탐색된 실행 파일을 쓰고, "확인" 때 그 경로·버전·로그인 상태를 연결에 남긴다.
- Codex 는 `codex exec --json -s read-only --skip-git-repo-check --ephemeral -C <임시 폴더> -m <모델> <프롬프트>` 로 실행하고 JSONL 의 `item.completed(agent_message)` 를 답, `turn.completed.usage` 를 사용량으로 쓴다. Claude 는 `claude -p … --output-format json`. 둘 다 HAL 도구는 없다(대화·분석 전용). bear 의 `ProtectSystem=strict` 유닛 안에서도 두 CLI 모두 실행된다(systemd-run 으로 확인).
- 로그인은 HAL 이 대신할 수 없다: 확인 결과 `cli_not_logged_in` 이면 연결이 `reauth_required` 가 되고 "서버(HAL 실행 계정)에서 `claude login` / `codex login`" 안내가 붙는다. 2026-10-29 bear 기준 codex 는 ChatGPT 로그인됨, claude 는 OAuth 세션 만료(`claude login` 필요).