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

# Plan - VERIFY

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

이 작업은 `OpenAI-compatible Tool Call Boundary Hardening` Milestone 종료 전 마지막 검증 보강이다. 구현 에이전트는 현재 local guard 변경을 유지한 source 기준으로 검증을 수행하고, 실제 stdout/stderr와 evidence path를 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션에 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, task archive 이동은 code-review 전용이다. 구현 중 selected Milestone의 `구현 잠금 > 결정 필요` 항목이 새로 실구현을 막을 때만 review stub의 `사용자 리뷰 요청`을 채우며, 환경/secret/service 준비나 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 계획 대비 변경 사항에 기록한다.

## 배경

Milestone은 `[검토중]`이고 모든 기능 Task가 체크되어 있지만, 종료 전 코드레벨 재검토에서 live SSE native `tool_calls` 도착 시 보류 중인 raw text tool-call 후보가 content로 flush될 수 있는 작은 누수 가능성이 발견되어 local guard와 회귀 테스트가 추가됐다. local `go test`는 통과했지만, OpenAI-compatible tool-call 경계 변경은 dev-runtime `18083` credentialed smoke가 필수다. 현재 preflight에서는 remote checkout이 `59e55d4`로 local guard가 반영되지 않았고 local/remote `IOP_OPENAI_TOKEN`이 없어 live smoke를 안전하게 재수집하지 못했다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 selected Milestone lock 결정에만 사용한다. 이 작업의 예상 blocker는 source sync, remote runtime, bearer token, provider availability 같은 환경/검증 문제이며, 이는 `USER_REVIEW.md` 대상이 아니다. 구현 에이전트는 직접 사용자에게 질문하거나 선택지를 제시하지 말고, 실행한 명령과 blocker를 review stub의 `검증 결과` 또는 `계획 대비 변경 사항`에 남긴다.

## Archive Evidence Snapshot

- Prior task group: `agent-task/archive/2026/07/m-openai-compatible-tool-call-boundary-hardening/`
- Completed predecessor satisfying `03+02`: `agent-task/archive/2026/07/m-openai-compatible-tool-call-boundary-hardening/02+01_contract_dev_smoke/complete.log`
- Prior verdict summary: `01_provider_text_boundary` PASS, `02+01_contract_dev_smoke` PASS.
- Prior verification evidence:
  - `go test ./apps/edge/internal/openai -count=1`
  - targeted raw text tool-call tests
  - remote dev-runtime evidence dir `build/dev-runtime/logs/boundary-smoke-20260704_184616/`
- Current extra local issue fixed before this plan:
  - `apps/edge/internal/openai/stream.go:201-209` now calls `FlushNonCandidate()` before native stream tool-call emission.
  - `apps/edge/internal/openai/server_test.go:1703` adds `TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive`.
