# task-2721 (★DRAFT v3 — dispatch 미승인) — P0-b: user-level systemd path driver → result.json 자동 pickup wake 결선

## 상태
**초안 v3/preflight 설계 단계.** 회장 승인 전까지 dispatch·설치·`systemctl enable/start`·crontab 등록·runner 활성화 **전부 금지**. 본 task md 는 보고용 초안이며 그 자체로 실행 권한이 아니다.
- v2(회장 8지시): scan 한정 · 완성 계약 · preflight 절단방지 6조건 · 4축 명시 분리.
- **v3(회장 결정)**: `scripts/finish-task.sh` **expected_files 에서 제거** — P0-b 본체 = OS-level driver 결선에 집중. writer atomic 화는 **별도 후속(P0-b+1 / writer-contract)** 분리. driver 가 final task-*.result.json 만 안전 소비하는 구조로 절단 파일 방어(소비 측 단독 안전).
- ★ **expected_files = 정확히 6개**(finish-task.sh 미포함).

## ★ 4축 단계 명시 분리 (회장 v2 지시 5 — 절대 혼용 금지)
- **implemented**: systemd path/unit 파일 + sh + py runner + test **파일 생성**(설치·enable 0).
- **wired candidate**: unit **설치 + enable**(`systemctl --user`). 회장 별도 승인.
- **wired pilot**: activation flag 생성 + pilot 1회 flag. 회장 별도 승인.
- **active = false (5단계 성공 전)**: §10 5단계 증거 5/5 + pilot 1회 성공 후에만 `callback_pickup: ACTIVE`.
- 각 단계는 독립 회장 승인. "driver 가 뜨는 것"(unit 기동) ≠ active.

## 회장 1차 결정 (2026-06-01)
1. driver = **A안 user-level systemd path unit** 채택(system-level/root 제외).
2. 기본 상태 = **DISABLED**. activation flag 부재 시 **전면 no-op**.
3. cokacdir agent cron 은 실행 경로 아님. 실행 = OS-level driver 가 sh/py runner 직접 실행.
4. dev bot = result.json 만 작성. callback schedule 생성 / ANU key 접근 / ANU wake 직접수행 금지.
5. ANU key sealed. unit/path/crontab/argv/prompt literal 노출 금지.
6. active 판정 = 5단계 증거 전부 확인 후에만.

## 목표 (production path 결선)
`dev result.json 작성` → `OS systemd .path event 감지` → `.service oneshot 가 sh entrypoint 실행` → `py runner pickup_once(result.json)` → `ANU key wake cron 발사(sealed)` → `독립 ANU 세션 spawn`. 사람 개입 0 전이.

## expected_files (신규 — 정확히 6개, 그 외 수정 금지)
1. `deploy/systemd/anu-pickup.path` (user unit 후보 — **설치 아님, 파일만**)
2. `deploy/systemd/anu-pickup.service` (oneshot, ExecStart=sh entrypoint)
3. `scripts/anu_pickup_entrypoint.sh` (sh entrypoint)
4. `dispatch/anu_pickup_driver.py` (py runner — driver 레벨 flock + scan + pickup_once 호출 + evidence)
5. `tests/regression/test_anu_pickup_driver_2721.py`
6. `docs/p0b_driver_runbook_260601.md` (enable/disable/rollback/pilot 절차서)
(evidence: memory/events/task-2721.done · memory/reports/task-2721.md)
★ `dispatch/anu_result_pickup_runner.py`(P0-a, main) **무수정** — import/호출만.
★ **`scripts/finish-task.sh` 수정 금지(회장 v3 결정)** — result.json writer atomic화는 P0-b 범위 밖. 별도 후속 task(P0-b+1 / writer-contract)로 분리. driver 가 **final task-*.result.json 만 안전 소비**하는 구조에 집중.

