iop/agent-task/glm_coding_plan/PLAN-local-G06.md
toki 9b2fc2ae47 docs(agent-task): GLM Coding Plan 구현 계획을 추가한다
일반 API와 구독 endpoint가 동일 모델명으로 섞이지 않도록 profile, credential route, Edge 모델 매핑 및 검증 경계를 구현 전에 확정한다.
2026-08-02 12:08:13 +09:00

29 KiB

Plan - GLM General API와 Coding Plan Profile 분리

For the Implementing Agent

CODE_REVIEW-cloud-G06.md의 implementation-owned section 작성은 구현의 필수 마지막 단계다. 모든 검증을 실행하고 실제 구현 메모와 stdout/stderr를 채운 뒤 active PLAN/CODE_REVIEW 파일을 그대로 두고 review 준비 완료를 보고한다. 최종 판정, archive, complete.log 작성은 code-review skill만 수행한다. 차단되면 implementation-owned evidence field에 정확한 blocker, 시도한 명령/출력, 재개 조건만 기록한다. 사용자에게 질문하거나 user-input tool/control-plane stop file을 만들거나 다음 상태를 분류하지 않는다.

Background

현재 built-in glm profile은 일반 종량제 endpoint만 가리키고 공식 function calling capability도 선언하지 않는다. Z.AI Coding Plan은 같은 Bearer/OpenAI Chat 계약을 사용하지만 별도 /api/coding/paas/v4 endpoint와 구독 quota를 사용하므로 endpoint override만으로 합치면 과금 경로와 managed credential route가 모호해진다. glmglm_coding을 독립 profile로 유지하고 Edge 외부 model id도 provider별로 분리해 일반 API/Coding Plan 사이 자동 fallback이나 pool 혼합이 발생하지 않게 한다. Coding Plan은 기본 활성화하지 않으며 공식 지원 도구·개인 사용·proxy 제한을 운영 예시와 smoke에 명시한다.

Analysis

Files Read

  • AGENTS.md
  • .gitignore
  • agent-ops/rules/project/rules.md
  • agent-ops/rules/common/rules-roadmap.md
  • agent-ops/rules/common/rules-agent-spec.md
  • agent-ops/rules/project/domain/platform-common/rules.md
  • agent-ops/rules/project/domain/control-plane/rules.md
  • agent-ops/rules/project/domain/node/rules.md
  • agent-ops/rules/project/domain/edge/rules.md
  • agent-ops/rules/project/domain/testing/rules.md
  • agent-ops/skills/common/router.md
  • agent-ops/skills/common/plan/SKILL.md
  • agent-ops/skills/common/update-test/SKILL.md
  • agent-ops/skills/common/finalize-task-routing/SKILL.md
  • agent-ops/skills/common/plan/templates/review-stub-template.md
  • agent-roadmap/ROADMAP.md
  • agent-roadmap/current.md
  • agent-roadmap/priority-queue.md
  • agent-roadmap/phase/operational-observability-provider-management/PHASE.md
  • agent-spec/index.md
  • agent-spec/runtime/provider-pool-config-refresh.md
  • agent-spec/input/openai-compatible-surface.md
  • agent-contract/index.md
  • agent-contract/inner/edge-config-runtime-refresh.md
  • agent-contract/outer/openai-compatible-api.md
  • agent-test/local/rules.md
  • agent-test/local/platform-common-smoke.md
  • agent-test/local/control-plane-smoke.md
  • agent-test/local/node-smoke.md
  • agent-test/local/edge-smoke.md
  • packages/go/config/protocol_profile.go
  • packages/go/config/protocol_profile_test.go
  • apps/control-plane/internal/credentialstore/route.go
  • apps/control-plane/internal/credentialstore/route_test.go
  • apps/node/internal/adapters/openai_compat/provider.go
  • apps/node/internal/adapters/openai_compat/protocol_profile_test.go
  • configs/edge.yaml

SDD Criteria

not applicable. 이 작업은 현재 활성 Milestone과 무관한 기존 provider-profile/credential-route 기능의 소규모 후속이며, 사용자가 Milestone 생성이 불필요하다고 확정했다. Roadmap과 agent-roadmap/current.md는 수정하지 않는다.

