# VERSION_HISTORY ## 0.4.1 (2026-11-06) - **작성자**: 개발 지시 — Oz(`Ariweb-Suitle_Dev-Order_v0.4.1.md`, 2026-11-06) · 구현 — Claude Fable 5.1(브랜치 `feature/hal-bootstrap-tls-v0.4.1`, 기준 main `0f5e01a` = 0.4.0). 커밋·push·`main` 병합·릴리스 빌드·bear `hal21.ariweb.net` HAL 배포·HAL 서버 Agent·HAL 작업으로 agent 배포까지 수행(아래 "배포"). - **이전 버전**: 0.4.0(2026-10-29). 지시문은 "HAL 0.4.0" 이라 썼으나 0.4.0 은 이미 승인·배포된 불변 릴리스(`release_immutable`)라 **0.4.1** 로 낸다. 도입 모듈 **Webmaker 1.9.0 · AriMail 1.1.0(halCompat 0.4.0) 변경 없음** — 올리지 않고 배포하지 않는다(지시: "Do not deploy if it was not modified"). Node Agent 0.4.1. ### 요구(사용자 요구 — 약화 없이 반영) 1. 서버 등록(Agent 설치) curl 명령의 오류 수정 — 기존 서버(WS1) `== 등록` 에서 `TypeError: fetch failed … SELF_SIGNED_CERT_IN_CHAIN`, 새 서버(WS2) `curl: (60) SSL certificate problem: self-signed certificate in certificate chain`. 이미 등록된 서버면 그렇다는 메시지가 나와야 한다. 2. HAL 배포 때 Web·Mail·Backup·Portal 을 제외한 운영용 Agent 등을 함께 배포해 "부트스트랩 미완료" 상태를 만들지 않는다. 부트스트랩은 완료되고 "기능 단위 서버(Web·Webmaker·Mail·HawkEye·Backup·Portal 등) 배포 대기/필요" 메시지가 되어야 한다. ### 원인 - (1) HAL 공개 주소의 인증서가 사설 CA·자체 서명이면 시스템 CA 로 검증되지 않는다. 설치 명령·설치 스크립트(curl)·Agent 등록(`fetch`)·Agent 서비스 모두 공개 신뢰 인증서를 전제했고, 오류는 스택으로만 나왔다. HAL 은 쓴 토큰·같은 서버의 두 번째 토큰을 "이미 등록된 서버" 로 구분해 알리지 않았다(두 번째 Agent 를 조용히 만들었다). - (2) `bootstrap-hal.sh` 는 HAL 까지만 설치했고 첫 Agent 설치·승인은 운영자 수동 단계라 그 사이 `bootstrap_state.completed_at` 이 비어 있었다. ### 추가한 기능 - `hal/tls.mjs`: 공개 주소 TLS 신뢰 확인(`probePublicTls` — 시스템 CA 검증 → 실패면 체인·공개키 핀 수집), 신뢰 앵커(체인 안의 자체 서명 인증서, 또는 운영자 지정 `/tls/hal-ca.pem` 우선), `ensureTlsInfo`(10분 캐시, 기동·주기·토큰 발급 때), `installCommand`(신뢰되지 않으면 `curl -fsSL -k --pinnedpubkey '<핀>' … --hal-pin '<핀>'`). 공개 경로 `GET /agent/v1/hal-ca.pem`(신뢰 앵커 PEM, 비밀 없음) · `GET /agent/v1/tls`. CLI `tls-probe`. - `deploy/agent-install.sh`: `--hal-pin`(핀으로 CA 받아 `/data/hal-ca.pem`) · `--ca ` · `--insecure`(경고). 이후 curl `--cacert`, Node `NODE_EXTRA_CA_CERTS` + `register --ca`, 유닛 `Environment=NODE_EXTRA_CA_CERTS=…`(자기 갱신 때 Environment 줄 보존). HAL 연결 실패는 원인·조치 한 줄(인증서 문제면 핀 명령/`--ca` 안내). 이미 등록된 서버(`agent.json`)는 "이미 등록된 서버(Agent·HAL·등록 시각) — 코드·유닛만 갱신" 안내, 새 CA 가 생기면 `caFile` 만 보완. - `agent/halca.mjs`: `applyHalCa`(기동 때 `caFile` 을 TLS 기본 CA 에 더함 — `tls.setDefaultCACertificates`, Node 22.19+/24; 유닛 env 가 있으면 그대로), `friendlyFetchError`(인증서 코드 → "HAL 인증서를 확인할 수 없습니다(코드) — 핀 명령/--ca", 연결 코드 → "연결할 수 없습니다"). `agent/config.mjs` `caFile`(`ARIWEB_HAL_CA` > agent.json). - `core.registerAgent`: 토큰에 묶인 서버(또는 같은 호스트 이름의 서버)에 살아 있는 Agent 가 있으면 409 `server_already_registered`("이미 등록된 서버입니다(서버·Agent·등록 시각) — 다시 설치할 필요가 없습니다. 새 Agent 로 바꾸려면 재등록 허용 토큰"). 쓴 토큰 재사용은 409 `token_used`(같은 문구 + 그 토큰으로 등록된 Agent). 거부된 토큰은 소비되지 않는다. 재등록 허용 토큰(`replace`)이면 기존 Agent 해지(비밀 제거·작업 취소, 감사 `agent.replace`) 후 새 Agent. 마이그레이션 `hal_0011_token_replace`(`registration_tokens.replace_existing`). 발급 응답에 `tls`·`existingAgent`·`replace`. - 운영자 API `POST /ops/api/servers/:id/token {replace}`(replace 는 재인증), 화면(ops.js)은 Agent 가 있는 서버면 "재등록 허용" 여부를 묻고, 사설 CA 면 핀 안내를 함께 보인다. CLI `server-token --replace --json`(스크립트용 한 줄 JSON, 기존 Agent 주의 출력). - `deploy/hal-host-agent.sh`: HAL 서버 자신을 관리 서버(역할 control, 프로필 `hal` = agent + hawkeye)로 만들고(`server-create --profile hal`) 토큰(`server-token --json`)으로 loopback(`http://127.0.0.1:8900`, DNS·인증서 무관) Agent 를 설치·등록 → `server-approve` 로 승인해 부트스트랩을 닫는다. 멱등(이미 `agent.json` 이 있으면 승인만). CLI `server-create`·`server-approve`. - `deploy/bootstrap-hal.sh`: 6/7 단계에서 `hal-host-agent.sh` 를 부른다(`--no-agent` 로 이전 방식). 5/7 뒤 `tls-probe` 로 공개 주소 신뢰 상태를 보인다. 완료 안내가 "기능 단위 서버를 역할별 통합 배포로 추가" 로 바뀐다. - `SERVER_PROFILES.hal`(HAL 서버: agent·hawkeye 만), `core.deploymentStatus`/`FEATURE_MODULES`(portal·webmaker·arimail·backup 의 배치 여부), `selfHealth().deployment {pending, deployed, agents}`, `/ops/api/releases.deployment`, `render.moduleLabel`. ### 수정한 기존 기능 - 운영자 콘솔 배너: 부트스트랩 완료 뒤 "ⓘ 부트스트랩 완료 · 기능 단위 서버 배포 대기: 고객 포털, Webmaker, AriMail, 백업 — 역할별 통합 배포(새 서버)로 …"(배포 릴리스 화면 링크). 완료 전은 "! 부트스트랩 진행 중(HAL 서버의 Node Agent 미설치) — bootstrap-hal.sh/hal-host-agent.sh 안내". 배포 릴리스 화면 "부트스트랩 미완료" → "진행 중(HAL 서버 Agent 등록·승인 전)" / "완료 · 기능 단위 서버 배포 대기: …". - `core.heartbeat`: 모듈 이름을 `canonicalModuleName` 으로 정규화(0.4.0 이하 Agent 의 `observability` 보고를 `hawkeye` 행으로, 옛 행 삭제) — 0.3.17 잔여 결함. Agent 0.4.1 은 `hawkeye` 로 보고. - `agent/index.mjs register`: 이미 등록된 Agent 가 있으면 조용히 덮어쓰지 않고 "이미 등록된 서버"(종료 2, `--force`), fetch 실패는 한 줄 원인·조치(종료 1), HAL 거부(`server_already_registered`/`token_used`)는 종료 3 — 설치 스크립트가 "재설치가 필요 없습니다" 로 안내. `status`·기동 로그에 CA 적용 상태. - `MODULE_GUIDES.agent.install` 문구, 버전 검사 테스트 0.4.0 → 0.4.1. ### 고객 화면 - 변경 없음. ### 운영자 화면 - 배너·배포 릴리스 화면 문구(위), 서버 등록 토큰 발급의 재등록 확인·핀 안내. ### 보안 결정 - 핀(`sha256//…`)·CA 인증서는 공개 정보다. 개인키·토큰·Agent 비밀은 어디에도 출력되지 않는다(`hal-host-agent.sh` 는 토큰을 변수로만 쓴다). `-k` 는 `--pinnedpubkey` 와 함께만 쓰며 핀이 다르면 curl 이 자료를 받기 전에 끊는다(중간자 방지). `--insecure` 는 경고를 내는 실험실 전용. - 재등록 허용(기존 Agent 해지)은 운영자 재인증(10분) + 확인. 거부된 등록은 토큰을 소비하지 않는다. HAL 서버 Agent 는 loopback 평문이지만 모든 요청이 HMAC 서명이다. - 부트스트랩이 첫 Agent 를 스스로 승인한다 — "사람 결정" 은 `bootstrap-hal.sh` 실행 자체(sudo 가능한 운영자)이며 감사에 `cli:bootstrap` 으로 남는다. ### 테스트 - `test/hal-041.test.mjs` RED(모듈·스크립트·문서 없음) → GREEN 7/7: 실제 자체 서명 HTTPS 픽스처(hal.local.test)로 TLS 확인·핀·CA 경로·설치 명령, 이미 등록 409·재등록 허용·재인증, Agent CLI(별도 프로세스) 오류 문구·`--ca`·`status` 런타임 CA, 부트스트랩 완료·배너·hawkeye, CLI 3종, 스크립트·문서·버전. 기존 테스트 갱신: 배너 문구(`hal-ui`·`hal-reauth-banner`: "부트스트랩 미완료(…첫 Agent 미등록)" → "부트스트랩 진행 중(…HAL 서버의 Node Agent 미설치)"), 프로필 목록에 `hal`(`hal-0317`), 버전 0.4.1(`hal-ai`·`webmaker-visual-editor` 등). 전체 Suite 32파일 통과. 로컬 E2E(실제 HAL 프로세스 + 자체 서명 TLS 앞단, sudo/systemctl 셈): 핀 없는 curl → 60(WS2 재현), 핀 명령 → CA 받기·등록·유닛 `NODE_EXTRA_CA_CERTS`, 같은 명령 재실행 → "이미 등록된 서버"(종료 0), 새 토큰 → HAL 409 안내(종료 1), `hal-host-agent.sh` → 부트스트랩 완료·pending 4. ## 0.4.0 (2026-10-29) - **작성자**: 개발 지시 — Oz(`Ariweb-Suitle_Dev-Order_v0.4.0.md`, 2026-10-29) · 구현 — Claude Opus 5.5(브랜치 `feature/hal-settings-v0.4.0`, 기준 main `54db663` = 0.3.17). - **이전 버전**: 0.3.17(2026-10-29). 지시된 버전 **0.4.0**. 도입 모듈 함께 올림: **Webmaker 1.8.0 → 1.9.0**, **AriMail 1.0.11 → 1.1.0**(가운데 버전 +1, 둘 다 `ariwebSuite.halCompat = "0.4.0"` 로 HAL 0.4.0 대응 명시). Node Agent 0.4.0(`module.configure`). ### 요구(사용자 요구 — 약화 없이 반영) 1. Webmaker 관리 콘솔의 권한 이전 — HAL 에서 각 Webmaker 시스템 콘솔의 주요 기능(API 키 관리, 마진·가격 정책, 모델 등 주요 설정, 기타 전체 적용 서비스, 개별 서버 설정)을 통합 관제하는 새 인터페이스, 배포된 모든 서버 관리. 2. AriMail 관리 콘솔 통합 — 도메인별 관리를 뺀 서버 단위 관리는 HAL 에서, 개별 메일 서버(도메인) 관리는 메일 서버 사용자가 직접. 3. 기타 서비스 서버의 설정도 HAL 에서 통합 관리. 4. 필요한 모듈 모두 업그레이드(가운데 버전 +1, HAL 0.4.0 대응 명시). ### 추가한 기능 - `hal/settings/catalog.mjs`: 모듈별 설정 목록 — Webmaker 5묶음 64항목(API 키·인증 / 마진·가격 / 모델 / 전체 적용 서비스 / 서버별), AriMail 8묶음 29항목(도메인별 `LIMIT_*` 제외), Agent `heartbeatMs`. 형식·범위·재시작 필요·서버 전용 범위. - `hal/settings/index.mjs` + 마이그레이션 `hal_0010_managed_settings`: 범위(global / server:) 저장, 비밀값은 Vault(`managed:::`)에만, 유효값 = 전체 ← 서버, hash(`sha256:` 접두어 — 긴 hex 는 redact 가 가림), 적용 상태(`managed_settings_state`)·개요·적용 필요 수. - API: `GET /ops/api/settings`, `GET|PUT /ops/api/settings/:module`, `DELETE /ops/api/settings/:module/:key`, `POST /ops/api/settings/:module/apply`(저장·삭제·적용은 재인증 10분), `GET /agent/v1/settings/:module`(Agent HMAC, 자기 서버 범위만). 감사 `settings.set`·`settings.apply` 는 키 이름만. - 작업: `settings.apply`(부모) → `settings.apply.target`(서버별) → Agent `module.configure`. payload 에는 hash 만 — Agent 가 HAL 에서 직접 받아 hash 를 확인하고 `/hal-managed.json`(0600, 실행 계정이 다르면 `module-as-user.sh write --mode 600`)으로 원자 교체, 재시작·건강 확인, 모듈의 `hal-managed.applied.json` 을 기다려 결과에 싣는다. hash 불일치 `settings_hash_mismatch`, 모듈 적용 오류 `module_apply_failed`. - 화면 `/ops/settings` "통합 설정"(메뉴): 모듈 탭 → 범위(전체 / 서버) → 묶음별 입력(비밀값은 설정됨/없음·새 값만, 서버 범위는 "전체 값 …" 안내, 재시작 필요 표시) → 저장 → "서버에 적용"(서버 체크·재시작) → 서버별 적용 상태. 운영 현황 서버 카드 "설정 적용 필요". - AI 도구 36개: `hal_settings_get`(읽기) · `hal_settings_set`(고위험, 비밀값 거부 `secret_not_allowed`) · `hal_settings_apply`(고위험). 스킬 §4.1. 도구 인자 형식에 object 추가. - Webmaker 1.9.0(원본 `release/1.9.0` → main `8f8716c`): `lib/hal-managed.js`(기동·파일 변경 때 settings/config/보관소 적용, 결과 파일), 관리자 콘솔 설정·보관소·설정 페이지에서 관리 항목 409 `managed_by_hal`, 설정 탭 "HAL 관리" 안내. 원본 test 119/119. - AriMail 1.1.0(원본 `release/1.1.0` → main `f959b65`): `server/hal-managed.mjs`(기본 도메인=서버 설정에만 적용, LIMIT_*·설치 항목 제외), 설정 페이지 관리 항목 값 변경 409·사용량 키 잠금, "HAL 관리" 카드. 원본 test 9파일 통과. - Agent 0.4.0: `module.configure`, `/hal-managed.json` 의 `heartbeatMs` 우선. ### 수정한 기존 기능 - `scripts/import-sources.mjs`: pin 을 올릴 때 Suite 사본이 "이전에 가져온 원본"과 같으면 새 원본으로 갱신(`updated`) — 예전에는 `--force` 없이 원본이 바뀐 파일이 모두 'modified' 로 남아 갱신되지 않았다. Suite 가 고친 파일(webmaker `server.js`, arimail `main.mjs`·`web/setup.js`·`package.json`)은 원본 변경분을 같은 패치로 옮겼다. pin: webmaker `8f8716c`, arimail `f959b65`. - `deploy/module-as-user.sh write --mode 600`. - 버전 검사 테스트 0.3.17 → 0.4.0, Webmaker 1.9.0. ### 고객 화면 - 변경 없음. Webmaker·AriMail 관리 콘솔(관리자)에 "HAL 관리" 안내·잠금이 생긴다(관리 항목이 지정된 서버만). ### 운영자 화면 - "통합 설정" 메뉴·화면, 운영 현황 "설정 적용 필요". ### 보안 결정 - 비밀값은 HAL Vault·모듈 보관소·0600 파일에만. 작업 payload·작업 결과·Agent 단계 기록·감사·API 응답·AI 도구 결과·모듈 결과 파일에는 비밀값이 없다(테스트로 확인). AI 도구는 비밀값을 넣을 수 없다. - 저장·삭제·적용은 CSRF + 최근 재인증. AI 의 설정 변경·적용은 고위험(승인·재인증·3단계). - Agent 는 자기 서버 범위의 설정만 받는다(다른 서버의 덮어쓰기 불가). ### 테스트 - `test/hal-040.test.mjs` RED(모듈 없음) → GREEN 5/5, `test/agent.test.mjs` `module.configure`·heartbeat RED → GREEN 11/11, 원본 저장소 `test/hal-managed.test.js` 4/4 · `test/hal-managed.test.mjs` 3/3. 전체 Suite 31파일. ### 배포(2026-10-29 UTC, bear `hal21.ariweb.net` HAL + HAL 작업으로 모듈) - main ff `54db663 → 9f1e490`, 깨끗한 main 빌드·verify(전체 31파일 통과). artifact sha256: hal `0633e4ef82d29fc8ce7c94019bd864eb28889167ba6f02e0de5323e8963ffff8` · agent `d76cdc81dc399fc9e2c134b70d210b251c7c2e9e8ced2b32132925e0d1ac09c1` · webmaker `b7bf920fc07de3e577def4776b35ca57a7586c6a343e03601dcd44f5724934f2` · arimail `918167b7d919953e9f10714c15b2bbf416764abe3a6388eeb88f3e21c982b08d` · portal `18ecca8b157a7f23124b9e7c0fcfb1c655e352cfbfa0f9a13157751ec9225d72`. - HAL: 백업 `/srv/ariweb/hal/backups/pre-v0.4.0-20261029T065026Z` → stage → `register-release --approve` → `bootstrap-release` → current `releases/0.4.0-9f1e490` → 재시작 뒤 500ms 에 health ok(0.4.0). config·vault 보존 sha256 전=후. 운영 DB read-back: 마이그레이션 `hal_0010_managed_settings`, 표 `managed_settings`·`managed_settings_state`(0행), 릴리스 0.4.0 approved. 배포 코드 read-back(메모리 DB): 통합 설정 화면·메뉴·묶음, 저장 응답에 비밀값 없음, 대상 없음 `no_targets`, Agent 설정 API 무서명 401, 도구 36개. - HAL 작업(CLI `module-deploy`, 감사 `cli:claude-opus-5.5`, 한 대씩·건강 확인): agent 0.4.0 → srv1953374(0.3.17→0.4.0) · jitsi(0.1.1→0.4.0) / webmaker 0.4.0(Webmaker 1.9.0) → srv1953374(`/srv/webmaker/releases/0.4.0`, webmaker.ariweb.net 200) / arimail 0.4.0(AriMail 1.1.0) → srv1953374(`/srv/arihakdang-mail/releases/0.4.0`, health 1.1.0·도메인 6, mail.arihakdang.com 200) · jitsi(`/srv/arimail/releases/0.4.0`, health 1.1.0, webmail.ibit.kr 200) / backup·hawkeye 0.4.0 기록(Agent 기능 확인). 실패 작업 0. 포털은 코드 변경이 없어 배포하지 않음(0.1.1 유지). - 되돌리기: HAL `ln -sfn /srv/ariweb/hal/releases/0.3.17-843b2b1 /srv/ariweb/hal/current && sudo systemctl restart ariweb-hal`. 모듈은 HAL 작업 `module.rollback`(이전 릴리스 디렉터리 보존: webmaker·arimail `releases/` 의 직전 디렉터리). - 운영자 조치: 통합 설정 화면(`/ops/settings`)에서 실제 값(API 키·마진 등)을 지정하고 "서버에 적용"을 처음 실행한다(지금은 관리 항목 0 — 모듈은 예전처럼 단독 설정으로 동작). Webmaker 를 jitsi 의 배치 목록에서 정리할지 결정(jitsi 에는 Webmaker 가 없음 — Agent 가 보고한 빈 행). ### 점검에서 발견(0.3.17 잔여 결함, 이번 범위 밖) - Agent heartbeat 가 기능 이름을 옛 `observability` 로 보고하고 HAL 이 그대로 저장해, 마이그레이션 뒤에도 `observability` 행이 다시 생기고 `hawkeye` 행의 확인 시각이 갱신되지 않는다(운영 현황 HawkEye 가 "정보 지연"으로 보일 수 있음). 수정: HAL `heartbeat` 에서 `canonicalModuleName` 적용 + Agent 보고 이름 `hawkeye`(다음 릴리스). ### 알려진 제한 - HAL 에서 지정을 지워도 모듈에 적용된 값은 남는다(잠금만 풀림). Webmaker 등급·내부 모델은 그 서버에서 사용 가능한 모델이어야 적용된다. - 실행 계정이 다른 모듈의 결과 파일을 Agent 가 읽지 못하면(디렉터리 권한) "쓰기 완료·적용 미확인"으로 남는다(`appliedVerified=false`). ## 0.3.17 (2026-10-29) - **작성자**: 개발 지시 — Oz(`Ariweb-Suitle_Dev-Order_v0.3.17.md`, 2026-10-29) · 구현 — Claude Fable 5.1(브랜치 `feature/hal-provision-v0.3.17`, 기준 main `c460f57` = 0.3.10). 커밋·push·`main` 병합·릴리스 빌드·bear `hal21.ariweb.net` HAL 배포까지 수행(아래 "배포"). - **이전 버전**: 0.3.10(2026-10-29). 지시된 버전 **0.3.17**(독립 개선 7항목). HAL 에만 적용, 다른 모듈 변경 없음(배포하지 않음). ### 요구(사용자 요구 — 약화 없이 반영) 1. observability 모듈 이름 → HawkEye. 2. 역할별 통합 배포(webmaker·webhosting·arimail·backup·portal 설치/이전, 슬롯 기반 배치는 HAL 이 관리). 3. 화면 갱신 때 전체를 다시 그리지 않고 바뀐 부분만. 4. 배포 릴리스에서 HAL 배포 삭제, 최신 버전 도구별 설치 안내·버전·기능·상호 연결을 기본 화면으로, 과거 버전 보기와 되돌리기. 5. 작업창 아래에 진행 상황·히스토리. 6. agent 배포 "처리 대기" 지속 원인 점검·수정. 7. 배포 때 서버 목록을 라디오로 선택. ### 점검 결과(6번, bear 운영 DB·로그) - 작업 `j_01a185952656159ecceb02ddd5`(agent 0.3.10 배포, 02:55:06Z): subject `serverIds: []` → precheck 가 1초 만에 `no_targets`("대상 서버가 없습니다")로 **실패**. 화면의 배포 창이 "비우면 해당 역할 전체"라고 안내했지만 backend 는 빈 목록을 거부했고, 릴리스 화면은 작업 상태를 보여 주지 않아 "처리 대기"로 보였다. Agent 2대는 heartbeat·long-poll 정상(0.1.1). - 수정: (a) 대상을 비우면 역할에 맞는 준비된 서버 전체로 풀고, 하나도 없으면 작업을 만들지 않고 API 에서 바로 `no_targets` 와 이유를 보인다 (b) 배포 창은 서버 목록(체크)에서 고른다(7번) (c) 작업 진행 창(5번)이 상태·단계·오류를 바로 보인다. ### 수정한 기존 기능 - `hal/modules/index.mjs`: `hawkeye`(label HawkEye, `legacyNames: ['observability']`), `LEGACY_MODULE_NAMES`·`canonicalModuleName`·`descriptor()` 별칭, `DEPLOY_ORDER = [agent, portal, webmaker, arimail, backup, hawkeye]`(HAL 제외), `DEPLOYABLE_MODULES`, 모듈 `label`, `MODULE_GUIDES`(설명·역할·requires·provides·설치 방법·singleton), `MODULE_LINKS`(상호 연결), `SERVER_PROFILES`(webmaker·webhosting·arimail·backup·portal — 모듈 순서·역할·슬롯·싱글턴). - `hal/schema.mjs`: `hal_0008_hawkeye` — `module_targets`·`modules` 의 observability 행을 hawkeye 로(이미 있으면 정리), `servers.profile`·`servers.slots_json`. - `hal/core.mjs`: `getServer/publicServer` 에 `profile·slots·slotsFree`(사이트는 capacity.sitesHosted, 메일 서비스는 등록부 수), `updateServer` 허용 키에 profile·slots_json, `pickServer` 가 역할별 슬롯 여유를 본다, `defaultTargets(d, module)`, `versionGte`. - `hal/jobs.mjs`: `precheckModule` — `hal` → `hal_not_deployable`, HAL 먼저 규칙을 "HAL 실행 버전 ≥ 릴리스 버전"으로 완화(HAL 은 별도 설치), agent-feature 모듈(backup·hawkeye)의 `module.deploy.target` 은 Agent 기능 확인(module.health)으로 배치 기록; 새 작업 `server.provision`(precheck → 역할·프로필·슬롯 기록 → 모듈 순서대로 자식 `module.deploy.target`, 실패 시 중단·partial 상세), `portal.relocate`(새 서버 설치 → `needs_user`(데이터·DNS 전환 확인) → 재개 → 옛 포털 비활성). - `hal/app.mjs`: `POST /modules/:name/deploy` — HAL 400 `hal_not_deployable`, 빈 대상 → 기본 대상, 없으면 400 `no_targets`, 응답에 `targets`; `GET /releases` → `releasesData`(latest·guides·links·profiles·servers·targets, HAL 제외); `GET /provision/profiles`, `POST /provision`(재인증, 준비 안 된 서버 409 `server_not_ready`, 포털 싱글턴 409 `portal_exists`), `POST /portal/relocate`(재인증 + 3단계, 식별정보 = 두 서버 id); `PATCH /servers/:id` 에 `slots`; 운영 현황·공개 상태 구성요소 hawkeye(HawkEye(모니터링)). - 화면: `hal/ui/render.mjs` `releasesView` 재구성(최신 릴리스 배포 카드 — 설명·필요한 것·제공·역할·설치 방법·현재 배치·배포 단추, 역할별 통합 배포, 상호 연결, 프로필 표, 과거 버전 보기(접힘, 이 버전 배포·되돌리기), 배치 상태), `jobsDockHtml`(진행 중·히스토리, 진행 막대), `serverChoiceHtml`(라디오/체크, 준비 안 된 서버 비활성, 역할·프로필·Agent 버전·모듈 표시), 서버 표에 프로필·슬롯. `hal/ui.mjs` 공통 틀에 작업 진행 창 `#ops-dock`(탭·접기), 서버 화면에 "역할별 통합 배포"·"포털 이전". 새 `hal/ui/public/dom-patch.mjs`(순수 `splitTopLevel`·`diffChildren` + `patchHtml`). `ops.js`: 주기 갱신(운영 현황·서버·작업 상세)이 바뀐 노드만, 배포 창은 서버 체크 목록(역할에 맞는 준비 서버 미리 체크), `provision`·`portal-relocate` 동작, dock 5초 갱신·탭·접기 저장, `ask()` 에 radio/checks 필드. `ai.js`: 대화 기록·상태 줄도 부분 갱신. `ops.css`. - `hal/ai/skill.md`·`tools.mjs`: HawkEye·배포 순서·HAL 배포 불가·통합 배포 설명, 배포 도구의 모듈 enum 은 배포 가능 모듈만. - 테스트: `test/hal.test.mjs` 의 "HAL 자체 배포" 계약을 "배포 불가(hal_not_deployable)"로 갱신. 버전 0.3.17(package.json·README·버전 검사 테스트). ### 고객 화면 - 변경 없음(공개 상태 페이지의 구성요소 이름만 "HawkEye(모니터링)"). ### 운영자 화면 - 배포 릴리스: 기본 화면이 모듈별 최신 릴리스 카드(설명·필요한 것·제공·설치 방법·현재 배치·"배포(서버 선택)")와 상호 연결·역할별 프로필이며, HAL 은 배포 대상이 아니라고 명시. 과거 버전은 "과거 버전 보기"에서. - 서버와 용량: "역할별 통합 배포(새 서버)"(프로필·릴리스·서버 라디오) · "포털 이전"(새 서버 라디오 → 3단계 확인). 서버 표에 프로필·슬롯(여유/전체). - 모든 화면 아래 작업 진행 창: 진행 중(단계·진행률·오류)·히스토리, 5초마다 바뀐 부분만 갱신, 접기. - 배포 창: 대상 서버를 체크 목록에서 고른다(역할에 맞는 준비된 서버 미리 체크). 운영 현황 등 주기 갱신이 번쩍이지 않는다. ### 보안 결정 - 통합 배포·포털 이전은 기존 배포와 같은 경계: 재인증 필수, 포털 이전은 3단계 확인(두 서버 id). 프로필 모듈은 `precheckModule`(릴리스 승인·artifact·HAL 건강·부트스트랩·HAL 버전) 그대로. 포털 싱글턴은 API 와 작업 양쪽에서 검사. - HAL 먼저 규칙 완화는 "HAL 이 더 새롭거나 같을 때"만 통과한다(HAL 이 오래되면 여전히 `hal_first`). HAL 자체 배포 경로는 닫혔다. - 화면 부분 갱신은 같은 렌더러의 HTML 을 노드 단위로 비교해 바꾸므로 이스케이프·내용은 그대로다. ### 테스트 - RED(구현 전): 새 `test/hal-0317.test.mjs` 는 `DEPLOYABLE_MODULES`·`SERVER_PROFILES` 가 없어 import 단계에서 실패(0/5). - GREEN: `test/hal-0317.test.mjs` **5/5** — HawkEye 기술서·별칭·배포 순서(HAL 제외)·마이그레이션 SQL 재실행(observability 행 → hawkeye)·운영 현황/공개 상태/화면 이름 / 릴리스 API(modules·latest·guides·profiles)·HAL 배포 400·빈 대상 → 역할 전체(portal→웹1, agent→전체)·없으면 400 `no_targets`·HAL 0.3.17 이 0.3.10 배포 허용·HAL 0.3.9 는 `hal_first`·릴리스 화면 문구 / 통합 배포(webmaker: agent→webmaker→hawkeye 순서, 서버 profile·role·slots, 배치 상태 3행, 자식 3개)·포털 싱글턴 409·포털 이전(새 서버 설치 → needs_user(DNS·데이터) → 재개 → 옛 비활성)·슬롯 배치(pickServer)·모르는 프로필 400·준비 안 된 서버 409 / diff 순수 함수·ops.js 정적 계약·dock 렌더·서버 선택 렌더·공통 틀 dock / 버전·문서. - 구현 중 테스트가 잡은 결함 3건: ① 마이그레이션 재실행 검사에서 ALTER 가 겹침 → hal_0008(멱등 UPDATE/DELETE)·hal_0009(ALTER) 분리 ② 부모·자식 작업은 `drain()` 으로는 자식이 시작되지 않음(테스트 러너를 tick 방식으로) ③ 운영 현황 카드가 모듈 id(hawkeye)만 보여 표시 이름(HawkEye) 추가. - 기존: `test/hal.test.mjs` 16/16(HAL 자체 배포 계약 → `hal_not_deployable`), `test/hal-ui.test.mjs` 7/7(릴리스 화면 문구 갱신), AI 테스트 12/12·6/6·6/6·4/4, `hal-cli` 3/3. 전체 Suite `node test/run.mjs`: **30개 파일 실패 0, 204건 통과**. secret-like 스캔: 새 코드 0건(`test/hal.test.mjs` 의 `password-example-1` 은 0.1.1 부터 있던 기존 예시 값, 이번에는 그 파일의 HAL 배포 계약 1줄만 수정). ### 배포(bear `hal21.ariweb.net`, HAL 만) - 2026-10-29 03:36 UTC, 코드 릴리스 + 마이그레이션(`hal_0008_hawkeye`: 배치 행 observability → hawkeye, `hal_0009_server_profile`: servers.profile·slots_json 열 추가; data/config/Vault 파일 무변경). HAL 만 배포, 다른 모듈 미변경·미배포(agent 0.1.1 은 운영자가 릴리스 화면에서 배포). - 저장소: 기능 커밋 `843b2b1a7987b4aac07171cfb44e5f47d6006929`(브랜치 `feature/hal-provision-v0.3.17` → `main` fast-forward c460f57..843b2b1, read-back `origin/main = 843b2b1`). 릴리스 빌드(그 커밋, 전체 테스트 재실행 30파일 실패 0): manifest `0.3.17 · mainCommit 843b2b1… · testsPassed=true · dirty=false · provenanceClean=true`, `verify` 5 artifact ✔. hal artifact sha256 `92d9321735e5f73fe35c0c5d7a571859d18048a0150b0663110649c9cc085b73`. - 적용(`~/oz-deploy/bear-apply.sh 0.3.17 …`, dist 5개 artifact 업로드): 해시 OK → 사전 백업 `/srv/ariweb/hal/backups/pre-v0.3.17-20261029T033614Z/`(hal.sqlite·-wal·-shm·config·vault, SHA256SUMS) → stage 추출·검증 → `/srv/ariweb/hal/releases/0.3.17-843b2b1` → `register-release --approve --by oz-dev-order-v0.3.17` → `bootstrap-release` → current 전환 → 재시작. restart → `/api/health` 200 **500ms**, active, `version 0.3.17 · bootstrap complete · releaseVersion 0.3.17 · mainCommit 843b2b1… · artifactSha256 92d93217…`. 보존 sha256 전=후(config.json `ae5963d2…`, vault.json `0e2dc987…`, vault.key `345d92e7…`), 백업 sums OK, 이전 릴리스 디렉터리 13개 보존. journal 오류 0. - read-back: 운영 DB `_migrations` 에 `hal_0008_hawkeye`·`hal_0009_server_profile`, `module_targets` 의 observability 행 0건·hawkeye 행 있음(서버 2대), releases 9행 approved(0.3.17 포함), bootstrap 0.3.17, 서버 2대 ready(profile 은 아직 null — 운영자가 통합 배포/PATCH 로 지정). 배포 코드 harness: 릴리스 API `modules=[agent, portal, webmaker, arimail, backup, hawkeye]`(HAL 없음), 프로필 5종, 릴리스 화면 "최신 릴리스·과거 버전 보기·HawkEye·HAL 은 배포 대상이 아닙니다·역할별 통합 배포", 운영 현황 구성요소 `hawkeye`, `POST /modules/hal/deploy` → 400 `hal_not_deployable`, 공통 틀 dock. 공개 `/api/status` 구성요소에 "HawkEye(모니터링)". 외부 `https://hal21.ariweb.net` `/api/health` `/ops/login` `/ops/releases` `/ops/servers` `/ops/ui/dom-patch.mjs` `/api/status` 모두 200. (부수 관찰: bear 의 claude CLI 가 0.3.9 텍스트 셸로 로그인되어 `loggedIn: true`(claude.ai) 로 바뀌어 있음.) - 운영자 조치: 배포 릴리스 화면에서 Node Agent 0.3.17 을 "배포(서버 선택)"로 두 서버에 배포(대상을 체크 목록에서 고름). 그 뒤 필요한 모듈을 같은 방식으로. 새 서버는 "역할별 통합 배포". 작업 진행은 화면 아래 작업 진행 창에서 본다. - 되돌리기(코드만): `ln -sfn /srv/ariweb/hal/releases/0.3.10-72b5e9c /srv/ariweb/hal/current && sudo systemctl restart ariweb-hal`(0.3.10 코드는 profile·slots_json 열과 hawkeye 행을 모르지만 건드리지 않는다; 운영 현황의 observability 카드는 "배치 없음"으로 보인다). 데이터까지: 위 백업. ### 알려진 제한 - 통합 배포는 같은 서버에 모듈을 순서대로 배포한다(여러 서버 동시 분할은 프로필을 서버마다 적용). 포털 이전의 데이터·DNS 전환은 운영자가 직접 수행하고 재개한다(자동 복제 없음). - 슬롯 사용량은 사이트 수(capacity.sitesHosted)와 메일 서비스 수만 자동 계산한다(backups·webmaker 슬롯은 선언값). - 실제 브라우저의 부분 갱신·dock 동작은 순수 diff·정적 계약으로 검증(자동 브라우저 테스트 없음). ### 미확정 정책값 - 프로필별 기본 슬롯(webhosting sites 50, arimail mailServices 20, backup backups 500)은 운영자가 서버 PATCH `slots` 로 바꾼다. dock 갱신 주기 5초, 화면 갱신 15초. ## 0.3.10 (2026-10-29) - **작성자**: 개발 지시 — Oz(`Ariweb-Suitle_Dev-Order_v0.3.10.md`, 2026-10-29) · 구현 — Claude Fable 5.1(브랜치 `feature/hal-shell-copy-v0.3.10`, 기준 main `bed038f` = 0.3.9). 커밋·push·`main` 병합·릴리스 빌드·bear `hal21.ariweb.net` HAL 배포까지 수행(아래 "배포"). - **이전 버전**: 0.3.9(2026-10-29). 끝 번호 1 증가(결함 대응 1항목). HAL 에만 적용, 다른 모듈 변경 없음(배포하지 않음). ### 요구(사용자 요구 — 약화 없이 반영) 텍스트 셸에서 브라우저를 열고 인증 후 프롬프트를 입력하라고 나오는데, 긴 브라우저 접속 주소를 복사할 수 없다. 이런 상황에 대한 대응책을 마련하라. ### 원인 - 0.3.9 단말 영역은 포커스 상태에서 모든 키를 셸로 보냈다 → Ctrl+C 가 복사 대신 ^C(중단)로 전달되어 복사가 되지 않았다. - 긴 인증 주소가 단말 열 수(100)에서 끊겨 두 줄로 그려져 한 번에 고르기 어려웠고, 클릭·복사 단추가 없었다. ### 수정한 기존 기능 - `hal/ui/public/term.mjs`: `joinedLines`(꽉 찬 줄을 다음 줄과 이어 붙임)·`plainText`·`extractUrls`(스크롤백·대체 화면 포함, 상자 문자(│┃║|) 제외, 꼬리 구두점 제거, 중복 제거)·`copyDecision`(선택 있으면 Ctrl+C 복사, Ctrl+Shift+C·Ctrl+Insert 항상 복사, Ctrl+V·Shift+Insert 붙여넣기, 그 밖은 전송). - `hal/ui/public/ai-render.mjs`: `shellModalHtml`(셸 모달 본문을 순수 템플릿으로)·`shellUrlsHtml`(감지된 주소: 복사 단추, 새 탭 열기 `rel="noopener noreferrer"`), 전체 출력 복사·선택 복사·텍스트로 보기(읽기 전용 textarea)·복사 안내. `hal/ui/public/ai.js`: `copyText`(Clipboard API → 숨은 textarea 폴백 → 안내), 키 처리에 `copyDecision`, 폴링마다 주소 목록·텍스트 보기 갱신. `ops.css`: 단말 `user-select:text`, 주소 목록 스타일. `hal/ui.mjs`: 셸 절에 복사 방법 안내. - 버전·문서: 0.3.10(package.json·README·버전 검사 테스트), `docs/HAL-AI.md` §9, 개발 지시 v0.3.10. ### 고객 화면 - 변경 없음. ### 운영자 화면 - 텍스트 셸 모달에 "감지된 주소" 목록(복사·열기), "전체 출력 복사", "선택 복사", "텍스트로 보기". 단말 글을 드래그로 선택하면 Ctrl+C 가 복사가 되고(Ctrl+Shift+C 는 항상 복사), 셸에 중단 신호를 보내려면 선택을 풀고 Ctrl+C 또는 키 단추. ### 보안 결정 - 열기 링크는 새 탭 + `noopener noreferrer`(HAL 창 접근 차단). 주소는 화면 표시·클립보드로만 가고 서버에 저장하지 않는다. 셸 입력·출력 저장 정책은 0.3.2 그대로(입력 미저장, 출력 메모리만). ### 테스트 - RED(구현 전): 새 `test/hal-ai-0310.test.mjs` **0/4**(`extractUrls`·`copyDecision`·`shellUrlsHtml` 없음, 화면 안내 없음). - GREEN: `test/hal-ai-0310.test.mjs` **4/4** — 줄 끝에서 끊긴 주소 이어 붙이기(40열)·꽉 차지 않은 줄은 잇지 않음·꼬리 마침표 제거·상자 문자 제외·스크롤백·중복 제거·대체 화면·전체 글 / 복사 판정(선택 Ctrl+C=복사, 없으면 전송, Ctrl+Shift+C·Ctrl+Insert=복사, Ctrl+V·Shift+Insert=붙여넣기) / 셸 모달 순수 템플릿(주소 목록 이스케이프·복사·열기 noopener·전체 복사·텍스트 보기·안내) / 화면·버전·문서. - 기존: `hal-ai-039` 6/6 · `hal-ai-032` 6/6 · `hal-ai` 12/12 · `hal-ui` 7/7 · `webmaker-visual-editor` 5/5. 전체 Suite `node test/run.mjs`: **29개 파일 실패 0, 199건 통과**. 변경 파일 secret-like 스캔 0건. 러너 임시 파일 미커밋. ### 배포(bear `hal21.ariweb.net`, HAL 만) - 2026-10-29 02:53 UTC, 코드 릴리스만(마이그레이션 없음; data/config/Vault 파일 무변경). HAL 만 배포, 다른 모듈 미변경·미배포. - 저장소: 기능 커밋 `72b5e9c7dc3f62ba8cff8dd72411f51a80ed2c85`(브랜치 `feature/hal-shell-copy-v0.3.10` → `main` fast-forward bed038f..72b5e9c, read-back `origin/main = 72b5e9c`). 릴리스 빌드(그 커밋, 전체 테스트 재실행 29파일 실패 0): manifest `0.3.10 · mainCommit 72b5e9c… · testsPassed=true · dirty=false · provenanceClean=true`, `verify` 5 artifact ✔. hal artifact sha256 `4f500da4ab7ab5e787aa7fcb82a9ca675bbc9559a7b9479c3502cc0d1c1fac31`. - 적용(`~/oz-deploy/bear-apply.sh 0.3.10 …`, dist 5개 artifact 업로드): 해시 OK → 사전 백업 `/srv/ariweb/hal/backups/pre-v0.3.10-20261029T025307Z/` → stage 추출·검증 → `/srv/ariweb/hal/releases/0.3.10-72b5e9c` → `register-release --approve --by oz-dev-order-v0.3.10`(approved, 5 artifact) → `bootstrap-release` → current 전환 → 재시작. restart → `/api/health` 200 **500ms**, active, `version 0.3.10 · bootstrap complete · releaseVersion 0.3.10 · mainCommit 72b5e9c… · artifactSha256 4f500da4…`. 보존 sha256 전=후(config.json `ae5963d2…`, vault.json `0e2dc987…`, vault.key `345d92e7…`), 백업 sums OK, 이전 릴리스 디렉터리 12개 보존. 운영 DB: releases 8행 approved, bootstrap 0.3.10, 서버 2대 ready(heartbeat 재개 02:53:10Z·02:53:30Z), journal 오류 0. 운영 AI 연결 2건(H9001 active · H9000 reauth_required) 그대로. - read-back(배포 코드 harness `~/oz-deploy/deployed-render-0.3.10.mjs`): 배포된 `term.mjs` 로 40열에서 끊긴 주소 이어 붙이기 `https://auth.example.test/oauth/authorize?c=abc123&state=xyz` ✔, `copyDecision` 선택 있음=copy·없음=send ✔, 셸 모달에 `ai-shell-urls`·`ai-shell-copy-all`·`ai-shell-textview` ✔, `/ops/ai` 에 "감지된 주소" 안내 ✔; 0.3.9 항목(슬래시 명령 20·셸 명령표·`/help`)·CLI 탐색(claude 로그인 필요·codex 로그인됨) 그대로. 외부 `https://hal21.ariweb.net` `/api/health` `/ops/login` `/ops/ai` `/ops/ui/ai.js` `/ops/ui/term.mjs` `/ops/ui/ai-render.mjs` 모두 200. - 운영자 조치: 텍스트 셸에서 `claude auth login` 을 열면 인증 주소가 "감지된 주소" 목록에 복사·열기 단추로 나타난다. 단말 글을 드래그하면 Ctrl+C 로도 복사된다. - 되돌리기(코드만): `ln -sfn /srv/ariweb/hal/releases/0.3.9-fe4800f /srv/ariweb/hal/current && sudo systemctl restart ariweb-hal`. ### 알려진 제한 - 주소가 두 줄 이상에 걸쳐 끊길 때 중간 줄이 꽉 차지 않으면(TUI 가 상자 안에서 임의로 줄을 나눈 경우) 이어 붙이지 못할 수 있다 — 그때는 "텍스트로 보기"에서 직접 고른다. - Clipboard API 는 HTTPS·사용자 동작 안에서만 동작한다(hal21 은 HTTPS). 실패 시 폴백·안내. ### 미확정 정책값 - 없음. ## 0.3.9 (2026-10-29) - **작성자**: 개발 지시 — Oz(`Ariweb-Suitle_Dev-Order_v0.3.9.md`, 2026-10-29) · 구현 — Claude Fable 5.1(브랜치 `feature/hal-ai-shell-v0.3.9`, 기준 main `f861c7e` = 0.3.2). 커밋·push·`main` 병합·릴리스 빌드·bear `hal21.ariweb.net` HAL 배포까지 수행(아래 "배포"). - **이전 버전**: 0.3.2(2026-10-29). 지시에 따라 버전은 **0.3.9**(끝 번호 7 증가 — 독립 개선 6항목 + 점검 수정). HAL 에만 적용, 다른 모듈 변경 없음(배포하지 않음). ### 요구(사용자 요구 — 약화 없이 반영) 1. 로그인 터미널 개선 — claude 는 설치되어 있으면 바로 실행되고 신뢰 여부를 입력하면 연결되므로, 무조건 `claude --login` 뒤 브라우저 인증을 하는 구조는 동작하지 않을 수 있다. 종합적으로 고려해 CLI 로그인 인증을 개선. 2. 텍스트 셸 개선 — 화면만 보여 주는 수준으로는 여러 입력 상황(예: Trust 선택)에 대응할 수 없고, 현재 입력창은 코드만 받는다. 범용적으로 쓸 수 있게. 로그인 안내 메시지는 나오는데 이를 수행할 텍스트 셸이 동작하지 않는다. 3. "열린 텍스트 셸이 2개입니다" 오류 — 화면에서는 모두 닫았는데 시스템은 다르게 인식. 원인을 찾아 수정. 4. LLM 연동 후 모델을 사용자가 선택. 5. 컨텍스트 입력 창에 `/order` 형태의 기본 명령어 셋, `/help` 로 목록. 6. 미확인 버그·오작동 점검·수정. ### 점검 결과와 원인 - (1) bear 의 Claude Code 2.1.312 에서 `claude login` 은 로그인 명령이 아니라 **대화형 세션에 "login" 을 입력하는 것**이다(비 TTY 로 실행하면 "Failed to authenticate: OAuth session expired" 만 출력). 실제 로그인은 `claude auth login`(`--claudeai`(기본)·`--console`·`--sso` 옵션, 화살표·Enter 로 고르는 TUI). bare `claude` 는 새 작업 폴더에서 "이 폴더의 파일을 신뢰합니까?" 를 묻는다(`~/.claude.json` 의 `projects` 에 HOME 은 이미 신뢰됨). - (2) 0.3.2 셸은 출력에서 ANSI 를 걷어 내 줄로 붙이고 입력은 "글 + Enter" 만 보내 TUI(커서 이동·선택 목록)를 그릴 수도, 화살표·단일 키(y/n)를 보낼 수도 없었다. - (3) 모달 "닫기"가 세션을 종료하지 않았다(폴링만 멈춤) → 대화형 CLI 가 입력을 기다리며 살아남아 한도 2를 채움. 또 세션 수를 `status==='running'` 으로만 세어 프로세스가 끝났는데 상태가 남는 경우도 한도에 들었다. - (6) 점검에서 더 고친 것: `window.open` 기능 문자열에 잘못된 `noopener=no`, 패널에서 `reauth_required` 연결을 고칠 길이 없음(단추 추가), 로그인 안내가 `claude login`(틀린 명령)을 가리킴, 확인 실패 메시지가 서버 셸을 가리키며 HAL 화면의 셸을 안내하지 않음. ### 수정한 기존 기능 - `hal/ai/shell.mjs`: 명령표 교체(claude `auth login`·`auth logout`·`auth status`·`login_console`·`interactive`, codex `interactive` 추가), 작업 폴더 = `cliHome()`(HOME), 단말 크기 `cols·rows`(40~250 × 10~80, 기본 100×30) → `COLUMNS/LINES`, 살아 있는 프로세스 기준 한도 3(끝났는데 running 인 세션 정리), 409 에 열린 세션 목록, `killAll(operator)`, 공개 객체에 `args·cwd·cols·rows`. 안내문(`SHELL_GUIDE`)을 TUI·신뢰 질문·종료 방법 중심으로 다시 씀. - `hal/ai/providers.mjs`: `CLI_TOOLS.claude.loginCommand = 'claude auth login'`, 로그인 안내가 HAL 텍스트 셸을 가리키도록, `cliHome` export. - 새 `hal/ui/public/term.mjs`(순수 단말 에뮬레이터): 글자·`\r\n\b\t`, CSI 커서 이동(A B C D E F G H f d)·지우기(J K X P @)·줄 삽입/삭제(L M)·스크롤(S T)·저장/복원(s u, ESC 7/8)·스크롤 영역(r)·모드(?25 커서, ?1049/?47 대체 화면)·SGR 무시·OSC 무시·불완전 시퀀스 보관, 줄 끝 자동 줄 바꿈, 스크롤백 1000줄, `renderHtml`(커서 표시, 이스케이프), `keyToBytes`(브라우저 키 → 바이트). - 새 `hal/ai/commands.mjs`: 명령 20개(local 4 + prompt 16), `parseCommand`·`helpText`·`publicCommands`. `hal/ai/engine.mjs`: `send()` 가 `/` 로 시작하면 명령 해석(모르면 400, 인자 누락 400), local 은 `localCommand`(help·tools·model(개별 방만, 목록 검증)·reset — 모델 호출 없음), prompt 는 user 메시지에 `command` 블록(원문 + 확장 지시)을 붙이고 `buildMessages` 가 모델에게 확장 지시를 준다. - `hal/ai/rooms.mjs`: `listRooms` 의 `connection.models·toolsVia·lastError`. `hal/ai/routes.mjs`: `POST /shell` 에 `cols·rows`, `POST /shell/kill-all`, `GET /ops/api/ai/commands`. - 화면: `hal/ui.mjs` 셸 절(claude auth login · 대화형(신뢰 설정) · 상태 / codex login · 대화형 · 상태 / 모두 종료, 세션 목록), 패널 입력 안내에 `/help`; `ai-render.mjs` `modelSelectHtml`(연결 표·패널 상태 줄)·`shellSessionsHtml`·`commandsHelpHtml`, 상태 줄 "로그인·재인증" 단추; `ai.js` 셸 모달을 단말 에뮬레이터로(포커스 키 전달·붙여넣기·키 단추·줄 입력·닫기=종료·세션 다시 열기), 세션 목록·모두 종료, 모델 선택 변경 → PATCH, `/` 만 치면 `/help`; `ops.css` 단말 스타일. - 버전·문서: 0.3.9(package.json·README·버전 검사 테스트), `docs/HAL-AI.md` §8, 개발 지시 v0.3.9. ### 고객 화면 - 변경 없음. ### 운영자 화면 - AI 연동 › 텍스트 셸: "claude auth login"(TUI 로그인), "claude 대화형(신뢰 설정)", 상태, codex login·대화형·상태, "열린 셸 모두 종료", 세션 목록(실행 중·끝남, 열기·종료). 셸 모달: 검은 단말 화면(클릭 후 키 직접 입력, 커서 표시), 키 단추, 줄 입력란, 안내문. 닫으면 세션이 끝난다. - 연결 표·패널 상태 줄: 모델 선택 상자(확인된 목록). 패널 상태 줄의 "로그인·재인증" 단추(재인증 필요 연결). - 지시 입력: `/help` 로 명령 목록, `/status` `/servers` `/jobs` `/releases` `/incidents` `/order <지시>` `/deploy …` `/model <이름>` `/tools` `/reset` 등. 운영자 원문이 기록에 남고 모델은 확장 지시를 받는다. ### 보안 결정 - 텍스트 셸은 여전히 허용 명령표만 실행한다(대화형 세션 추가 포함, 임의 명령 없음). 대화형 CLI 는 운영자 본인의 서버 계정 CLI 이며 HAL 환경 변수(Vault 키)는 넘기지 않는다. 입력은 저장·감사하지 않는다(시작·종료만 감사). - 슬래시 명령의 확장 지시는 모델에게 "어떤 도구로 확인·제안하라"를 말할 뿐 승인 규칙(쓰기·고위험 제안 → 운영자 재인증·3단계)을 바꾸지 않는다. `/model` 은 확인된 모델 목록 밖을 거부한다. - 단말 에뮬레이터는 출력을 이스케이프해 그린다(TUI 출력에 HTML 이 섞여도 실행되지 않음). ### 테스트 - RED(구현 전): 새 `test/hal-ai-039.test.mjs` **0/6**(명령표 `['login']`, interactive 거부 400, term.mjs 없음, `connection.models` 없음, `/ops/api/ai/commands` 404, 화면 문구·버전). - GREEN: `test/hal-ai-039.test.mjs` **6/6** — 명령표(`auth login`·`auth logout`·interactive)·안내·`claude auth login` 힌트 / auth login TUI 흉내(크기 120×40 전달·작업 폴더 HOME·화살표 바이트 그대로·코드 입력·종료 코드) / 대화형 신뢰 질문에 `y` 단일 키 / 옛 명령 문자열·옵션 거부 / 한도 3·끝난 세션 제외·409 세션 목록·DELETE 뒤 재시작·kill-all / 단말 에뮬레이터(CR 덮어쓰기·커서 이동·줄 지우기·SGR·OSC·커서 숨김·화면 지우기·스크롤백·대체 화면 복원·절대 이동·이스케이프·자동 줄 바꿈) / 모델 선택(목록·PATCH·`/model`·목록 밖 400·통합 방 400) / 슬래시 명령(`/help` 모델 호출 없음·`/status` 확장 지시·원문 기록·명령 블록·`/servers web`·`/order`·인자 누락·`/deploy portal 0.3.9`·`/tools`·`/reset`·모르는 명령·통합 방 `/help`·일반 글) / 화면·버전·문서. 중간 결함 2건을 테스트가 잡았다: ① CSI 의 private 표식(`?25l`·`?1049h`)을 매개변수 뒤에서 찾아 시퀀스가 영원히 보류되던 해석기 오류 → 표식을 앞에서 해석 ② 최소 열 수 10 → 4(좁은 화면 검사). - 기존: `test/hal-ai.test.mjs` 12/12(로그인 힌트 `claude auth login` 으로 갱신), `test/hal-ai-032.test.mjs` 6/6(가짜 CLI 를 `auth login` 형식으로), `hal-ui` 7/7, `webmaker-visual-editor` 5/5. 전체 Suite `node test/run.mjs`: **28개 파일 실패 0, 195건 통과**. 변경 파일 secret-like 스캔 0건. 러너 임시 파일 미커밋. ### 배포(bear `hal21.ariweb.net`, HAL 만) - 2026-10-29 02:25 UTC, 코드 릴리스만(새 마이그레이션 없음; data/config/Vault 파일 무변경). HAL 만 배포, 다른 모듈 미변경·미배포. - 저장소: 기능 커밋 `fe4800f94a55ed1199e6ff5b301176e9d9819b23`(브랜치 `feature/hal-ai-shell-v0.3.9` → `main` fast-forward f861c7e..fe4800f, read-back `origin/main = fe4800f`). 릴리스 빌드(그 커밋, 전체 테스트 재실행 28파일 실패 0): manifest `0.3.9 · mainCommit fe4800f… · testsPassed=true · dirty=false · provenanceClean=true`, `verify` 5 artifact ✔. hal artifact sha256 `ebf56b375635738feed9b8fe84640c6c96d4fcf4aafaa548343a819f91ffff2b`. - 적용(`~/oz-deploy/bear-apply.sh 0.3.9 …`, dist 5개 artifact 업로드): 해시 OK → 사전 백업 `/srv/ariweb/hal/backups/pre-v0.3.9-20261029T022542Z/` → stage 추출·검증 → `/srv/ariweb/hal/releases/0.3.9-fe4800f` → `register-release --approve --by oz-dev-order-v0.3.9`(approved, 5 artifact) → `bootstrap-release` → current 전환 → 재시작. restart → `/api/health` 200 **500ms**, active, `version 0.3.9 · bootstrap complete · releaseVersion 0.3.9 · mainCommit fe4800f… · artifactSha256 ebf56b37…`. 보존 sha256 전=후(config.json `ae5963d2…`, vault.json `0e2dc987…`, vault.key `345d92e7…`), 백업 sums OK, 이전 릴리스 디렉터리 11개 보존. 운영 DB: releases 7행 approved, bootstrap 0.3.9, 서버 2대 ready(heartbeat 재개 02:26:00Z·02:26:09Z), journal 오류 0. 운영 AI 연결 2건: `H9001`(local-cli codex, gpt-6.1-sol, active) · `H9000`(local-cli claude, claude-opus-5-5, `reauth_required` — claude 로그인 전). - read-back(배포 코드 harness `~/oz-deploy/deployed-render-0.3.9.mjs`, 메모리 DB·임시 data dir): 슬래시 명령 20개(`help status health servers server jobs job releases incidents audit dr emergency order deploy rollback isolate incident model tools reset`), 셸 명령표 claude `login=[auth,login]`·`login_console`·`logout=[auth,logout]`·`status=[auth,status]`·`version`·`interactive=[]`, `/ops/ui/term.mjs` 200, `/ops/ai` 에 "claude auth login·대화형·열린 셸 모두 종료", 패널 입력 안내에 `/help`, 공급자 6종, MCP 설정 경로 `releases/0.3.9-fe4800f/hal/index.mjs mcp`, 도구 33, CLI 탐색 claude 2.1.312(로그인 필요)·codex 0.162.1(로그인됨). 외부 `https://hal21.ariweb.net` `/api/health` `/ops/login` `/ops/ai` `/ops/ui/ai.js` `/ops/ui/term.mjs` 모두 200. - 운영자 조치: AI 연동 › 텍스트 셸 › "claude auth login"(화살표·Enter 로 방식 선택 → URL 열고 코드 붙여넣기) 뒤 H9000 의 "확인"; 처음 대화형을 쓰려면 "claude 대화형(신뢰 설정)"에서 신뢰 질문에 답한다. 채팅에서 `/help`. - 되돌리기(코드만): `ln -sfn /srv/ariweb/hal/releases/0.3.2-8a111bc /srv/ariweb/hal/current && sudo systemctl restart ariweb-hal`. ### 알려진 제한 - 단말 에뮬레이터는 색·굵게를 그리지 않고 한글 등 2칸 글자를 1칸으로 센다(TUI 가 2칸으로 계산하면 조금 어긋남). 단말 크기 변경(SIGWINCH)은 없다(세션 시작 때 고정). - 실제 `claude auth login` TUI 흐름(브라우저 URL → 코드 붙여넣기)은 bear 에서 운영자가 로그인한 뒤에만 확인된다(테스트는 TUI 를 흉내 낸 가짜 CLI). - 슬래시 명령은 고정 집합이다(운영자가 추가하려면 `hal/ai/commands.mjs`). ### 미확정 정책값 - 셸 한도(3)·단말 기본 크기(100×30)·폴링 간격(300ms). ## 0.3.2 (2026-10-29) - **작성자**: 개발 지시 — Oz(`Ariweb-Suitle_Dev-Order_v0.3.2.md`, 2026-10-29) · 구현 — Claude Fable 5.1(브랜치 `feature/hal-ai-convenience-v0.3.2`, 기준 main `ad96f0b` = 0.3.1). 커밋·push·`main` 병합·릴리스 빌드·bear `hal21.ariweb.net` HAL 배포까지 수행(아래 "배포"). - **이전 버전**: 0.3.1(2026-10-29, CLI 자동 탐색). 0.3.2 는 끝 번호를 올리는 HAL 개선 릴리스: AI 연동 편의성(사전 안내·OAuth 자동 진행·CLI 텍스트 셸·공급자 프리셋 4종 + 기타) + 도구 기본 설정·인증/승인 요청. HAL 에만 적용, 다른 모듈 변경 없음(배포하지 않음). ### 요구(사용자 요구 — 약화 없이 반영) 1. AI 연동 편의성: OAuth 연동 시 필요한 연동 화면을 자동으로 띄운다. CLI 로그인은 text shell 화면을 제공해 화면에서 필요한 작업을 하게 한다. 이 작업들이 자동으로 진행되고 사전에 필요한 작업 내용을 안내한다. API 연동은 Claude·ChatGPT·Gemini·OpenRouter 를 기본 서비스 업체로 선택할 수 있게 하고 그 밖의 API 서비스 업체 연동도 지원한다. 2. LLM·도구 연결 기본 설정과 인증: HAL 운영·관리용 도구와 스킬을 LLM 연동 시 즉시 쓸 수 있게 미리 설정해 제공하고, 인증·승인이 필요한 도구는 필요한 인증/승인을 요청할 수 있게 한다. 결과를 소스에 반영하고 `https://hal21.ariweb.net` 에 배포한다. ### 수정한 기존 기능 - `hal/ai/providers.mjs`: 공급자 프리셋 `PROVIDERS`(anthropic=Claude · openai=ChatGPT · gemini · openrouter · other · local-cli; `kind` 로 HTTP 형식 분기, `guide` 사전 안내, `oauthPreset`). **Gemini 어댑터**(`generateContent`, `systemInstruction`, `functionDeclarations`(대문자 type 스키마), `functionCall/functionResponse`, `usageMetadata`, 모델 목록 `models/` 접두어 제거, 인증 `x-goog-api-key`/Bearer). OpenRouter 는 OpenAI 호환 + `HTTP-Referer`/`X-Title`. `toolFormat()`. **MCP 설정 주입**(`mcpConfig(d)`): claude `--mcp-config <임시 파일> --strict-mcp-config --allowedTools mcp__hal`(실행 뒤 파일 삭제), codex `-c mcp_servers.hal.command/args/env`. - `hal/ai/connections.mjs`: `other` 는 기본 주소 필수(`base_url_required`), OAuth 프리셋 채우기(gemini: Google authorize/token·scope·`access_type=offline`·`prompt=consent`; openrouter: `mode=openrouter_pkce` — state 를 callback_url 에 실어 보내고 `/api/v1/auth/keys` 로 키를 받아 Vault 에 Bearer 로 저장), 공개 객체 `providerKind`·`tools`·`toolsVia`, 도구 정책 `tools_json`(프리셋 검증, `unknown_tool`). - `hal/ai/engine.mjs`: 연결별 도구 정책대로 선언·실행(꺼진 도구 호출은 실행 없이 오류 결과), 로컬 CLI 는 `mcp` 플래그, `proposeExternal`(외부 출처 승인 요청 → 통합 방 HAL 안내 + 제안 카드, `source`), 연결 없는 제안의 승인·거부 결과를 통합 방 안내로. - `hal/ai/mcp.mjs`: 쓰기·고위험 `tools/call` → 승인 요청(`{approvalRequested, actionId, …}`, isError 아님). `hal/ai/tools.mjs`: `hal_action_status`·`hal_actions_pending`(읽기), `TOOL_PRESETS`(all·read_write·read_only·none), `toolDefs(format, only)`(gemini 형식 포함). - 새 파일 `hal/ai/shell.mjs`: 텍스트 셸 세션(허용 명령표 `SHELL_COMMANDS`, 사전 안내 `SHELL_GUIDE`, `script -qfc` PTY, 운영자당 2개, 유휴 10분·최대 30분, 출력 256KB 메모리, 입력 미저장, 감사는 시작·종료만). `hal/deps.mjs`: `d.shell`. - `hal/ai/routes.mjs`: `POST /connections` 응답 `next`(oauth → start 경로 / cli_check / test), 콜백 → `/ops/ai/oauth/done?ok=…`(완료 페이지: 팝업이면 `postMessage` + 닫기, 아니면 `/ops/ai?oauth=ok|fail`), `error=` 콜백도 완료 페이지로, `GET /ops/api/ai/mcp-config`, `GET /ops/api/ai/tools` 에 `presets`, 텍스트 셸 `GET/POST /ops/api/ai/shell`, `GET /:id?after=`, `POST /:id/input`, `DELETE /:id`(시작은 `requireRecent`), `providers[].guide/kind/defaultBase/oauthPreset`·`callbackUrl`. - `hal/schema.mjs`: 마이그레이션 `hal_0007_ai_tools_policy`(`ai_connections.tools_json`, `ai_actions.source`). - 화면(`hal/ui.mjs`·`ai-render.mjs`·`ai.js`·`ops.css`): 공급자 선택 6종, **사전 준비 안내** 상자(공급자·인증 방식별, 콜백 주소 표시), OAuth 저장 직후 팝업 자동 열기·완료 자동 확인, 로컬 CLI 저장 직후 탐색·로그인 확인 → 미로그인이면 **텍스트 셸** 자동 열기(출력 0.5초 갱신, Enter 전송, Ctrl+C, 종료), 셸 단추(claude/codex login·상태), **도구 정책** fieldset(프리셋 + 직접 선택 체크박스, 위험 등급별 모두 켜기/끄기), **MCP 설정** 절(Claude Code 명령·JSON, Codex TOML), 패널 상태 줄에 "승인 대기 N", 연결 표에 도구 프리셋·MCP 표시. - 문서·버전: `docs/HAL-AI.md` §7, 개발 지시 v0.3.2, 0.3.2 표기(package.json·README·버전 검사 테스트). ### 고객 화면 - 변경 없음. ### 운영자 화면 - AI 연동: 공급자 고르면 사전 준비 안내가 바뀐다(키 발급 주소, OAuth 리디렉션 URI 등록 안내와 HAL 콜백 주소, CLI 로그인 절차). OAuth 연결은 저장하면 로그인 창이 자동으로 열리고 승인 뒤 스스로 닫히며 모델 확인까지 이어진다(팝업 차단 시 같은 창 이동). 로컬 CLI 연결은 저장하면 탐색·로그인 확인 → 미로그인이면 텍스트 셸(login)이 자동으로 열린다. 텍스트 셸 단추로 언제든 claude/codex login·상태를 실행할 수 있다. 도구 정책은 기본 "전부"이며 바꿀 수 있다. MCP 설정 절에서 외부 클라이언트용 설정을 복사한다. - 채팅룸: 외부(MCP) 승인 요청이 통합 방에 HAL 안내 + 제안 카드로 올라오고 상태 줄에 "승인 대기 N" 이 보인다. 승인·거부 흐름은 0.3.0 과 같다(재인증, 고위험 3단계). ### 보안 결정 - 텍스트 셸은 임의 셸이 아니다: (tool, command 이름)만 받아 허용표의 인자 배열을 실행하고, 실행 파일은 탐색된 절대 경로. 시작은 재인증, 입력은 저장·감사하지 않음, 출력은 메모리만. HAL 환경 변수(Vault 키)는 넘기지 않는다. - OAuth 완료 페이지는 외부 스크립트 없이 결과·연결 id 만 다루고 `postMessage` 는 같은 출처로만 보낸다. OpenRouter PKCE 로 받은 키는 Vault 에만. Google OAuth 는 client secret 을 서버→토큰 끝점에만. - MCP 승인 요청은 인자를 서버가 다시 검증해 제안으로만 올린다(실행 없음). 승인 라우트·재인증·3단계 확인은 0.3.0 그대로. 도구 정책으로 꺼진 도구는 선언도 실행도 하지 않는다. - Gemini 선언 스키마는 `additionalProperties` 를 뺀 OpenAPI 부분집합으로 변환하되 HAL 쪽 검증(`validateArgs`)은 원래 선언으로 한다(모르는 인자 거부 유지). ### 테스트 - RED(구현 전): 새 `test/hal-ai-032.test.mjs` **0/6**(공급자 gemini 없음 400 `bad_provider`, `/ops/api/ai/shell` 404, 도구 정책 없음, MCP 상태 도구 없음, 화면 문구 없음, 버전 0.3.1). - GREEN: `test/hal-ai-032.test.mjs` **6/6** — 프리셋·안내·OpenRouter 기본 주소·기타 주소 필수·Gemini 모델 목록(x-goog-api-key)·generateContent 도구 왕복 / OAuth 생성 `next`·Google 프리셋(access_type=offline)·완료 페이지(opener postMessage·close·대체 이동)·실패 콜백·OpenRouter PKCE(callback_url 에 state, `/api/v1/auth/keys` 교환 → Vault, Bearer) / 텍스트 셸(재인증 403·임의 명령 400·`script` PTY 출력·입력 Enter·종료 코드·입력 미감사·목록·강제 종료·404) / 도구 정책(기본 all·프리셋 목록·read_only·custom 검증·선언 필터)·MCP 설정 API·claude `--mcp-config` 파일 내용·삭제·`--allowedTools mcp__hal`·정책 none 이면 미주입·codex `-c mcp_servers.hal.*` / MCP 승인 요청(제안·`hal_action_status`·`hal_actions_pending`·콘솔 제안 목록(source mcp)·통합 방 카드·재인증 403·승인 실행·결과 안내·고위험 3단계·거부) / 화면·버전·문서. 기존 `test/hal-ai.test.mjs` 12/12(0.3.0 의 "MCP 쓰기 도구는 isError" 기대를 "승인 요청(제안)"으로 갱신). - targeted: `hal-ui` 7/7 · `hal-reauth-banner` 5/5 · `hal` 16/16 · `webmaker-visual-editor` 5/5. 전체 Suite `node test/run.mjs`: **27개 파일 실패 0, 189건 통과**. 변경 파일 secret-like 스캔 0건. 러너 임시 파일 미커밋. ### 배포(bear `hal21.ariweb.net`, HAL 만) - 2026-10-29 01:46 UTC, 코드 릴리스만(마이그레이션 `hal_0007_ai_tools_policy` 는 열 추가만; data/config/Vault 파일 무변경). HAL 만 배포, 다른 모듈 미변경·미배포. - 저장소: 기능 커밋 `8a111bcb8b79097c32ac309f948418d9c00e7d54`(브랜치 `feature/hal-ai-convenience-v0.3.2` → `main` fast-forward ad96f0b..8a111bc, read-back `origin/main = 8a111bc`). 릴리스 빌드(그 커밋, 전체 테스트 재실행 27파일 실패 0): manifest `0.3.2 · mainCommit 8a111bc… · testsPassed=true · dirty=false · provenanceClean=true`, `verify` 5 artifact ✔. hal artifact sha256 `5575bc18ad2921abdc4021edd2116388be48467b3f56a9527f0a2a6f7d168753`. - 적용(`~/oz-deploy/bear-apply.sh 0.3.2 …`, dist 5개 artifact 업로드): 해시 OK → 사전 백업 `/srv/ariweb/hal/backups/pre-v0.3.2-20261029T014617Z/` → stage 추출·검증 → `/srv/ariweb/hal/releases/0.3.2-8a111bc` → `register-release --approve --by oz-dev-order-v0.3.2`(approved, 5 artifact) → `bootstrap-release` → current 전환 → 재시작. restart → `/api/health` 200 **500ms**, active, `version 0.3.2 · bootstrap complete · releaseVersion 0.3.2 · mainCommit 8a111bc… · artifactSha256 5575bc18…`. 보존 sha256 전=후(config.json `ae5963d2…`, vault.json `0e2dc987…`, vault.key `345d92e7…`), 백업 sums OK, 이전 릴리스 디렉터리 10개 보존. 운영 DB: `_migrations` 에 `hal_0007_ai_tools_policy`, releases 6행 approved, bootstrap 0.3.2, 서버 2대 ready(heartbeat 재개 01:46:30Z·01:46:37Z), journal 오류 0. 운영 AI 연결 1건(`H9001`, local-cli, active — tools_json null → 기본 정책 all 로 해석). - read-back(배포 코드 harness `~/oz-deploy/deployed-render-0.3.2.mjs`, 메모리 DB·임시 data dir, 운영 데이터 미접근): 공급자 6종(anthropic·openai·gemini·openrouter·other·local-cli), 콜백 주소 `https://hal21.ariweb.net/ops/ai/oauth/callback`, MCP 설정 command `/usr/bin/node` args `[…/releases/0.3.2-8a111bc/hal/index.mjs, mcp]`, 텍스트 셸 허용 CLI claude·codex, 완료 페이지 200(postMessage 포함), `/ops/ai` 에 "사전 준비·텍스트 셸·MCP 설정·도구 정책·OpenRouter" 모두 있음, 도구 33, CLI 탐색 claude 2.1.312(로그인 필요)·codex 0.162.1(로그인됨). 외부 `https://hal21.ariweb.net` `/api/health` `/ops/login` `/ops/ai` `/ops/ui/ai.js` `/ops/ai/oauth/done` 모두 200. - 운영자 조치: claude 를 쓰려면 AI 연동 화면의 "claude login" 텍스트 셸(또는 CLI 연결 저장 시 자동으로 열림)에서 URL 을 열고 코드를 붙여 넣으면 된다. OAuth 공급자(Gemini: Google OAuth 클라이언트의 리디렉션 URI 에 위 콜백 주소 등록, OpenRouter: 설정 없음)는 저장하면 로그인 창이 자동으로 열린다. - 되돌리기(코드만): `ln -sfn /srv/ariweb/hal/releases/0.3.1-3775b7b /srv/ariweb/hal/current && sudo systemctl restart ariweb-hal`(0.3.1 은 `tools_json`·`source` 열을 모르지만 건드리지 않는다). ### 알려진 제한 - Anthropic·OpenAI 는 공개 OAuth 클라이언트가 없어 OAuth 는 자체 중계가 있을 때만 쓴다(안내에 명시). Gemini OAuth 는 운영자가 Google Cloud 에서 OAuth 클라이언트를 만들어야 한다. - 텍스트 셸은 로그인·로그아웃·상태·버전만 실행한다. 터미널 크기 변경(SIGWINCH)은 지원하지 않는다(100×30 고정). - 로컬 CLI 의 MCP 도구는 Claude Code 2.1.312 / Codex 0.162.1 기준 인자(`--mcp-config`, `-c mcp_servers.*`)로 붙인다 — CLI 가 바뀌면 인자가 달라질 수 있다. 실제 운영 CLI 로의 MCP 도구 호출은 운영자가 로그인한 뒤 확인한다. - 실제 브라우저에서 팝업·셸 모달 동작은 자동 테스트하지 않았다(순수 렌더러·API·완료 페이지 내용으로 검증). ### 미확정 정책값 - 텍스트 셸 유휴/최대 시간(10분/30분), 동시 세션 수(2), 출력 상한(256KB). OAuth state 만료(10분). ## 0.3.1 (2026-10-29) - **작성자**: 개발 지시 — Oz(`Ariweb-Suitle_Dev-Order_v0.3.1.md`, 2026-10-29) · 구현 — Claude Fable 5.1(브랜치 `feature/hal-cli-discovery-v0.3.1`, 기준 main `0d3a0b8` = 0.3.0). 커밋·push·`main` 병합·릴리스 빌드·bear `hal21.ariweb.net` HAL 배포까지 수행(아래 "배포"). - **이전 버전**: 0.3.0(2026-10-29, HAL AI 채팅룸). 0.3.1 은 **끝 버전 번호만 올리는 결함 수정**: 운영 서버에 이미 설치된 CLI 를 HAL 이 찾지 못하던 문제. HAL 에만 적용, 다른 모듈 변경 없음(배포하지 않음). ### 요구(사용자 요구 — 약화 없이 반영) bear 서버(HAL 운영 서버)에 이미 CLI 가 설치되어 있는데 설치된 것을 확인하지 못하고 있다. 서버의 설치 상황을 점검해서 이미 설치된 것은 찾아서 연결 가능하게 수정하라. ### 점검 결과(bear, 2026-10-29, HAL 실행 계정 master) - 설치됨: Claude Code CLI **2.1.312** `~/.local/bin/claude` → `~/.local/share/claude/versions/2.1.312`, OpenAI Codex CLI **0.162.1** `~/.local/bin/codex` → `~/.codex/packages/standalone/current/bin/codex`. 로그인 셸 PATH 에는 `~/.local/bin` 이 있지만 **HAL 서비스(systemd) PATH 는 `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/snap/bin`** 이라 0.3.0 의 `cliBin()`(지정 경로 또는 PATH 의 `claude`)이 찾지 못했다. 이것이 원인. - 로그인: codex `login status` = "Logged in using ChatGPT"(기본 모델 `gpt-6.1-sol`, `~/.codex/models_cache.json` 11개). claude `auth status` = `loggedIn:false`, headless 실행 결과 "Failed to authenticate: OAuth session expired and could not be refreshed" → **claude 는 서버에서 `claude login` 이 필요**(HAL 이 대신할 수 없다). - 유닛 샌드박스(`ProtectSystem=strict`, `ReadWritePaths=/srv/ariweb/hal`)와 같은 조건으로 `systemd-run` 실행: `claude --version`·`claude -p`(인증 오류 JSON 반환)·`codex exec --json`("OK", usage) 모두 실행됨 — 유닛 변경 불필요. ### 수정한 기존 기능 - `hal/ai/providers.mjs`: CLI **자동 탐색**(`findCliBin`·`discoverClis`·`cliSearchDirs`) — CLAUDE_BIN/CODEX_BIN → HAL_CLI_DIRS → HOME 의 알려진 설치 위치(`~/.local/bin`, `~/.npm-global/bin`, `~/.claude/local`, `~/.codex/packages/standalone/current/bin`, `~/node_modules/.bin`) → `/usr/local/bin`·`/usr/bin` → PATH. 로그인 상태(`cliLoginStatus`: claude `auth status` JSON / codex `login status`), codex 기본 모델·모델 목록(`~/.codex/config.toml`·`models_cache.json`, 숨김 제외). **Codex 지원**(`chatCodex`: `exec --json -s read-only --skip-git-repo-check --ephemeral -C <임시 폴더> -m <모델>`, JSONL 해석). Claude 의 `is_error` 결과(인증 만료)는 모델 답으로 꾸미지 않고 `cli_not_logged_in` 으로. CLI 환경 변수는 HOME·PATH(탐색 위치 포함)·LANG 등 최소. - `hal/ai/connections.mjs`: `cli.tool`(claude|codex) 검증, 모델 비우면 CLI 기본 모델, "확인" 때 탐색된 실행 파일·버전·로그인 상태를 연결에 기록, `cli_not_logged_in` → 상태 `reauth_required`. 확인 실패 응답은 5xx 대신 **424** 로(5xx 는 화면에 "일시적 오류"로 가려져 원인이 안 보였다). - `hal/ai/routes.mjs`: `GET /ops/api/ai/cli`(탐색 결과·탐색 위치·지원 CLI). `hal/ai/engine.mjs`: CLI 로그인 만료도 연결 상태 `reauth_required`. - 화면(`hal/ui.mjs`·`ai-render.mjs`·`ai.js`): AI 연동 화면에 "서버에서 찾은 CLI" 카드(실행 파일·실제 경로·버전·로그인 배지와 안내·기본 모델·"이 CLI 로 연결 추가"(양식 미리 채움)·"다시 탐색"), 연결 추가 양식에 CLI 종류(claude/codex) 선택과 경로 생략 시 자동 탐색 안내, 연결 표에 CLI 종류·버전·경로·로그인 상태. - 문서: `docs/HAL-AI.md` §2·§6, 개발 지시 `Ariweb-Suitle_Dev-Order_v0.3.1.md`. 버전 표기 0.3.1(package.json·README·Suite 버전 검사 테스트). ### 고객 화면 - 변경 없음. ### 운영자 화면 - AI 연동(`/ops/ai`): "서버에서 찾은 CLI" 카드. bear 에서는 Claude Code 2.1.312(로그인 필요 — `claude login` 안내)와 Codex 0.162.1(로그인됨, 기본 모델 gpt-6.1-sol)이 보이고, "이 CLI 로 연결 추가"로 바로 연결할 수 있다. 연결 뒤 "확인"을 누르면 상태가 `active`(codex) 또는 `reauth_required`(claude, 로그인 전)로 표시된다. - 운영자 조치: claude 를 쓰려면 bear 에서 `master` 계정으로 `claude login` 을 실행한 뒤 AI 연동 화면에서 "확인". codex 는 바로 연결 가능. ### 보안 결정 - HAL 은 CLI 의 세션 파일·자격(`~/.claude.json`, `~/.claude/.credentials.json`, `~/.codex/auth.json`)을 읽지도 넘기지도 않는다. 점검 때도 키 이름·존재 여부만 확인했다. 로그인은 서버에서 사람이 한다. - CLI 실행은 지정 또는 탐색된 **절대 경로의 실행 파일**만, 인자는 고정 형식(프롬프트는 인자 하나). Codex 는 읽기 전용 샌드박스·임시 폴더·세션 비보존으로 돈다. HAL 환경 변수(Vault 키 등)는 CLI 에 넘기지 않는다. - 연결 설정의 `cli.tool` 은 claude|codex 만, `cli.bin` 은 절대 경로만 받는다(상대 경로·다른 실행 파일 이름 거부). ### 테스트 - RED(구현 전): `test/hal-ai.test.mjs` 새 테스트 "0.3.1 CLI 자동 탐색…" → `GET /ops/api/ai/cli` 404 로 실패(+ 버전 0.3.1 검사 2건 실패). 통과 9 · 실패 3. - GREEN: `test/hal-ai.test.mjs` **12/12**(가짜 `HOME/.local/bin` 의 claude(로그인 만료)·codex(로그인됨) 스크립트, `HAL_CLI_HOME`·`HAL_CLI_DIRS` 로 탐색 위치 고정 — 탐색 API 결과·탐색 위치, codex 연결(tool 만) → 확인 active·모델 목록 → 대화 "OK from codex"·usage·`exec --json -s read-only --skip-git-repo-check --ephemeral -m gpt-6.1-sol` 인자, claude 연결 → 확인 424 `cli_not_logged_in` → `reauth_required` + `claude login` 안내, 모르는 tool 400, 빈 HOME → `cli_unavailable` + 탐색 위치). 중간 결함 1건을 테스트가 잡았다: 확인 실패를 502 로 내면 `lib/http` 가 메시지를 "일시적 오류"로 가려 원인이 안 보임 → 424. - targeted: `test/hal-ui.test.mjs` 7/7 · `test/hal-reauth-banner.test.mjs` 5/5 · `test/hal.test.mjs` 16/16 · `test/webmaker-visual-editor.test.mjs` 5/5. - 전체 Suite `node test/run.mjs`: **26개 파일 실패 0, 183건 통과**. 러너의 `docs/verification/local-e2e-*` 임시 파일 미커밋. 변경 파일 secret-like 스캔 0건. ### 배포(bear `hal21.ariweb.net`, HAL 만) - 2026-10-29 01:14 UTC, 코드 릴리스만(data/config/Vault/DB 파일 무변경, 새 마이그레이션 없음). HAL 만 배포, 다른 모듈은 바뀌지 않아 배포하지 않음. - 저장소: 기능 커밋 `3775b7bee429981dc0ee47054ec438096d741f52`(브랜치 `feature/hal-cli-discovery-v0.3.1` → `main` fast-forward 0d3a0b8..3775b7b, read-back `origin/main = 3775b7b`). 릴리스 빌드(그 커밋, 전체 테스트 재실행 26파일 실패 0): manifest `0.3.1 · mainCommit 3775b7b… · testsPassed=true · dirty=false · provenanceClean=true`, `verify` 5 artifact ✔. hal artifact sha256 `29f7286522d3ad43ca40896950de57ded206025dc0b368d2b763848efd98db6e`. - 적용(`~/oz-deploy/bear-apply.sh 0.3.1 …`, dist 5개 artifact 업로드): 해시 OK → 사전 백업 `/srv/ariweb/hal/backups/pre-v0.3.1-20261029T011449Z/`(hal.sqlite·-wal·-shm, config.json, vault.json, vault.key, SHA256SUMS) → stage 추출·검증(package.json 0.3.1, ai 파일 존재, `node --check`) → `/srv/ariweb/hal/releases/0.3.1-3775b7b` + REVISION.json → `register-release --approve --by oz-dev-order-v0.3.1`(0.3.1 approved, 5 artifact) → `bootstrap-release` → current 전환 → `systemctl restart ariweb-hal`. - 결과: restart → `/api/health` 200 **500ms**, active, 프로세스 cwd `releases/0.3.1-3775b7b`; health `version 0.3.1 · bootstrap complete · releaseVersion 0.3.1 · mainCommit 3775b7b… · artifactSha256 29f72865…`. 보존 sha256 전=후(config.json `ae5963d2…`, vault.json `0e2dc987…`, vault.key `345d92e7…`), 백업 sums OK, 이전 릴리스 디렉터리 9개 보존. 운영 DB: releases 5행(0.1.1·0.2.0·0.2.1·0.3.0·0.3.1 approved), bootstrap 0.3.1, 서버 2대 ready(heartbeat 재개 01:15:30Z·01:15:35Z), 재시작 창 Agent long-poll 502 2건 외 비-2xx 없음, journal 오류 0(배포 전 01:14:17Z 의 `…/test 500` 1건은 0.3.0 의 가려진 확인 실패 — 0.3.1 에서 424 로 원인 표시). - **배포 코드 탐색 read-back**(`~/oz-deploy/deployed-render-0.3.1.mjs`, 서비스와 같은 PATH `/usr/local/sbin:…:/snap/bin`·HOME=/home/master, 메모리 DB·임시 data dir, 운영 데이터·Vault 미접근): `GET /ops/api/ai/cli` → **claude** `/home/master/.local/bin/claude` → `…/.local/share/claude/versions/2.1.312`, `2.1.312 (Claude Code)`, loggedIn **false**(method none) / **codex** `/home/master/.local/bin/codex` → `…/.codex/packages/standalone/releases/0.162.1-…/bin/codex`, `codex-cli 0.162.1`, loggedIn **true**(chatgpt), 기본 모델 `gpt-6.1-sol`, 모델 8개. `/ops/`·`/ops/ai` 200(패널·연결 추가 양식), 도구 31. 외부 `https://hal21.ariweb.net` `/api/health` `/ops/login` `/ops/ai` `/ops/ui/ai.js` 200. - 운영 DB 의 기존 AI 연결(읽기 전용 확인, 비밀 없음): `H9000`(local-cli · login · claude-opus-5-5, 2026-10-29 00:52Z 생성) 상태 `error` "CLI 실행 파일을 실행할 수 없습니다(claude): ENOENT" — 0.3.0 의 결함 그대로. **운영자 조치**: AI 연동 화면에서 H9000 의 "확인"을 다시 누르면 0.3.1 이 claude 를 찾되 로그인 만료라 `reauth_required` + "`claude login`" 안내가 뜬다 → bear 에서 `master` 로 `claude login` 뒤 다시 "확인". 또는 "서버에서 찾은 CLI" 의 codex(로그인됨)로 연결 추가하면 바로 쓸 수 있다. - 되돌리기(코드만): `ln -sfn /srv/ariweb/hal/releases/0.3.0-ba3c5cb /srv/ariweb/hal/current && sudo systemctl restart ariweb-hal`. ### 알려진 제한 - claude 로그인 만료는 HAL 이 풀 수 없다(서버에서 `claude login`). 로그인 뒤 "확인"을 눌러야 상태가 바뀐다(자동 재확인 없음). - 로컬 CLI 연결은 대화·분석 전용(HAL 도구 없음). 도구가 필요하면 API 키/OAuth 연결을 쓴다(Codex 의 ChatGPT 로그인은 API 키가 아니라 도구 호출 경로가 없다). - gemini 등 다른 CLI 는 탐색하지 않는다(요청 시 CLI_TOOLS 에 추가). ### 미확정 정책값 - CLI 실행 시간 상한(기본 10분, 연결 설정 `cli.timeoutMs`), codex 샌드박스 모드(read-only 고정). ## 0.3.0 (2026-10-29) - **작성자**: 개발 지시 — Oz(`Ariweb-Suitle_Dev-Order_v0.3.0.md`, 2026-10-29) · 구현 — Claude Fable 5.1(브랜치 `feature/hal-ai-chat-v0.3.0`, 기준 origin/main `6bfde82c5ef607704cb05628dc33d9b6ef68c50f` = 0.2.1). 지시에 따라 커밋·push·`main` 병합·릴리스 빌드·bear `hal21.ariweb.net` HAL 배포까지 수행한다(아래 "배포" 절에 read-back). - **이전 버전**: 0.2.1(2026-10-29, HAL 고위험 재인증 안내 위치 수정 — 아래 절). 0.3.0 은 **가운데 버전 번호를 올리는 운영자 기능 릴리스**다: HAL 운영자 콘솔에 AI 채팅룸(통합 방 + 모델별 개별 방)과 AI 모델 연결(로그인·OAuth·API 키), HAL 제어·감시 스킬·도구·MCP 가 더해졌다. HAL 에만 적용. - **구성**: HAL 0.3.0. 고객 포털 · Node Agent · 도입 모듈 Webmaker 1.8.0 · AriMail 1.0.11 은 0.2.1 그대로(변경 없음 — 배포하지 않는다). 외부 npm 의존성 없음(공급자 HTTP 는 Node fetch, MCP 는 stdio JSON-RPC 직접 구현), Node 22.13+. ### 요구(사용자 요구 — 약화 없이 반영) HAL 화면 오른쪽에 칸을 나눠 AI 채팅룸을 둔다. AI 모델은 로그인·OAuth·API 키로 연결하고 여러 모델은 탭으로 전환한다. AI 모델이 HAL 의 모든 기능을 제어·감시할 수 있도록 스킬·도구·MCP 를 준비하고, 상태·로그 정보의 의미를 AI 가 이해하도록 미리 준비한다. 각 모델에는 이름·업무·특성 같은 고유 정보를 연동 때 지정한다. 통합 채팅룸은 항상 첫 탭(고정)이며 모든 모델이 함께 참여한다 — 내 지시는 모두에게, 이름을 지정한 지시는 그 모델만, 모델→모델 전달의 대답은 자동으로 진행되되 그 대답에 대한 두 번째 반응은 사용자 지시로. 개별 채팅룸은 모델별 1:1 이며 탭 이름은 모델 이름이 기본값, 수정 가능. 결과를 소스에 반영하고 `https://hal21.ariweb.net` 에 배포한다. 전문은 `Ariweb-Suitle_Dev-Order_v0.3.0.md` §1. ### 재사용한 기존 기능(그대로) - HAL 보안 경계 그대로 재사용: 운영자 미들웨어(네트워크 관문 → 운영자 세션 → CSRF), `requireRecent`(최근 10분 재인증), `threeStep`(3단계 확인), 감사(`lib/audit`, redact), Vault(`lib/vault`), 세션(`lib/session`). AI 라우트는 `hal/app.mjs` 가 같은 `requireRecent`·`threeStep`·`wrap` 을 넘겨 준다. - AI 도구는 운영자 API 와 **같은 core/jobs 함수**를 같은 인자로 부른다(`core.listServers/getServer/publicServer/metrics/listReleases/approveRelease/updateServer/createServer/approveServer/revokeAgent`, `d.jobs.enqueue/list/get/childrenSummary/cancel/retry/resume/pause/revert`, `emergency.listSessions`, `dr.readiness/listSnapshots/snapshot`, `mailsvc.readback`, `overviewLocal`, `changelog`, `listAudit`). - 재인증 창 계약 `hal/ui/public/reauth.mjs`(0.2.1): 패널의 승인 흐름도 같은 `callWithReauth`·`reauthPrompt` 를 써서 서버가 `reauth_required` 로 거부한 그 때만 고위험 안내가 붙는다. - 로컬 CLI 연결은 Webmaker `lib/claude.js` 와 같은 계열(`claude -p --output-format json`), 최소 환경 변수·프로세스 그룹 종료 방식. ### 수정한 기존 기능 - `hal/schema.mjs`: 마이그레이션 `hal_0006_ai_chat` — `ai_connections`(비밀값 없음, 참조·상태만) · `ai_rooms` · `ai_messages`(방 안 seq 유일) · `ai_actions`(제안) · `ai_oauth_states`(state 해시). 기존 표 변경 없음. - `hal/deps.mjs`: `d.ai = createEngine(d)`. `hal/app.mjs`: `registerAiRoutes(app, d, { requireRecent, threeStep, wrap })`. `hal/index.mjs`: 새 명령 `mcp`(stdout 은 JSON-RPC 전용, 로그 stderr). - `hal/ui.mjs`: 메뉴 `AI 연동`(/ops/ai), 모든 화면 공통 틀에 오른쪽 패널 `#ai-panel`(탭·상태·기록·입력·너비 조절)과 머리 단추 `AI`(열기/닫기), `/ops/ai` 화면(연결 표·연결 추가 양식(API 키/OAuth/로그인)·고유 정보·도구 목록), 스크립트 `/ops/ui/ai.js`. `hal/ui/public/ops.css`: 패널·탭·메시지·제안 카드·1100px 이하 반응형. - 새 파일: `hal/ai/{connections,providers,rooms,engine,tools,routes,mcp}.mjs`, `hal/ai/skill.md`, `hal/ui/public/ai.js`, `hal/ui/public/ai-render.mjs`(순수 렌더러), `docs/HAL-AI.md`, `test/hal-ai.test.mjs`. - 버전 표기: `package.json` 0.3.0(HAL 런타임·`scripts/release.mjs` 가 따름), README, `test/webmaker-visual-editor.test.mjs` 의 Suite 버전 검사 0.3.0. ### 고객 화면 - 변경 없음(포털·Webmaker·AriMail 미변경). ### 운영자 화면 - **AI 채팅룸 패널**(모든 운영자 화면 오른쪽, 머리의 `AI` 단추로 열고 닫음, 왼쪽 가장자리 끌어 너비 조절, 운영자별 localStorage 에 열림·너비·선택 탭 저장·복원, 1100px 이하에서는 본문 아래 전체 너비): 탭 줄(◎ 통합 고정 → 연결 순서대로 개별 탭, 비활성은 흐리게·점 표시, `+` → AI 연동), 상태 줄(통합: 규칙 안내·응답 중 수 / 개별: 연결 상태·모델·공급자), 기록(운영자·모델·도구 결과(접힘)·HAL 안내, 전달 응답 표시, 실패 메시지는 오류 코드와 함께), 입력(Ctrl/Cmd+Enter 전송, "현재 화면 문맥 포함", "새 대화"). 탭을 두 번 누르면 이름 변경(통합 불가). - **제안 카드**: 쓰기·고위험 도구 호출은 카드로 멈춘다 — `승인(재인증)` / `승인(재인증·3단계 확인)` / `거부`. 승인은 서버가 `reauth_required` 로 거부할 때만 재인증 창을 열고(0.2.1 계약), 고위험은 3단계 확인 창(영향 범위 → 식별정보 재입력 → 최종 실행). 처리 뒤 카드에 "승인·실행됨/거부됨(실행 안 함)/실행 실패"와 처리자가 남는다. - **AI 연동 화면(`/ops/ai`)**: 연결 표(이름·역할·특성, 공급자·인증 방식·키 힌트·토큰 만료, 모델·사용 가능 모델 수, 상태·마지막 확인·오류, 조치: 확인 / OAuth 로그인 / 키 교체 / 수정 / 비활성화·활성화 / 삭제(이름 재입력)), 연결 추가 양식(공급자에 맞는 인증 방식만 보임, 비밀 입력란은 저장 뒤 비움), 도구 목록(위험 등급). OAuth 콜백 뒤 `?oauth=ok` 안내. - 운영자 조치 항목: 모델을 쓰려면 AI 연동 화면에서 연결을 추가하고 "확인"을 눌러 모델 목록을 받아 둔다. 설정값·보관소 경로 변경 없음(비밀값은 기존 Vault 파일에 항목으로 추가된다). ### 연결 · 방 규칙 · 도구 동작 - 연결: 공급자 `anthropic`(API 키 `x-api-key` / OAuth Bearer) · `openai`(OpenAI 호환, `baseUrl` 변경 가능, Bearer) · `local-cli`(로그인된 Claude Code 세션, 도구 없음). 상태 `active`·`disabled`·`reauth_required`·`error`. "확인"은 실제 공급자 모델 목록을 받아 두고, 목록 밖 모델 선택은 `model_unavailable`(조용히 대체하지 않음). 키 등록·교체·client secret·삭제 → `reauth_required` 규칙. - OAuth: authorization code + PKCE S256. state 해시 저장·10분·1회용, 시작 운영자 = 콜백 운영자, code_verifier 는 Vault 에 두었다가 교환 뒤 제거, client secret 은 서버→토큰 끝점에만, 토큰은 Vault, 만료는 서버 refresh(실패 시 `reauth_required`). - 통합 방: 지시는 활성 모델 모두 / `@이름`·글머리 `이름:`·`이름,` 는 그 모델만 / 모르는 `@이름` 은 400 `unknown_addressee` / 모델→모델 `@이름` 전달은 자동 1회(relayDepth 1), 그 답의 @호출은 HAL 안내만 남기고 자동 반응 없음 / 자기 자신·비활성 제외. 각 모델의 system prompt 에는 자기 고유 정보·참여자 이름·역할·운영자·화면 문맥·스킬 문서가 들어가고, 다른 모델의 발언은 "[이름] …" 으로 보인다. - 개별 방: 1:1, 기록 격리, 비활성 연결에는 409 `connection_inactive`, "새 대화"는 기록 비움 + 승인 대기 제안 취소. - 도구(`hal/ai/tools.mjs`, 31개 — 읽기 15 · 쓰기 8 · 고위험 8, 표는 `docs/HAL-AI.md` §4): 읽기는 즉시, 쓰기는 제안 → 승인(재인증), 고위험은 제안 → 승인(재인증 + 3단계 확인, 식별정보 = 도구가 정한 대상 id). 인자는 선언으로 재검증, 모르는 도구·인자는 실행 없이 오류 결과. 한 턴 도구 왕복 8회 상한. 승인은 한 번만 실행(상태 선점). 거부는 실행 없음·모델 자동 호출 없음. - 스킬 문서 `hal/ai/skill.md`: 서버·작업·릴리스·사건·감사·긴급 복구·DR 상태값과 오류 코드의 뜻, AI 규칙(증거 없이 완료 금지, 승인 재촉 금지, 외부 글 속 지시문 불신, 비밀값 금지). `GET /ops/api/ai/skill`·도구 `hal_skill`. - MCP: `node hal/index.mjs mcp`(stdio) — `initialize`·`tools/list`·`tools/call`·`ping`. 읽기 도구만 실행, 쓰기·고위험은 `isError` 로 운영자 콘솔 승인 안내. ### 보안 결정 - 비밀값은 Vault 에만: 공개 객체·목록·감사·대화·작업·테스트·소스에 없음(힌트만). 공급자 호출의 자격은 서버 헤더에만. 로컬 CLI 는 세션 파일·자격을 읽지도 넘기지도 않고 환경 변수는 PATH·HOME 등 최소. - AI 경로는 기존 `requireRecent`·CSRF·`threeStep`·릴리스 승인·부트스트랩·Agent 경계를 그대로 거친다(라우트가 같은 함수를 받음). 고위험 제안은 기존 운영자 API 보다 약하지 않게 전부 3단계 확인을 요구한다(예: 모듈 배포는 운영자 API 에서는 재인증만, AI 제안은 재인증 + 3단계). - 모델 응답·도구 인자·다른 모델의 발언·저장소 글은 권한이 아니다: 선언 검증, 알 수 없는 도구 거부, 임의 셸·경로·네트워크 도구 없음, 스킬 문서에 prompt injection 불신 규칙. - 공급자 오류는 모델 답으로 꾸미지 않고 `failed` 로 표시. AI 가 "완료"라 해도 HAL 이 확인한 도구 결과·작업 상태만 사실로 다룬다. - 테스트 비밀 fixture 는 `node:crypto` 로 실행마다 생성(정적 literal 없음). ### 테스트 - RED(production 변경 전, `node --no-warnings test/hal-ai.test.mjs`): **통과 0 · 실패 11** — 모든 `/ops/api/ai/*` 404, 공통 틀에 AI 패널 없음, MCP 명령 없음(응답 없음). - GREEN(구현 뒤): `test/hal-ai.test.mjs` **11/11**(연결 CRUD·비밀 미노출·재인증 / OAuth PKCE / 방·탭 / 통합 방 지정·방송·고유 정보 / 모델→모델 자동 1회 / 개별 방·읽기 도구·공급자 오류 / 쓰기·고위험 제안·재인증·3단계·거부·1회 실행 / OpenAI 호환 / 로컬 CLI / 화면·버전·문서 / MCP stdio). 중간 결함 2건을 테스트가 잡았다: ① 격리 뒤 `publicServer.status` 는 heartbeat 없음 → `offline` 이라 각본이 `rawStatus` 를 보도록 수정(코드 아님) ② `/ops/ai` 화면 함수가 `d` 를 받지 않아 500 → `aiPage(d)`. - targeted: `test/hal-ui.test.mjs` 7/7 · `test/hal-reauth-banner.test.mjs` 5/5 · `test/hal.test.mjs` 16/16. - 전체 Suite `node test/run.mjs`: **26개 파일 실패 0, 182건 통과**(새 `hal-ai` 11 포함, 로컬 E2E 11). 러너가 만든 `docs/verification/local-e2e-2026-10-28.*` 는 커밋하지 않았다. 변경 파일 정적 secret-like 스캔 0건. 도입 소스 출처 검사 `checkProvenance` clean. ### 배포(bear `hal21.ariweb.net`, HAL 만) - 2026-10-28 17:45~17:47 UTC, 코드 릴리스만(data/config/Vault/DB 파일 무변경, 마이그레이션 `hal_0006_ai_chat` 는 기동 때 표 추가만). 바뀌지 않은 포털·Agent·Webmaker·AriMail 은 배포하지 않았다(지시: 수정되지 않은 것은 배포하지 않음). bear 에는 HAL 만 둔다. - 저장소: 기능 커밋 `ba3c5cb2f82374108f22f56badeab52cb07b308b`(브랜치 `feature/hal-ai-chat-v0.3.0` push → `main` fast-forward 6bfde82..ba3c5cb, 원격 read-back `origin/main = ba3c5cb`). 릴리스 빌드 `node scripts/release.mjs build`(깨끗한 그 커밋, 전체 테스트 재실행 26파일 실패 0): manifest `version 0.3.0 · mainCommit ba3c5cb… · testsPassed=true · dirty=false · provenanceClean=true`, `verify` 5 artifact ✔. hal artifact sha256 `c1377c276dab3f7dae67cd8b884db77aee7f748fad5b09e00be509c18905430f`(195,484 B; portal `b442a7f9…` agent `892d1b19…` webmaker `2b39fb01…` arimail `ef8037b3…`). - 적용(`~/oz-deploy/bear-apply-0.3.0.sh` → 4단계부터 수동 이어 실행): dist 5개 artifact 를 `/srv/ariweb/hal/dist/0.3.0/` 에 올림(HAL artifact 만 올리면 `register-release` 가 `artifact_missing: portal` 로 거부 — 등록은 manifest 의 전 artifact 를 요구한다) → 사전 백업 `/srv/ariweb/hal/backups/pre-v0.3.0-20261028T174538Z/`(hal.sqlite·-wal·-shm, config.json(+이전 사본), vault.json, vault.key, SHA256SUMS, 0700) → stage 추출·검증(package.json 0.3.0, `hal/ai/engine.mjs`·`hal/ui/public/ai.js` 존재, `node --check` 3개) → `/srv/ariweb/hal/releases/0.3.0-ba3c5cb` + REVISION.json → `register-release --manifest … --approve --by oz-dev-order-v0.3.0`(릴리스 0.3.0 approved, artifact hal·portal·agent·webmaker·arimail) → `bootstrap-release --version 0.3.0 --commit ba3c5cb… --sha256 c1377c27…` → `ln -sfn` current 전환 → `systemctl restart ariweb-hal`. - 결과: restart → `/api/health` 200 **831ms**, PID 54774, active, NRestarts 0, 프로세스 cwd `/srv/ariweb/hal/releases/0.3.0-ba3c5cb`, journal 오류 0, 기동 로그 "Ariweb HAL v0.3.0 … 부트스트랩 완료". 보존 확인 sha256 전=후: config.json `ae5963d29ae899bc…`, vault.json `0e2dc9875f2779a0…`, vault.key `345d92e77c3fcd78…`; 백업 `sha256sum -c` OK. 이전 릴리스 디렉터리 8개 전부 보존(0.1.1×6, 0.2.0-0a56d46, 0.2.1-6bfde82). - read-back: `/api/health`(로컬·`https://hal21.ariweb.net`) `ok · version 0.3.0 · db · jobs · bootstrap complete · releaseVersion 0.3.0 · mainCommit ba3c5cb… · artifactSha256 c1377c27… · halRunningVersion 0.3.0 · firstAgent ag_01a17bd7…`. 운영 DB(읽기 전용 node:sqlite): `_migrations` 에 `hal_0006_ai_chat`, `ai_rooms` = `room_group`(통합 방) 1행, `ai_connections` 0, releases 4행(0.1.1·0.2.0·0.2.1·0.3.0 approved), bootstrap_state `release_version=hal_running_version=0.3.0`, 서버 2대 ready(heartbeat 재개 17:46:45Z hostinger, 17:47:00Z jitsi), 열린 작업 0. 재시작 창(17:46:13Z)의 Agent long-poll `GET /agent/v1/jobs/next` 502 2건 외 Agent 요청 비-2xx 없음(이전 배포와 같은 양상). - **배포된 코드 서버 렌더 검증**(운영자 자격 없음 → `~/oz-deploy/deployed-render-0.3.0.mjs` 가 `/srv/ariweb/hal/current` 의 app/deps 만 import, 메모리 DB·임시 data dir, 운영 데이터·Vault 미접근): `/ops/` 200 + `#ai-panel`·`/ops/ui/ai.js`·메뉴 "AI 연동" 있음, `/ops/ai` 200 + "연결 추가", `/ops/api/ai/rooms` = [`room_group`], `/ops/api/ai/tools` 31개, 연결 0. 외부: `https://hal21.ariweb.net` `/api/health` `/ops/login` `/ops/ai` `/ops/` `/ops/ui/ai.js` `/ops/ui/ai-render.mjs` `/ops/ui/reauth.mjs` 모두 200. - 되돌리기(코드만): `ln -sfn /srv/ariweb/hal/releases/0.2.1-6bfde82 /srv/ariweb/hal/current && sudo systemctl restart ariweb-hal`(0.2.1 코드는 `ai_*` 표를 모르지만 건드리지 않으므로 그대로 둬도 된다). 데이터까지: 위 백업 디렉터리. - blocker/unknown: 운영자 자격이 없어 실제 운영자 세션으로 패널을 열어 보지는 못했다(배포 코드 렌더 harness·회귀 테스트·외부 200 으로 판단). 실제 모델 연결은 Oz/운영자가 AI 연동 화면에서 키 또는 OAuth 를 등록해야 시작된다. ### 알려진 제한 - v0.1.4 §41.3·§41.4·§42 의 상세 계약 중 이번 범위 밖: 탭 간 변경 잠금(원자적 server-side lock·fencing·compare-and-swap), AI 의 격리 작업공간 코드 수정·커밋·push·릴리스 생성 도구, 제한된 합의 절차(최초 의견 1회 + 반론 1회 + 최종안). 이번 통합 방은 Oz 지시의 "방송·이름 지정·모델→모델 자동 1회" 규칙까지다. - 응답은 스트리밍이 아니라 턴 완료 뒤 한 번에 보인다(패널은 응답 중 1초 간격으로 다시 읽음). 진행 중 턴은 HAL 재시작 때 사라진다(거짓 진행 표시 없음). - 로컬 CLI 연결은 bear 에 CLI 가 없고 유닛이 `ProtectSystem=strict` 라 운영 서버에서는 `cli_unavailable` 로 표시된다. 실제 모델 공급자 호출은 운영자가 키·OAuth 를 등록해야 시작된다(이 릴리스는 가짜 공급자 테스트까지 검증). - 실제 브라우저(DOM)에서 패널 동작은 자동 테스트하지 않았다(순수 렌더러·정적 계약·API 로 검증). 배포 뒤 운영자 로그인으로 패널 열기·연결 추가·확인·통합 방 지시 1회 눈 확인을 권한다. ### 미확정 정책값 - 토큰 왕복 상한(8), 지시 길이 상한(20000자), 도구 결과 전달 상한(24000자), 패널 기본 너비(400px) — 운영자 요청 시 설정값으로 뺄 수 있다. 공급자 비용 표시는 토큰 수만(가격표 없음). ## 0.2.1 (2026-10-29) - **작성자**: 개발 지시 — Neo(`Ariweb-Suitle_Dev-Order_v0.2.1.md`, 오즈 승인) · 구현 — Claude Fable 5.1(격리 작업 트리 `feature/hal-risk-banner-v0.2.1`, 기준 origin/main `0a56d4612c86dddfcbe644e775980d3c5b413b2e`). 커밋·push·병합·릴리스 빌드·운영 배포는 하지 않았다 — Neo 가 검토 뒤 수행한다. - **이전 버전**: 0.2.0(2026-10-28, Webmaker 직접 편집 — 아래 절). 0.2.1 은 **끝 버전 번호만 올리는 운영자 화면 수정**이다: 사용자 기능·도입 모듈·보안 경계 변경 없음. - **구성**: HAL · 고객 포털 · Node Agent · 도입 모듈 Webmaker 1.8.0 · AriMail 1.0.11 모두 0.2.0 그대로(HAL 운영자 콘솔 화면 2곳 + 브라우저 스크립트만 수정). 외부 npm 의존성 없음, Node 22.13+. ### 요구(사용자 요구 — 약화 없이 반영) HAL 운영자 콘솔의 모든 일반 화면 위에 재인증이 10분을 넘기면 늘 떠 있던 전역 배너 `! 고위험 작업(서버 삭제·되돌리기·인증 키 변경·복구)은 최근 10분 안의 재인증이 필요합니다 — 재인증` 을 없앤다. 그 안내는 backend 가 고위험 요청을 실제로 `reauth_required` 로 거부한 **바로 그 시점**의 재인증 창 안에서만 보인다. 단순 화면 조회·로그인 직후·10분 경과·일반 이동 중에는 보이지 않는다. 계정 화면의 수동 재인증 단추·동작은 유지하되 고위험 안내를 붙이지 않는다. backend 의 `requireRecent`·CSRF·3단계 확인·고위험 분류·거부 동작은 그대로다. ### 수정한 기존 기능 - `hal/ui.mjs` `shell()`: 재인증 만료(`!session.recentlyAuthed`)를 전역 배너로 그리던 한 줄을 제거(이력 주석으로 대체). 다른 전역 배너(기기 귀속 접근권 만료 안내·HAL 자체 건강 비정상·긴급 복구 세션 진행·부트스트랩 미완료·비밀번호 변경 필요)와 `.alerts` 영역은 그대로. - `hal/ui/public/ops.js`: `call()`(API 호출 + 재인증 재시도)과 `reauth()` 창이 새 순수 helper `hal/ui/public/reauth.mjs` 의 `callWithReauth`·`reauthPrompt` 를 쓴다. 서버가 `reauth_required` 로 거부했을 때 여는 재인증 창에만 고위험 안내(note, HTML 이스케이프)를 붙이고, 계정 화면의 수동 재인증(`data-act="reauth"`)은 `reauth()` 를 인자 없이 불러 안내 없이 비밀번호만 묻는다. 재시도 규칙은 전과 같다(성공 시 딱 한 번, 취소·실패 시 원 요청 미실행). - `hal/ui/render.mjs` `accountsView`(공개 사본 `hal/ui/public/render.mjs` 는 심볼릭 링크): 계정 화면 "최근 재인증" 상태 배지 문구 `재인증 필요(고위험 작업)` → `만료(10분 지남)`. 조회 화면이 고위험 안내를 품지 않게 하는 중립 표기이며 상태 판정·단추는 같다. - 새 파일 `hal/ui/public/reauth.mjs`(순수, DOM·fetch 없음, `/ops/ui/reauth.mjs` 로 정적 제공): `REAUTH_REQUIRED`·`HIGH_RISK_NOTICE`(옛 배너 문구 그대로 — 위치만 옮김)·`reauthPrompt({required})`·`callWithReauth(run, reauth)`. 브라우저와 Node 테스트가 같은 파일을 쓴다. - 버전 표기: `package.json` 0.2.1(HAL 런타임 `hal/deps.mjs VERSION`·`scripts/release.mjs` 는 이 값을 따른다), `README.md` 버전·개발 지시 목록, `test/webmaker-visual-editor.test.mjs` 의 Suite 버전 검사 0.2.1. ### 고객 화면 - 변경 없음. ### 운영자 화면 - 일반 화면(운영 현황·고객·주문·상품·사이트·메일·도메인·서버·백업·이전·릴리스·작업·장애·감사·복구·계정)에서 고위험 재인증 전역 배너가 사라진다. 운영자는 고위험 작업(서버 삭제·격리·역할 변경, 모듈 되돌리기·배포·재조정, 작업 되돌리기, 운영자 추가·접근권 발급, MFA 해제, 설정 변경, 메일 가져오기 적용·되돌리기, 릴리스 승인, 긴급 복구 선언·승인·자격, DR 설정·복원)을 실제로 시도해 서버가 거부한 순간에만 "재인증(최근 10분)" 창 위쪽에서 `고위험 작업(서버 삭제·되돌리기·인증 키 변경·복구)은 최근 10분 안의 재인증이 필요합니다` 를 본다. - 계정 화면: "재인증" 단추 유지(수동 재인증 창은 비밀번호만), "최근 재인증" 행은 `유효(10분)` / `만료(10분 지남)`. 운영자 조치 항목 없음(설정값·보관소·DB 변경 없음). 배포 뒤 브라우저 캐시의 옛 `ops.js` 가 새 `reauth.mjs` 를 못 찾는 경우는 없다(옛 ops.js 는 helper 를 가져오지 않음). ### 보안 결정 - 서버 쪽 판단 코드는 한 줄도 바꾸지 않았다(`hal/app.mjs` `requireRecent`·CSRF·`threeStep`·고위험 경로 분류, `lib/session.mjs` `recentlyAuthed`, `POST /ops/api/reauth`). 변경은 "거부됐을 때 브라우저가 어떻게 묻고 몇 번 다시 보내는가"와 안내 문구 위치뿐이다. - 재인증 창 취소·재인증 실패 → 원 요청을 다시 보내지 않고 원래 오류를 호출자에게 전파(고위험 작업이 몰래 실행되지 않음). 성공 → 같은 method·path·body 로 **딱 한 번** 재시도(`api` 직접 호출, `call` 재귀 아님). 재시도도 거부되면 다시 묻지 않는다(무한 반복 금지). `reauth_required` 외의 오류는 재인증을 묻지 않는다. - 안내 문구는 helper 단일 출처이며 `R.esc` 로 이스케이프해 modal 에 넣는다. 비밀번호·토큰·키·개인정보는 파일·테스트·출력에 없다 — 새 테스트 `test/hal-reauth-banner.test.mjs` 의 운영자 비밀번호 fixture 는 정적 literal 없이 `node:crypto` `randomBytes` 로 실행마다 생성하며(정답·오답 값이 서로 다름 보장) 출력에도 찍지 않는다. ### 테스트 - RED(production 변경 전, `node --no-warnings test/hal-reauth-banner.test.mjs`): **통과 1 · 실패 4** — ① `/ops/: 10분 경과 뒤에도 고위험 배너 없음 (실제 false)`(배너가 아직 그려짐) ② ③ `ERR_MODULE_NOT_FOUND hal/ui/public/reauth.mjs`(helper 없음, 문구·재시도 계약 테스트 2건) ④ `hal/ui.mjs 는 고위험 배너 문구를 더 이상 그리지 않음 (실제 false)`(정적 계약). 통과 1건은 backend 경계 보호 테스트(변경 대상이 아니므로 처음부터 통과해야 한다). - GREEN(최소 구현 뒤): `test/hal-reauth-banner.test.mjs` **5/5**; targeted `test/hal-ui.test.mjs` **7/7**, `test/hal.test.mjs` **16/16**(기존 reauth_required → 재인증 → 통과 흐름 포함). `node --check` 로 `ops.js`·`reauth.mjs` 구문 확인. - 전체 Suite `node test/run.mjs`: **25개 파일 실패 0**(새 `hal-reauth-banner` 5 포함, 로컬 E2E 11 통과; 러너가 만든 `docs/verification/local-e2e-2026-10-28.*` 임시 파일은 diff 에서 제거). 기존 테스트 변경은 `test/webmaker-visual-editor.test.mjs` 의 Suite 버전 검사 0.2.1 뿐. - 새 테스트가 못 박는 계약: 일반 화면 6곳 + 계정 화면에 로그인 직후·10분 경과 모두 전역 배너·배너 영역 재인증 링크·`고위험 작업(` 문구 없음, 계정 수동 재인증 단추 유지, 부트스트랩 배너 유지 / backend: 10분 경과 뒤 서버 삭제·격리 403 `reauth_required`(`detail.action=reauth`)·거부된 요청 미실행·CSRF 없는 재인증 403 `csrf`·틀린 비밀번호 거부·재인증 뒤 `recentlyAuthed`·3단계 확인 없이는 여전히 거부 / helper: 수동 창 note 없음·거부 창 note = 옛 배너 문구·취소 시 run 1회·오류 전파·성공 시 run 2회·재시도 거부 시 재질문 없음·다른 오류는 재인증 없음·정상 응답 그대로 / 정적: `hal/ui.mjs` 배너 코드 없음, `ops.js` 가 `./reauth.mjs`·`callWithReauth`·`reauthPrompt` 를 쓰고 `reauth: () => reauth()`(인자 없음)이며 문구를 직접 품지 않음. ### 알려진 제한 - 실제 브라우저(DOM)에서 재인증 modal 이 열리는 모습은 자동 테스트하지 않았다(저장소에 브라우저 러너 없음, 큰 dependency 추가 금지). 흐름·문구는 순수 helper 로, 결선은 정적 계약으로 검사했다. 배포 뒤 운영자가 10분 경과 상태에서 고위험 단추(예: 서버 격리) 하나를 눌러 안내가 붙은 재인증 창이 뜨고 취소 시 실행되지 않음을 한 번 눈으로 확인하는 것을 권한다. - `hal/ui/render.mjs` `relayError({code:'reauth_required'})` 가 포털 중계 응답에 "재인증 필요" 상태 패널을 그리는 동작은 그대로다(이것도 서버가 거부한 그 시점의 표시이며 고위험 문구는 없다). - `docs/verification/` 에 0.2.1 전용 검증 문서는 만들지 않았다(지시 범위: HAL UI·테스트·버전/이력·개발 지시). RED/GREEN 증거는 이 절과 인도 보고에 있다. ### 미확정 정책값 - 0.2.0 의 미확정 정책값 그대로. 재인증 유효 시간(10분)·고위험 작업 분류는 바꾸지 않았고 이번 범위 밖이다. ## 0.2.0 (2026-10-28) - **작성자**: 개발 지시 — Neo(`Ariweb-Suitle_Dev-Order_v0.2.0.md`) · 구현 — Claude Fable 5.1(격리 작업 트리 `feature/visual-editor-v0.2.0`, 기준 main `4b3714e37c543fcfa8862dbc68c76f62a9b0bf97`). 커밋·push·병합·배포·운영 서버 접근은 하지 않았다 — Neo 가 검토 뒤 수행한다. - **이전 버전**: 0.1.1(2026-10-26, 첫 구현 릴리스 — 아래 절) · 0.1.4 개발 지시(2026-10-27, HAL AI 대시보드·다중 AI 공동 작업창 지시 문서만 추가, `4b3714e`). 0.2.0 은 **가운데 버전 번호를 올리는 기능 릴리스**다: 도입 모듈 Webmaker 에 사용자 기능(직접 편집)이 더해졌고 Webmaker 자체도 1.7.3 → **1.8.0** 으로 올랐다. 0.1.1 의 모든 기준·0.1.4 의 지시는 그대로 유효하다(이번 범위 밖의 0.1.4 항목은 구현하지 않았다). - **구성**: HAL · 고객 포털 · Node Agent 는 0.1.1 그대로. 도입 모듈 **Webmaker 1.8.0**(직접 편집 포함) · AriMail 1.0.11(변경 없음). 외부 npm 의존성 없음, Node 22.13+. ### 요구(사용자 요구 — 약화 없이 반영) Webmaker 가 초안을 만들어 보이는 페이지가 생긴 뒤 **직접 시각 편집 모드**를 더한다(Wix·아임웹 방식 끌어다 놓기). 변경은 Suite 의 `modules/webmaker` 와 기존 Webmaker 저장소 양쪽에 반영한다. 필수 동작·보안 규칙은 `Ariweb-Suitle_Dev-Order_v0.2.0.md` 에 계약으로 적었다. ### 재사용한 기존 기능(그대로 도입) - Webmaker 1.8.0 의 **직접 편집(시각 편집기)** 전부 — 미리보기 도구 줄 "직접 편집" 전환(사이트·HTML 페이지가 골라져 있을 때만), 캔버스 글 바로 고치기(두 번 클릭/Enter), 요소 선택·도구 막대(↑↓·복제·삭제·끌기 손잡이), 기존 블록 끌어 옮기기, 구성 요소 팔레트 7종(섹션/컨테이너·제목·문단·버튼(링크)·이미지·구분선·여백) 끌어다 놓기 + "추가" 버튼, 속성 창(상위 선택·이동·복제·삭제·글·링크 주소·이미지 주소/대체 글·정렬·글자색·배경색·안쪽/바깥 여백), 저장 상태(변경 없음/저장되지 않음/저장 중/저장됨/저장 실패 — aria-live), 저장(Ctrl/Cmd+S), 실행 취소/다시 실행, 버리고 다시 불러오기, 편집 끝내기, 페이지·사이트·화면 전환·새로고침·소스 보기 전환·창 닫기 전 저장하지 않은 변경 보호. 기존 소스 편집기·모바일 미리보기·버전/복원·배포·가져오기·다국어·모듈·SEO·Site Brief 는 그대로. - 재사용한 함수(Webmaker 기존 코드를 그대로 호출): `resolveTextFile`(경로 검증·링크 거부), `listFiles`, `gitRecord`/`lib/gitstore.recordChange`(버전 기록 — 새 종류 `visual-edit`), `publicView`, `broadcast`, `jobOf`(AI 작업·배포 중 판정), `readBody`, `sessionUser`·`fromTenantPage`·`auth.csrfOk`(출처·CSRF), `siteUrl`, `isSecureReq`, `MAX_TEXT_BYTES`(5MB 상한), `store.save`(Suite `rev` 증가). 화면: `discardOk`·`renderPreview`·`renderGit`·`loadFrame`·`setEditorOpen`·`logLine`·`pushMsg`·`api`(CSRF 헤더)·`T`. ### 수정한 기존 기능 - **Webmaker(`modules/webmaker`, 파일 머리·줄 주석 `v1.8.0`)**: `server.js` — `require('./lib/visual-editor')`, `veRuntime()`·`requestOrigin()`, `GET /files` 응답에 `hash`, 새 경로 `GET/PUT /api/sites//visual/<페이지>`(아래 "직접 편집·끌어다 놓기·저장·버전·보안"). `public/app.js` — `discardOk`(직접 편집 변경도 묻기), `VER_KIND` 에 `visual-edit`(기존 `manual-edit` 는 "소스 편집" 으로 표기), `renderPreview`(직접 편집 중이면 미리보기 덮어쓰지 않고 `VisualEditor.onSiteUpdated`), `btnToggleSource`(한 번에 하나), `beforeunload`, `resetToNew`, `btnReload`·`fileSelect.onchange`, `openSite` 끝(사이트 바뀌면 편집 종료). `public/index.html` — `#btnVisual`, `#veBar`, `.ve-stage`(`#vePalette`·`#frameWrap`·`#veInspector`), 스크립트 2개. `public/style.css` — `.ve-*`(900px 이하 세로 배치·접기). `public/admin.js` — 버전 종류 표기. `package.json` 1.8.0. - **Suite 최소 연결 변경(`[Ariweb Suite 변경]`, 0.1.1 의 8개 파일 변경은 그대로 유지)**: `modules/webmaker/server.js` 의 직접 편집 `PUT` 에도 저장본 `rev` 충돌 검사(`revConflict`, §9.3)를 소스 편집기·기본 사항 저장과 같이 적용 — 화면이 보낸 `rev` 가 다르면 409 와 양쪽 요약(`conflict.currentRev` 등). 화면(`visual-editor-ui.js`)은 `GET /visual` 의 `rev` 를 기억해 저장 때 보내고 응답의 `rev` 로 갱신한다(upstream Webmaker 는 `rev` 를 돌려주되 무시한다). 그 밖의 Suite 전용 변경(SSO·내부 API·배너·`lib/ariweb.js`·`store.rev`)은 바뀌지 않았다 — `diff` 로 upstream 과의 차이가 0.1.1 의 Suite 블록 + 1.8.0 공통 변경만임을 확인했다. - **Suite 도구**: `scripts/import-sources.mjs` 에 `--allow-dirty`(참조 저장소를 나란히 개발 중일 때 고정 커밋 위의 작업 트리 변경·추적되지 않은 새 파일을 가져오고 `upstream{version·branch·headCommit·workingTreeDirty·note}` 를 기록) 와 `--only` 때 다른 소스 기록 유지. `hal/modules/index.mjs`·`agent/jobs/module.mjs` 의 Webmaker 버전 주석, `portal/webmaker/REUSE.md` 갱신. Agent·포털 코드 동작은 바뀌지 않았다(릴리스 artifact 는 `package.json` 0.2.0 으로 `webmaker-0.2.0.tar.gz` 등으로 만들어진다). **`scripts/release.mjs` 와 `hal/core.mjs` 는 바뀌었다** — 도입 소스 출처 fail-closed(아래 ⑪)와 릴리스 승인 세 검사 필수(아래 종결 검토 ③). - 오류 수정: Webmaker 직접 편집 저장에서 JSON 파싱 실패가 500 으로 나오던 것을 400 한국어 안내로(TDD 중 발견, 직접 편집 경로에만 적용). - **검토 수정(Neo 독립 검토)** — ① 출시 차단: 직접 편집 PUT 이 본문을 읽기(await) 전에 파일 hash 를 잡아 두어 두 저장이 나란히 들어오면 둘째가 첫째를 조용히 덮어쓸 수 있었다 → 본문을 다 읽은 뒤 사이트 기록(`store.load`)·파일을 다시 읽어 hash(Suite 는 `rev` 도)를 비교하고 검증·쓰기까지 await 없이 이어 간다(둘째는 409 stale, 첫 저장 보존). 결정적 회귀 테스트(chunked PUT 두 개의 본문을 순서대로 완료). ② 안전 강화: 직접 편집 저장을 `lib/visual-editor.writeTextAtomic`(같은 폴더 임시 파일 0644 → rename, 실패 때 정리·오류 전달, 파일 시스템 주입 가능)로 바꿔 쓰기 실패·중단 때 마지막 정상 페이지가 잘리지 않는다(500 + "기존 페이지는 그대로 있습니다", 버전 미생성). 소스 편집기의 기존 쓰기는 바꾸지 않았다. ### 고객 화면 - 포털 사이트 상세 › 제작 탭 › "제작 화면 열기"(SSO) → Webmaker 빌더 오른쪽 미리보기 도구 줄의 **직접 편집**. 포털 자체 화면은 바뀌지 않았다. 직접 편집은 AI Wallet(캐시)을 쓰지 않으므로 포털 AI Wallet 카드에 영향이 없고, 직접 편집 저장본(버전 `visual-edit`)은 포털 배포 화면의 저장본 선택·Source 변경 이력에 그대로 보인다(내부 API `projects/` 의 `versionList`). - 포털 상단 배너(그룹·운영 사이트)는 직접 편집 중에도 그대로 보인다. ### 운영자 화면 - HAL 운영자 콘솔 변경 없음. Webmaker 관리자 콘솔의 버전·변경 종류 표기에 "직접 편집"(`visual-edit`)이 추가되고 기존 `manual-edit` 는 "소스 편집" 으로 보인다. 운영자 조치 항목 없음(설정값·보관소 변경 없음). ### 직접 편집 · 끌어다 놓기 · 저장 · 버전 · 보안 동작 - **캔버스**: `GET /api/sites//visual/<페이지>` 가 저장된 HTML 을 편집 캔버스 문서로 바꿔 준다(`lib/visual-editor.prepareCanvas`). 페이지 `