## 1. systemd path/unit 파일 후보 (설치 금지 — 파일 작성만)
- `anu-pickup.path`:
  ```ini
  [Unit]
  Description=ANU result.json pickup watcher (P0-b, DISABLED by default)
  [Path]
  PathModified=%h/workspace/memory/events
  Unit=anu-pickup.service
  [Install]
  WantedBy=default.target
  ```
- `anu-pickup.service`:
  ```ini
  [Unit]
  Description=ANU pickup oneshot runner
  [Service]
  Type=oneshot
  ExecStart=%h/workspace/scripts/anu_pickup_entrypoint.sh
  # ★ ANU key 등 secret 환경변수 unit 에 literal 0. runner 가 .env.keys 로드.
  ```
- ★ `WantedBy` 존재해도 **enable 은 회장 승인 후 수동**. 본 task 는 파일만 생성.

## 2. sh entrypoint 역할 (`scripts/anu_pickup_entrypoint.sh`)
- **activation flag 확인**: `memory/state/p0b_driver_enabled` 부재 → 즉시 exit 0(no-op).
- driver 레벨 **flock**(`flock -n /run/user/$UID/anu-pickup.driver.lock` 또는 `$XDG_RUNTIME_DIR`) 으로 single-flight. 실패 시 exit 0(중첩 방지).
- `python3 -m dispatch.anu_pickup_driver` 호출. ANU key/secret **인자 전달 0**.
- 비상 차단: flag 파일 1줄 `disabled` 시 no-op.

## 3. py runner 역할 (`dispatch/anu_pickup_driver.py`)
- ★ **scan 한정(회장 v2 지시 2)**: `glob('memory/events/task-*.result.json')` **final 파일만** 스캔. path unit 이 디렉터리 변경(다른 marker/jsonl/md/tmp)을 트리거해도 **driver 는 task-*.result.json 외 전부 no-op**. tmp/부분파일(`*.result.json.tmp-*`)도 제외.
- terminal `*.pickup.done`/`*.pickup.acked`/dedupe 4-tuple 존재분 skip(§4 6조건).
- 각 final result.json → P0-a `pickup_once(path)`(lock-free) 호출.
- quarantine 판정(§6) → wake 미발사.
- 성공 시 `anu_runner_pickup_and_fire`(ANU key sealed, self-key REFUSE) 로 wake cron 발사.
- evidence jsonl(§7) append. **activation flag 재확인**(이중 게이트). secret 은 `utils.env_loader` 런타임 로드만.

## 3-b. result.json 완성 계약 (writer-side requirement / future follow-up — 회장 v3)
- ★ **이상적 writer 계약**(향후): dev bot/finish-task 가 `*.result.json` 을 직접 쓰지 않고 tmp(`*.result.json.tmp-$PID`)→`flush()`+`os.fsync()`→`os.replace(tmp, final)` 원자 교체.
- ★ **회장 v3 결정**: 이 writer atomic 화는 **P0-b 본체 범위 밖** — **`scripts/finish-task.sh` 이번 task 에서 직접 수정 금지**. 별도 후속 task(**P0-b+1 / writer-contract task**)로 분리 검토.
- ★ **P0-b driver 측 책임(이번 task)**: writer 보강 전이라도 driver 가 **소비 측에서 안전**하도록 한다 —
  - **final `task-*.result.json` 만** scan(§3), tmp/partial(`*.result.json.tmp-*`) scan 제외.
  - size 0 / JSON parse fail / schema fail / owner proof fail / self·foreign 의심 → **wake 금지 + quarantine**(§4 6조건).
  - 즉 writer 가 비원자라도 **절단 파일은 quarantine 으로 걸러** wake 미발사. driver 단독으로 안전성 확보.

## 4. activation flag 경로
- `memory/state/p0b_driver_enabled`. **부재 = DISABLED = 전면 no-op**(sh + py 양쪽 확인).
- 내용 `enabled` 일 때만 동작. 그 외 문자열/부재 → no-op. default 미생성.

