{ "generatedAt": "2026-10-26T13:02:10.786Z", "startedAt": "2026-10-26T12:57:29.622Z", "env": { "agents": 4, "ports": { "hal": 35361, "portal": 35287, "wm": 43619, "am": 33137, "mail": 40085 }, "mailServerRegistered": true, "node": "v24.22.1" }, "summary": { "verified_local": 13, "partial": 15, "not_verifiable_locally": 2, "failed": 4, "unrecorded": 0 }, "flows": [ { "no": 1, "title": "신규 고객 가입과 이메일 확인", "verdict": "verified_local", "note": "가입→확인 메일(포획)→1회용 토큰 확인→개인 그룹·별칭→중복 가입 안내→기기 목록·세션 종료", "blockedBy": null, "steps": [ { "name": "가입(이메일·표시 이름·비밀번호·필수 동의) → 세션 발급", "ok": true, "detail": { "status": 200, "hasSession": "[redacted]" } }, { "name": "확인 전 유료·운영 기능 차단(그룹 생성 403 email_unverified)", "ok": true, "detail": "email_unverified" }, { "name": "확인 메일 수신(발신 포획)에서 토큰 추출", "ok": true, "detail": { "subject": "[Ariweb] 이메일 주소를 확인해 주세요" } }, { "name": "이메일 확인(1회용) → 재사용 거부", "ok": true, "detail": { "first": 200, "second": "token_used" } }, { "name": "내 정보: 이메일 확인됨 · 개인 그룹 · owner · 별칭 user-01", "ok": true, "detail": { "verified": true, "groupKind": "personal", "role": "owner", "alias": "user-01" } }, { "name": "중복 가입은 존재를 드러내지 않는 안내(409 signup_unavailable)", "ok": true, "detail": "signup_unavailable" }, { "name": "로그인 기기 목록 · 다른 세션 종료", "ok": true, "detail": { "sessionsBefore": "[redacted]", "revoked": 2 } } ], "evidence": { "me": { "verified": true, "groupKind": "personal", "role": "owner", "alias": "user-01" } } }, { "no": 2, "title": "개인 사용자 그룹 생성과 다른 사용자 초대", "verdict": "verified_local", "note": "팀 그룹 생성·초대 메일·1회용 수락·별칭 user-02·역할 거부·그룹 전환 격리", "blockedBy": null, "steps": [ { "name": "팀 그룹 만들기 → 현재 그룹 전환", "ok": true, "detail": "g_01a17849a2c2dec27ee9f14777" }, { "name": "이메일 초대(7일) → 초대 메일 링크", "ok": true, "detail": "RAXi6X0pnmr-nCEKiIX63pk26vTbIB5d" }, { "name": "초대 수락(1회용) → 별칭 user-02 · 역할 member", "ok": true, "detail": { "alias": "user-02", "role": "member", "reuse": "invite_invalid" } }, { "name": "권한 없는 구성원의 직접 API 접근 → 403 + 필요한 역할", "ok": true, "detail": { "requiredRole": [ "owner", "admin" ], "yourRole": "member" } }, { "name": "그룹 전환 격리: 개인 그룹으로 바꾸면 팀 데이터가 보이지 않음", "ok": true, "detail": { "team": { "live": 0, "purchased": 0, "inUse": 0, "reserved": 0, "unallocated": 0 }, "personal": { "live": 0, "purchased": 0, "inUse": 0, "reserved": 0, "unallocated": 0 } } } ], "evidence": {} }, { "no": 3, "title": "상품 주문, 결제 확인, 권리 활성화", "verdict": "verified_local", "note": "상품 목록→주문(멱등·수동 결제)→입금 확인 전 승인 거부→입금 확인→승인→권리 활성화→재승인 중복 없음→청구 화면", "blockedBy": null, "steps": [ { "name": "상품 목록(공개 버전만·가격·한도·미확정 정책 표시)", "ok": true, "detail": { "count": 2, "team": { "id": "pv_01a17849a127747d9c2bbbb24c", "price": 49000, "sites": 3 }, "deferred": 3, "paymentMethods": [ "manual_transfer" ] } }, { "name": "주문 제출(수동 결제) → 결제 대기·주문 번호·입금 안내 · 같은 키 재제출은 중복 없음", "ok": true, "detail": { "orderNo": "AW-20261026-0001", "status": "awaiting_payment", "total": 63800, "dueAt": "2026-11-02T12:57:34.263Z", "hasInstructions": true } }, { "name": "입금 확인 전 승인 거부(409 payment_not_confirmed)", "ok": true, "detail": "payment_not_confirmed" }, { "name": "운영자 입금 확인 → 승인 대기 목록에 권리 변화·현재 사용량 표시", "ok": true, "detail": { "status": "awaiting_approval", "delta": [ "sites", "web_disk", "web_out", "mail_storage", "mail_msgs", "mailboxes", "mail_services", "mail_storage", "mail_msgs", "mailboxes" ] } }, { "name": "운영자 승인 → 권리 활성화(재승인은 중복 없음)", "ok": true, "detail": { "granted": 10, "already": true } }, { "name": "고객 결제 화면: 활성 상품·권리·다음 청구일", "ok": true, "detail": { "sites": 3, "subs": 1, "nextBilling": "2026-11-26T12:57:34.327Z" } } ], "evidence": { "ap": { "granted": 10, "already": true }, "bill": { "sites": 3, "subs": 1, "nextBilling": "2026-11-26T12:57:34.327Z" } } }, { "no": 4, "title": "초안 사이트 생성", "verdict": "verified_local", "note": "초안 생성·Webmaker 프로젝트 연결·사이트 수 미소비·목록 필터", "blockedBy": null, "steps": [ { "name": "사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내", "ok": true, "detail": { "id": "s_01a17849a3be6f07d129dbdb60", "lifecycle": "draft", "projectId": "00000000000000000000000000000001", "nextSteps": [ "build", "import", "domain", "mode", "perms" ] } }, { "name": "사이트 목록 필터(초안)·사용량 요약에 예약 0", "ok": true, "detail": { "drafts": 1, "sitesReserved": 0, "sitesAllocated": 0 } } ], "evidence": {} }, { "no": 5, "title": "Webmaker에서 페이지 제작", "verdict": "verified_local", "note": "SSO 티켓(1회용) 진입·저장본 목록·배포 전 확인(가짜 Webmaker — 실제 편집 UI 는 포함하지 않음)", "blockedBy": null, "steps": [ { "name": "제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용)", "ok": true, "detail": { "portalRedirect": "http://127.0.0.1:43619/api/auth/sso?ticket=…", "webmaker": 302, "reuse": 401 } }, { "name": "Webmaker 에서 페이지 저장(저장본 2개) → 포털 저장본 목록·배포 전 확인 통과", "ok": true, "detail": { "versions": 1, "precheckOk": true } } ], "evidence": { "sso": { "portalRedirect": "http://127.0.0.1:43619/api/auth/sso?ticket=…", "webmaker": 302, "reuse": 401 }, "ver": { "versions": 1, "precheckOk": true } } }, { "no": 6, "title": "관리형 배포", "verdict": "failed", "note": "시간 초과 — 포털 작업 queued(attempt 8, 배포 서버 작업 진행 중) · HAL site.deploy 작업 8개(최근: [\"failed\",\"Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni\"])", "blockedBy": null, "steps": [ { "name": "배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나", "ok": true, "detail": { "jobId": "j_01a17849a4096e33f476b17d1b", "steps": 10, "again": false, "siteCount": "reserved", "server": null } }, { "name": "HAL·Agent 를 거쳐 작업 완료 대기(최대 40초)", "ok": false, "detail": "시간 초과 — 포털 작업 queued(attempt 8, 배포 서버 작업 진행 중) · HAL site.deploy 작업 8개(최근: [\"failed\",\"Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni\"])" }, { "name": "[HAL 직접] 같은 Agent 에 로컬 저장본으로 site.deploy → 성공하면 결함은 포털→Agent 번들 전달 구간", "ok": true, "detail": { "status": "done", "error": null, "layout": true, "externalCheck": { "ok": false, "status": 404, "ms": 19, "url": "http://direct_e2e.preview.localhost/", "markerFound": false } } }, { "name": "사용자 취소(진행 중 작업) → 중간 결과 정리·예약 반환", "ok": true, "detail": { "cancel": 200, "cleanup": 200, "siteCount": "reserved", "lifecycle": "building" } } ], "evidence": { "halLog": [ "2026-10-26T12:58:01.231Z [jobs] site.deploy j_01a1784a0781bd4cb2d160762e 실패: Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni", "2026-10-26T12:58:06.242Z [jobs] site.deploy j_01a1784a1b1b6362b7ba3a604c 실패: Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni", "2026-10-26T12:58:11.248Z [jobs] site.deploy j_01a1784a2eb913f75494650303 실패: Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni" ] } }, { "no": 7, "title": "도메인 연결과 HTTPS 발급", "verdict": "partial", "note": "도메인 추가·복사 가능한 TXT/DNS 값·외부 대기 상태까지 확인. 실 DNS 반영·인증서 발급·대표 도메인 확정은 로컬 확인 불가", "blockedBy": null, "steps": [ { "name": "도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT)", "ok": true, "detail": { "domainId": "d_01a1784a491476e7f378e8217c", "txtName": "_ariweb-verify.aw-e2e.localhost", "txtValueShape": "ariweb-verify=…", "copyable": true } }, { "name": "도메인 목록·상세: 소유 확인·DNS·인증서 상태 표시", "ok": true, "detail": {} }, { "name": "(정리) 도메인 연결 작업 취소 — 포털이 외부 대기 중에도 5초마다 HAL domain.probe 를 새로 만들어 큐를 채우므로(결함 항목) 뒤 흐름을 위해 멈춘다", "ok": true, "detail": { "queuedProbes": 1 } } ], "evidence": { "dm": { "domainId": "d_01a1784a491476e7f378e8217c", "txtName": "_ariweb-verify.aw-e2e.localhost", "txtValueShape": "ariweb-verify=…", "copyable": true } } }, { "no": 8, "title": "외부 주소에서 사이트 확인", "verdict": "not_verifiable_locally", "note": "외부 주소 확인은 공인 서버·DNS 필요", "blockedBy": 6, "steps": [], "evidence": {} }, { "no": 9, "title": "사이트 정보와 접속·사용량 통계 확인", "verdict": "partial", "note": "미배포 사이트는 통계 대신 준비 단계 표시(§8.1) 확인. 실제 접속·네트워크 통계는 배포 뒤 수집", "blockedBy": 6, "steps": [ { "name": "개요: 배포 전/후에 따라 준비 단계 또는 실제 통계", "ok": true, "detail": { "kind": "not_deployed", "lifecycle": "building", "hasStats": false, "readiness": [ { "id": "build", "label": "Webmaker 에서 사이트 제작", "href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=build", "done": true }, { "id": "import", "label": "콘텐츠 가져오기", "href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=build", "done": false, "unsupported": "이번 버전에서 지원하지 않음" }, { "id": "domain", "label": "도메인 연결", "href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=domains", "done": false }, { "id": "mode", "label": "배포 방식 선택", "href": "/sites/s_01a17849a3be6f07d129dbdb60/deploy/mode", "done": true }, { "id": "perms", "label": "구성원 권한 설정", "href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=settings", "done": false } ], "disk": null } }, { "name": "사용량 탭(배정·실제 사용·마지막 계측)", "ok": true, "detail": { "allocations": [ "web_disk", "web_out" ] } } ], "evidence": { "ov": { "kind": "not_deployed", "lifecycle": "building", "hasStats": false, "readiness": [ { "id": "build", "label": "Webmaker 에서 사이트 제작", "href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=build", "done": true }, { "id": "import", "label": "콘텐츠 가져오기", "href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=build", "done": false, "unsupported": "이번 버전에서 지원하지 않음" }, { "id": "domain", "label": "도메인 연결", "href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=domains", "done": false }, { "id": "mode", "label": "배포 방식 선택", "href": "/sites/s_01a17849a3be6f07d129dbdb60/deploy/mode", "done": true }, { "id": "perms", "label": "구성원 권한 설정", "href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=settings", "done": false } ], "disk": null }, "us": { "allocations": [ "web_disk", "web_out" ] } } }, { "no": 10, "title": "사이트 대표 메일과 메일함 생성", "verdict": "partial", "note": "대표 메일 생성(AriMail 테넌트·보장량·ariweb 표시)·메일함(비밀번호 1회·초대)·DNS 미반영 구분·활성화 작업 외부 대기까지 확인. DNS 반영 뒤 활성화 완료는 실 DNS 필요", "blockedBy": null, "steps": [ { "name": "사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시", "ok": true, "detail": { "id": "msv_01a1784ad1a3c31d4ea6cb7447", "status": "needs_setup", "tenant": true, "ariwebKind": "site_bound", "limitAccounts": 6, "siteCountAllocated": 0 } }, { "name": "같은 사이트에 두 번째 대표 메일 거부", "ok": true, "detail": "mail_domain_in_use" }, { "name": "메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음", "ok": true, "detail": { "id": "mb_01a1784ad1df7ea80529a8cf8a", "address": "kim@aw-e2e.localhost", "passwordShownOnce": "[redacted]", "mustChange": true, "imapHost": "127.0.0.1", "leakedOnReload": false, "inArimail": true } }, { "name": "초대 방식 메일함(링크 만료·재발송·사용 완료 상태)", "ok": true, "detail": { "inviteState": "pending", "accepted": 200, "reuse": "invite_used" } }, { "name": "DNS 확인: 미반영/잘못된 값 구분 표시(로컬 DNS 에는 레코드 없음 → missing)", "ok": true, "detail": { "ok": false, "missing": 3, "items": [ "mx:missing", "spf:missing", "dkim:missing", "dmarc:recommended" ] } }, { "name": "활성화 작업 → DNS 외부 대기(awaiting_external) · 멱등", "ok": true, "detail": { "status": "awaiting_external", "waitReason": "DNS 레코드가 아직 반영되지 않았습니다(누락 3, 잘못된 값 0). 도메인 DNS 에 안내된 값을 넣은 뒤 '이어서 진행' 을 누르세요", "sameJob": true, "steps": [ "entitlement:done", "dns:waiting", "prepare:pending", "verify:pending", "activate:pending" ] } }, { "name": "보안 보고서(SPF·DKIM·DMARC·MX 상태 + 복사 값)", "ok": true, "detail": { "items": [ "sender:error", "mx:error", "spf:error", "dkim:error", "dmarc:recommended", "tls:error", "roundtrip:recommended" ], "records": 4 } } ], "evidence": { "svc": { "id": "msv_01a1784ad1a3c31d4ea6cb7447", "status": "needs_setup", "tenant": true, "ariwebKind": "site_bound", "limitAccounts": 6, "siteCountAllocated": 0 }, "mb": { "id": "mb_01a1784ad1df7ea80529a8cf8a", "address": "kim@aw-e2e.localhost", "passwordShownOnce": "[redacted]", "mustChange": true, "imapHost": "127.0.0.1", "leakedOnReload": false, "inArimail": true }, "dns": { "ok": false, "missing": 3, "items": [ "mx:missing", "spf:missing", "dkim:missing", "dmarc:recommended" ] }, "act": { "status": "awaiting_external", "waitReason": "DNS 레코드가 아직 반영되지 않았습니다(누락 3, 잘못된 값 0). 도메인 DNS 에 안내된 값을 넣은 뒤 '이어서 진행' 을 누르세요", "sameJob": true, "steps": [ "entitlement:done", "dns:waiting", "prepare:pending", "verify:pending", "activate:pending" ] } } }, { "no": 11, "title": "외부 송신, 수신, 회신 확인", "verdict": "not_verifiable_locally", "note": "외부 송신·수신·회신은 실제 SMTP 제출(587)·수신함·외부 자동 회신 주소가 필요하다. 포털은 검증 주소 미설정 시 needs_user, 왕복 실패 시 활성화하지 않는 규칙(단위 테스트 test/mail.test.mjs)으로만 확인됨", "blockedBy": null, "steps": [], "evidence": {} }, { "no": 12, "title": "독립 메일 서비스 생성", "verdict": "verified_local", "note": "독립 메일 서비스 생성: 독립 옵션 1 소비, 운영 사이트 수 미소비, 목록에 백업 미제공 문구", "blockedBy": null, "steps": [ { "name": "메일 전용 도메인 등록(solo-e2e.localhost, purpose mail)", "ok": true, "detail": { "id": "d_01a1784af801ae5cf98ef55e1f", "jobCancelled": null } }, { "name": "독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비)", "ok": true, "detail": { "id": "msv_01a1784af809f14b89bc606f73", "kind": "standalone", "mailServicesAllocated": 1, "sitesAllocated": 0 } } ], "evidence": { "r": { "id": "msv_01a1784af809f14b89bc606f73", "kind": "standalone", "mailServicesAllocated": 1, "sitesAllocated": 0 }, "listCount": 2, "noBackupNote": "메일 원문은 고객용 백업·복원에 포함되지 않으며, 메일 백업 기능을 제공하지 않습니다." } }, { "no": 13, "title": "백업 옵션 구매와 첫 백업 확인", "verdict": "partial", "note": "옵션 구매·활성화까지 확인. 실제 백업 생성은 배포된 사이트 파일이 있어야 함(흐름 6 결함에 막힘)", "blockedBy": 6, "steps": [ { "name": "백업 옵션 주문(대상 사이트)·입금 확인·승인 → backup 권리", "ok": true, "detail": { "granted": 7, "orderStatus": "approved" } }, { "name": "백업 화면: 구매 가능 여부·저장 한도·일정·보존·첫 백업 기한·검증 방식(상품정책에서)", "ok": true, "detail": { "eligible": true, "reasons": [], "deferred": [], "policy": null, "status": 200 } }, { "name": "옵션 활성화(확인 필수) → 첫 백업 예정 시각", "ok": true, "detail": { "status": "active", "firstDue": "2026-10-27T12:59:01.665Z", "nextRun": "2026-10-26T12:59:01.665Z" } }, { "name": "지금 백업 실행 → HAL backup.create(저장 서버 ≠ 운영 서버) → Agent → 검증", "ok": true, "detail": { "timedOut": true, "optionStatus": "active", "lastJob": null, "siteServer": null, "backup": null, "job": null } } ], "evidence": { "el": { "eligible": true, "reasons": [], "deferred": [], "policy": null, "status": 200 }, "opt": { "status": "active", "firstDue": "2026-10-27T12:59:01.665Z", "nextRun": "2026-10-26T12:59:01.665Z" }, "run1": { "timedOut": true, "optionStatus": "active", "lastJob": null, "siteServer": null, "backup": null, "job": null } } }, { "no": 14, "title": "백업 복원과 외부 사이트 확인", "verdict": "partial", "note": "완료된 백업이 없어 복원을 요청할 수 없음", "blockedBy": 13, "steps": [], "evidence": {} }, { "no": 15, "title": "관리형 사이트 서버 이전과 외부 확인", "verdict": "partial", "note": "이전 경계값·요청 규칙 확인. 실제 두 Agent 간 사전 복사·전환·외부 확인은 배포된 사이트(흐름 6)와 공인 확인 주소가 필요", "blockedBy": 6, "steps": [ { "name": "이전 화면 경계값: 취소 가능 마지막 단계·쓰기 제한·목표 중단 시간·되돌림 기간·이전 위치 보존·비용(플랫폼 사유 0원)", "ok": true, "detail": { "auto": { "ok": false, "reasons": [ "사이트 대표 메일이 있는 사이트는 승인형으로 진행합니다" ] }, "boundaries": { "lastCancel": "새 위치 확인(연결 전환 직전)까지 취소할 수 있습니다", "revertHours": 72, "cost": 10000, "platformCost": 0 } } }, { "name": "고객 요청(유료·승인 대기) 또는 조건 미충족 사유", "ok": true, "detail": { "status": 200, "relocation": { "status": "awaiting_approval", "kind": "customer_paid", "cost": 10000 } } }, { "name": "전환 전 고객 취소 가능(요청 취소 → 소유권 이전 등 다른 작업 차단 해제)", "ok": true, "detail": { "status": 200, "rstatus": "cancelled" } } ], "evidence": { "b": { "auto": { "ok": false, "reasons": [ "사이트 대표 메일이 있는 사이트는 승인형으로 진행합니다" ] }, "boundaries": { "lastCancel": "새 위치 확인(연결 전환 직전)까지 취소할 수 있습니다", "revertHours": 72, "cost": 10000, "platformCost": 0 } }, "req": { "status": 200, "relocation": { "status": "awaiting_approval", "kind": "customer_paid", "cost": 10000 } } } }, { "no": 16, "title": "외부 배포 사이트 생성과 배포", "verdict": "failed", "note": "동의(버전)·토큰 발급 거부/허용 규칙까지 확인했으나 토큰 발급 API 가 DB 제약으로 500 — 결함 항목 참조", "blockedBy": null, "steps": [ { "name": "외부 배포 사이트 생성(초안) + 저장본", "ok": true, "detail": { "id": "s_01a1784b5d03e229abe92a46a1", "group_id": "g_01a17849a2c2dec27ee9f14777", "name": "E2E 외부 사이트", "purpose": "e2e", "webmaker_project_id": "00000000000000000000000000000004", "template": null, "lang": "ko", "timezone": "Asia/Seoul", "lifecycle": "draft", "deploy_mode": "external", "primary_domain": null, "site_count_state": "none", "site_count_reservation_id": null, "server_id": null, "external_agent_id": null, "current_version": null, "current_deployment_id": null, "deploy_verify": null, "health": "unknown", "health_checked_at": null, "maintenance": 0, "search_visible": 1, "description": null, "settings_json": null, "paused_at": null, "cancel_at": null, "retention_until": null, "delete_after": null, "deleted_at": null, "created_by": "u_01a17849a21df78a8debb70c18", "created_at": "2026-10-26T12:59:27.363Z", "updated_at": "2026-10-26T12:59:27.368Z", "runtime": "static", "consent_version": null, "pending_cleanup_json": null, "return_due_at": null, "last_deploy_at": null, "last_external_check_json": null, "next_job_json": null, "mail_choice": null, "next_notice_at": null, "delete_hold_reason": null, "public_stopped_at": null, "prev_lifecycle": null, "settings": {}, "pendingCleanup": null, "lastExternalCheck": null, "nextJob": null } }, { "name": "배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전", "ok": true, "detail": { "notProvided": "[redacted]", "consentVersion": "external-deploy-2026-10-0.1.1", "agreed": false } }, { "name": "동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전)", "ok": true, "detail": { "before": "consent_required", "consent": 200 } }, { "name": "외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음)", "ok": false, "detail": "500 {\"ok\":false,\"error\":\"ERR_SQLITE_ERROR\",\"message\":\"일시적인 오류가 발생했습니다. 잠시 후 다시 시도하세요\"} | portal: 2026-10-26T12:59:27.404Z [http] POST /api/sites/s_01a1784b5d03e229abe92a46a1/external 500: Error: NOT NULL constraint failed: external_agents.hal_agent_id | at Object.run (file:///home/master/work/Ar" }, { "name": "[HAL 직접] 외부 Agent 토큰 발급(포털 서비스 키)", "ok": true, "detail": { "token": "[redacted]", "expiresAt": "2026-10-26T13:09:27.408Z" } }, { "name": "[HAL 직접] 외부 Agent 등록·실행(사이트 귀속) → by-site 상태 connected", "ok": true, "detail": { "agentId": "[redacted]", "status": "connected", "siteId": "s_01a1784b5d03e229abe92a46a1" } }, { "name": "[HAL 직접] 외부 Agent 배포(site.deploy, 로컬 저장본) → releases/current 생성", "ok": true, "detail": { "status": "done", "error": null, "errorCode": null, "result": { "version": "20261026125937-v1", "leftovers": [] }, "layout": true } }, { "name": "[HAL 직접] 재배포(v2) → 이전 버전 기록", "ok": true, "detail": { "status": "done", "error": null, "errorCode": null, "result": { "version": "20261026125950-v2", "leftovers": [] } } }, { "name": "[HAL 직접] 실패 배포(표식 불일치) → 이전 버전 유지·남은 파일(leftovers) 보고", "ok": true, "detail": { "status": "failed", "error": "내부 확인 실패(404) — 이전 버전을 유지합니다", "errorCode": "internal_check_failed", "result": null } }, { "name": "[HAL 직접] 다른 사이트 작업 거부(site_mismatch) · 허용되지 않는 종류 거부", "ok": true, "detail": {} }, { "name": "[HAL 직접] 배포 제거(site.remove) → 파일·vhost 제거", "ok": true, "detail": { "layoutAfter": false } } ], "evidence": { "mode": { "notProvided": "[redacted]", "consentVersion": "external-deploy-2026-10-0.1.1", "agreed": false } } }, { "no": 17, "title": "사용량 경고와 한도 동작 확인", "verdict": "verified_local", "note": "메일 저장 공간 95%·월간 통수 100% 를 수집해 구간·제한 사유·KST 초기화·알림을 확인(웹 디스크/아웃바운드는 배포가 있어야 수집)", "blockedBy": null, "steps": [ { "name": "가짜 AriMail 사용량 95%/월간 100% → 수집(refresh) → 구간·제한 사유·다음 KST 초기화·마지막 계측 표시", "ok": true, "detail": { "storageLevel": 95, "monthlyLevel": 100, "nextReset": "2026-10-31T15:00:00.000Z", "measuredAt": true, "limit100": "월간 통수 100%: 새 발신을 막고 새 수신은 임시 실패로 응답합니다. 다음 KST 월 시작 또는 추가 용량 적용 후 복구됩니다.", "countingRule": "per_final_recipient", "evaluate": { "status": 200, "notified": 2, "limited": 1 } } }, { "name": "알림 센터: 95%·100% 경고가 사건으로 묶여 표시(필수 알림)", "ok": true, "detail": { "count": 3, "titles": [ "aw-e2e.localhost 메일 저장 공간 96% — 곧 제한됩니다(최종 경고)", "aw-e2e.localhost 월간 메일 통수(송신+수신) 100% — 제한 적용", "메일함 kim@aw-e2e.localhost 저장 공간 95% 경고" ], "mandatory": true } }, { "name": "사용량 화면 제한 플래그(/api/usage/limits)·그룹 요약", "ok": true, "detail": { "limits": [ "mail_msgs:100", "mail_storage:95" ], "warnLevels": [ 70, 80, 90, 95 ] } } ], "evidence": { "warn": { "storageLevel": 95, "monthlyLevel": 100, "nextReset": "2026-10-31T15:00:00.000Z", "measuredAt": true, "limit100": "월간 통수 100%: 새 발신을 막고 새 수신은 임시 실패로 응답합니다. 다음 KST 월 시작 또는 추가 용량 적용 후 복구됩니다.", "countingRule": "per_final_recipient", "evaluate": { "status": 200, "notified": 2, "limited": 1 } }, "notif": { "count": 3, "titles": [ "aw-e2e.localhost 메일 저장 공간 96% — 곧 제한됩니다(최종 경고)", "aw-e2e.localhost 월간 메일 통수(송신+수신) 100% — 제한 적용", "메일함 kim@aw-e2e.localhost 저장 공간 95% 경고" ], "mandatory": true }, "lim": { "limits": [ "mail_msgs:100", "mail_storage:95" ], "warnLevels": [ 70, 80, 90, 95 ] } } }, { "no": 18, "title": "상품 축소 보류와 정리 후 적용", "verdict": "verified_local", "note": "축소 미리보기→보류→정리(배정 축소·독립 메일 해지)→재검증 적합→축소 주문·승인→권리 교체", "blockedBy": null, "steps": [ { "name": "배정 화면 수치(구매·내 배정·다른 서비스·예약·미배정·변경 후 최대) + 마지막 단위 동시 요청", "ok": true, "detail": { "purchased": 45, "mine": 6, "others": 2, "unallocated": 37, "race": [ 409, 409 ], "mineAfter": 6 } }, { "name": "축소 미리보기(팀→베이직): 줄여야 할 항목(메일함 6→5) 구체 표시", "ok": true, "detail": { "shortages": [ "mailboxes:1" ] } }, { "name": "축소 요청 → 보류(사용 중 권리 즉시 회수 없음)", "ok": true, "detail": { "fits": false, "hold": "holding", "sitesStillPurchased": 3 } }, { "name": "정리: 메일함 배정 6→5 축소 → 재검증 적합 → 축소 주문 생성", "ok": true, "detail": { "allocate": 200, "fits": true, "shortages": [], "order": "awaiting_payment" } }, { "name": "축소 주문 입금 확인·승인 → 권리 교체(베이직)", "ok": true, "detail": { "approve": 200, "sites": 1, "mailboxes": 5, "product": "pv_01a17849a126e838aa8bb17065" } } ], "evidence": { "alloc": { "purchased": 45, "mine": 6, "others": 2, "unallocated": 37, "race": [ 409, 409 ], "mineAfter": 6 }, "pv": { "shortages": [ "mailboxes:1" ] }, "hold": { "fits": false, "hold": "holding", "sitesStillPurchased": 3 }, "clean": { "allocate": 200, "fits": true, "shortages": [], "order": "awaiting_payment" }, "applied": { "approve": 200, "sites": 1, "mailboxes": 5, "product": "pv_01a17849a126e838aa8bb17065" } } }, { "no": 19, "title": "사이트 수 반환과 대표 메일의 독립 전환", "verdict": "partial", "note": "반환 점검표와 메일 유지/종료 분기 규칙 확인. 실제 반환은 운영 사이트(사이트 수 사용 중) 필요(단위 테스트 test/lifecycle.test.mjs·test/mail.test.mjs 로 확인)", "blockedBy": 6, "steps": [ { "name": "§14 반환 점검표(8단계)·차단 사유(대표 메일 유지 선택·독립 옵션)", "ok": true, "detail": { "ok": false, "blocking": [ "domains_unrouted", "mail_choice", "mail_converted" ], "steps": 8 } }, { "name": "메일 유지 선택: 독립 옵션 부족이면 반환하지 않음 / 종료 선택 규칙", "ok": true, "detail": { "status": 409, "error": "quota_insufficient", "detail": [ "resource", "needed", "available", "link" ] } } ], "evidence": { "ck": { "ok": false, "blocking": [ "domains_unrouted", "mail_choice", "mail_converted" ], "steps": 8 }, "keep": { "status": 409, "error": "quota_insufficient", "detail": [ "resource", "needed", "available", "link" ] } } }, { "no": 20, "title": "사이트 소유권 이전", "verdict": "verified_local", "note": "미리보기→요청·알림→수락(보류·재확인)→심사·최종 확인→원자적 완료→접근 전환·감사", "blockedBy": null, "steps": [ { "name": "이전 범위 미리보기(사이트·도메인·대표 메일·배정·진행 작업·미납 / 제외 항목)", "ok": true, "detail": { "sites": 1, "domains": 1, "siteMail": 1, "needed": { "backup": 1, "web_disk": 1073741824, "mail_msgs": 500, "mail_storage": 2147483648, "mailboxes": 6 }, "excluded": 3 } }, { "name": "요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림", "ok": true, "detail": { "id": "tr_01a1784c7a9e2007160a39f536", "status": "requested", "expiresAt": "2026-11-02T13:00:40.478Z", "bobNotified": true } }, { "name": "(정리) 받는 쪽 권리에 맞게 범위 조정: 백업 옵션 종료·메일함 배정 6→5(받는 그룹 베이직 5개)", "ok": true, "detail": { "endBackup": 200, "alloc": 200 } }, { "name": "받는 그룹 수락 → 권리 부족이면 보류(필요한 상품 안내)", "ok": true, "detail": { "status": 200, "tstatus": "reviewing", "hold": null } }, { "name": "운영자 심사(소유 증빙·도메인 확인) 미입력 거부 → 최종 확인 일치 승인 → 원자적 완료·청구 전환 시각", "ok": true, "detail": { "bad": "final_confirmation_mismatch", "ok": 200, "status": "completed", "cutover": "2026-10-26T13:00:40.527Z" } }, { "name": "완료 뒤: 보내는 쪽은 접근 불가(404), 받는 쪽 대시보드·사용량 일치, 불변 감사", "ok": true, "detail": { "aliceSees": 404, "bobSees": 200, "bobUsageSites": 0, "audit": true } } ], "evidence": { "pv": { "sites": 1, "domains": 1, "siteMail": 1, "needed": { "backup": 1, "web_disk": 1073741824, "mail_msgs": 500, "mail_storage": 2147483648, "mailboxes": 6 }, "excluded": 3 }, "rq": { "id": "tr_01a1784c7a9e2007160a39f536", "status": "requested", "expiresAt": "2026-11-02T13:00:40.478Z", "bobNotified": true }, "acc": { "status": 200, "tstatus": "reviewing", "hold": null }, "ap": { "bad": "final_confirmation_mismatch", "ok": 200, "status": "completed", "cutover": "2026-10-26T13:00:40.527Z" }, "after": { "aliceSees": 404, "bobSees": 200, "bobUsageSites": 0, "audit": true } } }, { "no": 21, "title": "중지, 해지, 보존 예정일 표시", "verdict": "partial", "note": "생명주기 화면·중지/해지 영향 안내·보존 정책 표시 확인. 운영 중 사이트의 실제 중지·해지는 배포 성공(흐름 6) 필요", "blockedBy": 6, "steps": [ { "name": "§23.3 한 화면 보기: 공개/관리자 접근·사이트 수·청구·도메인/메일/백업 동작·내보내기·되돌릴 수 없는 시점·보존·다음 알림", "ok": true, "detail": { "state": "제작 중", "siteCount": false, "publicAccess": "중지됨", "retention": { "site_days": 30, "mail_days": 30, "source_days": 30, "backup_days": 7, "delete_grace_days": 1, "notice_before_days": 3, "readonly_retention_days": 30, "delete_after_days": 30, "pause_keeps_site_count": true, "cancel_effective": "next_billing_date", "source": "상품정책 베이직 (retention 정책) v1" }, "pauseImpactKeys": [ "publicAccess", "adminAccess", "mail", "backups", "usageAndCost", "resume", "siteCount" ], "cancelImpactKeys": [ "target", "linked", "immediateStops", "remainingData", "retentionUntil", "deleteAfter", "retentionSource", "export", "unpaid", "refund", "pointOfNoReturn", "mailOptions", "siteCount" ], "steps14": 8 } }, { "name": "일시 중지(확인 필수) 또는 현재 상태에서 거부 사유", "ok": true, "detail": { "noConfirm": "confirmation_required", "status": 409, "error": "lifecycle_transition" } }, { "name": "메일 서비스 생명주기 화면(해지된 독립 메일: 송수신 중지·보존 종료·삭제 예정·백업 미제공)", "ok": true, "detail": { "status": 200, "sendReceive": "송수신 가능", "retentionUntil": false, "deleteAfter": false, "backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)" } } ], "evidence": { "v": { "state": "제작 중", "siteCount": false, "publicAccess": "중지됨", "retention": { "site_days": 30, "mail_days": 30, "source_days": 30, "backup_days": 7, "delete_grace_days": 1, "notice_before_days": 3, "readonly_retention_days": 30, "delete_after_days": 30, "pause_keeps_site_count": true, "cancel_effective": "next_billing_date", "source": "상품정책 베이직 (retention 정책) v1" }, "pauseImpactKeys": [ "publicAccess", "adminAccess", "mail", "backups", "usageAndCost", "resume", "siteCount" ], "cancelImpactKeys": [ "target", "linked", "immediateStops", "remainingData", "retentionUntil", "deleteAfter", "retentionSource", "export", "unpaid", "refund", "pointOfNoReturn", "mailOptions", "siteCount" ], "steps14": 8 }, "pause": { "noConfirm": "confirmation_required", "status": 409, "error": "lifecycle_transition" }, "mailLc": { "status": 200, "sendReceive": "송수신 가능", "retentionUntil": false, "deleteAfter": false, "backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)" } } }, { "no": 22, "title": "HAL 우선 배포 후 HAL을 통한 전체 모듈 배포", "verdict": "failed", "note": "HAL 우선 순서(hal_first 거부)·릴리스 승인·부트스트랩 잠김은 확인. 모듈 배포 작업은 완료되지 않음(진단 증거 참조)", "blockedBy": null, "steps": [ { "name": "릴리스 화면: 버전·main 커밋·승인 상태·HAL 실행 버전·부트스트랩(최초 Agent)·배포 순서", "ok": true, "detail": { "releases": [ "0.1.2:approved", "0.1.1:approved" ], "running": { "complete": true, "firstInstall": false, "releaseVersion": null, "mainCommit": null, "artifactSha256": null, "halRunningVersion": "0.1.1", "firstAgent": "[redacted]", "manualInstallLocked": true }, "order": [ "hal", "agent", "portal", "webmaker", "arimail", "backup", "observability" ] } }, { "name": "HAL 이 아직 0.1.2 가 아닐 때 다른 모듈 0.1.2 배포 → hal_first 거부", "ok": true, "detail": { "status": "failed", "error": "hal_first" } }, { "name": "승인되지 않은 릴리스는 거부 / 수동 설치 잠김 표시", "ok": true, "detail": { "manualInstallLocked": true, "complete": true } }, { "name": "HAL 모듈 0.1.1 을 제어 서버에 배포(실제 Agent module.install: artifact 내려받기·sha256·releases/current·유닛(stub)·실제 버전 확인)", "ok": true, "detail": { "status": "queued", "steps": [ "precheck:pending", "fanout:pending", "collect:pending" ], "error": null, "children": [], "agentJobs": [], "timedOut": true } } ], "evidence": { "rel": { "releases": [ "0.1.2:approved", "0.1.1:approved" ], "running": { "complete": true, "firstInstall": false, "releaseVersion": null, "mainCommit": null, "artifactSha256": null, "halRunningVersion": "0.1.1", "firstAgent": "[redacted]", "manualInstallLocked": true }, "order": [ "hal", "agent", "portal", "webmaker", "arimail", "backup", "observability" ] }, "hf": { "status": "failed", "error": "hal_first" }, "na": { "manualInstallLocked": true, "complete": true }, "dep": { "status": "queued", "steps": [ "precheck:pending", "fanout:pending", "collect:pending" ], "error": null, "children": [], "agentJobs": [], "timedOut": true }, "diag": { "stuckNow": [ { "id": "j_01a1784d7f02143ed47d1fcaf9", "kind": "domain.probe", "status": "queued", "attempt": 0, "step": "dispatch", "detail": "", "updatedAt": "2026-10-26T13:01:47.138Z" }, { "id": "j_01a1784d6b6a20949d03d2dee8", "kind": "domain.probe", "status": "queued", "attempt": 0, "step": "dispatch", "detail": "", "updatedAt": "2026-10-26T13:01:42.122Z" }, { "id": "j_01a1784d671c8f951049094a53", "kind": "module.deploy", "status": "queued", "attempt": 0, "step": "precheck", "detail": "", "updatedAt": "2026-10-26T13:01:41.020Z" }, { "id": "j_01a1784d57cf28f7528a1a8ccb", "kind": "domain.probe", "status": "queued", "attempt": 0, "step": "dispatch", "detail": "", "updatedAt": "2026-10-26T13:01:37.103Z" }, { "id": "j_01a1784d44338bf61b6691d5a8", "kind": "domain.probe", "status": "running", "attempt": 1, "step": "await", "detail": "", "updatedAt": "2026-10-26T13:01:46.885Z" } ], "halLog": [ "2026-10-26T12:58:56.345Z [jobs] site.deploy j_01a1784ac7d0815275cc3e88aa 실패: Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/lIw5d3l2igVGOIlJa1BCaKwXYo7r2bJJ", "2026-10-26T12:59:11.375Z [jobs] backup.create j_01a1784afb5dd2de92b7a63b4f 실패: 대상 서버 또는 Agent 가 필요합니다", "2026-10-26T13:00:04.476Z [jobs] site.deploy j_01a1784bbc086436dc87f4b997 실패: 내부 확인 실패(404) — 이전 버전을 유지합니다", "2026-10-26T13:00:18.498Z [jobs] site.deploy j_01a1784befea6c15eb6d7782eb 실패: 이 외부 Agent 는 다른 사이트에 귀속되어 있습니다", "2026-10-26T13:00:31.517Z [jobs] backup.create j_01a1784c1721827ff3a7fd81e1 실패: 외부 Agent 는 backup.create 작업을 받을 수 없습니다", "2026-10-26T13:01:40.630Z [jobs] module.deploy j_01a1784d2b6df1c41a7815b555 실패: HAL 이 아직 0.1.2 으로 실행되고 있지 않습니다(현재 0.1.1). HAL 을 먼저 배포하세요" ], "ctlAgentLog": [ "2026-10-26T12:57:32.329Z [agent] Ariweb Node Agent v0.1.1 ag_01a178499afa793eb4b22d9b01 (managed) → http://127.0.0.1:35361" ] } } }, { "no": 23, "title": "실패 작업의 재시도와 되돌리기", "verdict": "partial", "note": "실패 기록·재시도 API·되돌리기 거부/허용 조건 확인. 성공한 재배포 뒤 실제 되돌리기는 배포 성공이 필요", "blockedBy": 6, "steps": [ { "name": "배포 이력: 실패/성공 기록 · 재시도 가능 여부 · 문의용 작업 번호", "ok": true, "detail": { "status": "cancelled", "failedStep": null, "canRetry": false, "jobId": "j_01a17849a4096e33f476b17d1b" } }, { "name": "재시도 요청(POST /api/sites/:id/jobs/:jobId/retry)", "ok": true, "detail": { "status": 500, "error": "job_not_retryable" } }, { "name": "되돌리기 요청(POST /api/sites/:id/rollback) — 이전 버전 없으면 거부", "ok": true, "detail": { "status": 409, "error": "no_previous", "message": "되돌릴 이전 버전이 없습니다" } } ], "evidence": { "dep": { "status": "cancelled", "failedStep": null, "canRetry": false, "jobId": "j_01a17849a4096e33f476b17d1b" }, "rt": { "status": 500, "error": "job_not_retryable" }, "rb": { "status": 409, "error": "no_previous", "message": "되돌릴 이전 버전이 없습니다" } } }, { "no": 24, "title": "운영자 승인, 감사 기록, 고객 알림의 일치", "verdict": "verified_local", "note": "승인 → 고객 알림·불변 감사 기록·운영자/고객 화면 상태 일치(주문 기준)", "blockedBy": null, "steps": [ { "name": "고객 알림 센터에 승인 알림", "ok": true, "detail": { "title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다", "category": "billing", "link": "/orders/o_01a17849a33601ca26016fa94a" } }, { "name": "감사 기록: order.approve 는 삭제 불가(immutable)", "ok": true, "detail": { "action": "order.approve", "immutable": true } }, { "name": "운영자 화면과 고객 주문 화면의 상태 일치", "ok": true, "detail": { "status": "approved", "approvedAt": "2026-10-26T12:57:34.327Z" } } ], "evidence": { "n": { "title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다", "category": "billing", "link": "/orders/o_01a17849a33601ca26016fa94a" }, "a": { "action": "order.approve", "immutable": true }, "same": { "status": "approved", "approvedAt": "2026-10-26T12:57:34.327Z" } } }, { "no": 25, "title": "HAL, 로그인, 비밀정보 관리, 인증서 기반의 재해복구 훈련", "verdict": "partial", "note": "DR 설정(RTO/RPO/주기/보존)·사본 생성·검증·외부 보관·복원 승인/기존 제어 중지 확인/완료 항목·훈련 실패/성공→준비 완료 기록까지 확인. 실제 다른 호스트 복원은 실 서버 필요", "blockedBy": null, "steps": [ { "name": "설정 전 준비 상태(미설정 항목)", "ok": true, "detail": { "ready": false, "missingConfig": [ "rto_minutes", "rpo_minutes", "snapshot_interval_minutes", "retention_count" ], "lastDrill": null, "lastSnapshot": null, "config": { "rto_minutes": null, "rpo_minutes": null, "snapshot_interval_minutes": null, "retention_count": null, "external_store": null }, "label": "정식 운영 준비 전(복구 훈련 또는 설정 필요)" } }, { "name": "RTO·RPO·사본 주기·보존 개수·외부 보관 위치 설정", "ok": true, "detail": { "id": 1, "rto_minutes": 60, "rpo_minutes": 30, "snapshot_interval_minutes": 60, "retention_count": 3, "external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026125729/dr-external", "updated_at": "2026-10-26T13:02:07.823Z", "updated_by": "op_01a178499a27f1ba88344e1145" } }, { "name": "복구 사본 생성·검증·외부 보관(vault.key 제외)", "ok": true, "detail": { "verify": "ok", "external": true, "sha": true } }, { "name": "복원 요청 → 승인 전 시작 거부 → 승인 → 기존 제어 중지 확인 없이는 거부 → 시작(명령 안내) → 확인 항목 부족 거부 → 완료", "ok": true, "detail": { "early": "restore_not_approved", "noStop": "old_control_not_confirmed", "command": true, "incomplete": "checks_incomplete", "final": "completed_with_findings" } }, { "name": "복구 훈련: 항목 하나 실패 → failed, 모두 성공·측정 RTO/RPO → 준비 완료", "ok": true, "detail": { "first": "failed", "second": "success", "ready": true, "measured": {} } } ], "evidence": { "r0": { "ready": false, "missingConfig": [ "rto_minutes", "rpo_minutes", "snapshot_interval_minutes", "retention_count" ], "lastDrill": null, "lastSnapshot": null, "config": { "rto_minutes": null, "rpo_minutes": null, "snapshot_interval_minutes": null, "retention_count": null, "external_store": null }, "label": "정식 운영 준비 전(복구 훈련 또는 설정 필요)" }, "cfg": { "id": 1, "rto_minutes": 60, "rpo_minutes": 30, "snapshot_interval_minutes": 60, "retention_count": 3, "external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026125729/dr-external", "updated_at": "2026-10-26T13:02:07.823Z", "updated_by": "op_01a178499a27f1ba88344e1145" }, "snap": { "verify": "ok", "external": true, "sha": true }, "rs": { "early": "restore_not_approved", "noStop": "old_control_not_confirmed", "command": true, "incomplete": "checks_incomplete", "final": "completed_with_findings" }, "drill": { "first": "failed", "second": "success", "ready": true, "measured": {} } } }, { "no": 26, "title": "새로 고침, 재로그인, HAL 재시작 후 진행 작업 상태 유지", "verdict": "verified_local", "note": "포털·HAL 재시작 뒤 세션·진행 작업(외부 대기)·작업 목록 유지 확인. 진행 중(running) 배포의 단계 재개는 배포 성공(흐름 6) 뒤 확인 필요", "blockedBy": null, "steps": [ { "name": "재시작 전: 포털 진행 작업(메일 활성화 외부 대기)·HAL 작업 목록", "ok": true, "detail": { "mailJob": { "id": "j_01a1784ae61b14370f923139a1", "status": "awaiting_external" }, "halJobs": 50 } }, { "name": "포털 재시작 → 재로그인 없이 세션 유지·같은 작업 id·상태 유지", "ok": true, "detail": { "sessionKept": "[redacted]", "mailJob": { "id": "j_01a1784ae61b14370f923139a1", "status": "awaiting_external" }, "same": true } }, { "name": "HAL 재시작 → 운영자 세션·작업 목록·서버 등록 유지", "ok": true, "detail": { "opsSessionKept": "[redacted]", "halJobs": 50, "sameCount": true } } ], "evidence": { "before": { "mailJob": { "id": "j_01a1784ae61b14370f923139a1", "status": "awaiting_external" }, "halJobs": 50 }, "pr": { "sessionKept": "[redacted]", "mailJob": { "id": "j_01a1784ae61b14370f923139a1", "status": "awaiting_external" }, "same": true }, "hr": { "opsSessionKept": "[redacted]", "halJobs": 50, "sameCount": true } } }, { "no": 27, "title": "마지막 사이트 수·용량·메일함 동시 요청 시 하나만 성공", "verdict": "verified_local", "note": "마지막 사이트 수 1개를 두 배포가 동시에 요청 → 하나만 예약 성공, 다른 쪽은 부족 사유(§13.3)", "blockedBy": null, "steps": [ { "name": "밥 개인 그룹: 베이직(사이트 1) 주문·승인", "ok": true, "detail": { "granted": 6 } }, { "name": "사이트 2개 생성·저장본", "ok": true, "detail": [ { "id": "s_01a1784ac5a389af03b57b32a3", "group_id": "g_01a17849a2ee454ab48b65c13d", "name": "동시 A", "purpose": "e2e", "webmaker_project_id": "00000000000000000000000000000002", "template": null, "lang": "ko", "timezone": "Asia/Seoul", "lifecycle": "draft", "deploy_mode": "managed", "primary_domain": null, "site_count_state": "none", "site_count_reservation_id": null, "server_id": null, "external_agent_id": null, "current_version": null, "current_deployment_id": null, "deploy_verify": null, "health": "unknown", "health_checked_at": null, "maintenance": 0, "search_visible": 1, "description": null, "settings_json": null, "paused_at": null, "cancel_at": null, "retention_until": null, "delete_after": null, "deleted_at": null, "created_by": "u_01a17849a2eef6c9267cb0d204", "created_at": "2026-10-26T12:58:48.611Z", "updated_at": "2026-10-26T12:58:48.619Z", "runtime": "static", "consent_version": null, "pending_cleanup_json": null, "return_due_at": null, "last_deploy_at": null, "last_external_check_json": null, "next_job_json": null, "mail_choice": null, "next_notice_at": null, "delete_hold_reason": null, "public_stopped_at": null, "prev_lifecycle": null, "settings": {}, "pendingCleanup": null, "lastExternalCheck": null, "nextJob": null }, { "id": "s_01a1784ac5a7fa23018c39de03", "group_id": "g_01a17849a2ee454ab48b65c13d", "name": "동시 B", "purpose": "e2e", "webmaker_project_id": "00000000000000000000000000000003", "template": null, "lang": "ko", "timezone": "Asia/Seoul", "lifecycle": "draft", "deploy_mode": "managed", "primary_domain": null, "site_count_state": "none", "site_count_reservation_id": null, "server_id": null, "external_agent_id": null, "current_version": null, "current_deployment_id": null, "deploy_verify": null, "health": "unknown", "health_checked_at": null, "maintenance": 0, "search_visible": 1, "description": null, "settings_json": null, "paused_at": null, "cancel_at": null, "retention_until": null, "delete_after": null, "deleted_at": null, "created_by": "u_01a17849a2eef6c9267cb0d204", "created_at": "2026-10-26T12:58:48.615Z", "updated_at": "2026-10-26T12:58:48.622Z", "runtime": "static", "consent_version": null, "pending_cleanup_json": null, "return_due_at": null, "last_deploy_at": null, "last_external_check_json": null, "next_job_json": null, "mail_choice": null, "next_notice_at": null, "delete_hold_reason": null, "public_stopped_at": null, "prev_lifecycle": null, "settings": {}, "pendingCleanup": null, "lastExternalCheck": null, "nextJob": null } ] }, { "name": "두 사이트 동시 최초 배포 요청 → 하나만 200, 다른 쪽 409 precheck_failed(sites 부족)", "ok": true, "detail": { "ok": 1, "blocked": "precheck_failed", "shortage": "block" } }, { "name": "(정리) 선행 결함으로 영원히 진행 중인 배포 취소·예약 반환", "ok": true, "detail": { "cancel": 200, "cleanup": 200, "unallocated": 0 } } ], "evidence": { "race": { "ok": 1, "blocked": "precheck_failed", "shortage": "block" } } }, { "no": 28, "title": "외부 Linux 서버 Agent 연결·배포·재배포·제거·실패 정리", "verdict": "partial", "note": "HAL·외부 Agent 경로(등록·연결·배포·재배포·실패 시 이전 버전 유지와 남은 파일·다른 사이트/허용되지 않는 종류 거부·제거)는 확인. 포털 화면 경로는 토큰 발급 결함으로 막힘", "blockedBy": null, "steps": [ { "name": "외부 배포 사이트 생성(초안) + 저장본", "ok": true, "detail": { "id": "s_01a1784b5d03e229abe92a46a1", "group_id": "g_01a17849a2c2dec27ee9f14777", "name": "E2E 외부 사이트", "purpose": "e2e", "webmaker_project_id": "00000000000000000000000000000004", "template": null, "lang": "ko", "timezone": "Asia/Seoul", "lifecycle": "draft", "deploy_mode": "external", "primary_domain": null, "site_count_state": "none", "site_count_reservation_id": null, "server_id": null, "external_agent_id": null, "current_version": null, "current_deployment_id": null, "deploy_verify": null, "health": "unknown", "health_checked_at": null, "maintenance": 0, "search_visible": 1, "description": null, "settings_json": null, "paused_at": null, "cancel_at": null, "retention_until": null, "delete_after": null, "deleted_at": null, "created_by": "u_01a17849a21df78a8debb70c18", "created_at": "2026-10-26T12:59:27.363Z", "updated_at": "2026-10-26T12:59:27.368Z", "runtime": "static", "consent_version": null, "pending_cleanup_json": null, "return_due_at": null, "last_deploy_at": null, "last_external_check_json": null, "next_job_json": null, "mail_choice": null, "next_notice_at": null, "delete_hold_reason": null, "public_stopped_at": null, "prev_lifecycle": null, "settings": {}, "pendingCleanup": null, "lastExternalCheck": null, "nextJob": null } }, { "name": "배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전", "ok": true, "detail": { "notProvided": "[redacted]", "consentVersion": "external-deploy-2026-10-0.1.1", "agreed": false } }, { "name": "동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전)", "ok": true, "detail": { "before": "consent_required", "consent": 200 } }, { "name": "외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음)", "ok": false, "detail": "500 {\"ok\":false,\"error\":\"ERR_SQLITE_ERROR\",\"message\":\"일시적인 오류가 발생했습니다. 잠시 후 다시 시도하세요\"} | portal: 2026-10-26T12:59:27.404Z [http] POST /api/sites/s_01a1784b5d03e229abe92a46a1/external 500: Error: NOT NULL constraint failed: external_agents.hal_agent_id | at Object.run (file:///home/master/work/Ar" }, { "name": "[HAL 직접] 외부 Agent 토큰 발급(포털 서비스 키)", "ok": true, "detail": { "token": "[redacted]", "expiresAt": "2026-10-26T13:09:27.408Z" } }, { "name": "[HAL 직접] 외부 Agent 등록·실행(사이트 귀속) → by-site 상태 connected", "ok": true, "detail": { "agentId": "[redacted]", "status": "connected", "siteId": "s_01a1784b5d03e229abe92a46a1" } }, { "name": "[HAL 직접] 외부 Agent 배포(site.deploy, 로컬 저장본) → releases/current 생성", "ok": true, "detail": { "status": "done", "error": null, "errorCode": null, "result": { "version": "20261026125937-v1", "leftovers": [] }, "layout": true } }, { "name": "[HAL 직접] 재배포(v2) → 이전 버전 기록", "ok": true, "detail": { "status": "done", "error": null, "errorCode": null, "result": { "version": "20261026125950-v2", "leftovers": [] } } }, { "name": "[HAL 직접] 실패 배포(표식 불일치) → 이전 버전 유지·남은 파일(leftovers) 보고", "ok": true, "detail": { "status": "failed", "error": "내부 확인 실패(404) — 이전 버전을 유지합니다", "errorCode": "internal_check_failed", "result": null } }, { "name": "[HAL 직접] 다른 사이트 작업 거부(site_mismatch) · 허용되지 않는 종류 거부", "ok": true, "detail": {} }, { "name": "[HAL 직접] 배포 제거(site.remove) → 파일·vhost 제거", "ok": true, "detail": { "layoutAfter": false } } ], "evidence": { "halTok": { "expiresAt": "2026-10-26T13:09:27.408Z" }, "d1": { "status": "done", "error": null, "errorCode": null, "result": { "version": "20261026125937-v1", "leftovers": [] }, "layout": true }, "d2": { "status": "done", "error": null, "errorCode": null, "result": { "version": "20261026125950-v2", "leftovers": [] } }, "d3": { "status": "failed", "error": "내부 확인 실패(404) — 이전 버전을 유지합니다", "errorCode": "internal_check_failed", "result": null }, "d4": {}, "d5": { "layoutAfter": false } } }, { "no": 29, "title": "HAL 장애 중 기존 서비스 유지와 신규 변경 제한 표시", "verdict": "verified_local", "note": "HAL 중단 중 포털 조회 유지·신규 배포 차단(정보 확인 지연), HAL 재기동 뒤 서버·Agent 복귀", "blockedBy": null, "steps": [ { "name": "HAL 중단 중: 기존 조회(사이트 목록·개요)는 제공, 배포 전 확인/배포는 정보 확인 지연·차단", "ok": true, "detail": { "list": 200, "overview": 200, "precheck": { "status": 200, "blocks": [ "target:HAL(중앙 관리)에 연결할 수 없어 배포 위치를 확인하지 못했습니다(정" ] }, "deploy": { "status": 409, "error": "precheck_failed", "message": "배포 전 확인에서 차단된 항목이 있습니다" } } }, { "name": "대시보드/알림에 중앙 상태 확인 지연 표시(가능하면)", "ok": true, "detail": { "status": 200, "halState": null } }, { "name": "HAL 재기동(같은 호스트의 죽은 인스턴스 임대 이어받기) → 서버·Agent 상태 복귀", "ok": true, "detail": { "takeoverNeeded": false, "bootstrap": true, "readyServers": 4 } } ], "evidence": { "during": { "list": 200, "overview": 200, "precheck": { "status": 200, "blocks": [ "target:HAL(중앙 관리)에 연결할 수 없어 배포 위치를 확인하지 못했습니다(정" ] }, "deploy": { "status": 409, "error": "precheck_failed", "message": "배포 전 확인에서 차단된 항목이 있습니다" } }, "dash": { "status": 200, "halState": null }, "back": { "takeoverNeeded": false, "bootstrap": true, "readyServers": 4 } } }, { "no": 30, "title": "모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정", "verdict": "failed", "note": "대상별 자식 작업 생성·되돌리기 3단계 확인 거부는 확인. 일부 실패 요약·건강 확인·drift 조정은 작업 엔진 교착(흐름 22 결함)으로 완료되지 않음", "blockedBy": 22, "steps": [ { "name": "agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed)", "ok": true, "detail": { "status": "queued", "errorCode": null, "overall": "none", "children": [], "timedOut": true } }, { "name": "모듈 건강 확인 작업·목표/실제 버전 차이(drift) 표시·다시 맞추기 작업", "ok": true, "detail": { "health": "timeout", "drift": 0, "reconcile": 200, "overviewComponents": [ "HAL:ok", "portal:unknown", "webmaker:unknown", "arimail:unknown", "backup:ok", "observability:ok" ] } }, { "name": "되돌리기는 3단계 확인(영향 범위·식별정보·최종 실행) 없이는 거부", "ok": true, "detail": { "step1": "confirm_impact_required", "step2": "identifiers_mismatch" } } ], "evidence": { "d": { "status": "queued", "errorCode": null, "overall": "none", "children": [], "timedOut": true }, "h": { "health": "timeout", "drift": 0, "reconcile": 200, "overviewComponents": [ "HAL:ok", "portal:unknown", "webmaker:unknown", "arimail:unknown", "backup:ok", "observability:ok" ] }, "rb": { "step1": "confirm_impact_required", "step2": "identifiers_mismatch" } } }, { "no": 31, "title": "긴급 복구의 승인 부족·만료 자격·허용 범위 밖 작업 거부와 정상 복구", "verdict": "partial", "note": "정상 상태 선언 거부·3단계 승인(순서/식별정보/문구 거부)·자격 1회 발급·일반 배포/범위 밖/잘못된 자격 거부·수행·폐기 뒤 거부·조정·종료 확인. 자격 만료 거부는 유효 시간(분 단위 설정) 때문에 로컬 실행에서 생략(단위 테스트 test/hal.test.mjs 로 확인)", "blockedBy": null, "steps": [ { "name": "HAL 정상 상태에서 선언 거부(hal_healthy)", "ok": true, "detail": "hal_healthy" }, { "name": "critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회)", "ok": true, "detail": { "id": "em_01a1784dd0ec3f08f6ddf4281a", "noCred": "approvals_insufficient", "wrongStep": "wrong_step", "badIds": "identifiers_mismatch", "badText": "final_confirm_mismatch", "approved": "approved", "approvals": 3, "credential": "[redacted]", "expiresAt": "2026-10-26T13:32:08.135Z" } }, { "name": "일반 배포(module.deploy) 거부 · 범위 밖 거부 · 잘못된 자격 거부 · 허용 행동 수행 · 확인 뒤 자격 폐기 · 폐기 뒤 거부 · 조정 · 종료", "ok": true, "detail": { "forbidden": "action_forbidden", "outOfScope": "out_of_scope", "badCred": "bad_credential", "performed": 400, "revoked": true, "afterRevoke": "credential_revoked", "reconcile": 200, "closed": "closed" } } ], "evidence": { "healthy": "hal_healthy", "sess": { "id": "em_01a1784dd0ec3f08f6ddf4281a", "noCred": "approvals_insufficient", "wrongStep": "wrong_step", "badIds": "identifiers_mismatch", "badText": "final_confirm_mismatch", "approved": "approved", "approvals": 3, "expiresAt": "2026-10-26T13:32:08.135Z" }, "perf": { "forbidden": "action_forbidden", "outOfScope": "out_of_scope", "badCred": "bad_credential", "performed": 400, "revoked": true, "afterRevoke": "credential_revoked", "reconcile": 200, "closed": "closed" } } }, { "no": 32, "title": "Source 옵션 만료 후 30일 read-only, clone·export와 삭제 완료", "verdict": "partial", "note": "활성화·export·해제→읽기 전용(삭제 예정일)까지 확인. 30일 뒤 삭제 완료는 시간 경과가 필요(단위 테스트 test/sites.test.mjs 에서 sweep 로 확인)", "blockedBy": null, "steps": [ { "name": "Source 옵션 주문(대상 사이트)·승인 → source 권리", "ok": true, "detail": { "granted": 7 } }, { "name": "Source 옵션 활성화(권리 안에서) → 저장소·브랜치·변경 이력·clone 정보", "ok": true, "detail": { "activate": 200, "status": "active", "provider": "gitea", "changes": 1, "clone": true } }, { "name": "export(저장본 내려받기)", "ok": true, "detail": { "status": 200, "type": "application/gzip" } }, { "name": "옵션 해제 → 30일 읽기 전용(만료일·삭제 예정일·재활성화 안내) · push 차단", "ok": true, "detail": { "status": "readonly", "deleteAfter": "2026-11-25T13:00:40.412Z", "readonlyUntil": "2026-11-25T13:00:40.412Z" } }, { "name": "삭제 정리 작업(sweep)은 삭제 예정일 전이면 지우지 않음", "ok": true, "detail": { "sweep": 200, "statusAfter": "readonly" } } ], "evidence": { "act": { "activate": 200, "status": "active", "provider": "gitea", "changes": 1, "clone": true }, "ex": { "status": 200, "type": "application/gzip" }, "un": { "status": "readonly", "deleteAfter": "2026-11-25T13:00:40.412Z", "readonlyUntil": "2026-11-25T13:00:40.412Z" }, "sweep": { "sweep": 200, "statusAfter": "readonly" } } }, { "no": 33, "title": "메일 서비스의 사이트 대표→독립 전환 후 항목 유지", "verdict": "partial", "note": "전환 점검표 8항목과 미충족 시 차단 확인. 전후 실제 송수신 확인·DNS 유지 검증은 실 DNS/AriMail 필요(단위 테스트 test/mail.test.mjs 에서 가짜로 전환 완료·되돌리기 확인)", "blockedBy": null, "steps": [ { "name": "사이트 대표→독립 전환 점검표(주소·메일함·별칭·전달·비밀번호·DNS·옵션·작업)", "ok": true, "detail": { "canConvert": false, "items": [ "addresses:ok", "data:ok", "aliases:ok", "forwards:ok", "passwords:ok", "dns:x", "entitlement:x", "jobs:x" ] } }, { "name": "DNS 미확인 상태에서 전환 요청은 차단", "ok": true, "detail": { "status": 409, "error": "conversion_blocked" } } ], "evidence": { "ck": { "canConvert": false, "items": [ "addresses:ok", "data:ok", "aliases:ok", "forwards:ok", "passwords:ok", "dns:x", "entitlement:x", "jobs:x" ] }, "tr": { "status": 409, "error": "conversion_blocked" } } }, { "no": 34, "title": "일시 중지·해지 예약·보존·삭제 단계의 표시 일치", "verdict": "partial", "note": "해지된 메일 서비스의 읽기 전용 보존·삭제 예정 표시와 사이트 생명주기 화면의 단계별 항목 확인. 사이트의 해지 예약→보존→삭제 전이는 운영 중 사이트 필요(단위 테스트 test/lifecycle.test.mjs 로 확인)", "blockedBy": 6, "steps": [ { "name": "§23.3 한 화면 보기: 공개/관리자 접근·사이트 수·청구·도메인/메일/백업 동작·내보내기·되돌릴 수 없는 시점·보존·다음 알림", "ok": true, "detail": { "state": "제작 중", "siteCount": false, "publicAccess": "중지됨", "retention": { "site_days": 30, "mail_days": 30, "source_days": 30, "backup_days": 7, "delete_grace_days": 1, "notice_before_days": 3, "readonly_retention_days": 30, "delete_after_days": 30, "pause_keeps_site_count": true, "cancel_effective": "next_billing_date", "source": "상품정책 베이직 (retention 정책) v1" }, "pauseImpactKeys": [ "publicAccess", "adminAccess", "mail", "backups", "usageAndCost", "resume", "siteCount" ], "cancelImpactKeys": [ "target", "linked", "immediateStops", "remainingData", "retentionUntil", "deleteAfter", "retentionSource", "export", "unpaid", "refund", "pointOfNoReturn", "mailOptions", "siteCount" ], "steps14": 8 } }, { "name": "일시 중지(확인 필수) 또는 현재 상태에서 거부 사유", "ok": true, "detail": { "noConfirm": "confirmation_required", "status": 409, "error": "lifecycle_transition" } }, { "name": "메일 서비스 생명주기 화면(해지된 독립 메일: 송수신 중지·보존 종료·삭제 예정·백업 미제공)", "ok": true, "detail": { "status": 200, "sendReceive": "송수신 가능", "retentionUntil": false, "deleteAfter": false, "backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)" } } ], "evidence": { "v": { "state": "제작 중", "siteCount": false, "publicAccess": "중지됨", "retention": { "site_days": 30, "mail_days": 30, "source_days": 30, "backup_days": 7, "delete_grace_days": 1, "notice_before_days": 3, "readonly_retention_days": 30, "delete_after_days": 30, "pause_keeps_site_count": true, "cancel_effective": "next_billing_date", "source": "상품정책 베이직 (retention 정책) v1" }, "pauseImpactKeys": [ "publicAccess", "adminAccess", "mail", "backups", "usageAndCost", "resume", "siteCount" ], "cancelImpactKeys": [ "target", "linked", "immediateStops", "remainingData", "retentionUntil", "deleteAfter", "retentionSource", "export", "unpaid", "refund", "pointOfNoReturn", "mailOptions", "siteCount" ], "steps14": 8 }, "mailLc": { "status": 200, "sendReceive": "송수신 가능", "retentionUntil": false, "deleteAfter": false, "backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)" } } } ], "bugs": [ { "title": "Agent 가 포털 저장본(bundleUrl, 절대 URL)을 내려받지 못함(HAL 주소를 앞에 덧붙임)", "file": "agent/client.mjs (download: fetchImpl(base + path)) / agent/jobs/site.mjs (fetchBundle)", "route": "HAL site.deploy → Agent fetch 단계", "expected": "bundleUrl 이 절대 URL 이면 그대로 내려받는다(서명은 HAL 전용이므로 포털 번들 URL 에는 붙이지 않음)", "actual": "Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni", "repro": "포털 배포 → HAL site.deploy → Agent: \"Failed to parse URL from http://http:///internal/bundles/…\"", "flows": [ 6, 8, 9, 13, 14, 15, 23 ] }, { "title": "포털 배포 작업이 실패한 HAL 작업을 실패로 보지 않고 5초마다 새 HAL 작업을 무한 재제출(배포가 영원히 \"진행 중\")", "file": "portal/sites/service.mjs (halJobFor: prev HAL 작업이 failed/cancelled 면 fresh 가 아니어도 새 작업을 만든다 → awaitHal 이 실패를 보지 못함)", "route": "POST /api/sites/:id/deploy", "expected": "HAL 작업이 failed 면 포털 작업도 failed(이전 버전 유지·재시도 버튼), 새 HAL 작업은 사용자 재시도(fresh) 때만", "actual": "portal job queued attempt 9 (배포 서버 작업 진행 중); HAL 에 같은 사이트의 site.deploy 실패 작업이 attempt 수만큼 쌓임", "repro": "Agent 가 실패하는 배포를 요청하고 /api/sites/:id/jobs/:id 를 지켜보면 attempt 가 5초마다 증가", "flows": [ 6, 23, 13, 20 ] }, { "title": "관리형 배포 종단 흐름 실패(포털→HAL→Agent 저장본 전달)", "file": "agent/client.mjs (download: fetchImpl(base + path)) / agent/jobs/site.mjs (fetchBundle: client.download(p.bundleUrl))", "route": "site.deploy → Agent fetch 단계", "expected": "절대 URL(bundleUrl=http:///internal/bundles/)은 HAL base 를 붙이지 않고 그대로 내려받아야 한다", "actual": "시간 초과 — 포털 작업 queued(attempt 8, 배포 서버 작업 진행 중) · HAL site.deploy 작업 8개(최근: [\"failed\",\"Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni\"])", "repro": "사이트 생성→저장본→POST /api/sites/:id/deploy→HAL site.deploy→Agent site.deploy fetch 단계에서 \"HAL 내려받기 실패\" 또는 잘못된 URL 오류", "flows": [ 6, 8, 9, 13, 14, 15, 23 ] }, { "title": "취소된 배포 작업의 재시도 요청이 500(서버 오류)으로 응답(작업 상태 오류는 4xx 여야 함)", "file": "portal/sites/api.mjs (jobs/:jobId/:action → lib/jobs JobError 를 HttpError 로 바꾸지 않음)", "route": "POST /api/sites/:id/jobs/:jobId/retry", "expected": "409 job_not_retryable + 안내", "actual": "500 job_not_retryable", "repro": "취소된 작업에 retry", "flows": [ 23 ] }, { "title": "외부 Agent 토큰 발급이 DB 제약(external_agents.hal_agent_id NOT NULL)으로 500", "file": "portal/sites/service.mjs (createExternalAgent) / portal/sites/model.mjs (external_agents.hal_agent_id)", "route": "POST /api/sites/:id/external", "expected": "HAL 토큰 발급 응답의 agentId 가 null(등록 전)이어도 토큰·설치 명령을 돌려주고, Agent 등록 뒤 by-site 조회로 hal_agent_id 를 채운다", "actual": "2026-10-26T12:59:27.404Z [http] POST /api/sites/s_01a1784b5d03e229abe92a46a1/external 500: Error: NOT NULL constraint failed: external_agents.hal_agent_id | at Object.run (file:///home/master/work/Ariweb-suite/lib/db.mjs:52:41) | at Object.createExternalAgent (file:///home/master/work/Ariweb", "repro": "외부 배포 사이트 → 동의 → POST /api/sites/:id/external → 500 \"NOT NULL constraint failed: external_agents.hal_agent_id\"", "flows": [ 16, 28 ] }, { "title": "HAL 작업 엔진이 작업을 한 번에 하나씩만 실행(tick 이 execute 를 await)해 느린 작업이 큐 전체를 막고, module.deploy 부모가 자식 완료를 기다리면 자식이 실행될 수 없어 교착", "file": "lib/jobs.mjs (createJobEngine.start/tick) / hal/jobs.mjs (fanout 이 자식 완료를 폴링)", "route": "POST /ops/api/modules/:name/deploy", "expected": "엔진은 running 수가 concurrency 미만이면 실행을 분리(detach)해 여러 작업을 동시에 진행해야 한다(부모가 자식을 기다려도 자식이 실행됨)", "actual": { "status": "queued", "steps": [ "precheck:pending", "fanout:pending", "collect:pending" ], "error": null, "children": [], "agentJobs": [], "timedOut": true }, "repro": "(1) Agent 가 수십 초 걸리는 domain.probe 가 queued 로 쌓이면 다른 모든 HAL 작업이 기다린다 (2) 큐가 비어도 모듈 배포는 부모 collect:running + 자식 queued 로 영원히 멈춘다(test/hal.test.mjs 는 pump() 로 tick 을 동시에 돌려 통과)", "flows": [ 22, 30 ] } ], "needsInfrastructure": [ "도메인 소유 확인(TXT)·DNS 반영·HTTPS 발급·외부 접속 확인은 실제 DNS 와 공인 서버가 필요하다(흐름 7·8·11·14·15)", "메일 DNS(MX·SPF·DKIM·DMARC) 반영과 외부 송신·수신·회신 왕복은 실 도메인·실 AriMail(SMTP 587/수신함)·외부 검증 주소가 필요하다(흐름 10 활성화 완료·11·33 완료)", "재해복구의 실제 복원(hal-restore.sh 로 다른 호스트에 HAL 재기동)·실제 운영자 로그인·HTTPS·고객 서비스 외부 확인은 실 서버가 필요하다(흐름 25 기록·훈련 절차만 로컬 확인)" ] }