Verification Context

  • Handoff: supplied. 사용자가 token/.glm credential로 Coding Plan endpoint 접속 확인, SOPS 저장, agent-test 절차 기록을 요청했고 이전 검증에서 GET /models 200(8 models), glm-5.1 non-stream Chat 200, SSE 200/[DONE], usage 응답을 확인했다. token 원문은 기록하지 않았다.
  • update-test resolve-context:
    • Rules State: usable.
    • Sources: agent-test/local/rules.md, agent-test/local/platform-common-smoke.md, agent-test/local/control-plane-smoke.md, agent-test/local/node-smoke.md, agent-test/local/edge-smoke.md.
    • Required repository commands: fresh go test for changed config/control-plane/node packages; make build-edge; make test-e2e for the touched Edge/config user path.
    • Cache: fresh required; all Go commands use -count=1.
    • Constraint: token/API key/private endpoint values must not appear in tracked files or stdout/stderr.
  • Repository baseline:
    • go test -count=1 ./packages/go/config ./apps/control-plane/internal/credentialstore ./apps/node/internal/adapters/openai_compat: PASS before changes.
    • The existing adapter already resolves profile-specific models and chat_completions operations; no new driver or adapter branch is required.
  • Official references reviewed on 2026-08-02:
    • https://docs.z.ai/scenario-example/develop-tools/others — Coding Plan base URL differs from General API.
    • https://docs.z.ai/api-reference/introduction — Bearer authentication and endpoint distinction.
    • https://docs.z.ai/api-reference/llm/chat-completion — Chat, stream, tools/tool calls.
    • https://docs.z.ai/legal-agreement/subscription-terms and https://docs.z.ai/devpack/usage-policy — supported-tool, personal-use, and proxy/sharing restrictions.
  • External Verification Preflight:
    • Runner/workdir: local shell, /config/workspace/iop-s2.
    • Branch/HEAD/dirty: feature/glm-coding-plan, 32c0754f91b05ee95ab25b1062016d44fba18bf2, clean at plan preflight.
    • Source sync: origin/dev is the same commit; left/right delta 0 0.
    • OS/arch: Linux/aarch64.
    • Tools: Go go1.26.2 linux/arm64 at /config/opt/go/bin/go (module minimum is Go 1.24), SOPS 3.13.1, jq 1.7, curl 8.5.0, Python 3.12.3.
    • Config/evidence paths: repository configs/edge.yaml; encrypted SOPS /config/.config/iop/secrets/dev-openai-toki.sops.yaml; age key /config/.config/sops/age/keys.txt.
    • Secret state: SOPS filestatus reports encrypted=true; both files mode 0600; tokens.glm-coding-plan decrypts to a non-empty value without printing it.
    • External host: api.z.ai:443 reachable; unauthenticated https://api.z.ai/api/coding/paas/v4/models returns 401.
    • Binary/artifact/ports/process: direct provider smoke needs no IOP binary or listening port. make build-edge and make test-e2e build/start their own repository-native artifacts/runtime.
    • Mismatch handling: if branch/HEAD/source sync or secret modes differ, stop and restore the exact checkout/0600 state before live smoke. Do not copy the secret into the repository.
  • Gaps: no repository implementation gap remains unidentified. A live IOP-as-proxy Coding Plan deployment is intentionally not part of completion because provider terms require an eligible supported use or separate authorization; the profile and example remain explicit opt-in.
  • Confidence: high. Official endpoint/auth/tool facts, successful live upstream evidence, current code, focused baseline tests, and repository-native smoke rules agree.
  • Maintenance: update-test-candidate by user request; extend the existing edge-smoke profile rather than creating a new test profile.

Test Coverage Gaps

  • Built-in catalog has no glm_coding ID, Coding Plan URL assertion, or explicit “no Responses” assertion. Add literal catalog/URL/capability tests.
  • glm omits tool_calling despite the official Chat API supporting tools. Assert both GLM profiles advertise it.
  • Provider-pool config has no regression proving two external model IDs select two distinct provider/profile IDs while both rewrite to glm-5.1. Add a YAML load test.
  • Node loopback operation fixtures cover only the general glm URL. Add glm_coding across JSON, SSE, and provider-error fixture outcomes.
  • Managed credential compatibility allows only glm for glm/bearer. Add a same-slot dual-profile route test including distinct aliases and exact resource selectors, plus a cross-vendor rejection.
  • Local Edge smoke has no SOPS-backed Coding Plan procedure or usage-policy guard. Add the exact redacted preflight and models/chat/stream/tool checks.
  • No real Edge proxy request is added to the completion gate; deterministic config/adapter/managed-route tests plus repository E2E cover IOP behavior, while direct authenticated upstream evidence covers the endpoint.