## 5. disable / rollback 방법
- disable: flag 파일 삭제 → 즉시 no-op(코드 잔존). + `systemctl --user disable --now anu-pickup.path`.
- rollback: unit 파일 미설치 상태가 기본. 설치했다면 `systemctl --user disable`, `~/.config/systemd/user/anu-pickup.*` 제거, `systemctl --user daemon-reload`.
- 이중 안전: pickup_once `FIRE_NOT_ACTIVATED`/activation_flag 로 실 cron 발사 추가 게이팅.
- runbook(`docs/p0b_driver_runbook_260601.md`)에 enable/disable/rollback/pilot 절차 명문화.

## 6. quarantine 정책
- 대상 → `memory/events/quarantine/` 이동 + 로그, **wake 금지**:
  - schema 검증 실패(필수 필드/타입)
  - foreign/self-key 의심 (owner_key ≠ ANU)
  - owner proof 실패 (`verify_collector_authoritative` = SELF_COLLECTOR_QUARANTINE / PENDING_OWNER_PROOF)
- quarantine 사유 evidence jsonl 기록.

## 7. evidence marker / jsonl 스키마
- `memory/events/p0b_driver_runs.jsonl` (per-invocation append):
  ```json
  {"ts":"ISO8601+09:00","result_path":"...","verdict":"WAKE_BUILT|PICKUP_SKIP|QUARANTINE|FIRE_FAILED",
   "wake_built":true,"owner_key_class":"ANU|FOREIGN|SELF","quarantined":false,
   "fire_cron_id":"...|null","retry":0,"error":null,"driver":"systemd-path","activation":"enabled|disabled"}
  ```
- terminal: `*.pickup.done`(atomic) → `*.pickup.acked`. dedupe `callback_4tuple_index.jsonl`.
- ★ ANU key literal 절대 미기록(owner_key_class 로만 분류).

## 8. pilot 1회 제한 조건
- 승인 후 **low-risk 단일 task 1회만** 활성. pilot flag `memory/state/p0b_pilot_oneshot`(1회 소비 후 self-clear).
- pilot 동안 evidence 5단계(§active) 전부 수집. 2건째 이상 자동 처리 금지(pilot=1).
- pilot 실패 시 자동 disable + 회장 보고.

## 9. forbidden actions (회장 verbatim)
- dispatch/설치/`systemctl enable·start`/crontab 등록/runner 실활성화 **금지**(승인 전).
- cokacdir agent cron 을 실행 경로로 사용 금지(relay-only).
- dev bot 의 callback schedule 생성 / ANU key 접근 / ANU wake 직접수행 금지.
- ANU key literal 을 unit/path/sh/py/argv/prompt/jsonl 어디에도 노출 금지.
- pickup_once 에 lock 재도입 금지(lock-free 유지). P0-a main 파일 무수정.
- ★ **`scripts/finish-task.sh` 수정 금지(회장 v3)** — writer atomic 화는 P0-b 범위 밖, 별도 후속.
- system-level/root 설치 금지. default ENABLED 금지.
- active/wired 과장 금지(5단계 증거 전 active 주장 0).

## 10. active 승격 조건 (5단계 증거 전부 — 회장 v2 지시 6 유지)
1) dev bot 은 result.json 만 작성(callback schedule/ANU key/wake 직접수행 0 입증)
2) OS-level event driver 가 감지(systemd .path → journald 로그)
3) pickup_once 호출(evidence jsonl WAKE_BUILT)
4) ANU key wake cron 발사(sealed, self-key 0 — cron-history owner key 교차검증)
5) 독립 ANU 세션 spawn(별도 session_id, RECEIVED_INBOUND)
→ 5/5 확인 + pilot 1회 성공 후에만 `callback_pickup: ACTIVE` 승격.
- ★ 그 전 단계 표기(혼용 금지): 파일생성=implemented / unit 설치·enable=wired candidate / activation flag+pilot=wired pilot / **5단계 성공 전 active=false**.
- "driver(unit)가 뜨는 것" ≠ active. unit 기동/이벤트 감지만으로 active 주장 절대 금지.

