# DeskAide

자연어로 작업을 던지면 백그라운드에서 에이전트가 도구(RAG·웹)로 처리하고, 채팅(위젯 폴링)과 메일로 결과를 돌려주는 임베드형 업무 에이전트 챗봇 v1.

기준 스펙: `docs/superpowers/specs/2026-07-15-deskaide-design.md`
구현 계획: `docs/superpowers/plans/2026-07-15-deskaide.md`

## 아키텍처

```
widget.html --POST /api/task--> server.py --create--> jobstore.jsonl
                                                            ^
                                                            | next_queued / mark_*
                                                        worker.py --run_agent--> agent_runner.py (mock | 헤드리스 CLI)
widget.html --GET /api/task/{id}--(폴링)--> server.py --get--> jobstore.jsonl
```

- `core/modes.py` — 빠른(quick)/신중(careful) 모드 프리셋(model·effort·tool_budget·timeout). 순수 데이터.
- `core/jobstore.py` — 잡 영속화(jsonl append-only)·상태 전이(queued→running→done|failed)·진행 로그.
- `core/agent_runner.py` — 헤드리스 에이전트 실행의 격리 유닛. `DESKAIDE_MOCK=1`이면 토큰 0으로 결정론 결과.
- `core/tools_config.py` — 에이전트에 줄 MCP 도구 구성(RAG + 웹).
- `core/worker.py` — queued 픽업 → agent_runner 실행 → jobstore 갱신. 실패는 job 단위로 격리.
- `core/delivery.py` — 결과 메일 배송(선택, SMTP 미설정 시 no-op).
- `web/server.py` — HTTP API(`POST /api/task`, `GET /api/task/{id}`) + 위젯 서빙(stdlib `http.server`만 사용, 외부 의존성 없음).
- `web/widget.html` — 임베드 챗 위젯(단일 파일, 의존성 0).
- `tools/run_local.py`: 개발 기본값만 설정하고 검증된 `web/server.py` 진입점에 위임하는 런처.

## 실행 (개발용, mock)

토큰 소모 없이 전체 플로우(작업 접수 → 진행 → 결과)를 확인하려면 mock 모드로 로컬 런처를 띄운다.

```
cd E:\Software\Reporting\DeskAide
set DESKAIDE_MOCK=1
python tools/run_local.py
```

브라우저에서 `http://127.0.0.1:8910` 접속. 개발 런처도 `web/server.py`의 중복 서버 검사를 먼저 수행한 뒤 워커와 HTTP 서버를 기동한다.

주요 환경변수:

- `DESKAIDE_MOCK=1` — 실 엔진 대신 mock 결과(테스트·UI 검증용). 미설정 시 실 엔진 경로(`claude -p` 헤드리스, 배선 완료·검증됨 — "엔진 통합" 절 참고).
- `DESKAIDE_HOST` — 바인드 주소(기본 `127.0.0.1`·**로컬 per-user 기본**). 팀 중앙 공유 시만 `0.0.0.0`(아래 "임베드·배포").
- `DESKAIDE_PORT` — HTTP 포트(기본 8910).
- `DESKAIDE_TASK_TOKEN` — 작업 토큰(시크릿). 설정 시 `POST /api/task`가 `X-Deskaide-Token` 헤더(또는 body `token`) 일치를 요구(상수시간 비교). **로컬(127.0.0.1)은 불요**. 팀 중앙 공유(`0.0.0.0`)에서는 반드시 설정(미설정 시 사내 누구나 클라우드 과금 에이전트를 무제한 실행).
- `DESKAIDE_PUBLIC_ORIGIN` — (중앙 공유 모델용) 호스트 앱이 참조할 DeskAide 주소. 로컬 모델은 3앱 origin 기본값 `http://127.0.0.1:8910`을 그대로 쓴다. 중앙 공유 시 Tailscale MagicDNS `http://pc-071.tail437e12.ts.net:8910` 등으로 오버라이드.
- `DESKAIDE_USAGE` — 요청자별 usage 로그 경로(기본 `C:\deskaide\usage.jsonl`).
- `DESKAIDE_RAG_TOKEN` — RAG(geoview-rag) 활성화 토큰. "기밀 가드" 절 참고.
- `DESKAIDE_JOBS` — jobs.jsonl 경로(기본: 런처 기준 `../jobs.jsonl`).
- `DESKAIDE_SMTP_HOST` / `DESKAIDE_SMTP_PORT` / `DESKAIDE_SMTP_FROM` — 결과 메일 배송(선택). `DESKAIDE_SMTP_HOST` 미설정이면 메일은 no-op.