Symbol References

none. No symbol is renamed or removed.

Split Judgment

Single plan. The indivisible invariant is that one stable glm_coding profile identity must agree across the built-in catalog, managed credential allow-list, Node operation URL fixture, Edge provider/model example, spec, and smoke procedure. Splitting could leave a selectable profile without a compatible managed route or a credential route without the expected transport endpoint.

Scope Rationale

  • Do not add a protocol driver, config/schema field, protobuf message, adapter implementation, OpenAI handler branch, provider category, or automatic endpoint selection; existing openai_chat, category: api, profile resolution, and model-driven provider-pool routing already cover the behavior.
  • Do not change agent-contract/inner/edge-config-runtime-refresh.md or agent-contract/outer/openai-compatible-api.md. Their stable contracts already define profile selection, exact managed route binding, external model routing, and no-fallback behavior; this task only adds catalog data and examples.
  • Do not modify roadmap/Milestone state, SOPS/age files, token/.glm, or any tracked secret. The host-local encrypted token already exists and is verification input only.
  • Do not add responses to either GLM profile, infer a model list in code, combine glm-api and glm-coding beneath one external model ID, or fall back between General API and Coding Plan.
  • Do not enable the Coding Plan provider in active example config. All GLM provider/model entries remain comment-only and include the official usage restriction.

Final Routing

  • evaluation_mode: first-pass
  • finalizer: finalize-task-policy.sh, mode pair
  • Build closures: scope/context/verification/evidence/ownership/decision all true; capability gap none.
  • Build scores: scope_coupling=2, state_concurrency=0, blast_irreversibility=1, evidence_diagnosis=1, verification_complexity=2 -> G06.
  • Build route: base=local-fit, route=local-fit, lane=local, filename=PLAN-local-G06.md.
  • large_indivisible_context: false.
  • matched loop risk: boundary_contract; count=1; risk boundary=false.
  • recovery signals: review_rework_count=0, evidence_integrity_failure=false; recovery boundary=false.
  • Review closures: scope/context/verification/evidence/ownership/decision all true; capability gap none.
  • Review scores: scope_coupling=2, state_concurrency=0, blast_irreversibility=1, evidence_diagnosis=1, verification_complexity=2 -> G06.
  • Review route: official-review, lane=cloud, adapter=codex, model=gpt-5.6-sol, reasoning=xhigh, filename=CODE_REVIEW-cloud-G06.md.

Implementation Checklist

  • [API-1] Register distinct glm General API and glm_coding Coding Plan profiles, including tool-calling capability, and add literal config plus Node transport regressions.
  • [API-2] Permit one glm/bearer managed credential slot to bind independently to both GLM profiles and test exact route alias/resource-selector isolation.
  • [API-3] Document the model-driven two-provider Edge configuration and synchronize the current provider-pool spec without enabling Coding Plan or adding fallback.
  • [API-4] Extend the local Edge smoke profile with the existing SOPS-backed Coding Plan checks and run fresh focused, build, repository E2E, and redacted upstream verification.
  • Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

[API-1] Add the Coding Plan protocol profile and transport regression

Problem

packages/go/config/protocol_profile.go:186-195 defines only the General API profile and omits tool_calling. packages/go/config/protocol_profile_test.go:301-420 and :689-717 therefore cannot detect a missing Coding Plan identity, wrong URL/auth, accidental Responses support, or catalog count drift. apps/node/internal/adapters/openai_compat/protocol_profile_test.go:19-44 exercises only /api/paas/v4/chat/completions.

Current:

// packages/go/config/protocol_profile.go:186
"glm": {
    Driver:  ProtocolDriverOpenAIChat,
    BaseURL: "https://api.z.ai/api/paas/v4",
    // ...
    Capabilities: []string{"models", "chat", "streaming"},
},

Solution

Keep glm as General API, add its official tool_calling capability, and register a separate built-in glm_coding with the exact Coding Plan base URL. Both profiles expose only models and chat_completions operations with Bearer auth; neither exposes Responses.

