<!-- task=m-openai-compatible-tool-call-boundary-hardening/03+02_closure_verification plan=3 tag=REVIEW_REVIEW_REVIEW_VERIFY -->

# Plan - REVIEW_REVIEW_REVIEW_VERIFY

## 이 파일을 읽는 구현 에이전트에게

이 작업은 직전 리뷰의 Required 하나만 닫는 후속 루프다. 사용자에게 직접 질문하거나 선택지를 제시하지 않는다. secret/env 준비 공백, 원격 runtime 접근 실패, smoke evidence 공백은 `USER_REVIEW.md` 대상이 아니며 `검증 결과`와 `계획 대비 변경 사항`에 blocker로 기록한다.

## 배경

직전 루프는 guarded source 기준 remote dev-runtime `18083` non-stream/stream smoke와 marker 검사를 수집했지만, 코드 리뷰에서 `FlushNonCandidate()`가 부분 tool-call 후보 suffix를 native `tool_calls` 완료 시 flush할 수 있는 누수 가능성이 발견됐다. 기존 테스트는 완성된 `<tool_call>` block만 덮어서 이 경계를 놓쳤다.

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-openai-compatible-tool-call-boundary-hardening/03+02_closure_verification/plan_cloud_G07_2.log`
- Current archived review: `agent-task/m-openai-compatible-tool-call-boundary-hardening/03+02_closure_verification/code_review_cloud_G07_2.log`
- Verdict: FAIL
- Required summary:
  - `apps/edge/internal/openai/stream.go:333` `FlushNonCandidate()`가 `<tool_ca` 또는 `{{run_`처럼 chunk 경계 때문에 pending에 남은 부분 후보를 native `tool_calls` complete 경로에서 `Flush()`로 내보낼 수 있다.
  - 부분 후보 leak은 OpenAI-compatible 계약의 raw tool-call/sentinel SSE 노출 금지와 Milestone `stream-flush-guard` completion 조건을 아직 완전히 닫지 못한다.
- Affected files and evidence:
  - `apps/edge/internal/openai/stream.go:314` `Append()`는 `streamToolTextSafeFlushLen()`으로 부분 후보 suffix를 pending에 남긴다.
  - `apps/edge/internal/openai/stream.go:333` `FlushNonCandidate()`는 full candidate만 검사하고 pending 전체를 flush한다.
  - Existing targeted test rerun PASS: `go test ./apps/edge/internal/openai -run 'TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive|TestChatCompletionsStreamPassesThroughNativeToolCallsFromProviderRoute|TestChatCompletionsProviderStreamBlocksUnknownTextToolCall|TestChatCompletionsProviderStreamBlocksMalformedTextToolCall' -count=1`.
  - Remote evidence `agent-test/runs/boundary-closure-20260705_041039` exists and a review rerun with escaped marker coverage found no `<tool_call`, `\\u003ctool_call`, `{{`, `<|mask_end|>`, `Authorization`, or `Bearer` markers and found `tool_calls|tool_validation_error` positive evidence. This evidence becomes stale after the required source fix and must be recollected against the final binary.
- Required verification still missing:
  - partial candidate suffix regression test for native `tool_calls` complete.
  - guarded final source rebuild/restart on dev-runtime `18083`.
  - dev `18083` non-stream/stream smoke and marker check against the final source/binary.
- Allowed narrow archive reread, if exact prior context is needed:
  - `agent-task/m-openai-compatible-tool-call-boundary-hardening/03+02_closure_verification/plan_cloud_G07_2.log`
  - `agent-task/m-openai-compatible-tool-call-boundary-hardening/03+02_closure_verification/code_review_cloud_G07_2.log`

## Roadmap Targets

- Milestone: `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-tool-call-boundary-hardening.md`
- Task ids:
  - `stream-flush-guard`: streaming 경로에서 tool-call 후보를 보류한 뒤 terminal event에서 구조화/차단한다.
  - `native-preserve`: native OpenAI-compatible `tool_calls` pass-through와 schema-normalization 동작을 유지한다.
  - `client-smoke`: Pi/Cline형 긴 agent prompt와 `tools[]` 요청을 직접 Edge에 호출해 raw text leak이 없는 근거를 남긴다.
- Completion mode: check-on-pass

## 분석 결과

- Selected Milestone은 `SDD: 불필요`이며 구현 잠금은 `해제`다.
- 이번 follow-up은 새 public field나 계약을 만들지 않고, streaming filter의 native `tool_calls` complete flush 경계를 보수적으로 막는다.
- `streamToolTextSafeFlushLen()`은 부분 XML/mustache hint를 pending에 남기는 기존 보호 장치다. `FlushNonCandidate()`도 이 보호 기준을 사용해 safe prefix만 반환하고 candidate-like suffix는 폐기해야 한다.
- source를 수정하면 직전 remote smoke binary identity가 final source와 달라진다. PASS 후보가 되려면 final source 기준 remote rebuild/restart/smoke evidence를 다시 남긴다.
- token 원문, Authorization header 원문, private endpoint secret은 stdout/stderr, tracked file, roadmap, review output에 남기지 않는다.

## 범위 결정 근거

- In scope: `FlushNonCandidate()` 부분 후보 처리, native `tool_calls` stream 회귀 테스트, local Go regression, final source 기준 remote dev-runtime rebuild/restart/smoke/marker evidence.
- Out of scope: provider prompt/template 재설계, text tool-call parser 전체 리팩터링, capacity smoke 확장, git commit/push, roadmap 직접 갱신.

## 빌드 등급

`cloud-G07`: 코드 수정 자체는 좁지만 PASS 판정은 최종 source와 원격 dev-runtime binary identity, credentialed HTTP smoke, evidence trust에 달려 있다.

## 구현 체크리스트

- [ ] `FlushNonCandidate()`가 full candidate뿐 아니라 부분 XML/mustache 후보 suffix도 native `tool_calls` 경로에서 flush하지 않도록 수정한다. safe non-candidate prefix가 있으면 보존하고 candidate-like suffix는 폐기한다.
- [ ] native `tool_calls` complete 직전 pending에 부분 `<tool_ca` 또는 `{{run_` suffix가 남은 streaming 회귀 테스트를 추가한다. 테스트는 safe prefix 보존, native `tool_calls` pass-through, raw/escaped/partial marker 부재를 함께 검증한다.
- [ ] local targeted regression으로 `go test ./apps/edge/internal/openai -run 'TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive|TestChatCompletionsStreamPassesThroughNativeToolCallsFromProviderRoute|TestChatCompletionsProviderStreamBlocksUnknownTextToolCall|TestChatCompletionsProviderStreamBlocksMalformedTextToolCall' -count=1`을 실행한다.
- [ ] 최종 local regression으로 `go test ./apps/edge/internal/openai -count=1`, `go test ./apps/edge/... -count=1`, `go test ./... -count=1`을 실행하거나, 실행하지 못한 명확한 사유를 기록한다.
- [ ] final source를 remote runner `/Users/toki/agent-work/iop-dev`에 좁게 반영하고, guarded final source 기준으로 `build/dev-runtime/bin/edge`를 rebuild한 뒤 running Edge가 해당 binary를 사용하도록 재시작하거나 동등한 process identity evidence를 남긴다.
- [ ] dev `18083` `/v1/chat/completions`에 Pi/Cline형 `tools[]` non-stream/stream smoke를 수행하고, ignored evidence path를 남긴다. `IOP_OPENAI_TOKEN`이 smoke shell에 없으면 live smoke를 실행하지 말고 blocker로 기록한다.
- [ ] stored evidence에서 `<tool_call`, `\\u003ctool_call`, `{{`, `<|mask_end|>`, `Authorization`, `Bearer` marker가 없는지 deterministic grep/rg로 확인하고, `tool_calls` 또는 `tool_validation_error` 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_REVIEW_VERIFY-1] Partial Candidate Native Flush Guard

- 문제: native `tool_calls` complete가 도착하면 `FlushNonCandidate()`가 pending 전체를 flush하는데, pending은 chunk 경계 보호 때문에 부분 tool-call 후보 suffix를 포함할 수 있다.
- 해결 방법:
  - `FlushNonCandidate()`에서 full candidate가 있으면 기존처럼 pending을 비우고 반환하지 않는다.
  - full candidate가 없더라도 `streamToolTextSafeFlushLen()`이 pending 전체보다 짧으면 safe prefix만 반환하고 나머지 부분 후보 suffix는 폐기한다.
  - safe prefix trimming 때문에 공백만 남는 경우도 pending을 비워 부분 후보가 뒤늦게 flush되지 않게 한다.
  - 회귀 테스트는 XML 부분 후보와 mustache 부분 후보 중 최소 XML partial은 필수로, 가능하면 table-driven으로 둘 다 검증한다.
- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/stream.go`: partial candidate suffix flush 방지.
  - [ ] `apps/edge/internal/openai/server_test.go`: native `tool_calls` + partial raw candidate regression.
- 테스트 작성: 필수. 새 테스트는 부분 후보가 JSON escaping 형태(`\\u003c...`)나 plain partial marker로도 body에 남지 않음을 확인한다.
- 중간 검증:
  ```bash
  go test ./apps/edge/internal/openai -run 'TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive|TestChatCompletionsStreamPassesThroughNativeToolCallsFromProviderRoute|TestChatCompletionsProviderStreamBlocksUnknownTextToolCall|TestChatCompletionsProviderStreamBlocksMalformedTextToolCall' -count=1
  ```
  기대: PASS.

### [REVIEW_REVIEW_REVIEW_VERIFY-2] Final Source Dev Runtime Evidence

- 문제: source 수정 후에는 직전 remote guarded smoke evidence가 최종 binary 기준이 아니다.
- 해결 방법:
  - remote runner source가 final local source와 일치하는지 확인하고, 필요한 경우 `apps/edge/internal/openai/stream.go`와 `server_test.go` 변경만 좁게 반영한다.
  - secret 원문 출력 없이 token presence를 확인한다.
  - final source로 Edge binary를 rebuild하고 dev-runtime Edge를 재시작한 뒤 binary path/mtime/process lstart/health/models evidence를 남긴다.
  - non-stream/stream/unknown smoke를 ignored run dir에 저장하고 raw/secret marker 부재와 positive evidence를 확인한다.
- 수정 파일 및 체크리스트:
  - [ ] tracked source에는 secret을 쓰지 않는다.
  - [ ] 원격 source, runtime identity, restart 여부를 `검증 결과`에 기록한다.
  - [ ] smoke evidence path와 raw marker 검사 결과를 `검증 결과`에 기록한다.
- 중간 검증:
  ```bash
  ssh -o BatchMode=yes -o ConnectTimeout=5 toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && printf "head=" && git rev-parse --short HEAD && printf "status_start\n" && git status --short && printf "symbols_start\n" && grep -n "FlushNonCandidate\|TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive" apps/edge/internal/openai/stream.go apps/edge/internal/openai/server_test.go && printf "token_check\n" && if [ -n "${IOP_OPENAI_TOKEN:-}" ]; then printf "token_present\n"; else printf "token_missing\n"; fi'
  ```
  기대: final guard/test symbols are present, token is `token_present`, and token value is not printed. `token_missing`이면 live smoke를 실행하지 않고 blocker로 기록한다.

  ```bash
  ssh -o BatchMode=yes -o ConnectTimeout=5 toki@toki-labs.com 'zsh -lc "cd /Users/toki/agent-work/iop-dev && go test ./apps/edge/internal/openai -count=1 && go build -trimpath -o build/dev-runtime/bin/edge ./apps/edge/cmd/edge && build/dev-runtime/bin/edge --config build/dev-runtime/edge.yaml config check && curl -fsS http://127.0.0.1:18083/healthz"'
  ```
  기대: final source test/build/config/health pass.

  ```bash
  # Restart/smoke command may reuse the secret-safe shape from the previous loop.
  # It must print only token_present/token_missing, process identity, evidence path,
  # marker_check=none, and positive evidence summary. It must not print token values.
  ```
  기대: running Edge uses final `build/dev-runtime/bin/edge`, evidence path is printed, raw/secret marker check returns no matches including escaped `\\u003ctool_call`, and `tool_calls` or `tool_validation_error` evidence exists.

## 최종 검증

```bash
go test ./apps/edge/internal/openai -count=1
go test ./apps/edge/... -count=1
go test ./... -count=1
```

위 Go 검증과 REVIEW_REVIEW_REVIEW_VERIFY-2 dev smoke가 모두 통과해야 PASS 후보가 된다. source sync, runtime restart, or token presence가 막히면 `사용자 리뷰 요청`을 채우지 말고 `검증 결과`에 blocker로 기록한다.
