75 lines
14 KiB
Markdown
75 lines
14 KiB
Markdown
# HAL AI 채팅룸 — 연결·방·도구·MCP (Ariweb Suite 0.3.0)
|
||
|
||
운영자 콘솔(`/ops/*`) 오른쪽의 AI 채팅룸과 그 백엔드(`hal/ai/*`) 설명. 개발 지시 `Ariweb-Suitle_Dev-Order_v0.3.0.md`(Oz) · v0.1.4 §41·§42. 비밀값은 이 문서에 없다.
|
||
|
||
## 1. 구성
|
||
| 파일 | 역할 |
|
||
|---|---|
|
||
| `hal/ai/connections.mjs` | 모델 연결(API 키·OAuth PKCE·로컬 CLI 로그인 세션), 비밀값은 Vault(`ai:conn:<id>:api_key` / `:oauth` / `:client_secret`), 모델 목록 확인, 상태 |
|
||
| `hal/ai/providers.mjs` | 공급자 어댑터: Anthropic Messages, OpenAI 호환 Chat Completions(도구 호출 포함), 로컬 CLI(`claude -p --output-format json`, 도구 없음) |
|
||
| `hal/ai/rooms.mjs` | 방(통합 `room_group` 고정 + 연결별 개별)·메시지(블록 JSON, seq)·통합 방 호출 규칙(`parseAddressing`, `mentionedParticipants`) |
|
||
| `hal/ai/engine.mjs` | 턴 실행: system prompt(고유 정보·참여자·스킬 문서) → 모델 호출 → 읽기 도구 즉시 실행 / 쓰기·고위험은 제안 → 통합 방 @전달 1회 → 승인 실행·거부 |
|
||
| `hal/ai/tools.mjs` | 도구 레지스트리(이름·설명·위험 등급·인자 선언·실행), 공급자별 선언 변환(`toolDefs`), 인자 검증 |
|
||
| `hal/ai/skill.md` | 스킬 문서: HAL 상태·작업·릴리스·사건·감사·긴급 복구·DR·오류 코드의 뜻과 AI 규칙(system prompt 와 `hal_skill`·`GET /ops/api/ai/skill`) |
|
||
| `hal/ai/mcp.mjs` | MCP(stdio JSON-RPC) 서버 — `node hal/index.mjs mcp`, 같은 도구 정의, 읽기 도구만 실행 |
|
||
| `hal/ai/routes.mjs` | 운영자 API `/ops/api/ai/*`, OAuth 콜백 `/ops/ai/oauth/callback` |
|
||
| `hal/ui.mjs` · `hal/ui/public/ai.js` · `ai-render.mjs` · `ops.css` | 패널(#ai-panel)·AI 연동 화면(`/ops/ai`)·순수 렌더러·스타일 |
|
||
| 표 | `ai_connections` · `ai_rooms` · `ai_messages` · `ai_actions` · `ai_oauth_states`(마이그레이션 `hal_0006_ai_chat`) |
|
||
|
||
## 2. 연결(AI 연동 화면 `/ops/ai`)
|
||
- 공급자: `anthropic`(API 키 또는 OAuth Bearer) · `openai`(OpenAI 호환, `baseUrl` 로 다른 호환 공급자) · `local-cli`(서버에 설치·로그인된 Claude Code 또는 Codex CLI 세션, 대화·분석 전용 — 0.3.1 자동 탐색, §6).
|
||
- 고유 정보 `identity`: `role`(역할·업무) · `traits`(특성) · `instructions`(추가 지시). 통합 방에서 다른 참여자에게도 이름·역할이 안내된다.
|
||
- 상태: `active` · `disabled`(탭은 보이되 응답하지 않음) · `reauth_required`(OAuth 토큰 없음·만료·401) · `error`(마지막 확인 실패 — `lastError`).
|
||
- "확인"(`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` |
|
||
| 쓰기(write) | 제안 → 운영자 승인(재인증) | `hal_server_create` `hal_server_approve` `hal_server_maintenance` `hal_incident_open` `hal_incident_resolve` `hal_job_control` `hal_module_health` `hal_dr_snapshot` |
|
||
| 고위험(high) | 제안 → 승인(재인증 + 3단계 확인, 식별정보 = 도구가 정한 identifiers) | `hal_server_isolate` `hal_server_delete` `hal_module_deploy` `hal_module_rollback` `hal_module_reconcile` `hal_release_approve` `hal_job_revert` `hal_config_set` |
|
||
|
||
- 인자는 `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 를 거친다(비밀값 없음).
|
||
|
||
## 5. MCP 서버
|
||
```bash
|
||
ARIWEB_HAL_DATA=/srv/ariweb/hal/data node hal/index.mjs mcp # stdio JSON-RPC: initialize · tools/list · tools/call · ping
|
||
```
|
||
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` 필요).
|
||
|
||
## 7. 0.3.2 — 연동 편의성·도구 기본 설정·승인 요청
|
||
- **공급자 프리셋**: `anthropic`(Claude) · `openai`(ChatGPT) · `gemini`(Google, 자체 generateContent API, functionDeclarations) · `openrouter`(OpenAI 호환, 기본 주소 `https://openrouter.ai/api/v1`) · `other`(기타 OpenAI 호환 서비스 — 기본 주소 필수) · `local-cli`. `GET /ops/api/ai/connections` 의 `providers[].guide`(키 발급 주소·절차·OAuth 안내)와 `callbackUrl` 로 화면이 **사전 준비 안내**를 그린다.
|
||
- **OAuth 자동 진행**: `POST /connections` 응답의 `next.action === 'oauth'` 이면 화면이 바로 `oauth/start` 를 불러 로그인 팝업을 연다. 콜백은 `/ops/ai/oauth/done?ok=1|0` 완료 페이지로 가고, 팝업이면 `postMessage({type:'hal-oauth'})` 로 부모 창에 알린 뒤 닫힌다(부모는 자동으로 "확인"까지). Gemini 는 Google 끝점·scope 프리셋(client id/secret 만), OpenRouter 는 PKCE 프리셋(설정값 없음 → `openrouter.ai/auth` → `/api/v1/auth/keys` 로 키 발급 → Vault).
|
||
- **텍스트 셸**(`hal/ai/shell.mjs`, `/ops/api/ai/shell`): 허용 명령(claude: login·logout·auth status·--version / codex: login·logout·login status·--version)만 util-linux `script` PTY 로 실행. 시작은 재인증 필요, 운영자당 2개, 유휴 10분·최대 30분. 입력은 저장·감사하지 않는다. 로컬 CLI 연결 저장·확인에서 `cli_not_logged_in` 이면 화면이 자동으로 login 셸을 연다.
|
||
- **도구 기본 설정**: 연결마다 `tools: {preset, enabled}`(기본 `all` — 읽기 즉시, 쓰기·고위험은 승인). 프리셋 `all`·`read_write`·`read_only`·`none`·`custom`. 엔진은 켜진 도구만 공급자에 선언하고, 꺼진 도구 호출은 실행하지 않는다. 로컬 CLI 연결에는 HAL MCP 설정이 자동으로 붙는다(claude `--mcp-config … --strict-mcp-config --allowedTools mcp__hal`, codex `-c mcp_servers.hal.*`). `GET /ops/api/ai/mcp-config` 가 외부 클라이언트용 스니펫을 준다.
|
||
- **승인 요청**(MCP 경로): 쓰기·고위험 `tools/call` 은 실행하지 않고 `d.ai.proposeExternal` 로 통합 방에 HAL 안내 + 제안 카드를 올리며 `{approvalRequested, actionId}` 를 돌려준다. 클라이언트는 `hal_action_status {actionId}` / `hal_actions_pending` 으로 상태를 본다. 승인은 같은 라우트(`/ops/api/ai/actions/:id/approve`, 재인증·고위험 3단계)이며 결과는 통합 방 안내로 남는다.
|
||
|
||
## 8. 0.3.9 — 로그인 터미널·텍스트 셸·모델 선택·슬래시 명령
|
||
- **claude 로그인 명령 수정**: 실제 명령은 `claude auth login`(`claude login` 은 대화형 세션에 "login" 을 넣는 것 — 세션이 만료되면 인증 오류만 낸다). 셸 명령표(`SHELL_COMMANDS`): claude `login`→`auth login` · `login_console` · `logout`→`auth logout` · `status`→`auth status` · `version` · `interactive`(bare `claude`, 신뢰(trust)·첫 설정 TUI 용), codex `login` · `logout` · `status`→`login status` · `version` · `interactive`. 작업 폴더는 HAL 실행 계정 HOME(이미 신뢰된 폴더). 로그인 안내 문구도 `claude auth login` 으로.
|
||
- **텍스트 셸 범용화**: 브라우저는 순수 단말 에뮬레이터 `hal/ui/public/term.mjs`(VT100/xterm 부분집합 — 커서 이동·지우기·대체 화면·스크롤백, 색 무시)로 TUI 출력을 그리고, 단말 영역에 포커스하면 키를 바이트 그대로 보낸다(`keyToBytes`: Enter·Tab·Esc·화살표·Home/End·Delete·Ctrl+문자). 키 단추(↑↓←→ Enter Esc y n Ctrl+C Ctrl+D)·줄 입력란(한글·붙여넣기)·붙여넣기도 지원. 단말 크기(cols·rows)는 세션 시작 때 넘겨 `COLUMNS/LINES` 로 전달(기본 100×30).
|
||
- **세션 한도 오류 수정**: 0.3.2 는 모달 "닫기"가 세션을 끝내지 않아 유령 세션이 한도(2)를 채웠다. 0.3.9: 닫기(모달 밖 클릭 포함)가 세션을 종료하고, 한도는 **살아 있는 프로세스**만 세며(끝났는데 running 으로 남은 것은 정리) 3개, 409 응답에 열린 세션 목록을 담고, "열린 셸 모두 종료"(`POST /ops/api/ai/shell/kill-all`)와 세션 목록(열기·종료)을 화면에 둔다.
|
||
- **모델 선택**: 연결 표와 패널 상태 줄의 모델 선택 상자(확인된 목록) → `PATCH /connections/:id {model}`(목록 밖 거부). `/model <이름>` 명령(개별 방). `GET /rooms` 의 `connection.models`.
|
||
- **슬래시 명령**(`hal/ai/commands.mjs`, `GET /ops/api/ai/commands`): local — `/help` `/tools` `/model` `/reset`(모델 호출 없음) / prompt — `/status` `/health` `/servers [역할]` `/server <id>` `/jobs [상태]` `/job <id>` `/releases` `/incidents` `/audit [접두어]` `/dr` `/emergency` `/order <지시>` `/deploy <모듈> <버전> [서버]` `/rollback …` `/isolate <서버> <사유>` `/incident <key> <제목>`. prompt 명령은 운영자 원문을 기록에 남기고 모델에게는 확장 지시(`content[].type=command.expanded`)를 보낸다. 확장 지시는 승인 규칙을 바꾸지 않는다(쓰기·고위험은 제안). 모르는 명령 400 `unknown_command`, 인자 누락 400 `command_args_required`, 통합 방 `/model` 400 `command_needs_direct_room`.
|
||
- 점검 수정: 패널 상태 줄에 `reauth_required` 연결용 "로그인·재인증" 단추(OAuth → 로그인 창, CLI → 로그인 셸), `window.open` 기능 문자열 정리.
|