zcode: a Start Plan request goes as the ZCode app sends it, its system prompt and headers (#425; the user allowed mimicking ZCode's client, for this vendor only) — zcode.z.ai turns a Start Plan (体验套餐) request without ZCode's own system prompt away with 405 "request has been blocked due to unusual activity", code 3012, as zcode2api found (a137460387/zcode2api src/upstream/system-prompt.js, zcode-plan.js; dengyie/zcode2api app/body_transform.py), and magpie sent the agent's request as it was. A Start Plan /v1/messages request now carries ZCode 3.11's three system blocks, each cached (its opening line; its identity, harness and desktop context; "\n\n" and its dynamic sections around an environment section that leaves the working directory and OS version unknown), the agent's own system after them, and the day in a <system-reminder> before the first user turn unless one is there; ZCode's blocks take three of the four cache breakpoints, so the agent's system and tools give up theirs and only its last in the messages is kept. Model, max_tokens and the rest of the body go as sent (numbers as written). Its headers are ZCode's as zcode2api's headers.js has them: User-Agent ZCode/3.14.3 ai-sdk/anthropic/3.0.81, X-ZCode-Agent glm, X-Release-Channel production, X-Os-Category, X-Client-Language and X-Client-Timezone from the machine, a fresh X-Request-Id and X-ZCode-Trace-Id, X-ZCode-Session-Type main, no X-Session-Id or X-Query-Id, beside the X-Title, X-Platform, HTTP-Referer and X-Device-Mid sent before. The GLM Coding Plan and team seats are unchanged. The 405 hint no longer says magpie won't pretend to be ZCode: a block that still happens is the network blocking the IP or ZCode checking something new. No ZCode account was signed in; TestZCodeStartSentAsTheApp (httptest) fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; routing-zcode-blocked, click-scroll, old-webkit pass.

This commit is contained in:
yetone committed 2026-10-02 08:20:33 +08:00
1 parent 6e858c6641
commit 31568cb0b0
11 files changed
+524 -23

No files matched your search

+1 -1
View File
@@ -1642,7 +1642,7 @@ const I18N = {
"It said: {error}": "原话:{error}",
"WorkBuddy refuses chats from Codex and Claude Code (their system prompt); use it from Hermes, OpenCode or Pi, or add another provider to this group": "WorkBuddy 会拒绝来自 Codex 和 Claude Code 的对话(因为它们的系统提示词);请在 Hermes、OpenCode 或 Pi 里使用它,或在这个分组里再加一个供应商",
"the provider's network firewall blocked requests from this IP; wait a while, or switch to another network or proxy": "供应商的网络防火墙拦截了来自这个 IP 的请求;请稍等一会儿,或换个网络或代理",
"ZCode's Start Plan turns away requests that don't come from the ZCode app itself, and magpie doesn't pretend to be it; it can also be a network block of this IP. Use an account with a GLM Coding Plan, or add another provider to this group": "ZCode 体验套餐会拒绝不是来自 ZCode 客户端本身的请求,magpie 不会冒充它;也可能是网络拦截了这个 IP。请改用有 GLM Coding Plan 的账号,或在这个分组里加上其他供应商",
"ZCode's Start Plan still turned this request away, though magpie sends it as the ZCode app does; it can be a network block of this IP, or ZCode checking for something new. Use an account with a GLM Coding Plan, or add another provider to this group": "ZCode 体验套餐仍拒绝了这个请求(magpie 已按 ZCode 客户端的方式发送);可能是网络拦截了这个 IP,或 ZCode 又加了新的校验。请改用有 GLM Coding Plan 的账号,或在这个分组里加上其他供应商",
"Show the latest request that failed": "查看最近一个失败的请求",
"rate limited": "限流",
"out of credit": "余额不足",
+1 -1
View File
@@ -507,7 +507,7 @@
const BLOCKED = "the provider's network firewall blocked requests from this IP; wait a while, or switch to another network or proxy";
// what it says of ZCode's Start Plan turning a request away (#425,
// provider.ZCodeStartBlockedHint), in place of BLOCKED
const ZCODE_BLOCKED = "ZCode's Start Plan turns away requests that don't come from the ZCode app itself, and magpie doesn't pretend to be it; it can also be a network block of this IP. Use an account with a GLM Coding Plan, or add another provider to this group";
const ZCODE_BLOCKED = "ZCode's Start Plan still turned this request away, though magpie sends it as the ZCode app does; it can be a network block of this IP, or ZCode checking for something new. Use an account with a GLM Coding Plan, or add another provider to this group";
const HINTS = [WB_REFUSED, BLOCKED, ZCODE_BLOCKED];
function trySaid(r, i) {
+3 -3
View File
@@ -840,9 +840,9 @@ leaves the page where it is. Chromium and WebKit, API faked.
`routing-zcode-blocked.test.cjs` opens a request ZCode's Start Plan turned
away (#425: 405 "request has been blocked due to unusual activity") on the
Routing page: what the gateway made of it is given without the hint it adds,
and the hint (ZCode takes only its own app's requests and magpie doesn't
pretend to be it, or the network blocked this IP; use a GLM Coding Plan or
another provider in the group) is on its own line in English and Chinese;
and the hint (ZCode still turned it away though magpie sends it as the ZCode
app does: the network blocked this IP, or ZCode checks something new; use a
GLM Coding Plan or another provider in the group) is on its own line in English and Chinese;
another 405 gets none, and picking the request leaves the page where it is.
Chromium and WebKit, API faked.
@@ -1,9 +1,10 @@
// Run with Node's test runner and Playwright on the module path; see README.md.
// ZCode's Start Plan turning a request away (#425: 405 "request has been
// blocked due to unusual activity", code 3012): the Routing page gives
// what the gateway made of it, and apart from that what it means (ZCode
// takes only its own app's requests, or the network blocked the address,
// and what to use instead), in the page's language.
// what the gateway made of it, and apart from that what it means (it was
// sent as the ZCode app sends it, so the network blocked the address or
// ZCode checks something new, and what to use instead), in the page's
// language.
const assert = require("node:assert/strict");
const fs = require("node:fs/promises");
const path = require("node:path");
@@ -14,7 +15,7 @@ const assets = path.resolve(__dirname, "../assets");
const now = new Date();
const day = [now.getFullYear(), now.getMonth() + 1, now.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
const at = (i) => new Date(now.getTime() - (i + 1) * 60e3).toISOString();
const hint = "ZCode's Start Plan turns away requests that don't come from the ZCode app itself, and magpie doesn't pretend to be it; it can also be a network block of this IP. Use an account with a GLM Coding Plan, or add another provider to this group";
const hint = "ZCode's Start Plan still turned this request away, though magpie sends it as the ZCode app does; it can be a network block of this IP, or ZCode checking for something new. Use an account with a GLM Coding Plan, or add another provider to this group";
const vendor = "ZCode: 405 Method Not Allowed";
const key = { id: "zcode", provider: "zcode", name: "ZCode", kind: "provider", model: "GLM-5.3-Flash" };
// newest first: the block, then another 405 with nothing to add
@@ -56,7 +57,7 @@ function serve(lang) {
const want = {
en: { said: "It said: " + vendor, hint },
zh: { said: "原话:" + vendor, hint: "ZCode 体验套餐会拒绝不是来自 ZCode 客户端本身的请求,magpie 不会冒充它;也可能是网络拦截了这个 IP。请改用有 GLM Coding Plan 的账号,或在这个分组里加上其他供应商" },
zh: { said: "原话:" + vendor, hint: "ZCode 体验套餐仍拒绝了这个请求(magpie 已按 ZCode 客户端的方式发送);可能是网络拦截了这个 IP,或 ZCode 又加了新的校验。请改用有 GLM Coding Plan 的账号,或在这个分组里加上其他供应商" },
};
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
+1 -1
View File
@@ -318,7 +318,7 @@ func zcodeProvider(who, plan string, k zcodeKey) Provider {
return errZCodeExpired
}
key = k.JWT
zcodeSourceHeaders(req)
zcodeStartRequest(req, body)
}
req.Header.Del("Authorization")
req.Header.Set("x-api-key", key)
+3 -3
View File
@@ -12,9 +12,9 @@ const zcodeBlockPage = `<!DOCTYPE html><html><head><title>405</title></head><bod
<h1>Sorry, your request has been blocked due to unusual activity.</h1>
<a href="https://errors.aliyun.com/error/405?code=blocked">errors.aliyun.com</a></body></html>`
// The Start Plan's block is said for what it is (#425): ZCode turns away
// requests that aren't its app's own, or the network blocked the address,
// and what to use instead; not only the firewall. A Coding Plan account,
// The Start Plan's block is said for what it is (#425): sent as ZCode's
// app sends it, it is the network blocking the address or ZCode checking
// something new, and what to use instead; not only the firewall. A Coding Plan account,
// and any other provider, keep the generic hint.
func TestZCodeStartBlockedSaid(t *testing.T) {
say := func(p Provider, status int, body string) string {
+220
View File
@@ -0,0 +1,220 @@
package provider
// ZCode's Start Plan serves only what looks like the ZCode app's own
// request: one without ZCode's system prompt is turned away with 405
// "request has been blocked due to unusual activity", code 3012 (#425).
// So a Start Plan request goes as ZCode sends it: the app's system prompt
// first, three blocks (its opening line; its identity and desktop
// context; "\n\n" and its dynamic sections around the environment), each
// cached, the agent's own system prompt after them; the day in a
// <system-reminder> before the first user turn; and the app's headers.
// The text is ZCode 3.11's (zcode_prompt.json), as zcode2api
// (github.com/a137460387/zcode2api, src/upstream/system-prompt.js,
// zcode-system.json, headers.js) takes it from the app. The GLM Coding
// Plan, and every other provider, get the agent's request as it is.
import (
_ "embed"
"bytes"
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
//go:embed zcode_prompt.json
var zcodePromptJSON []byte
var zcodePrompt = func() (p struct {
Prefix string `json:"prefix"`
Stable string `json:"stable"`
BeforeEnvironment string `json:"beforeEnvironment"`
AfterEnvironment string `json:"afterEnvironment"`
Environment struct {
Heading string `json:"heading"`
InvokedLine string `json:"invokedLine"`
CwdLabel string `json:"cwdLabel"`
GitLabel string `json:"gitLabel"`
GitNo string `json:"gitNo"`
PlatformLabel string `json:"platformLabel"`
ShellLabel string `json:"shellLabel"`
OSVersionLabel string `json:"osVersionLabel"`
PoweredByLine string `json:"poweredByLine"`
} `json:"environment"`
Context struct {
Intro string `json:"intro"`
Outro string `json:"outro"`
CurrentDateHeading string `json:"currentDateHeading"`
CurrentDateLine string `json:"currentDateLine"`
} `json:"context"`
}) {
if err := json.Unmarshal(zcodePromptJSON, &p); err != nil {
panic("zcode_prompt.json: " + err.Error())
}
return p
}()
// zcodePlatform is the platform as ZCode (Electron) names it.
func zcodePlatform() string {
if runtime.GOOS == "windows" {
return "win32"
}
return runtime.GOOS
}
// zcodeEnvironment is the prompt's environment section. Where the agent
// runs isn't magpie's to say, so that is left unknown.
func zcodeEnvironment(model string) string {
e := zcodePrompt.Environment
shell := "unknown"
if s := os.Getenv("SHELL"); s != "" {
shell = filepath.Base(s)
} else if runtime.GOOS == "windows" {
shell = "cmd"
}
lines := []string{
e.Heading,
e.InvokedLine,
"- " + e.CwdLabel + ": unknown",
"- " + e.GitLabel + ": " + e.GitNo,
"- " + e.PlatformLabel + ": " + zcodePlatform(),
"- " + e.ShellLabel + ": " + shell,
"- " + e.OSVersionLabel + ": unknown",
}
if model != "" {
lines = append(lines, strings.NewReplacer("{provider}", "bigmodel-api", "{model}", strings.ToLower(model)).Replace(e.PoweredByLine))
}
return strings.Join(lines, "\n")
}
// zcodeSystem is ZCode's three system blocks for model.
func zcodeSystem(model string) []any {
cached := func(text string) map[string]any {
return map[string]any{"type": "text", "text": text, "cache_control": map[string]any{"type": "ephemeral"}}
}
dynamic := strings.Join([]string{zcodePrompt.BeforeEnvironment, zcodeEnvironment(model), zcodePrompt.AfterEnvironment}, "\n\n")
return []any{cached(zcodePrompt.Prefix), cached(zcodePrompt.Stable), cached("\n\n" + dynamic)}
}
// zcodeDateReminder is what ZCode puts before the first user turn.
func zcodeDateReminder(now time.Time) map[string]any {
c := zcodePrompt.Context
text := strings.Join([]string{c.Intro, c.CurrentDateHeading + "\n" + strings.ReplaceAll(c.CurrentDateLine, "{date}", now.Format("2006-01-02")), "", c.Outro}, "\n")
return map[string]any{"type": "text", "text": "<system-reminder>" + text + "</system-reminder>"}
}
// zcodeStartBody is an Anthropic messages request as ZCode would send it
// to the Start Plan: ZCode's system blocks before the agent's, the date
// before the first user turn when nothing is reminded there already. As
// ZCode's blocks take three of the four cache breakpoints Anthropic's API
// allows, the agent's are dropped from its system and tools, and only its
// last in the messages is kept. A body that isn't one, or already starts
// with ZCode's prompt, is left as it is.
func zcodeStartBody(body []byte, now time.Time) []byte {
var m map[string]json.RawMessage
if json.Unmarshal(body, &m) != nil || m["messages"] == nil {
return body
}
var model string
json.Unmarshal(m["model"], &model)
var own []any
if raw := m["system"]; raw != nil {
var s string
if json.Unmarshal(raw, &s) == nil {
if strings.TrimSpace(s) != "" {
own = []any{map[string]any{"type": "text", "text": s}}
}
} else if zcodeDecode(raw, &own) != nil {
return body
}
}
if len(own) > 0 {
if b, ok := own[0].(map[string]any); ok && b["text"] == zcodePrompt.Prefix {
return body
}
}
for _, b := range own {
if b, ok := b.(map[string]any); ok {
delete(b, "cache_control")
}
}
system, _ := zcodeEncode(append(zcodeSystem(model), own...))
var msgs []map[string]any
if zcodeDecode(m["messages"], &msgs) != nil {
return body
}
last := true
for i := len(msgs) - 1; i >= 0; i-- {
blocks, _ := msgs[i]["content"].([]any)
for j := len(blocks) - 1; j >= 0; j-- {
if b, ok := blocks[j].(map[string]any); ok && b["cache_control"] != nil {
if !last {
delete(b, "cache_control")
}
last = false
}
}
}
if len(msgs) > 0 && msgs[0]["role"] == "user" {
var blocks []any
switch c := msgs[0]["content"].(type) {
case string:
blocks = []any{map[string]any{"type": "text", "text": c}}
case []any:
blocks = c
}
reminded := false
for _, b := range blocks {
if b, ok := b.(map[string]any); ok {
if t, _ := b["text"].(string); b["type"] == "text" && strings.HasPrefix(t, "<system-reminder>") {
reminded = true
}
}
}
if !reminded {
msgs[0]["content"] = append([]any{zcodeDateReminder(now)}, blocks...)
}
}
messages, err := zcodeEncode(msgs)
if err != nil {
return body
}
if raw := m["tools"]; raw != nil {
var tools []map[string]any
if zcodeDecode(raw, &tools) == nil {
for _, t := range tools {
delete(t, "cache_control")
}
m["tools"], _ = zcodeEncode(tools)
}
}
m["system"], m["messages"] = system, messages
out, err := zcodeEncode(m)
if err != nil {
return body
}
return out
}
// zcodeDecode reads JSON keeping its numbers as they are written.
func zcodeDecode(raw []byte, v any) error {
d := json.NewDecoder(bytes.NewReader(raw))
d.UseNumber()
return d.Decode(v)
}
// zcodeEncode writes JSON leaving <, > and & as they are.
func zcodeEncode(v any) ([]byte, error) {
var b bytes.Buffer
e := json.NewEncoder(&b)
e.SetEscapeHTML(false)
if err := e.Encode(v); err != nil {
return nil, err
}
return bytes.TrimSuffix(b.Bytes(), []byte("\n")), nil
}
+197
View File
@@ -0,0 +1,197 @@
package provider
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"time"
)
// A request as Claude Code sends it: its own system blocks and tools, each
// cached, cached turns, and a number too long for a float.
const zcodeAgentBody = `{"model":"GLM-5.3-Flash","max_tokens":32000,"stream":true,` +
`"system":[{"type":"text","text":"You are Claude Code.","cache_control":{"type":"ephemeral"}},{"type":"text","text":"Be brief & <exact>.","cache_control":{"type":"ephemeral","ttl":"1h"}}],` +
`"tools":[{"name":"Read","input_schema":{"type":"object"},"cache_control":{"type":"ephemeral"}}],` +
`"messages":[{"role":"user","content":"hello"},` +
`{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Read","input":{"n":12345678901234567890}}]},` +
`{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"ok","cache_control":{"type":"ephemeral"}},{"type":"text","text":"go on","cache_control":{"type":"ephemeral"}}]}]}`
// countCached counts the cache breakpoints anywhere in v.
func countCached(v any) int {
n := 0
switch x := v.(type) {
case map[string]any:
if x["cache_control"] != nil {
n++
}
for _, y := range x {
n += countCached(y)
}
case []any:
for _, y := range x {
n += countCached(y)
}
}
return n
}
// ZCode's Start Plan turns away a request without ZCode's own system
// prompt as unusual activity (#425, 405 / 3012), as zcode2api found. A
// Start Plan request reaches zcode.z.ai as the ZCode app sends it: its
// three cached system blocks before the agent's own, the day reminded
// before the first turn, at most four cache breakpoints, and the app's
// headers; the rest of the request as the agent sent it. A Coding Plan
// account's request goes as it is.
func TestZCodeStartSentAsTheApp(t *testing.T) {
signIn(t)
t.Setenv("LANG", "zh_CN.UTF-8")
t.Setenv("LC_ALL", "")
t.Setenv("LC_MESSAGES", "")
t.Setenv("TZ", "Asia/Shanghai")
jwt := zcodeTestJWT(time.Now().Add(24 * time.Hour))
u := newZCodeStartUpstream(t, jwt)
u.balance = zcodeActiveStart(time.Now(), "active")
send := func(k zcodeKey, body string) *http.Request {
t.Helper()
p := zcodeProvider("a@example.com", "", k)
req, _ := http.NewRequest("POST", p.Anthropic+"/v1/messages?beta=true", strings.NewReader(body))
req.Header.Set("User-Agent", "magpie/test")
if err := p.Sign(context.Background(), req, Anthropic, []byte(body)); err != nil {
t.Fatal(err)
}
return req
}
req := send(zcodeKey{Base: ZCodeZaiBase, JWT: jwt}, zcodeAgentBody)
res, err := http.DefaultClient.Do(req)
if err != nil || res.StatusCode != 200 {
t.Fatalf("sent: %v %v", err, res)
}
res.Body.Close()
h := u.model.Header
for k, want := range map[string]string{
"User-Agent": "ZCode/" + zcodeAppVersion + " ai-sdk/anthropic/3.0.81", "X-ZCode-App-Version": zcodeAppVersion,
"X-Title": "Z Code@electron", "X-ZCode-Agent": "glm", "X-ZCode-Session-Type": "main", "X-Release-Channel": "production",
"X-Client-Language": "zh-CN", "X-Client-Timezone": "Asia/Shanghai", "HTTP-Referer": zcodeAPI, "Authorization": "Bearer " + jwt,
} {
if got := h.Get(k); got != want {
t.Errorf("%s: %q, want %q", k, got, want)
}
}
for _, k := range []string{"X-Request-Id", "X-ZCode-Trace-Id", "X-Platform", "X-Os-Category", "X-Device-Mid"} {
if h.Get(k) == "" {
t.Errorf("no %s", k)
}
}
for _, k := range []string{"X-Session-Id", "X-Query-Id"} {
if h.Get(k) != "" {
t.Errorf("%s sent", k)
}
}
if bytes.Contains(u.body, []byte("\\u00"+"3c")) || !bytes.Contains(u.body, []byte(`12345678901234567890`)) {
t.Fatalf("body not kept as written: %s", u.body)
}
var got struct {
Model string `json:"model"`
MaxTokens float64 `json:"max_tokens"`
Stream bool `json:"stream"`
System []struct {
Text string `json:"text"`
Cache map[string]any `json:"cache_control"`
} `json:"system"`
Messages []struct {
Role string `json:"role"`
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
} `json:"messages"`
}
if err := json.Unmarshal(u.body, &got); err != nil {
t.Fatal(err)
}
if got.Model != "GLM-5.3-Flash" || got.MaxTokens != 32000 || !got.Stream || len(got.Messages) != 3 {
t.Fatalf("the request changed: %s", u.body)
}
if len(got.System) != 5 || got.System[0].Text != "You are ZCode, an interactive coding agent" ||
!strings.HasPrefix(got.System[1].Text, "\nYou are an interactive ZCode agent that helps users with software engineering tasks.") ||
!strings.Contains(got.System[1].Text, "# ZCode Desktop Context") ||
!strings.HasPrefix(got.System[2].Text, "\n\n# Communicating with the user") ||
!strings.Contains(got.System[2].Text, "\n# Environment\nYou have been invoked in the following environment:\n") ||
!strings.Contains(got.System[2].Text, "- You are powered by the model named bigmodel-api/glm-5.3-flash.") ||
!strings.HasSuffix(got.System[2].Text, "may have a different cause.") {
t.Fatalf("ZCode's system prompt: %s", u.body)
}
for i, s := range got.System {
if cached := s.Cache != nil; cached != (i < 3) {
t.Errorf("system block %d cached: %v", i, cached)
}
}
if got.System[3].Text != "You are Claude Code." || got.System[4].Text != "Be brief & <exact>." {
t.Fatalf("the agent's system prompt: %+v", got.System[3:])
}
first := got.Messages[0].Content
if len(first) != 2 || !strings.HasPrefix(first[0].Text, "<system-reminder>As you answer the user's questions") ||
!strings.Contains(first[0].Text, "# currentDate\nToday's date is "+time.Now().Format("2006-01-02")+".") || first[1].Text != "hello" {
t.Fatalf("first turn: %+v", first)
}
var all any
json.Unmarshal(u.body, &all)
if n := countCached(all); n != 4 {
t.Fatalf("%d cache breakpoints: %s", n, u.body)
}
last := all.(map[string]any)["messages"].([]any)[2].(map[string]any)["content"].([]any)
if last[0].(map[string]any)["cache_control"] != nil || last[1].(map[string]any)["cache_control"] == nil {
t.Fatalf("the last turn's breakpoint: %v", last)
}
// a request already ZCode's, and one with a reminder of its own, keep it
again := zcodeStartBody(u.body, time.Now())
if !bytes.Equal(again, u.body) {
t.Fatalf("shaped twice:\n%s\n%s", u.body, again)
}
own := `{"model":"GLM-5.2","system":"mine","messages":[{"role":"user","content":[{"type":"text","text":"<system-reminder>x</system-reminder>"},{"type":"text","text":"hi"}]}]}`
var o struct {
System []map[string]any `json:"system"`
Messages []struct {
Content []map[string]any `json:"content"`
} `json:"messages"`
}
json.Unmarshal(zcodeStartBody([]byte(own), time.Now()), &o)
if len(o.System) != 4 || o.System[3]["text"] != "mine" || len(o.Messages[0].Content) != 2 {
t.Fatalf("a string system and its own reminder: %+v", o)
}
// what isn't a messages request is left alone
for _, b := range []string{`not json`, `{"model":"GLM-5.2"}`} {
if got := zcodeStartBody([]byte(b), time.Now()); string(got) != b {
t.Fatalf("%s became %s", b, got)
}
}
// the GLM Coding Plan: the agent's request as it was, and none of the app's headers
u.plans = []any{map[string]any{"productName": "GLM Coding Lite", "status": "VALID"}}
zcodeRoutes.Lock()
zcodeRoutes.m = map[string]zcodeRoute{}
zcodeRoutes.Unlock()
req = send(zcodeKey{Key: "key.secret", Base: ZCodeZaiBase, JWT: jwt}, zcodeAgentBody)
if !strings.HasPrefix(req.URL.Path, "/api/anthropic/") {
t.Fatalf("coding plan went to %s", req.URL)
}
b, _ := io.ReadAll(req.Body)
if string(b) != zcodeAgentBody {
t.Fatalf("coding plan body changed: %s", b)
}
for _, k := range []string{"X-ZCode-Agent", "X-ZCode-Trace-Id", "X-Title", "X-Client-Language"} {
if req.Header.Get(k) != "" {
t.Errorf("coding plan sent %s", k)
}
}
if req.Header.Get("User-Agent") != "magpie/test" {
t.Errorf("coding plan User-Agent: %q", req.Header.Get("User-Agent"))
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"prefix": "You are ZCode, an interactive coding agent",
"stable": "\nYou are an interactive ZCode agent that helps users with software engineering tasks.\n\nIMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.\n\n# Harness\n- Text you output outside of tool use is displayed to the user as Github-flavored markdown in a terminal.\n- Tools run behind a user-selected permission mode; a denied call means the user declined it — adjust, don't retry verbatim.\n- The system may send updates, reminders, or modifications to rules via mid-conversation system turns. These are system-controlled, unlike function results. Hooks may intercept tool calls; treat hook output as user feedback.\n- Prefer the dedicated file/search tools over shell commands when one fits. Independent tool calls can run in parallel in one response.\n- Reference code as `file_path:line_number` — it's clickable.\n\n# ZCode Desktop Context\n\n### Files & URLs\n- Return local web URLs as Markdown links (e.g., [label](http://127.0.0.1:8080)).\n- File should be an absolute path or include the workspace folder segment so it can be resolved relative to the workspace.\n- Unless otherwise specified, return local file references as Markdown links (e.g., [name.md](/absolute/path/to/name.md)).\n\n### Inline Code Comments\n- Use the ::code-comment{...} directive when you need to attach feedback directly to specific code lines.\n- Emit one directive per inline comment; emit none when there are no actionable inline comments.\n- Required attributes: title (short label), body (one-paragraph explanation), file (path to the file).\n- Optional attributes: start, end (1-based line numbers), priority (0-3).\n- file should be an absolute path or include the workspace folder segment so it can be resolved relative to the workspace.\n- Keep line ranges tight; end defaults to start.\n- Example: ::code-comment{title=\"[P2] Off-by-one\" body=\"Loop iterates past the end when length is 0.\" file=\"/path/to/foo.ts\" start=10 end=11 priority=2}",
"beforeEnvironment": "# Communicating with the user\n\nYour text output is what the user reads; they usually can't see your thinking or the raw tool results. Write it for a teammate who stepped away and is catching up, not for a log file: they don't know the codenames or shorthand you created along the way, and they didn't watch your process unfold. Before your first tool call, say in a sentence what you're about to do; while working, give brief updates when you find something load-bearing or change direction.\n\nText you write between tool calls may not be shown to the user. Everything the user needs from this turn — answers, summaries, findings, conclusions, deliverables — must be in the final text message of your turn, with no tool calls after it. Keep text between tool calls to brief status notes. If something important appeared only mid-turn or in your thinking, restate it in that final message.\n\nLead with the outcome. Your first sentence after finishing should answer \"what happened\" or \"what did you find\" — the thing the user would ask for if they said \"just give me the TLDR.\" Supporting detail and reasoning come after, for readers who want them.\n\nBeing readable and being concise are different things, and readable matters more. If the user has to reread your summary or ask you to explain, any time saved by brevity is gone. The way to keep output short is to be selective about what you include (drop details that don't change what the reader would do next), not to compress the writing into fragments, abbreviations, arrow chains like `A → B → fails`, or jargon. What you do include, write in complete sentences with the technical terms spelled out. Don't make the reader cross-reference labels or numbering you invented earlier; say what you mean in place.\n\nMatch the response to the question: a simple question gets a direct answer in prose, not headers and sections. Use tables only for short enumerable facts, with explanations in the surrounding prose rather than the cells. Calibrate to the user — a bit tighter for an expert, more explanatory for someone newer.\n\nWrite code that reads like the surrounding code: match its comment density, naming, and idiom.\nOnly write a code comment to state a constraint the code itself can't show — never to say where it came from, what the next line does, or why your change is correct; that's you talking to the reviewer, not the next reader, and it's noise the moment the PR merges.\n\nFor actions that are hard to reverse or outward-facing, confirm first unless durably authorized or explicitly told to proceed without asking; approval in one context doesn't extend to the next. Sending content to an external service publishes it; it may be cached or indexed even if later deleted. Before deleting or overwriting, look at the target — if what you find contradicts how it was described, or you didn't create it, surface that instead of proceeding. Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.",
"afterEnvironment": "# Context management\nWhen the conversation grows long, some or all of the current context is summarized; the summary, along with any remaining unsummarized context, is provided in the next context window so work can continue — you don't need to wrap up early or hand off mid-task.\n\nWhen you have enough information to act, act. Do not re-derive facts already established in the conversation, re-litigate a decision the user has already made, or narrate options you will not pursue. If you are weighing a choice, give a recommendation, not an exhaustive survey\n\nYou are operating autonomously. The user is not watching in real time and cannot answer questions mid-task, so asking 'Want me to…?' or 'Shall I…?' will block the work. For reversible actions that follow from the original request, proceed without asking. Stop only for destructive actions or genuine scope changes the user must decide. Offering follow-ups after the task is done is fine; asking permission before doing the work is not.\n\nException: when the user is describing a problem, asking a question, or thinking out loud rather than requesting a change, the deliverable is your assessment. Report your findings and stop. Don't apply a fix until they ask for one.\n\nBefore ending your turn, check your last paragraph. If it is a plan, an analysis, a question, a list of next steps, or a promise about work you have not done ('I'll…', 'let me know when…'), do that work now with tool calls. That includes retrying after errors and gathering missing information yourself. Do not stop because the context or session is long. End your turn only when the task is complete or you are blocked on input only the user can provide.\n\nBefore running a command that changes system state — restarts, deletes, config edits — check that the evidence actually supports that specific action. A signal that pattern-matches to a known failure may have a different cause.",
"environment": {
"heading": "# Environment",
"invokedLine": "You have been invoked in the following environment:",
"cwdLabel": "Primary working directory",
"gitLabel": "Is a git repository",
"gitNo": "no",
"platformLabel": "Platform",
"shellLabel": "Shell",
"osVersionLabel": "OS Version",
"poweredByLine": "- You are powered by the model named {provider}/{model}."
},
"context": {
"intro": "As you answer the user's questions, you can use the following context:",
"outro": " IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.",
"currentDateHeading": "# currentDate",
"currentDateLine": "Today's date is {date}."
}
}
+66 -9
View File
@@ -16,13 +16,16 @@ package provider
// key at all; to the Coding Plan otherwise, as before.
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"strconv"
@@ -52,9 +55,10 @@ var zcodeStartModels = func() []catalog.Model {
// due to unusual activity" (HTTP 405, code 3012 "method not allowed")
// means (#425): zcode.z.ai looks at what a request carries and turns away
// one that isn't the ZCode app's own, its system prompt and all, as
// zcode2api found; it can still be Alibaba Cloud's firewall blocking the
// address (BlockedHint). magpie sends the agent's request as it is.
const ZCodeStartBlockedHint = "ZCode's Start Plan turns away requests that don't come from the ZCode app itself, and magpie doesn't pretend to be it; it can also be a network block of this IP. Use an account with a GLM Coding Plan, or add another provider to this group"
// zcode2api found, so magpie sends it as the app does (zcode_client.go).
// One turned away still is Alibaba Cloud's firewall blocking the address
// (BlockedHint), or ZCode checking for something magpie doesn't send.
const ZCodeStartBlockedHint = "ZCode's Start Plan still turned this request away, though magpie sends it as the ZCode app does; it can be a network block of this IP, or ZCode checking for something new. Use an account with a GLM Coding Plan, or add another provider to this group"
// zcodeStartRefused matches that refusal: the block page, or its code.
var zcodeStartRefused = regexp.MustCompile(`(?i)unusual activity|"code"\s*:\s*"?3012\b`)
@@ -142,24 +146,77 @@ func zcodeRebase(req *http.Request, from, to string) {
}
// zcodeSourceHeaders are the headers ZCode names itself with on a model
// request.
// request to the Start Plan (zcode_client.go), as zcode2api found them
// (src/upstream/headers.js): the app, its agent and release, the
// machine, the language and time zone, a new request and trace id, and
// no query or session id.
func zcodeSourceHeaders(req *http.Request) {
platform := runtime.GOOS
if platform == "windows" {
platform = "win32"
}
platform := zcodePlatform()
arch := runtime.GOARCH
if arch == "amd64" {
arch = "x64"
}
req.Header.Set("User-Agent", "ZCode/"+zcodeAppVersion)
category := map[string]string{"darwin": "macos", "win32": "windows"}[platform]
if category == "" {
category = "linux"
}
req.Header.Set("User-Agent", "ZCode/"+zcodeAppVersion+" ai-sdk/anthropic/3.0.81")
req.Header.Set("X-ZCode-App-Version", zcodeAppVersion)
req.Header.Set("X-Title", "Z Code@electron")
req.Header.Set("X-ZCode-Agent", "glm")
req.Header.Set("HTTP-Referer", zcodeAPI)
req.Header.Set("X-Platform", platform+"-"+arch)
req.Header.Set("X-Os-Category", category)
req.Header.Set("X-Release-Channel", "production")
req.Header.Set("X-Client-Language", zcodeLanguage())
if tz := zcodeTimezone(); tz != "" {
req.Header.Set("X-Client-Timezone", tz)
}
req.Header.Set("X-Request-Id", randomUUID())
req.Header.Set("X-ZCode-Session-Type", "main")
req.Header.Set("X-ZCode-Trace-Id", randomUUID())
zcodeDeviceHeader(req)
}
// zcodeLanguage is the user's language as ZCode gives it (zh-CN, en-US).
func zcodeLanguage() string {
for _, k := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} {
v, _, _ := strings.Cut(os.Getenv(k), ".")
if v != "" && v != "C" && v != "POSIX" {
return strings.ReplaceAll(v, "_", "-")
}
}
return "en-US"
}
// zcodeTimezone is the IANA name of the local time zone, "" when it can't
// be told.
func zcodeTimezone() string {
if tz := os.Getenv("TZ"); tz != "" && !strings.HasPrefix(tz, ":") {
return tz
}
if l, err := os.Readlink("/etc/localtime"); err == nil {
if _, name, ok := strings.Cut(l, "zoneinfo/"); ok {
return name
}
}
return ""
}
// zcodeStartRequest makes req, a request to the Start Plan, ZCode's own:
// its headers, and a model request's body shaped as ZCode's
// (zcodeStartBody).
func zcodeStartRequest(req *http.Request, body []byte) {
zcodeSourceHeaders(req)
if len(body) == 0 || !strings.HasSuffix(req.URL.Path, "/v1/messages") {
return
}
nb := zcodeStartBody(body, time.Now())
req.Body = io.NopCloser(bytes.NewReader(nb))
req.ContentLength = int64(len(nb))
req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(nb)), nil }
}
// zcodeJWTExpired says whether ZCode's token has run out; ZCode then asks
// to sign in again, and so does magpie.
func zcodeJWTExpired(jwt string) bool {
+3
View File
@@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
@@ -29,6 +30,7 @@ type zcodeStartUpstream struct {
balance map[string]any
jwt string
model *http.Request // the last request to the Start Plan's endpoint
body []byte // and its body
}
func newZCodeStartUpstream(t *testing.T, jwt string) *zcodeStartUpstream {
@@ -66,6 +68,7 @@ func newZCodeStartUpstream(t *testing.T, jwt string) *zcodeStartUpstream {
ok(u.balance)
case strings.HasPrefix(r.URL.Path, "/api/v1/zcode-plan/anthropic/"):
u.model = r
u.body, _ = io.ReadAll(r.Body)
w.Write([]byte(`{}`))
default:
w.WriteHeader(404)