Target:

"glm": {
    Driver:  ProtocolDriverOpenAIChat,
    BaseURL: "https://api.z.ai/api/paas/v4",
    Operations: map[string]string{
        string(OperationModels):          "/models",
        string(OperationChatCompletions): "/chat/completions",
    },
    Auth:         ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
    Capabilities: []string{"models", "chat", "streaming", "tool_calling"},
},
"glm_coding": {
    Driver:  ProtocolDriverOpenAIChat,
    BaseURL: "https://api.z.ai/api/coding/paas/v4",
    Operations: map[string]string{
        string(OperationModels):          "/models",
        string(OperationChatCompletions): "/chat/completions",
    },
    Auth:         ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
    Capabilities: []string{"models", "chat", "streaming", "tool_calling"},
},

Add literal tests that:

  • include glm_coding in the exact built-in ID/auth matrix;
  • resolve General and Coding URLs to /api/paas/v4/... and /api/coding/paas/v4/... respectively;
  • assert both profiles have models/chat/streaming/tool_calling and lack Responses operation/capability;
  • load a provider-pool YAML with external glm-5.1-api -> provider glm-api/profile glm and glm-5.1-coding -> provider glm-coding/profile glm_coding, both rewriting to upstream glm-5.1, and assert no cross-mapping;
  • add glm_coding to the Node loopback operation fixture so JSON, SSE, and provider-error responses all hit the Coding Plan path with Bearer auth.

Modified Files and Checklist

  • packages/go/config/protocol_profile.go — add glm_coding and tool_calling for both GLM profiles.
  • packages/go/config/protocol_profile_test.go — update exact catalog/auth matrices and add URL/capability/provider-pool isolation regressions.
  • apps/node/internal/adapters/openai_compat/protocol_profile_test.go — add the Coding Plan operation fixture and update the official-doc verification comment/date.

Test Strategy

Write tests. Extend TestBuiltInProtocolProfileCatalog, TestBuiltInProtocolProfileURLs, and TestProtocolProfileBuiltInAuthMatrix; add TestBuiltInGLMProtocolProfiles and TestGLMProfilesRemainDistinctThroughProviderPoolConfig; add glm_coding to TestProtocolProfileOperationURLFixtures. Use only literal expected values, temporary YAML, and httptest.Server—never production catalog values as expected fixtures or a live key.

Verification

Run:

gofmt -w packages/go/config/protocol_profile.go packages/go/config/protocol_profile_test.go apps/node/internal/adapters/openai_compat/protocol_profile_test.go
go test -count=1 ./packages/go/config ./apps/node/internal/adapters/openai_compat

Expected: both packages pass fresh; the loopback fixture records /api/coding/paas/v4/chat/completions and no live provider is called.

[API-2] Bind the same GLM slot to isolated General and Coding routes

Problem

apps/control-plane/internal/credentialstore/route.go:82-95 intentionally uses a closed compatibility matrix, but glm/bearer admits only glm. This prevents a managed GLM credential slot from selecting glm_coding and leaves no regression for same vendor/model with different endpoint profiles.

Current:

// apps/control-plane/internal/credentialstore/route.go:91
"glm/bearer": {"glm": {header: "Authorization", scheme: "Bearer"}},

Solution

Add glm_coding to the existing glm/bearer profile rule with the same exact Bearer declaration. Do not introduce a subscription credential kind: subscription/pay-go is a protocol-profile endpoint distinction, while the credential remains vendor glm and kind bearer.

Target:

"glm/bearer": {
    "glm":        {header: "Authorization", scheme: "Bearer"},
    "glm_coding": {header: "Authorization", scheme: "Bearer"},
},

Add TestGLMSlotSupportsGeneralAndCodingProfiles beside the multi-profile MiniMax test at apps/control-plane/internal/credentialstore/route_test.go:222-240. Create one draft GLM Bearer slot, then create:

  • alias glm-5.1-api, profile glm, upstream glm-5.1, selector glm-api;
  • alias glm-5.1-coding, profile glm_coding, upstream glm-5.1, selector glm-coding.

Assert both routes share the slot but preserve distinct profile/alias/selector values, listing returns both, and an OpenAI Bearer slot rejects glm_coding with ErrIncompatibleProfile.

