Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- GPT 槽在 TTL(10 分钟)内有可用 credits(`has_credits || unlimited`)时仅豁免窗口 cap;显式 park 仍优先;上游 429 清除 credits 并 park。(#508)
- 修复 KVM 交互终端:交互/非 detach 不再包 `timeout 8`(仅保留 SSH ConnectTimeout);`-e` 按次独立转义,保留值内换行(含 `KIN_PANEL_RC`)。(#509)
- Claude Desktop local-agent(UA 含 local-agent 与 agent-sdk、平台 desktop_app、合法 anthropic-client-version)不再仅凭 cc_entrypoint=local-agent 判成 OMP,避免 claude_code_only 误 403;OMP 专属与 body-only relay 仍保守。(#511)

## 1.3.187 — 2026-10-17

Expand Down
2 changes: 1 addition & 1 deletion docs/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
| UA | `^claude-cli/\d+\.\d+\.\d+`(sub2api 同款前缀;vscode / cowork / desktop 都算) |
| `metadata.user_id` | 合法官方 user id |
| tools | 不能是 OpenAI `{type:function}` / `tool_choice.type=function`。`/v1/messages` 上这种 tools 会先转 Anthropic,但仍判非官方 |
| system | 含 `x-anthropic-billing-header:`、官方 `cc_entrypoint`(`cli` / `sdk-cli` / `vscode` / `cowork` / `desktop`,不含 `local-agent`)、或官方身份行(含 `You are Claude Code` / `Claude Agent SDK` 变体) |
| system | 含 `x-anthropic-billing-header:`、官方 `cc_entrypoint`(`cli` / `sdk-cli` / `vscode` / `cowork` / `desktop`)、或官方身份行(含 `You are Claude Code` / `Claude Agent SDK` 变体)。`local-agent` 仅在 UA 含对应入口和 Agent SDK、平台为 `desktop_app` 且客户端版本有效时继续原有官方校验;OMP 专属特征仍优先排除 |

四闸不全 → 非官方,走人设。虚拟机测试入站会先铺 4 块官方 system + 官方 UA,因此分类为 `claude_code_official`。

Expand Down
2 changes: 1 addition & 1 deletion src/lib/identity/claude-code-only.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function isCliQuotaProbe(headers, body) {
* Relayed Claude Code (UA rewritten) is admitted only while `detect_proxied_official_cc` is on.
*/
export function isClaudeCodeFamilyRequest(headers = {}, body = {}, { routing = {}, countTokens = false } = {}) {
if (looksLikeOhMyPiSystem(body?.system) || inboundHasOpenAIToolShape(body)) return false
if (looksLikeOhMyPiSystem(body?.system, headers) || inboundHasOpenAIToolShape(body)) return false
if (hasClaudeCodeClientHeaders(headers)) {
if (countTokens || isCliQuotaProbe(headers, body)) return true
if (
Expand Down
23 changes: 15 additions & 8 deletions src/lib/identity/crs-persona.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1590,13 +1590,20 @@ export function isValidOfficialUserId(raw) {
/**
* oh-my-pi / omp inbound: their own ROLE + PROJECT blocks, or the OAuth
* stealth prefix (`cc_entrypoint=local-agent` + Agent SDK one-liner).
* That prefix reuses the official identity string, so billing/identity
* alone must not classify this as Claude Code.
* Claude Desktop shares that entrypoint, so its desktop headers let it continue
* through the existing official checks; OMP-specific system markers still reject it.
*/
export function looksLikeOhMyPiSystem(system) {
export function looksLikeOhMyPiSystem(system, headers = {}) {
const text = systemToText(system)
if (!text) return false
if (/cc_entrypoint\s*=\s*local-agent/i.test(text)) return true
const ua = String(headers['user-agent'] || headers['User-Agent'] || '').trim()
const desktop =
isOfficialClaudeUa(ua) &&
/[ (,]local-agent(?=[,)])/.test(ua) &&
/[ (,]agent-sdk\/\d+\.\d+\.\d+(?=[,)])/.test(ua) &&
headers['anthropic-client-platform'] === 'desktop_app' &&
/^\d+\.\d+\.\d+$/.test(String(headers['anthropic-client-version'] || '').trim())
if (/cc_entrypoint\s*=\s*local-agent/i.test(text) && !desktop) return true
if (/<system-conventions>/i.test(text) && /Oh My Pi/i.test(text)) return true
if (/<system-conventions>/i.test(text) && /<workstation>/i.test(text)) return true
if (/^ROLE\s*\n\s*=+/m.test(text) && /^PROJECT\s*\n\s*=+/m.test(text)) return true
Expand All @@ -1614,8 +1621,8 @@ export function thirdPartyClientPrompt(system) {
}

/** Official CLI system: billing header, official entrypoint, identity, or child-hop prompt. */
export function looksLikeOfficialClaudeSystem(system) {
if (looksLikeOhMyPiSystem(system)) return false
export function looksLikeOfficialClaudeSystem(system, headers = {}) {
if (looksLikeOhMyPiSystem(system, headers)) return false
const text = systemToText(system)
if (!text) return false
if (/x-anthropic-billing-header:/i.test(text)) return true
Expand All @@ -1642,10 +1649,10 @@ export function inboundHasOpenAIToolShape(body = {}) {
export function isOfficialClaudeCodeTraffic(headers = {}, body = {}) {
const ua = String(headers['user-agent'] || headers['User-Agent'] || '').trim()
if (!isOfficialClaudeUa(ua) || !CLAUDE_CLI_UA_RE.test(ua)) return false
if (looksLikeOhMyPiSystem(body?.system)) return false
if (looksLikeOhMyPiSystem(body?.system, headers)) return false
if (!isValidOfficialUserId(body?.metadata?.user_id)) return false
if (inboundHasOpenAIToolShape(body)) return false
return looksLikeOfficialClaudeSystem(body?.system)
return looksLikeOfficialClaudeSystem(body?.system, headers)
}

/**
Expand Down
74 changes: 74 additions & 0 deletions test/unit/claude-code-only.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { isClaudeCodeFamilyRequest } from '../../src/lib/identity/claude-code-on
import { createHandleProtocol } from '../../src/lib/protocol/handle-protocol.mjs'
import { handleUserCountTokens } from '../../src/lib/protocol/user-count-tokens.mjs'
import { validatePersonaRoutingPatch } from '../../src/lib/admin/panel-api.mjs'
import {
applyCrsUnofficialPersona,
isOfficialClaudeCodeTraffic,
isProxiedOfficialClaudeCode,
} from '../../src/lib/identity/crs-persona.mjs'

const CLI_UA = 'claude-cli/2.1.293 (external, cli)'
const SESSION = '11111111-2222-4333-8444-555555555555'
Expand Down Expand Up @@ -40,6 +45,68 @@ const ON = { compatibility: { claude_code_only: true } }

const NO_RELAY = { routing: { compatibility: { detect_proxied_official_cc: false } } }

const DESKTOP_HEADERS = {
...CLI_HEADERS,
'user-agent': 'claude-cli/2.1.299 (external, local-agent, agent-sdk/0.3.299)',
'anthropic-client-platform': 'desktop_app',
'anthropic-client-version': '2.42304.0',
'x-stainless-package-version': '0.128.0',
}
const DESKTOP_BODY = {
...OFFICIAL_BODY,
system: [
{ type: 'text', text: 'x-anthropic-billing-header: cc_version=2.1.299.fa6; cc_entrypoint=local-agent;' },
{ type: 'text', text: "You are Claude Code, built on Anthropic's Claude Agent SDK." },
],
}

test('local-agent with desktop markers passes the existing Claude Code checks', () => {
assert.equal(isClaudeCodeFamilyRequest(DESKTOP_HEADERS, DESKTOP_BODY, NO_RELAY), true)
assert.equal(isClaudeCodeFamilyRequest(DESKTOP_HEADERS, DESKTOP_BODY), true)
assert.equal(isOfficialClaudeCodeTraffic(DESKTOP_HEADERS, DESKTOP_BODY), true)
assert.equal(applyCrsUnofficialPersona(DESKTOP_BODY, { headers: DESKTOP_HEADERS, mode: 'rewrite' }), DESKTOP_BODY)
assert.equal(isProxiedOfficialClaudeCode(DESKTOP_BODY, DESKTOP_HEADERS), false)
})

test('local-agent still rejects incomplete headers, inconsistent sessions/builds, and OMP', () => {
for (const key of Object.keys(DESKTOP_HEADERS)) {
const { [key]: dropped, ...headers } = DESKTOP_HEADERS
assert.equal(isClaudeCodeFamilyRequest(headers, DESKTOP_BODY), false, key)
}
for (const headers of [
{ ...DESKTOP_HEADERS, 'anthropic-client-platform': 'web' },
{ ...DESKTOP_HEADERS, 'anthropic-client-version': 'unknown' },
{ ...DESKTOP_HEADERS, 'user-agent': 'claude-cli/2.1.299 (external, cli)' },
{ ...DESKTOP_HEADERS, 'user-agent': 'claude-cli/2.1.299 (external, local-agent)' },
{ ...DESKTOP_HEADERS, 'user-agent': 'Go-http-client/1.1' },
{ ...DESKTOP_HEADERS, 'x-claude-code-session-id': '99999999-2222-4333-8444-555555555555' },
{ ...DESKTOP_HEADERS, 'user-agent': 'claude-cli/2.1.280 (external, local-agent, agent-sdk/0.3.280)' },
]) {
assert.equal(isClaudeCodeFamilyRequest(headers, DESKTOP_BODY), false)
}
assert.equal(isClaudeCodeFamilyRequest(DESKTOP_HEADERS, { ...DESKTOP_BODY, metadata: undefined }), false)
for (const text of [
'You are operating in the Oh My Pi coding harness.',
'<system-conventions>rules</system-conventions>\n<workstation>test</workstation>',
'ROLE\n====\nAssistant\nPROJECT\n====\nWorkspace',
'You are operating in the pi coding harness.',
'You are operating in the omp coding harness.',
]) {
const body = { ...DESKTOP_BODY, system: [...DESKTOP_BODY.system, { type: 'text', text }] }
assert.equal(isClaudeCodeFamilyRequest(DESKTOP_HEADERS, body), false, text)
assert.equal(isClaudeCodeFamilyRequest(DESKTOP_HEADERS, body, { countTokens: true }), false, text)
assert.equal(isOfficialClaudeCodeTraffic(DESKTOP_HEADERS, body), false, text)
}
const tools = [{ type: 'function', function: { name: 'read_file' } }]
assert.equal(isClaudeCodeFamilyRequest(DESKTOP_HEADERS, { ...DESKTOP_BODY, tools }), false)
})

test('local-agent desktop count_tokens follows the existing header-only admission contract', () => {
const body = { model: DESKTOP_BODY.model, system: DESKTOP_BODY.system, messages: DESKTOP_BODY.messages }
assert.equal(isClaudeCodeFamilyRequest(DESKTOP_HEADERS, body, { countTokens: true }), true)
assert.equal(isClaudeCodeFamilyRequest(DESKTOP_HEADERS, body), false)
})

test('Claude Code family recognition needs the full client fingerprint, not the UA', () => {
assert.equal(isClaudeCodeFamilyRequest(CLI_HEADERS, OFFICIAL_BODY, NO_RELAY), true)
// UA plus a copied official body is not enough without the client header set.
Expand Down Expand Up @@ -221,6 +288,13 @@ test('switch on: official Claude Code and its own quota probe reach the schedule
assert.deepEqual(await runProtocol({ routing: ON, body: essay, headers: CLI_HEADERS }), BLOCKED)
})

test('switch on: local-agent desktop reaches the scheduler without relay detection', async () => {
const routing = { compatibility: { claude_code_only: true, detect_proxied_official_cc: false } }
assert.equal((await runProtocol({ routing, body: DESKTOP_BODY, headers: DESKTOP_HEADERS })).reached, true)
const body = { ...DESKTOP_BODY, system: [...DESKTOP_BODY.system, { type: 'text', text: 'Oh My Pi coding harness' }] }
assert.deepEqual(await runProtocol({ routing, body, headers: DESKTOP_HEADERS }), BLOCKED)
})

test('switch on: master-key pinned panel diagnostics are not inbound clients', async () => {
const result = await runProtocol({
routing: ON,
Expand Down
Loading