## preflight 체크리스트 (dispatch 전 ANU 확인)
- [ ] user-level systemd 사용 가능 환경 확인(`systemctl --user`, lingering 필요 여부)
- [ ] `%h/workspace` 경로 = canonical `/home/jay/workspace` 일치
- [ ] PathModified vs PathExistsGlob 적합성(result.json 다중 생성 대응)
- [ ] flock 경로(`$XDG_RUNTIME_DIR`) 존재성
- [ ] P0-a `pickup_once`/`anu_runner_pickup_and_fire` import 시그니처 확인
- [ ] default DISABLED no-op 경로 단위테스트 가능성

### ★ 설치 전 선행 조건 (preflight WARN 보완 — 2026-06-01)
- ★ **로컬 `/home/jay/workspace` 를 origin/main(P0-a 29175f80 포함) 동기 선행 필수.** 현재 로컬은 다른 task 브랜치(task-2716, origin/main-4)라 P0-a 파일 미체크아웃 → service import FAIL 위험. 설치 단계에서 workspace 가 P0-a 포함 main 동기 상태여야 driver 가 `dispatch.anu_result_pickup_runner` import 가능. (dispatch 차단 사유 아님 = 환경 상태, 설치 전 sync 로 해소.)
- service unit: `WorkingDirectory=/home/jay/workspace` + `Environment=PYTHONPATH=/home/jay/workspace` + jay user 실행(.env.keys 600 owner jay 접근). unit 에 key literal 0.

### ★ preflight 절단/부분 write 방지 — wake 전 6조건 (회장 v2 지시 4, 전부 PASS 시에만 pickup)
1. **size > 0** (size 0 → quarantine)
2. **JSON parse PASS** (parse 실패 → quarantine)
3. **schema PASS** (필수 필드/타입 — 실패 → quarantine)
4. **owner proof PASS** (`verify_collector_authoritative` — 실패 → quarantine)
5. **self/foreign key 아님** (owner_key = ANU 가 아니면 → quarantine)
6. **dedupe/done/acked 미처리** (이미 처리된 4-tuple → **no-op**, quarantine 아님)
→ 1~5 중 하나라도 실패 = quarantine + wake 금지. 6 = no-op. 6조건 전부 통과 시에만 `pickup_once` 진입.
- tmp/부분파일(`*.result.json.tmp-*`)은 애초에 scan 제외(§3) → driver 가 절단 파일을 보지 않음(완성 계약 §3-b 의 1차 방어).

## allowed_resources (승인 시 적용 — 현재는 설계만)
```yaml
allowed_resources:
  paths:
    - "deploy/systemd/anu-pickup.path"
    - "deploy/systemd/anu-pickup.service"
    - "scripts/anu_pickup_entrypoint.sh"
    - "dispatch/anu_pickup_driver.py"
    - "tests/regression/test_anu_pickup_driver_2721.py"
    - "docs/p0b_driver_runbook_260601.md"
    - "memory/events/task-2721.done"
    - "memory/reports/task-2721.md"
  forbidden_paths:
    - "scripts/finish-task.sh"   # ★ 회장 v3: P0-b 범위 밖. writer atomic 화는 P0-b+1 분리.
    - "dispatch/anu_result_pickup_runner.py"
    - "dispatch/anu_owned_callback_enforcement.py"
    - ".github/**"
  commands:
    - "pytest"
    - "python3 -m pytest"
    - "python3 -m py_compile"
  merge_policy: "none"
  ttl_hours: 48
```

## ANU 후속 (승인 시)
로컬 commit only → ANU 독립 재검증(default no-op/quarantine/sealed key/lock-free/evidence schema) → PR 별도 승인 → merge 별도 승인. 설치·enable·pilot 활성화는 각각 별도 회장 승인.
