# HAL ↔ Node Agent 프로토콜·설치·운영 (v0.1.1) Dev-Order §5(아키텍처)·§28·§29·§32·§33·§34 의 백엔드 구현. 코드: `hal/`, `agent/`, `deploy/`, `scripts/release.mjs`, `portal/hal/client.mjs`. ## 1. 구성 | 구성요소 | 위치 | 역할 | |---|---|---| | HAL | `/srv/ariweb/hal/current`, 127.0.0.1:8900, nginx `hal.ariweb.net` | 서버·Agent 등록부, 릴리스 등록부, 내구성 작업 엔진(lib/jobs), 모듈 배포, 포털 API, 운영자 API, 긴급 복구, 재해복구, 공개 상태 | | Node Agent | `/srv/ariweb/agent/current`, 유닛 `ariweb-agent` | heartbeat(30초, 지표·모듈 버전·시계 차이), 작업 long-poll·실행(단계 저장·재개), 모듈 설치, 사이트 배포·백업·이전 | | External Pull Agent | 고객 외부 서버, 같은 코드 `--external --site ` | `site.deploy / site.remove / site.check / site.rollback` 만, 사이트 하나에 귀속 | ## 2. 인증 - Agent·포털·HAL 간 모든 요청은 `lib/serviceauth` HMAC: `X-Service-Key-Id`(`agent:` / `portal` / `hal`), `X-Service-Ts`(±5분), `X-Service-Sign`. - Agent 비밀은 등록 응답에서 한 번만 전달되어 Agent 보관소(`data/vault.json`, 키 `agent-secret`)에, HAL 쪽은 `service:agent:` 로 보관. 표에는 해시만. - 큰 전송(`POST /agent/v1/transfers/:id`)은 빈 본문 서명 + `X-Transfer-Sha256` 헤더, HAL 이 받은 뒤 해시를 다시 계산해 비교. ## 3. Agent 프로토콜(`/agent/v1`) | 경로 | 설명 | |---|---| | `GET install.sh` | 설치 스크립트(deploy/agent-install.sh) | | `GET hal-ca.pem` | [0.4.1] HAL 공개 주소가 사설 CA·자체 서명일 때의 신뢰 앵커 PEM(공개, 무서명). 시스템 CA 로 신뢰되면 404 `no_private_ca` | | `GET tls` | [0.4.1] 공개 주소 TLS 신뢰 상태(trusted·reason·pin·caAvailable, `?refresh=1`) | | `GET bootstrap-artifact?module=agent&version=` + `X-Registration-Token` | 등록 전 Agent 코드 내려받기(유효 토큰 확인, 소비하지 않음) | | `POST register {token, facts, version}` | 일회용 토큰(10분) → `{agentId, secret(1회), serverId, kind, siteId}` | | `POST heartbeat {version, metrics, modules, clockSkewMs, facts, sitesHosted}` | 지표 저장(90일+), 서버 last_seen·용량·모듈 실제 버전 갱신. 90초 무응답 → offline | | `GET jobs/next?wait=25` | long-poll. `{job:{id, kind, payload, halJobId}}` 또는 null | | `GET jobs/unfinished` | 재시작 뒤 이어서 할 running 작업 목록 | | `POST jobs/:id/progress {progress, steps, log}` → `{cancelRequested}` / `POST jobs/:id/result {status: done\|failed\|needs_user, result, error, errorCode, userMessage}` | | | `GET releases/:version/artifact?module=` | tar.gz 스트리밍, `X-Sha256` | | `POST/GET transfers/:id` | HAL 경유 파일 전송(이전·원격 백업), 24시간 보관 | | `GET settings/:module` | [0.4.0] 자기 서버 범위의 HAL 관리 설정(유효값·비밀값·hash). 작업 `module.configure` 가 부른다 — docs/HAL-SETTINGS.md | ## 4. 포털 API(`/portal/v1`, 키 `portal`) — docs/HAL-JOBS.md 부트스트랩이 끝나기 전에는 `health` 외 503 `bootstrap_incomplete`. 작업 상태 변화는 outbox 를 통해 포털 `POST /internal/hal-events` 로 밀린다(실패 시 지수 백오프, 7일 보관). ## 5. 작업 모델 - HAL 작업(lib/jobs) 한 개가 Agent 작업(agent_jobs) 하나 이상을 배정하고 결과를 기다린다(`core.awaitAgentJob`: 진행률 반영, 취소 전달, Agent 10분 무응답 → `사용자 확인 필요`). - 복합 작업: `backup.create`(생성 → 저장 서버로 복사 → 검증), `backup.restore`(가져오기 → 보호 시점 → 격리 확인 → 복원 → 외부 확인, 되돌리기 지원), `site.relocate`(precheck → prepare → precopy → verify_copy → final_sync → verify_new → switch → external_check → cleanup; `verify_new` 실패 시 전환하지 않음, 되돌리기 지원). - 운영자 작업: `module.deploy`/`module.rollback`/`module.health`/`module.reconcile` 은 서버마다 자식 작업(`module.deploy.target`)을 만들고 `childrenSummary` 로 대상별 성공·실패·대기를 보인다. 일부 실패는 `partial_failure` 로 끝난다. - 배포 순서(§32.2): 릴리스 승인 → HAL 자체 건강 → 부트스트랩 완료 → `hal` 모듈이 그 릴리스로 실행 중일 때만 다른 모듈 배포(`hal_first`). ## 6. 설치(부트스트랩)와 첫 Agent ``` # 작업 PC: main 에 병합된 커밋에서 node scripts/release.mjs build # dist/0.1.1/*.tar.gz + manifest.json (테스트 통과 필요) scp -r dist/0.1.1 hostinger:/tmp/ariweb-dist # 대상 서버(ariweb.net 호스트, master, sudo): bash /tmp/ariweb-dist/… (artifact 안의 deploy/bootstrap-hal.sh) --dist /tmp/ariweb-dist --host hal.ariweb.net --cert /etc/letsencrypt/live/wh-wild-ariweb.net ``` [0.4.1] 부트스트랩은 HAL 설치·보관소·유닛·nginx·릴리스 등록(승인)에 이어 **이 서버의 Node Agent·HawkEye 를 함께 설치·등록·승인**해 부트스트랩을 닫는다(`deploy/hal-host-agent.sh`: `server-create --profile hal` → `server-token --json` → `agent-install.sh --hal http://127.0.0.1:8900 --role control`(loopback, DNS·인증서 무관) → `server-approve`). 그래서 설치 직후 운영자 콘솔은 "부트스트랩 미완료" 가 아니라 "부트스트랩 완료 · 기능 단위 서버 배포 대기: 고객 포털, Webmaker, AriMail, 백업" 을 보인다(`/api/health.deployment.pending`). Web·Mail·Backup·Portal 은 설치하지 않는다 — 역할별 통합 배포(새 서버)로 추가한다. 0.4.0 이하로 설치된 HAL 서버는 `cd /srv/ariweb/hal/current && bash deploy/hal-host-agent.sh` 를 한 번 실행한다(`--no-agent` 로 부트스트랩하면 이전처럼 첫 Agent 승인 때 닫힌다). 마지막에 운영자 설정 토큰을 낸다. **사설 CA·자체 서명 인증서로 HAL 을 운영할 때(0.4.1)**: HAL 은 기동 때와 토큰 발급 때 자기 공개 주소에 접속해 시스템 CA 신뢰 여부를 확인한다(`hal/tls.mjs`, CLI `node hal/index.mjs tls-probe`). 신뢰되지 않으면 운영자 콘솔이 만드는 설치 명령에 서버 공개키 핀이 붙는다: ``` curl -fsSL -k --pinnedpubkey 'sha256//…' https://hal.local.test/agent/v1/install.sh | bash -s -- --hal https://hal.local.test --token … --hal-pin 'sha256//…' ``` `-k` 는 CA 검증만 끄고 `--pinnedpubkey` 는 서버 공개키가 핀과 다르면 자료를 받기 전에 끊는다. 스크립트는 같은 핀으로 `GET /agent/v1/hal-ca.pem`(체인 안의 자체 서명 인증서, 또는 HAL 서버의 `/tls/hal-ca.pem` 이 있으면 그 파일)을 받아 `/data/hal-ca.pem` 에 두고, 이후 curl 은 `--cacert`, Node 는 `NODE_EXTRA_CA_CERTS` + `agent.json caFile`(Agent 가 기동 때 `tls.setDefaultCACertificates` 로 더함), 유닛은 `Environment=NODE_EXTRA_CA_CERTS=…` 로 같은 CA 를 쓴다. 운영자가 CA 파일을 직접 줄 때는 `--ca `. 루트를 체인에 싣지 않는 사설 CA 는 HAL 서버에 `/tls/hal-ca.pem` 을 두면 된다. **이미 등록된 서버(0.4.1)**: 같은 서버에 두 번째 토큰으로 등록하면 HAL 이 409 `server_already_registered`("이미 등록된 서버입니다(서버·Agent·등록 시각) — 다시 설치할 필요가 없습니다") 로 거부하고 토큰을 소비하지 않는다. 쓴 토큰을 다시 쓰면 409 `token_used` 에 같은 문구. 설치 스크립트는 `agent.json` 이 있으면 등록을 건너뛰고 "이미 등록된 서버" 를 안내한다. 서버를 다시 설치해 새 Agent 로 바꾸려면 운영자 콘솔 › 서버 › 등록 토큰에서 "재등록 허용"(재인증) 토큰을 발급한다 — 기존 Agent 는 해지된다(CLI `server-token --server --replace`). Agent 쪽 연결 오류는 스택 대신 원인·조치 한 줄로 나온다(`등록 실패: HAL(…) 인증서를 확인할 수 없습니다(SELF_SIGNED_CERT_IN_CHAIN) — …`). 그 뒤 `portal`·`webmaker`·`arimail` 은 운영자 API `POST /ops/api/modules//deploy {version, serverIds}` 또는 역할별 통합 배포로 배포한다. 도입 모듈 배치: Agent facts 가 서버의 기존 유닛(`webmaker`, `arihakdang-mail`/`arimail`/`heart-mail`)과 WorkingDirectory 를 읽어 그 appDir(`/srv/webmaker`, `/srv/arihakdang-mail` …)의 `releases/` 에 풀고 `current` 를 바꾼다. Webmaker 는 원본 `remote-update.sh` 순서(데이터 백업 → 교체 → 재시작 → unit-check), AriMail 은 원본 `deploy.sh` 순서(DB 백업 → 교체 → db-migrate → 재시작 → health → haraka 재시작)를 Agent 가 수행한다. ## 7. 운영자 접근(§25) `/ops/*` 는 허용 CIDR(`config.opsAllowCidrs`) 안이거나 접근권 쿠키(`aw_ops_grant`, 사용자·기기 귀속, 최대 72시간)가 있어야 하며 아니면 로그인 화면도 403. 접근권: `node hal/index.mjs grant --login --hours 8` 또는 허용 네트워크 안의 운영자가 `POST /ops/api/access-grants`. 고위험 작업은 재인증(10분)과 3단계 확인(`confirmImpact`, `confirmIdentifiers`, `confirmExecute`). ## 8. 긴급 복구(§33)·재해복구(§34) - `POST /ops/api/emergency` 선언(HAL 정상이고 열린 critical 사건이 없으면 거부) → `approve` 1·2·3단계(서로 다른 입력) → `credential`(최대 60분, 1회 표시) → `perform`(허용 범위·허용 행동만; 일반 배포 거부) → `verify`(자격 폐기) → `reconcile`(목표·실제 비교) → `close`. - DR: `PUT /ops/api/dr/config`(RTO·RPO·주기·보존·외부 보관), 사본 `POST /ops/api/dr/snapshots`(VACUUM INTO + vault.json + config.json, vault.key 제외), 복원 요청→승인→시작(기존 제어 중지 확인 문구)→`deploy/hal-restore.sh`→완료 확인(운영자 로그인·HAL 제어·HTTPS·고객 서비스), 훈련 기록과 측정 RTO/RPO. `readiness.ready` 가 true 여야 정식 운영 준비 완료. - 하나의 HAL: 기동 때 `bootstrap_state.live_instance_id/live_lease_at` 임대(20초 갱신, 60초 유효). 다른 인스턴스의 임대가 살아 있으면 기동 거부(`--takeover` 는 복원 절차에서만). ## 9. 테스트 `node --no-warnings test/hal.test.mjs`, `node --no-warnings test/agent.test.mjs`. Agent 테스트는 `ARIWEB_AGENT_STUB_CMDS=1` 과 임시 루트로 실제 시스템을 건드리지 않는다.