Modified Files and Checklist

  • apps/control-plane/internal/credentialstore/route.go — extend only the glm/bearer closed rule.
  • apps/control-plane/internal/credentialstore/route_test.go — add same-slot dual-profile and cross-vendor rejection coverage.

Test Strategy

Write TestGLMSlotSupportsGeneralAndCodingProfiles using the existing SQLite temp store, fake key registry, opaque envelope, and real catalog resolution. It must assert the stored route fields rather than only checking a nil error.

Verification

Run:

gofmt -w apps/control-plane/internal/credentialstore/route.go apps/control-plane/internal/credentialstore/route_test.go
go test -count=1 ./apps/control-plane/internal/credentialstore

Expected: the GLM dual-profile route test passes and the existing incompatible-profile tests remain green.

[API-3] Show model-driven Edge selection and synchronize the current spec

Problem

configs/edge.yaml:200-240 lists only glm and provides no safe example for choosing the endpoint through an external model ID. agent-spec/runtime/provider-pool-config-refresh.md:91-116 describes generic model/profile routing but not the two stable GLM profiles or their no-fallback invariant.

Current:

#   glm             — openai_chat driver, https://api.z.ai/api/paas/v4

Solution

Add glm_coding to the built-in list and a comment-only provider-pool example. External model IDs—not an extra request field—select separate provider IDs:

# models:
#   - id: "glm-5.1-api"
#     providers:
#       glm-api: "glm-5.1"
#   - id: "glm-5.1-coding"
#     providers:
#       glm-coding: "glm-5.1"
# nodes:
#   - id: "node-glm-example"
#     providers:
#       - id: "glm-api"
#         type: "openai_api"
#         category: "api"
#         profile: "glm"
#         models: ["glm-5.1"]
#         capacity: 1
#       - id: "glm-coding"
#         type: "openai_api"
#         category: "api"
#         profile: "glm_coding"
#         models: ["glm-5.1"]
#         capacity: 1

State beside the example that both provider IDs must never be placed under the same external model ID when endpoint/quota isolation is required, no General/Coding fallback exists, no raw key belongs in YAML, and Coding Plan may be enabled only for use allowed by current Z.AI terms. Update the current runtime spec feature/config/limitation/change-record sections with the same implemented facts; do not copy credential values or create a new contract.

Modified Files and Checklist

  • configs/edge.yaml — document the built-in profile and the comment-only two-model/two-provider mapping with usage/fallback guardrails.
  • agent-spec/runtime/provider-pool-config-refresh.md — synchronize the implemented GLM profile/routing capability, no-fallback boundary, validation references, and 2026-08-02 change record.

Test Strategy

No separate prose-only test. API-1's TestGLMProfilesRemainDistinctThroughProviderPoolConfig loads the same mapping shape and is the executable contract. Review the tracked diff for raw-key patterns and ensure the example stays fully commented.

Verification

Run:

rg --sort path -n 'glm_coding|glm-5\.1-api|glm-5\.1-coding|glm-api|glm-coding' configs/edge.yaml agent-spec/runtime/provider-pool-config-refresh.md
git diff --check -- configs/edge.yaml agent-spec/runtime/provider-pool-config-refresh.md

Expected: both IDs/profiles appear with distinct provider mappings, and no whitespace error or secret value is present.

[API-4] Record and execute the SOPS-backed Coding Plan smoke

Problem

agent-test/local/edge-smoke.md:25-48 only says to use a provider-specific command. It does not record the existing encrypted token location, permission/encryption preflight, exact Coding Plan URL, chat/stream/tool acceptance checks, or the prohibition on printing credentials and silently switching to General API.

Solution

Update the existing local profile (it is intentionally gitignored/operator-local) and last_rule_updated_at. Add a GLM Coding Plan subsection containing:

  • opt-in/terms precondition and a ban on shared/proxy production use without provider authorization;
  • exact SOPS/age paths, 0600 checks, sops filestatus encryption assertion, and non-empty scalar extraction without echo;
  • a mode-0600 temporary curl config carrying the Bearer header so the raw key is absent from command arguments/output;
  • GET /api/coding/paas/v4/models with a glm-5.1 assertion;
  • non-stream Chat with model/content/usage assertions;
  • SSE Chat with at least one data: event and terminal data: [DONE];
  • an auto function-calling request whose response contains the declared function name;
  • cleanup and an explicit statement that this procedure never calls /api/paas/v4 as fallback.