## API

- `POST /api/task` — body `{"task": "...", "mode": "quick"|"careful", "email"?, "profile"?}` → `{"id": "..."}`. `task`가 공백이면 `{"error": "..."}`. `DESKAIDE_TASK_TOKEN` 설정 시 `X-Deskaide-Token` 헤더(또는 body `token`) 필요, 불일치 401.
- `GET /api/task/{id}` — `{"id", "task", "mode", "status", "progress": [...], "result"?, "error"?}`. 위젯이 1.2초 간격으로 폴링, `status`가 `done`/`failed`가 될 때까지 반복. (참고: 상태 조회는 토큰 미검증 — id는 uuid4 48-bit·생성자에게만 반환되어 사실상 추측 불가.)
- `GET /embed.js` — 호스트 페이지 주입용 loader(아래 "임베드·배포").
- `GET /` — `widget.html` 서빙. `?profile=`(general·equipman·docs)·`?token=` 쿼리 인식.

## 임베드·배포

호스트 페이지에 `embed.js` loader 한 줄을 넣으면, loader가 자기 `<script src>` origin에서 DeskAide 주소를 유도해 우하단 고정 iframe(위젯)을 주입한다. iframe 내부는 DeskAide origin과 동일해 CORS가 불필요하고, 위젯이 열림/닫힘 시 `postMessage`로 iframe을 84px↔408×640으로 리사이즈한다.

```html
<script src="http://127.0.0.1:8910/embed.js" data-profile="equipman"></script>
```

- `data-profile` — `general`(RAG+웹) · `equipman`(장비 read-only DB 도구+RAG) · `docs`(RAG). 위젯 외형(제목·추천칩)과 에이전트 페르소나·도구셋이 프로필별로 특화된다.
- `data-token` — 로컬 모델에선 불요(생략). 팀 중앙 공유 모델에서만 `DESKAIDE_TASK_TOKEN`을 인증 사용자에게만 렌더.

### 로컬 per-user 배포 (기본·권장)

각 사용자가 **자기 PC에서 DeskAide를 돌리고(자기 Claude 로그인), 위젯 origin은 `http://127.0.0.1:8910`**(각 접속자 본인 로컬)이다. 과금·rate limit이 각자 구독에 걸리고 토큰을 중앙에 모으지 않는다. 3앱의 위젯 origin 기본값이 이미 `127.0.0.1:8910`이라 **per-app env 없이도 맞다**(모두에게 동일한 로컬 주소). 세팅 절차는 `deploy/DEPLOY.md`.

- 각 사용자: Claude 구독 로그인(`claude /login`) → `deploy/start_deskaide.ps1`(127.0.0.1·토큰 없음) 상시 구동. 위젯은 각 앱의 로그인 사용자에게 렌더되고, 그 PC에 로컬 DeskAide가 떠 있으면 뜬다(아니면 조용히 미표시·호스트 무영향).
- 설치기와 시작 스크립트는 기존 포트 리스너의 DeskAide health, Python 실행 파일, 정확한 `web/server.py` 명령행을 모두 확인한 경우에만 종료한다. 하나라도 확인되지 않으면 기존 프로세스를 건드리지 않고 새 서버도 시작하지 않는다.
- 3앱 배선은 완료(각 앱 커밋). 위젯 렌더 조건: ControlTower=전역·EquipMan=`session.get('actor_id')`·ReportLLM=`showApp` `if(name)`(실로그인).

### 팀 중앙 공유 배포 (옵션)