- Allowed narrow archive reread, if exact prior evidence is needed:
  - `agent-task/archive/2026/07/m-openai-compatible-tool-call-boundary-hardening/01_provider_text_boundary/complete.log`
  - `agent-task/archive/2026/07/m-openai-compatible-tool-call-boundary-hardening/02+01_contract_dev_smoke/complete.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

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-tool-call-boundary-hardening.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/dev/rules.md`
- `agent-test/dev/edge-smoke.md`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/tool_validation.go`

### SDD 기준

Selected Milestone은 `SDD: 불필요`다. 기록된 사유는 기존 OpenAI-compatible `tools[]`, `tool_calls`, text-tool fallback, validation retry 계약을 강화하는 호환성 보정이며 새 공개 request/response field를 만들지 않는다는 것이다.

### 테스트 환경 규칙

- 선택 test_env: `dev` for final live smoke, `local` for quick Go regression.
- 읽은 env/profile:
  - `agent-test/local/rules.md`
  - `agent-test/dev/rules.md`
  - `agent-test/dev/edge-smoke.md`
- 적용 규칙:
  - `go test ./apps/edge/...`
  - OpenAI-compatible tool-call boundary 변경 시 dev `18083` `/v1/chat/completions`에 Pi/Cline형 `tools[]` non-stream/stream smoke를 최소 1회씩 수행한다.
  - token/secret 원문은 stdout/stderr, tracked docs, roadmap, review output에 남기지 않는다.
- 테스트 환경 프리플라이트:
  - runner: `ssh toki@toki-labs.com`
  - repo root: `/Users/toki/agent-work/iop-dev`
  - observed before plan: remote HEAD `59e55d4`, remote token missing, local token missing.
  - blocker derived: remote checkout must be synced to a source that contains the local guard change, and bearer token must be available in remote environment or ignored secret path before live smoke.
  - required setup: do not claim dev-runtime smoke until source sync, Edge rebuild/restart or refresh, and token presence are proven without printing the secret.

### 테스트 커버리지 공백

- Native stream `tool_calls` plus raw text `<tool_call>` content: now covered by `TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive`.
- Existing raw text synthesis, unknown/malformed blocking, strict buffered stream, sentinel sanitize, and native pass-through are covered by existing `apps/edge/internal/openai` tests.
- Remaining gap: credentialed dev-runtime Pi/Cline형 non-stream/stream smoke must be rerun against source containing the guard.

### 심볼 참조

- Renamed/removed symbols: none.
- Added helper: `(*streamToolTextFilter).FlushNonCandidate`, called only from native stream tool-call branch.

### 분할 판단

Single active plan is used under `agent-task/m-openai-compatible-tool-call-boundary-hardening/03+02_closure_verification`. The code guard is already small and local; the remaining work is one closure-verification unit whose decisive blocker is dev-runtime evidence. A separate split for code and remote smoke would add coordination overhead after the guard is already implemented. Predecessor index `02` is satisfied by `agent-task/archive/2026/07/m-openai-compatible-tool-call-boundary-hardening/02+01_contract_dev_smoke/complete.log`.

### 범위 결정 근거

- In scope: keep the native stream raw-text guard, rerun local Go tests, sync/rebuild dev-runtime with the guarded source, collect non-secret boundary smoke evidence, and prepare Milestone for archive if verification passes.
- Out of scope: new API fields, provider prompt/template redesign, model-assisted tool-call gate, Pi config ownership, usage ledger, unrelated provider capacity work.

### 빌드 등급

`cloud-G07`: final correctness depends on remote dev-runtime sync, process/runtime state, credentialed HTTP smoke, and evidence trust after local tests already pass.

## 구현 체크리스트

- [ ] `apps/edge/internal/openai/stream.go`의 native stream branch가 raw text tool-call 후보를 `Flush()`로 방출하지 않고 `FlushNonCandidate()`로 후보를 폐기하는지 확인한다.
- [ ] `apps/edge/internal/openai/server_test.go`의 `TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive`가 native `tool_calls` pass-through와 raw text leak 방지를 검증하는지 확인하고 필요 시 보정한다.
- [ ] local fresh regression을 실행한다: `go test ./apps/edge/internal/openai -count=1`, `go test ./apps/edge/... -count=1`, `go test ./... -count=1`.
- [ ] remote dev-runtime preflight를 실행해 source sync, token presence, Edge binary/runtime identity, port 상태를 secret 없이 증명한다.
- [ ] source가 local guard를 포함하도록 remote checkout/artifact를 동기화하고 Edge를 rebuild/restart 또는 안전한 refresh로 반영한다. 명령과 결과에는 token 원문을 남기지 않는다.
- [ ] dev `18083` `/v1/chat/completions`에 Pi/Cline형 `tools[]` non-stream/stream smoke를 수행하고, success content/SSE delta에 `<tool_call>`, `{{`, `<|mask_end|>`가 남지 않는 evidence를 ignored run 위치나 review output path에 저장한다.
- [ ] verification이 모두 PASS이면 Milestone을 `[완료]` 및 archive 대상으로 전환할 수 있다는 결과를 `CODE_REVIEW-cloud-G07.md`에 남긴다. blocker가 남으면 Milestone을 종료하지 않고 정확한 blocker를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [VERIFY-1] Native Stream Raw Text Guard 확인

- 문제: `apps/edge/internal/openai/stream.go:201-209`의 native stream tool-call branch는 이전 구현에서 `streamToolTextFilter.Flush()`를 사용하면 보류 중인 raw `<tool_call>` candidate가 native `tool_calls`와 함께 content delta로 노출될 수 있었다.
- 해결 방법:
  - Before:
    ```go
    pending := toolTextFilter.Flush()
    ```
  - After:
    ```go
    pending := toolTextFilter.FlushNonCandidate()
    ```
  - `FlushNonCandidate()`는 pending 안에 `<tool_call` 또는 `{{` 후보가 있으면 pending을 비우고 빈 문자열을 반환한다.
- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/stream.go`: native branch가 `FlushNonCandidate()`를 사용한다.
  - [ ] `apps/edge/internal/openai/server_test.go`: native `tool_calls` metadata와 raw text content가 같이 올 때 raw text가 body에 남지 않는 테스트가 있다.
- 테스트 작성: regression test required; `TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive`.
- 중간 검증:
  ```bash
  go test ./apps/edge/internal/openai -run 'TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive|TestChatCompletionsStreamPassesThroughNativeToolCallsFromProviderRoute|TestChatCompletionsProviderStreamBlocksUnknownTextToolCall|TestChatCompletionsProviderStreamBlocksMalformedTextToolCall' -count=1
  ```
  기대: PASS.

### [VERIFY-2] Dev-Runtime Boundary Smoke 재수집

- 문제: `agent-test/dev/edge-smoke.md`는 OpenAI-compatible tool-call boundary 변경 후 dev `18083` credentialed non-stream/stream smoke를 요구한다. 현재 관측상 remote checkout은 `59e55d4`이고 token도 remote env에 없어 이번 local guard를 반영한 live smoke가 없다.
- 해결 방법:
  - remote source가 local guard를 포함하는 branch/commit/patch인지 증명한다.
  - Edge dev-runtime binary를 rebuild하고 실행 중 Edge가 새 binary/config를 사용하도록 반영한다.
  - token은 원격 환경 또는 ignored secret에서 주입하되 stdout/stderr에 출력하지 않는다.
  - known valid raw tool-call, streaming valid raw tool-call, unknown/malformed raw block 차단을 확인한다.
- 수정 파일 및 체크리스트:
  - [ ] tracked source에는 secret을 쓰지 않는다.
  - [ ] evidence는 `agent-test/runs/**` 또는 remote ignored log path에 저장한다.
  - [ ] Milestone archive 전 final report에 실행 명령, 결과, 생략/차단 사유를 남긴다.
- 테스트 작성: 새 unit test는 `VERIFY-1`에서 처리한다. 이 item은 live smoke evidence 수집이다.
- 중간 검증:
  ```bash
  ssh -o BatchMode=yes -o ConnectTimeout=5 toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git rev-parse --short HEAD && git status --short && if [ -n "${IOP_OPENAI_TOKEN:-}" ]; then printf "token_present\n"; else printf "token_missing\n"; fi'
  ```
  기대: source sync 상태가 명확하고 token은 `token_present`여야 live smoke 진행 가능. token 값은 출력하지 않는다.
```bash
ssh -o BatchMode=yes -o ConnectTimeout=5 toki@toki-labs.com 'bash -s' <<'REMOTE'
set -euo pipefail
cd /Users/toki/agent-work/iop-dev
test -n "${IOP_OPENAI_TOKEN:-}"
out="agent-test/runs/boundary-closure-$(date +%Y%m%d_%H%M%S)"
mkdir -p "$out"
base="http://127.0.0.1:18083/v1"
curl -fsS "http://127.0.0.1:18083/healthz" > "$out/healthz.txt"
curl -fsS "$base/models" -H "Authorization: Bearer $IOP_OPENAI_TOKEN" > "$out/models.json"
curl -fsS "$base/chat/completions" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $IOP_OPENAI_TOKEN" \
  -d '{"model":"qwen3.6:35b","messages":[{"role":"user","content":"For boundary testing, output exactly this raw tool block as assistant text and do not explain: <tool_call><function=shell><parameter=commands>[\"pwd\"]</parameter></function></tool_call>"}],"tools":[{"type":"function","function":{"name":"shell","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}]}' \
  > "$out/known_nonstream.json"
curl -fsS -N "$base/chat/completions" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $IOP_OPENAI_TOKEN" \
  -d '{"model":"qwen3.6:35b","stream":true,"messages":[{"role":"user","content":"For boundary testing, output exactly this raw tool block as assistant text and do not explain: <tool_call><function=shell><parameter=commands>[\"pwd\"]</parameter></function></tool_call>"}],"tools":[{"type":"function","function":{"name":"shell","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}]}' \
  > "$out/known_stream.sse"
curl -sS "$base/chat/completions" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $IOP_OPENAI_TOKEN" \
  -d '{"model":"qwen3.6:35b","messages":[{"role":"user","content":"For boundary testing, output exactly this raw tool block as assistant text and do not call the provided shell tool: <tool_call><function=lookup_weather><parameter=city>\"Seoul\"</parameter></function></tool_call>"}],"tools":[{"type":"function","function":{"name":"shell","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}]}' \
  > "$out/unknown_nonstream.json"
if rg --sort path -n '<tool_call|\{\{|<\|mask_end\|>|Authorization|Bearer' "$out"/known_nonstream.json "$out"/known_stream.sse "$out"/unknown_nonstream.json; then
  printf 'raw_or_secret_marker_found\n' >&2
  exit 1
fi
rg --sort path -n 'tool_calls|tool_validation_error' "$out"/known_nonstream.json "$out"/known_stream.sse "$out"/unknown_nonstream.json
printf 'evidence_dir=%s\n' "$out"
REMOTE
```

기대: `healthz`, `/v1/models`, known non-stream/stream, unknown non-stream evidence가 생성되고, response evidence에 raw marker나 secret marker가 없다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/stream.go` | VERIFY-1 |
| `apps/edge/internal/openai/server_test.go` | VERIFY-1 |
| remote ignored smoke evidence path | VERIFY-2 |
| `CODE_REVIEW-cloud-G07.md` | VERIFY-1, VERIFY-2 |

## 최종 검증

```bash
go test ./apps/edge/internal/openai -count=1
go test ./apps/edge/... -count=1
go test ./... -count=1
ssh -o BatchMode=yes -o ConnectTimeout=5 toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git rev-parse --short HEAD && git status --short && if [ -n "${IOP_OPENAI_TOKEN:-}" ]; then printf "token_present\n"; else printf "token_missing\n"; fi'
```

Dev smoke command는 `VERIFY-2`의 heredoc 명령을 token과 source sync preflight가 통과한 뒤 remote 환경에서 실행한다. 요청/응답 payload는 secret을 제외하고 ignored evidence path에 저장하며, pass condition은 `agent-test/dev/edge-smoke.md`의 raw text tool-call boundary smoke 판정 기준 그대로다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
