Files
dingtalk-workspace-cli/internal/auth/agent_code_detect.go
2026-07-01 10:16:59 +08:00

165 lines
6.9 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// agent_code_detect.go resolves the agent_code — which agent HOST is driving
// dws (claudecode / qoder / cursor / vscode / openclaw / hermes / ...). It
// fills the x-dingtalk-dws-agent-code header for per-channel statistics.
//
// SEPARATE axis from DWS_CHANNEL / x-dws-channel (a distribution channel code);
// the two are never conflated here.
//
// Design contract — ACCURACY OVER COVERAGE, but maximize accurate coverage:
// - Prefer generalizable, host-declared signals so one rule covers a whole
// family (VSCODE_BRAND covers every VS Code fork, present and future).
// - Every per-host signature below is OBSERVED on a real host (live process
// env via `ps eww`, or the app bundle Info.plist), not guessed.
// - Anything unidentified stays empty — never guess or synthesize a PAT key.
// - Deliberately NOT used: TERM_PROGRAM (reports the terminal, e.g. iTerm,
// not the agent host) and fuzzy parent-process name matching.
package auth
import (
"os"
"strings"
)
// AgentCodeCustom is the literal code a host may explicitly declare for a
// custom integration. It is not used as an implicit fallback.
const AgentCodeCustom = "custom"
// hostSignature is a verified env fingerprint for a known agent host. EnvKeys
// match when any listed key is present and non-empty.
type hostSignature struct {
Code string
EnvKeys []string
}
// knownSignatures: CLI / daemon agents that inject a distinctive env var, which
// the dws subprocess they spawn inherits. All verified on a real machine
// (2026-06-16) via live process env / launch env — not guessed.
var knownSignatures = []hostSignature{
// Claude Code — verified: CLAUDECODE=1, CLAUDE_CODE_ENTRYPOINT=cli.
{Code: "claudecode", EnvKeys: []string{"CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"}},
// OpenClaw — verified on the running daemon: OPENCLAW_BUNDLE_ROOT.
{Code: "openclaw", EnvKeys: []string{"OPENCLAW_BUNDLE_ROOT", "OPENCLAW_RUNTIME_ROLE"}},
// Hermes — verified on the running gateway: HERMES_HOME.
{Code: "hermes", EnvKeys: []string{"HERMES_HOME"}},
// OpenAI Codex — CODEX_SANDBOX is auto-set by Codex for the subprocesses it
// spawns (e.g. CODEX_SANDBOX=seatbelt on macOS), and Codex filters this
// CODEX_-prefixed name out of user .env to prevent spoofing — so its
// presence reliably means "running under Codex".
// Source: developers.openai.com/codex/concepts/sandboxing
{Code: "codex", EnvKeys: []string{"CODEX_SANDBOX"}},
}
// NOTE on coverage limits (honest, not a TODO to silently ignore):
// Most terminal agents (gemini-cli/antigravity, aider, opencode, qwen-code,
// crush, goose, kimi, amazon-q, continue, ...) expose NO reliable
// self-identifying env marker — only user-set API-key/config vars, which we
// must not key off (a user setting GEMINI_API_KEY is not "running under
// gemini"). They therefore resolve to empty unless they declare themselves.
//
// The authoritative, fully-general path to 100% coverage is the T0 declaration
// contract: a host sets DINGTALK_DWS_AGENTCODE=<code> when it launches dws.
// That is accurate for ANY agent (present or future) on ANY OS, and is what an
// integrating host should wire up. Auto-detection (signatures / VSCODE_BRAND /
// bundle id) is a best-effort supplement for hosts that have not declared.
// bundleIDToCode maps macOS app bundle identifiers to agent codes. The bundle
// id is exposed via __CFBundleIdentifier and inherited by child processes the
// IDE spawns (including dws), so it identifies the host even from an integrated
// terminal. Verified from each app's Info.plist (2026-06-16). Only known agent
// bundles map; everything else (iTerm, Terminal, ...) falls through to empty.
//
// macOS-only signal: __CFBundleIdentifier does not exist on Linux/Windows, so
// this map is simply a no-op there (os.Getenv returns "").
var bundleIDToCode = map[string]string{
"com.qoder.ide": "qoder",
"com.todesktop.230313mzl4w4u92": "cursor", // Cursor's ToDesktop bundle id
"com.microsoft.VSCode": "vscode",
"com.workbuddy.workbuddy": "workbuddy",
}
// DetectAgentCode resolves the agent_code via a confidence ladder and returns
// the normalized code plus the signal that decided it:
//
// T0 explicit host declaration (DINGTALK_DWS_AGENTCODE — dedicated field)
// T1 verified per-agent env signature (CLI/daemon agents)
// T2 VSCODE_BRAND value (every VS Code fork declares its brand)
// T3 macOS app bundle id (known agent bundles only)
// T4 unresolved -> empty (never guess)
func DetectAgentCode() (code string, signal string) {
// T0: host explicitly declares its agent_code — highest confidence.
if v, name := AgentCodeFromEnv(); v != "" {
return v, "env:" + name
}
// T1: verified per-agent env signature (most specific — wins over the IDE
// it may be running inside).
for _, sig := range knownSignatures {
for _, k := range sig.EnvKeys {
if strings.TrimSpace(os.Getenv(k)) != "" {
return sig.Code, "sig:" + k
}
}
}
// T2: VS Code fork family. The brand value IS the host's self-declaration,
// so this single rule covers Qoder/Cursor/VS Code/Windsurf/Trae/Kiro/... —
// including forks that don't exist yet.
if b := strings.TrimSpace(os.Getenv("VSCODE_BRAND")); b != "" {
return normalizeAgentCode(b), "env:VSCODE_BRAND"
}
// T3: macOS app bundle id (known agent bundles only).
if id := strings.TrimSpace(os.Getenv("__CFBundleIdentifier")); id != "" {
if c, ok := bundleIDToCode[id]; ok {
return c, "bundle:" + id
}
}
// T4: unknown host — leave agent_code empty, no guessing.
return "", ""
}
// normalizeAgentCode maps host-declared names/brands to canonical agent_code
// values. Unrecognized but non-empty input is lowercased, space-stripped and
// kept as-is — still a host declaration, so still accurate (this is what gives
// automatic coverage of new VS Code forks via VSCODE_BRAND).
func normalizeAgentCode(raw string) string {
s := strings.ToLower(strings.TrimSpace(raw))
s = strings.ReplaceAll(s, " ", "")
switch s {
case "":
return ""
case "claude", "claude-code", "claude_code", "claudecode":
return "claudecode"
case "qoder":
return "qoder"
case "qoderwork":
return "QoderWork"
case "workbuddy", "work-buddy":
return "workbuddy"
case "visualstudiocode", "code", "code-oss", "vscode":
return "vscode"
case "cursor":
return "cursor"
case "windsurf":
return "windsurf"
case "trae", "traecn":
return "trae"
default:
return s
}
}