Keep all response artifacts in a mktemp -d directory outside the repository and print only a final PASS marker. Then run the repository commands and live smoke exactly as recorded.

Modified Files and Checklist

  • agent-test/local/edge-smoke.md — add the redacted SOPS-backed Coding Plan preflight, calls, acceptance criteria, cleanup, terms guard, and date.

Test Strategy

Update the existing test profile; do not create a new profile or tracked secret fixture. The live check uses the user's host-local encrypted token, while repository behavior remains covered by API-1/API-2 loopback/config tests. If provider terms or token eligibility block the live call, record the exact blocker in review evidence and do not try the General API endpoint.

Verification

Run the exact redacted command in Final Verification step 4 and copy the same command/criteria into the GLM Coding Plan section of agent-test/local/edge-smoke.md.

Expected: preflight passes without revealing the token; models, non-stream, stream, and function-call assertions pass against only https://api.z.ai/api/coding/paas/v4; the command prints glm coding smoke: PASS and removes temporary files.

Modified Files Summary

File Item
packages/go/config/protocol_profile.go API-1
packages/go/config/protocol_profile_test.go API-1
apps/node/internal/adapters/openai_compat/protocol_profile_test.go API-1
apps/control-plane/internal/credentialstore/route.go API-2
apps/control-plane/internal/credentialstore/route_test.go API-2
configs/edge.yaml API-3
agent-spec/runtime/provider-pool-config-refresh.md API-3
agent-test/local/edge-smoke.md API-4
agent-task/glm_coding_plan/CODE_REVIEW-cloud-G06.md API-1, API-2, API-3, API-4

Final Verification

  1. Confirm the checkout and external secret preflight. These commands must not print decrypted data:
test "$(git branch --show-current)" = "feature/glm-coding-plan"
test "$(git merge-base HEAD origin/dev)" = "32c0754f91b05ee95ab25b1062016d44fba18bf2"
test "$(stat -c '%a' /config/.config/iop/secrets/dev-openai-toki.sops.yaml)" = "600"
test "$(stat -c '%a' /config/.config/sops/age/keys.txt)" = "600"
SOPS_AGE_KEY_FILE=/config/.config/sops/age/keys.txt sops filestatus /config/.config/iop/secrets/dev-openai-toki.sops.yaml | jq -e '.encrypted == true'
test "$(SOPS_AGE_KEY_FILE=/config/.config/sops/age/keys.txt sops decrypt --extract '["tokens"]["glm-coding-plan"]' /config/.config/iop/secrets/dev-openai-toki.sops.yaml | wc -c)" -gt 1

Expected: every command exits 0 and only the non-secret filestatus JSON is emitted.

  1. Confirm formatting and run fresh focused tests:
test -z "$(gofmt -d packages/go/config/protocol_profile.go packages/go/config/protocol_profile_test.go apps/control-plane/internal/credentialstore/route.go apps/control-plane/internal/credentialstore/route_test.go apps/node/internal/adapters/openai_compat/protocol_profile_test.go)"
go test -count=1 ./packages/go/config ./apps/control-plane/internal/credentialstore ./apps/node/internal/adapters/openai_compat

Expected: no gofmt diff and all three packages pass.

  1. Run the Edge/config user-path build and repository-native full cycle:
make build-edge
make test-e2e

Expected: Edge builds and the repository Edge-Node E2E exits 0. Record actual stdout/stderr; this verifies the existing user pipeline but does not claim a live Coding Plan proxy deployment.

  1. Execute the redacted Coding Plan smoke. This command uses only the Coding endpoint and prints no response body or credential:
bash <<'BASH'
set -euo pipefail
set +x

sops_file=/config/.config/iop/secrets/dev-openai-toki.sops.yaml
age_file=/config/.config/sops/age/keys.txt
base_url=https://api.z.ai/api/coding/paas/v4
smoke_dir="$(mktemp -d /tmp/iop-glm-coding-smoke.XXXXXX)"
cleanup() {
  rm -rf -- "$smoke_dir"
}
trap cleanup EXIT

