Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/external-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ jobs:

- name: Run external smoke sequentially
env:
CLI_SMOKE_SERVICES: daiso daiso_jp gs25 seveneleven emart24 oliveyoung megabox lottecinema cgv opinet dtryx
MCP_SMOKE_SERVICES: daiso daiso_jp gs25 seveneleven emart24 opinet
CLI_SMOKE_SERVICES: daiso daiso_jp cu gs25 seveneleven emart24 oliveyoung megabox lottecinema cgv opinet dtryx
MCP_SMOKE_SERVICES: daiso daiso_jp cu gs25 seveneleven emart24 oliveyoung megabox lottecinema cgv opinet dtryx
SMOKE_ATTEMPT_TIMEOUT_SECONDS: 90
run: |
set +e
Expand Down
59 changes: 59 additions & 0 deletions docs/macos-ops-runbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 현재 사용자 Mac 운영 보조 도구

`install-macos.py`의 전용 표준 계정 설치는 그대로 유지한다. `macos-ops.py`는 현재 GUI 사용자에게 숨김 도우미와 로컬 감시기를 추가한다. 현재 사용자 소유의 중계 런타임, 브라우저, Node 의존성, 터널과 로그인 세션은 먼저 준비해야 한다. 이 도구는 런타임 소유권을 검사하므로 기존 전용 계정 설치기의 root 소유 런타임을 현재 사용자 모드에서 직접 지정할 수 없다. 전용 계정 설치는 별도 운영 방식으로 유지한다. [전용 계정 Olive Young 설치기](../scripts/relay/install-macos.py)와 [디트릭스 중계 준비 절차](dtryx-relay.md)를 먼저 따른다. 이 보조 도구는 전체 중계 설치기가 아니다. 새 Mac에서 인증 자료는 운영자가 직접 제공하며 이 도구는 인증·환경·호출 원장 파일을 만들거나 교체하지 않는다.

## 계획과 설치

저장소 루트에서 실행한다. 기본 상태 디렉터리는 현재 사용자의 `~/Library/Application Support/DaisoRelay`, 기본 런타임은 그 아래 `runtime`이다. 기존 중계를 재설치하거나 Node 프로세스를 실행하지 않는다.

```sh
/usr/bin/python3 scripts/relay/macos-ops.py plan
/usr/bin/python3 scripts/relay/macos-ops.py install
/usr/bin/python3 scripts/relay/macos-ops.py install --notify-local --apply
```

앞의 두 명령은 파일을 쓰지 않는다. `--state-base /absolute/private/state`와 `--runtime /absolute/runtime`으로 위치를 바꿀 수 있다. 심볼릭 링크 경로, 다른 소유자, 쓰기 가능한 공개 파일은 거부한다. 상태 디렉터리와 환경 파일은 각각 0700, 0600이어야 한다. `/usr/bin/python3`와 `/usr/bin/swiftc`를 사용한다. 관리자·root 실행은 거부한다.

설치는 소스 해시별 `ops-helpers/<hash>`에 실행 파일과 소스를 보관하고 두 새 plist만 교체한다. 기존 plist는 개인 권한으로 백업하며 관리 표식이 없는 동일 이름 파일은 거부한다. 기존 helper 버전은 실행 중인 프로세스를 보호하도록 남긴다. 재사용하는 숨김 실행 파일에 실행 권한이 없으면 설치를 거부한다. 실행 중인 작업을 자동으로 내리거나 새 작업을 자동으로 올리지 않는다.

## GUI 세션 활성화

실제 경로를 설치 계획에 맞춘다. 이전 수동 숨김 작업 `page.aka.daiso-oliveyoung-hide`를 사용 중이면 검토 후 그 작업만 먼저 `launchctl bootout`한다. 중계 브라우저는 종료하지 않는다. 새 숨김 작업은 `page.aka.daiso-ops-hide`, 감시기는 `page.aka.daiso-ops-monitor`이다.

```sh
launchctl bootout "gui/$(id -u)/page.aka.daiso-oliveyoung-hide"
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/page.aka.daiso-ops-hide.plist"
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/page.aka.daiso-ops-monitor.plist"
```

업데이트할 때는 새 plist 설치 후 이 두 보조 작업만 `bootout`하고 다시 `bootstrap`한다. 중계·터널 작업의 종료나 재시작은 포함하지 않는다. 숨김 도우미는 비공개 `state/browser-owner.json`의 PID·부모 PID·생성 시각과 실제 프로세스 UID·실행 파일이 모두 맞을 때만 숨긴다. 이미 숨겨진 앱도 소유권을 검증한다. 사용자 Chrome이나 소유권이 불확실한 프로세스에는 요청하지 않는다.

## 감시와 상태 확인