각자 구독 대신 한 계정 공유(비용·rate limit 그 계정 집중). `deskaide.env`에 `DESKAIDE_HOST=0.0.0.0`+`DESKAIDE_TASK_TOKEN` 설정, 중앙 1대 구동(Tailscale MagicDNS `pc-071.tail437e12.ts.net:8910`), 3앱 origin을 그 주소로 env 오버라이드(`DESKAIDE_ORIGIN`·`NEXT_PUBLIC_DESKAIDE_ORIGIN`)+토큰 배선(EquipMan context processor·ReportLLM `/api/me` 인증세션 주입·ControlTower 빌드타임). 토큰이 `:8910` 드라이브바이 차단. 상세 `deploy/DEPLOY.md` 옵션 절.

## 기밀 가드 (RAG)

`core/tools_config.py`는 fail-closed 방식이다.

- `DESKAIDE_RAG_TOKEN`이 설정된 경우에만 `geoview-rag` MCP 서버를 도구 목록에 포함한다. 토큰이 없으면 RAG는 통째로 제외되고 웹검색만 허용된다(web-only).
- `DESKAIDE_NO_RAG=1`은 토큰 유무와 무관하게 RAG를 강제 제외한다(토큰보다 우선).
- 등급 제한은 로컬 env가 아니라 **토큰이 가리키는 ReportLLM 사용자 레코드**로 강제된다. DeskAide 전용 RAG 서비스계정 토큰은 배포 시 clearance를 technical 이하로 프로비저닝해야 한다(계약·영업·메일 tier는 에이전트에 전달하지 않는다).

## 엔진 통합 (멀티 엔진)

`core/agent_runner.py`는 `DESKAIDE_MOCK` 미설정 시 `DESKAIDE_ENGINE`(기본 `claude`)로 claude/codex/gemini 중 하나를 골라 `core/engines.py`에 위임한다. 각 사용자가 보유한 구독(Claude/Codex/Gemini)으로 실행해 단일 계정 트래픽을 분산하기 위한 교체점. `agent_runner.py`는 mock 분기 + 엔진 선택만, 엔진별 CLI 배선은 `engines.py`.

- **claude** (기본·권장·가장 안전): `claude -p` 헤드리스. model=`--model`, 도구예산=`--max-turns`, MCP=`--mcp-config`(+`--strict-mcp-config`로 ambient 상속 차단), 허용도구=`--allowedTools`로 RAG+웹만 — Bash/Write/Edit 미허용 = **읽기 전용 에이전트**.
- **codex**: `codex exec` 헤드리스. 인증=**ChatGPT 구독**(플랜토큰). 페르소나=프롬프트 프리픽스(codex는 시스템프롬프트 API 없음), 프롬프트=stdin(.CMD 인자 mangling 회피), tier=`-c model_reasoning_effort=<low|high>`, MCP=`-c mcp_servers.*` 주입, 최종답=`-o` 파일. 실행파일은 `shutil.which`로 풀경로 해석(npm .CMD). `DESKAIDE_CODEX_MODEL`로 모델 지정. ⚠ **보안 주의**: codex exec는 헤드리스에서 MCP 도구 호출을 `--dangerously-bypass-approvals-and-sandbox`로만 허용한다(실측: read-only·approval_policy=never·trust_level="trusted" 전부 "user cancelled MCP tool call"로 차단). 즉 샌드박스가 꺼져 모델이 shell을 쓸 수 있어 **claude 엔진보다 느슨**하다. 웹검색도 bypass 모드에서 기본 가용(실측 — 별도 config 불요·프로필별 웹 제한이 codex에선 강제 불가). 로컬 per-user(각자 본인 PC·프롬프트 인젝션 blast radius=본인 머신)에서 수용. 최대 제한이 필요하면 claude 엔진 사용.
- **gemini**: gemini CLI 미설치 → 스텁(설계만: `gemini -p "<persona+task>" -m <quick=gemini-2.5-flash|careful=gemini-2.5-pro>` + `~/.gemini/settings.json` MCP). 설치·로그인 후 `engines.run_gemini`를 `run_codex` 패턴으로 구현하면 활성화.

### 알려진 v1 한계

- 실모드 진행 표시는 굵은 단위다(`"에이전트 시작"` → `"완료"` 2단계뿐). 도구별 세분화 진행(`--output-format stream-json` 파싱)은 v1.1로 연기.

## 테스트

```
cd E:\Software\Reporting\DeskAide
python -m pytest tests/ -q
```