test "$(stat -c '%a' "$sops_file")" = "600"
test "$(stat -c '%a' "$age_file")" = "600"
SOPS_AGE_KEY_FILE="$age_file" sops filestatus "$sops_file" | jq -e '.encrypted == true' >/dev/null
token="$(SOPS_AGE_KEY_FILE="$age_file" sops decrypt --extract '["tokens"]["glm-coding-plan"]' "$sops_file")"
test -n "$token"

umask 077
auth_config="$smoke_dir/curl-auth.conf"
printf 'header = "Authorization: Bearer %s"\nheader = "Content-Type: application/json"\n' "$token" >"$auth_config"
unset token

curl --fail --silent --show-error --config "$auth_config" \
  --output "$smoke_dir/models.json" "$base_url/models"
jq -e '(.data | type == "array") and any(.data[]; (.id | ascii_downcase) == "glm-5.1")' \
  "$smoke_dir/models.json" >/dev/null

jq -n '{
  model: "glm-5.1",
  messages: [{role: "user", content: "Reply exactly GLM_CODING_PLAN_OK"}],
  stream: false,
  max_tokens: 32
}' >"$smoke_dir/chat.json"
curl --fail --silent --show-error --config "$auth_config" \
  --request POST --data-binary "@$smoke_dir/chat.json" \
  --output "$smoke_dir/chat-response.json" "$base_url/chat/completions"
jq -e '(.choices[0].message.content | type == "string" and length > 0) and (.usage.total_tokens | type == "number")' \
  "$smoke_dir/chat-response.json" >/dev/null

jq -n '{
  model: "glm-5.1",
  messages: [{role: "user", content: "Reply exactly GLM_CODING_PLAN_STREAM_OK"}],
  stream: true,
  max_tokens: 32
}' >"$smoke_dir/stream.json"
curl --fail --silent --show-error --no-buffer --config "$auth_config" \
  --request POST --data-binary "@$smoke_dir/stream.json" \
  --output "$smoke_dir/stream-response.txt" "$base_url/chat/completions"
grep -Eq '^data: .+' "$smoke_dir/stream-response.txt"
grep -Fq 'data: [DONE]' "$smoke_dir/stream-response.txt"

jq -n '{
  model: "glm-5.1",
  messages: [{role: "user", content: "Call emit_marker with marker GLM_CODING_PLAN_TOOL_OK. Do not answer directly."}],
  tools: [{
    type: "function",
    function: {
      name: "emit_marker",
      description: "Emit the requested verification marker",
      parameters: {
        type: "object",
        properties: {marker: {type: "string"}},
        required: ["marker"]
      }
    }
  }],
  tool_choice: "auto",
  stream: false,
  max_tokens: 64
}' >"$smoke_dir/tool.json"
curl --fail --silent --show-error --config "$auth_config" \
  --request POST --data-binary "@$smoke_dir/tool.json" \
  --output "$smoke_dir/tool-response.json" "$base_url/chat/completions"
jq -e 'any(.choices[0].message.tool_calls[]?; .function.name == "emit_marker")' \
  "$smoke_dir/tool-response.json" >/dev/null

printf 'glm coding smoke: PASS\n'
BASH

Expected: /models, non-stream Chat, SSE [DONE], and one tool call pass at https://api.z.ai/api/coding/paas/v4 only; the command prints glm coding smoke: PASS and removes all temporary files.

  1. Inspect scope and secret safety:
git diff --check
git diff --name-only -- packages/go/config/protocol_profile.go packages/go/config/protocol_profile_test.go apps/control-plane/internal/credentialstore/route.go apps/control-plane/internal/credentialstore/route_test.go apps/node/internal/adapters/openai_compat/protocol_profile_test.go configs/edge.yaml agent-spec/runtime/provider-pool-config-refresh.md
rg --sort path -n 'glm_coding|glm-5\.1-api|glm-5\.1-coding' packages/go/config/protocol_profile.go packages/go/config/protocol_profile_test.go apps/control-plane/internal/credentialstore/route.go apps/control-plane/internal/credentialstore/route_test.go apps/node/internal/adapters/openai_compat/protocol_profile_test.go configs/edge.yaml agent-spec/runtime/provider-pool-config-refresh.md agent-test/local/edge-smoke.md

Expected: only the claimed implementation files plus active review evidence are modified; GLM split identifiers are present; no token/API key value appears in the diff or command output.

After completing all code changes, fill implementation-owned sections in CODE_REVIEW-*-G??.md.