```sh
/usr/bin/python3 scripts/relay/macos-ops.py monitor
/usr/bin/python3 scripts/relay/macos-ops.py status
```

감시 LaunchAgent는 로그인 시와 매 60초 실행한다. `relay.env`의 `OY_RELAY_TOKEN`, `dtryx.env`의 `DTRYX_RELAY_TOKEN`·`CONVENIENCE_RELAY_TOKEN`을 개인 파일에서 읽는다. 셸을 실행하거나 비밀값을 출력하지 않는다. 선택 환경 변수의 `KEY=` 빈 값은 허용하고 인증 토큰은 별도로 비어 있지 않은지 확인한다. 환경 파일의 포트·URL 값으로 감시 대상을 바꾸지 않는다. `GS25_AUTH_SESSION_FILE`을 지정한 경우 편의점 health의 인증 상태·로그인 필요 여부가 없거나 유효하지 않으면 `authentication-unobservable` 실패로 기록한다. `ready`는 access·refresh 만료 시각이 모두 미래인 경우에만 관측 가능한 정상 인증으로 판단한다. GS 세션을 구성하지 않은 중계는 인증 메타데이터 없이도 지원한다. 세션 파일 경로는 출력하지 않는다.

- Olive Young: `127.0.0.1:4319/health`
- 디트릭스: `127.0.0.1:4320/v1/dtryx/health`
- CGV: `127.0.0.1:4320/v1/cgv/health`
- 편의점: `127.0.0.1:4320/v1/convenience/health`
- 선택 터널 준비 상태: `127.0.0.1:20242/ready`

터널 포트는 설치·monitor 명령에 `--tunnel-port 20242`처럼 지정한다. 1024~65535만 허용하며 0은 터널 검사 생략이다. 터널의 loopback metrics 준비 엔드포인트는 별도로 구성해야 한다. `/ready` 성공은 로컬 터널 준비 상태만 의미하며 외부 공개 경로의 성공을 보장하지 않는다.

프록시를 사용하지 않고 리디렉션을 거부하며, 3초 연결 제한, 연결 후 헤더·본문 전체 4초 제한과 64KiB 본문 제한을 적용한다. `ops-status.json`은 0600으로 원자적으로 교체한다. 검사별 실패 여부, 연속 실패 횟수, 첫 실패 시각, GS 인증 상태·로그인 필요 여부·만료 시각·재시도 시각, 허용한 로그 오류·쿼터 숫자만 보존한다. 자체 쿼터의 `blockedBy`는 `null`, `minute`, `daily`만 허용하며 `minute`·`daily`이면 `reason: own-quota` 실패로 표시한다. 이는 원본 429와 별개인 중계 자체 한도이며 재고 0으로 바꾸지 않는다. status는 마지막 검사에서 180초가 넘거나 미래 시각이면 `stale: true`를 표시한다. 감시가 실행되지 않으면 파일은 자동 갱신되지 않는다.

`authentication.loginRequired: true`이면 [GS25 정상 로그인 복구 절차](gs25-auth-recovery-runbook.md)에 따라 사용자가 로그인한다. `retry_wait`는 정상 갱신 실패 후 대기이며 로그인 필요와 구분한다. `--notify-local`을 설치 또는 monitor에 지정하면 로그인 필요가 확인될 때 고정 문구의 macOS 로컬 알림을 요청한다. 로그인 필요로 전환한 첫 검사와 이후 6시간 간격으로만 요청하며, 실패한 요청도 같은 간격으로 제한한다. 상태의 `notification.state`는 `accepted`(osascript 요청 수락), `failed`, `disabled`, `not-required`를 구분한다. OS 알림 권한·집중 모드에 따라 보이지 않을 수 있으며 수락은 실제 열람을 의미하지 않는다. 외부 네트워크 메시지나 직접 대화 푸시는 지원하지 않는다. 지속 상태는 파일과 status에서 확인한다. 기존 launchd가 죽은 중계를 복구하고 Olive Young watchdog이 실패한 lifecycle을 복구한다. 감시기는 프로세스 종료·브라우저 재시작·쿠키 초기화·원본 재고 요청을 하지 않는다.

## 해제

```sh
launchctl bootout "gui/$(id -u)/page.aka.daiso-ops-hide"
launchctl bootout "gui/$(id -u)/page.aka.daiso-ops-monitor"
```

그 뒤 두 `page.aka.daiso-ops-*.plist`와 필요 없는 해당 plist 백업, `ops-helpers`, `ops-status.json`만 검토하여 삭제한다. 중계·터널 plist와 `relay.env`, `dtryx.env`, `state`, 인증·쿼터 원장은 보존한다. 기존 수동 숨김 도우미로 되돌리려면 보존한 기존 plist를 다시 bootstrap한다. 재부팅 후 실동작은 별도로 확인한다.
8 changes: 8 additions & 0 deletions docs/relay-observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,11 @@ CGV는 `CGV_UPSTREAM_UNAVAILABLE`, API HTTP 503, 원본 HTTP 403이 모두 확
무료 릴레이 요청의 기본 제한 시간은 대기와 재시도를 합쳐 60초다. 직접 조회는 기존 15초이며 명시적인 timeout은 우선한다. 릴레이 큐는 최대 30초 대기를 허용한다. 브라우저 교체가 22~23초 걸릴 때 Worker가 먼저 연결을 끊던 문제를 줄이기 위한 조정이며, 새 브라우저나 탭을 병렬로 만들지 않는다. `queue/canceled`는 클라이언트 취소, `queue/expired`는 큐 대기 초과로 구분한다. 따라서 예전 expired 수치와 단순 비교하면 안 된다.

Zyte의 키·계정 상태와 관계없이 유료 경로는 계속 차단한다. 정상 조회는 직접 연결과 무료 릴레이로 처리하며, 릴레이 중단·전역 한도 소진·원본 차단까지 무조건 성공으로 바꾸지는 않는다.

## Mac 상시 감시와 인증 준비

현재 사용자 환경의 설치·업데이트·해제는 [Mac 운영 보조 도구](macos-ops-runbook.md)를 따른다. 분당 로컬 감시는 실제 상품·재고 API를 추가 호출하지 않고 인증된 loopback health와 선택한 터널 ready를 확인한다. `ops-status.json`이 3분 넘게 오래되면 정상으로 판단하지 않는다. 이 파일의 정상은 공개 API 실재고 성공을 대신하지 않으므로 정기점검은 기존 캐시 우회 검사와 실제 CLI/MCP 검증도 유지한다.

GS25 세션은 재고 요청이 없어도 중계 시작 시와 분당 준비한다. access 만료 5분 전에는 정상 갱신 경로를 이용하며, 실제 요청과 자동 준비가 중복 갱신과 실패 대기를 공유한다. 편의점 인증 health의 `authentication`은 토큰 대신 상태·만료 시각·로그인 필요 여부·재시도 시각만 반환한다. `login_required`는 사용자 입력이 필요하고 `retry_wait`는 일시적인 갱신 실패 대기다. 전자는 확인 즉시 사용자에게 알리며 과거 로그인 대기 기록만으로 현재 복구를 생략하지 않는다.

공개 MCP 외부 스모크는 CLI와 같은 12개 서비스를 검사한다. 소매 재고는 상품 존재뿐 아니라 실제 매장 확인과 알려진 수량을 요구하며 정상 수량0과 미확인을 구분한다. 일본 다이소 상세는 다른 상품도 원본 진입을 직렬화하여 첫429 이후 공유 대기를 우회하지 못한다. 외부429와 자체 분당·일일 한도는 계속 구분한다. 메가박스의 기존 상세 캐시·중복 요청 공유·취소 기한·지연 경고 기준은 유지하며 원본 지연을 해결했다고 간주하지 않는다.
48 changes: 48 additions & 0 deletions docs/superpowers/plans/2026-10-25-operational-reliability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 운영 안정성 구현 계획

> 구현 스킬: superpowers:subagent-driven-development. 각 작업은 회귀 검증과 독립 검토를 거친다.

**Goal:** 실제 사용자 호출 검사와 Mac 운영 재현성을 개선하고 인증·원본 제한을 지속적으로 관리한다.
**Architecture:** 공개 도구 계약과 기존 중계를 유지하며 운영 모듈과 설치 보조 기능만 확장한다.
**Tech Stack:** TypeScript, Vitest, Python 표준 라이브러리, AppKit Swift, launchd.

## 1. 공개 MCP 서비스 검사 확대

- 수정: scripts/ops/mcp-smoke.ts, scripts/ops/mcp-smoke-validation.ts, .github/workflows/external-smoke.yml.
- 검사: tests/scripts/mcp-smoke*.test.ts, cinema-smoke.test.ts.
- [x] 빠진 서비스의 실제 공개 호출과 상품·매장 재고 증거를 거절/수용하는 회귀를 먼저 작성한다.
- [x] `npx vitest run tests/scripts`로 실패 재현 후 최소 구현한다.
- [x] 모든 서비스가 워크플로에 포함되고 도구10개·카탈로그·기존 상품명 조회가 유지되는지 확인한다.

## 2. GS 인증 상시 준비·안전한 상태

- 수정: scripts/relay/gs25-session.ts, dtryx-start.ts.
- 검사: tests/relay/gs25-session.test.ts 및 신규 상태·주기 준비 회귀.
- [x] stock fetch 없이 prepare/status를 호출하고 만료·정상갱신·일시실패 backoff·중복갱신을 검증한다.
- [x] 실패 전용 테스트를 실행한 후 transport에 준비·상태 메서드를 추가한다.
- [x] 분당 준비와 health 노출을 연결하며 shutdown 뒤 추가 갱신을 막는다.

## 3. Mac 사용자 범위 운영 도구

- 생성: scripts/relay/macos-ops.py, scripts/relay/hide-oliveyoung.swift.
- 검사: tests/scripts/macos-ops.test.ts 및 실제 macOS 빌드·소유검증.
- [x] plan/status/apply/watch 설정의 임의 URL·인증 출력·기존파일 덮어쓰기를 막는 테스트를 작성한다.
- [x] 전용 숨김 작업과 loopback 상태 스냅샷 감시를 등록한다. 감시는 인증된 로컬 health·GS 인증 상태·설정된 터널 ready만 읽는다.
- [x] 실패를 재고0이나 정상으로 바꾸지 않고 private 상태 파일에 기록한다. 인증·원장은 변경하지 않는다.

## 4. 원본 진입 제한

- 수정: scripts/relay/daiso-jp.ts.
- 검사: tests/relay/daiso-jp.test.ts.
- [x] 다른 상품 동시 요청에서 첫429 후 원본 추가 호출이 없어야 하는 회귀를 작성한다.
- [x] 기존 HTTP 계층의 동일 요청 공유·취소를 유지하며 원본 호출만 직렬화한다.
- [x] deadline·취소·성공 후 제한 해제·Retry-After와 자체quota 구분을 검증한다.

## 5. 통합·배포

- [ ] `npm run check`, `npm run test:coverage`, `npm run build` 통과.
- [ ] 기능 요구 검토 후 코드 품질 검토, 지적 수정 및 재검증.
- [ ] 논리 단위 커밋과 PR, GitHub 검사 성공 후 병합·배포.
- [ ] Mac 유휴·정상 종료·원본 백업·SHA 대조 후 변경 파일 반영.
- [ ] 인증된 fresh OY/GS와 실제 MCP/CLI, 감시 상태·숨김·전체 routes 신선도 확인.
- [ ] PROJECT.md에 결과와 실제 남은 문제를 기록한다.
10 changes: 10 additions & 0 deletions docs/superpowers/specs/2026-10-25-operational-reliability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 운영 검사와 Mac 중계 재현성

사용자가 승인한 네 가지 개선을 기존 운영 구조 안에서 수행한다. 공개 MCP 도구는 정확히 10개이며 구형 호출과 JSON/HTML API 호환성을 유지한다.

1. 실제 공개 MCP 호출의 서비스 범위를 CLI와 일치시키고, 재고는 상품 존재만이 아니라 실제 매장 확인 여부를 검증한다. 기존 카탈로그와 상품명 재고 시나리오는 유지한다. 선택적 데이터 누락과 외부 실패를 숨기지 않는다.
2. GS 인증은 원본 재고 요청 없이 주기적으로 준비·정상 갱신하고 안전한 만료 시각·로그인 필요 상태를 인증된 health에 노출한다. 다른 중계와 터널은 loopback health 기반 감시로 장애 지속을 기록하고 프로세스가 죽은 경우 기존 launchd 복구를 활용한다. 불확실 소유 프로세스나 사용 중 프로세스를 추정 종료하지 않는다. 외부 알림은 이 대화의 사용자 개입 요청을 통해 처리하며 새로운 외부 메시지 경로는 만들지 않는다.
3. 이 Mac의 운영 설정을 재현할 수 있는 사용자 범위 관리 도구를 추가한다. 기존 전용 계정 설치기를 보존하며 자동 숨김 Swift 소스, 계획 출력, 상태 확인, 명시적 적용과 해제 방법을 제공한다. 기존 인증·환경·원장·터널 설정은 덮어쓰지 않는다. 새 Mac의 로그인 세션과 인증은 필요하다.
4. 기존 동일 요청 합치기·TTL 캐시를 유지한다. 일본 상세의 서로 다른 상품 동시 요청이 제한을 증폭하지 않도록 원본 진입을 직렬화하고 첫429 이후 대기를 모든 요청이 공유한다. 이미 대기 중인 요청도 원본 재시도 전에 제한을 확인한다. 메가박스 지연은 실제 기한 내 취소와 정확한 경고를 유지하며 검사 임계치를 완화하지 않는다.

각 기능은 실패 회귀 재현 후 구현하고 4지표100% 커버리지·필수검사·독립리뷰를 통과한다. GitHub 병합과 배포 뒤 필요한 Mac 파일만 정상 종료 확인 후 교체하고 실제 캐시 우회·MCP·CLI 응답으로 검증한다. 원본 제한의 영구 해소나 재부팅 실검증을 수행하지 않고 주장하지 않는다.
Loading
Loading