Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd6e854bf1 | ||
|
|
61124f8768 | ||
|
|
6cfeac3179 | ||
|
|
acd293cc83 | ||
|
|
d2045c3441 | ||
|
|
4de41c27c7 | ||
|
|
5df2860e66 | ||
|
|
f3390b6875 | ||
|
|
55f25d8d48 | ||
|
|
67fbf65916 | ||
|
|
1fb1ae3e23 | ||
|
|
1d1c06aaae | ||
|
|
a54ee24acb | ||
|
|
320582f98c | ||
|
|
c569def067 | ||
|
|
125a101487 |
@@ -6,6 +6,24 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.56-beta.2] - 2026-07-30
|
||||
|
||||
This beta adds PRs #831 and #835 on top of v1.0.56-beta.1. It separates
|
||||
Agent Product observability and IM display identity from the stable
|
||||
edition-owned PAT and routing identity, and reduces common-path Skill context
|
||||
loading without changing the public command or Runtime Schema surface.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Agent Product identity separation** (#831) — sends `DWS_AGENT_PRODUCT` through the new `x-dws-agent-product` observability Header and uses a valid non-empty value for the IM `clawType` display label whenever `--ai-tag` is enabled. Because `--ai-tag` defaults to `true`, callers that set `DWS_AGENT_PRODUCT` change the displayed label by default. With `--ai-tag=false`, native `chat message send` / `reply` calls preserve their existing wire shape by sending an empty IM `clawType`, while shortcut calls omit the argument. Unset or empty Product values omit the Header and preserve the active edition's IM display default.
|
||||
- **Agent Host dimension convention** (#831) — new integrations should send the runtime form (`cloud` or `desktop`) through `DWS_AGENT_HOST` and report the product separately through `DWS_AGENT_PRODUCT`. Legacy combined labels such as `qwenwork_cloud` remain syntactically valid for compatibility.
|
||||
- **Reduced common-path Skill context** (#835) — keeps the complete 97-command Chat Shortcut inventory in Runtime Catalog and leaf Schema while routing common intents through compact Skill tables and references. When an exact command path is already known, the mono Skill no longer requires eager loading of a complete product reference. The generated Skill policy now detects drift, forced full-reference loading, and context-budget regressions; the common Chat plus shared activation estimate drops from 7,301 to 4,771 `o200k_base` tokens without changing the 845-tool Schema surface.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Stable PAT/routing identity** (#831) — restores the CLI-emitted open-source HTTP `claw-type` and PAT `hostControl.clawType` to the edition-fixed `openClaw` value. `DWS_AGENT_PRODUCT` no longer changes those wire values, and the client continues to derive PAT, authentication, routing, and Discovery behaviour from the existing independent signals.
|
||||
- **Portable generated Skill validation** (#835) — resolves the mono Skill name by scanning upward from the generated target, keeping `--check` independent of the repository checkout path and preventing false drift failures when an ancestor directory resembles a Skill name.
|
||||
|
||||
## [1.0.56-beta.1] - 2026-07-30
|
||||
|
||||
This beta starts the v1.0.56 line on top of v1.0.55 and packages PRs #817,
|
||||
|
||||
+47
-25
@@ -5,8 +5,8 @@
|
||||
| Variable | Purpose / 用途 |
|
||||
|---------|---------|
|
||||
| `DWS_CONFIG_DIR` | Override default config directory / 覆盖默认配置目录 |
|
||||
| `DWS_AGENT_PRODUCT` | Optional, caller-declared Agent product sent through the existing HTTP `claw-type` header (for example `qwenwork`). Surrounding ASCII spaces/tabs are trimmed; the remaining value must be at most 64 bytes and match `^[A-Za-z0-9][A-Za-z0-9_-]*$`. Unset or empty values preserve the edition default (`openClaw` in the open-source build). / 可选、由调用方声明的 Agent 产品标识,经校验后覆盖 HTTP `claw-type` 请求头;未设置或为空时保持当前发行版默认值 |
|
||||
| `DWS_AGENT_HOST` | Optional, caller-declared Agent runtime form sent as `x-dws-agent-host` (for example `cloud` or `desktop`). Surrounding ASCII spaces/tabs are trimmed; the remaining value must be at most 64 bytes and match `^[a-z0-9][a-z0-9_-]*$`; unset values are omitted. / 可选、由调用方声明的 Agent 运行形态,经校验后作为 `x-dws-agent-host` 发送;未设置时省略 |
|
||||
| `DWS_AGENT_PRODUCT` | Optional, caller-declared Agent product sent as `x-dws-agent-product` (for example `qwenwork`) for downstream logs/BI and used as the IM `clawType` display label when `--ai-tag` is enabled. `--ai-tag` defaults to `true`, so a configured Product changes the displayed label by default. With `--ai-tag=false`, native `chat message send` / `reply` calls send an empty `clawType`, while shortcut calls omit the argument. Surrounding ASCII spaces/tabs are trimmed; the remaining value must be at most 64 bytes and match `^[A-Za-z0-9][A-Za-z0-9_-]*$`. Unset or empty values omit the Header and use the edition's IM display default. This client never uses Product to change the separate HTTP `claw-type` PAT/routing label. / 可选、由调用方声明的 Agent 产品标识,经校验后作为 `x-dws-agent-product` 发送,并用于 IM 小尾巴;`--ai-tag` 默认为 `true`,因此配置 Product 后默认会改变展示标签。使用 `--ai-tag=false` 时,原生 `chat message send` / `reply` 发送空的 `clawType`,shortcut 调用则省略该参数。未设置时省略请求头且 IM 使用发行版默认值;本客户端不会用 Product 修改独立的 HTTP `claw-type` |
|
||||
| `DWS_AGENT_HOST` | Optional, caller-declared Agent runtime form sent as `x-dws-agent-host` (for example `cloud` or `desktop`) for downstream logs/BI. Surrounding ASCII spaces/tabs are trimmed; the remaining value must be at most 64 bytes and match `^[a-z0-9][a-z0-9_-]*$`; unset values are omitted. This client does not use Host for PAT, authentication, Discovery, or MCP endpoint selection. / 可选、由调用方声明的 Agent 运行形态,经校验后作为 `x-dws-agent-host` 发送给下游日志/BI;本客户端不使用该值进行 PAT、鉴权、Discovery 或 MCP 端点选择,未设置时省略 |
|
||||
| `DWS_<PRODUCT>_MCP_URL` | Override a product MCP endpoint for local development / 本地开发时覆盖指定产品 MCP endpoint |
|
||||
| `DWS_CLIENT_ID` | OAuth client ID (DingTalk AppKey) |
|
||||
| `DWS_CLIENT_SECRET` | OAuth client secret (DingTalk AppSecret) |
|
||||
@@ -14,23 +14,36 @@
|
||||
| `DWS_ALLOW_HTTP_ENDPOINTS` | Set `1` to allow HTTP for loopback during dev / 设为 `1` 允许回环地址 HTTP,仅用于开发调试 |
|
||||
| `DWS_DISABLE_KEYCHAIN` | macOS only. Set `1` to skip system Keychain for the encryption key and use file-based storage (same scheme as Linux). For sandboxed runtimes (e.g. Codex App) that block Keychain APIs. Weakens at-rest protection — DEK and ciphertext live in the same directory. / 仅 macOS。设为 `1` 时跳过系统 Keychain,密钥以文件形式存储(与 Linux 一致)。用于 Keychain API 被拦截的沙盒环境(如 Codex App)。代价是 DEK 与密文同目录,保护强度低于默认方案 |
|
||||
|
||||
### Agent Product and Host trust model / Agent 产品与运行形态的信任模型
|
||||
### Agent Product, Host, and `claw-type` / Agent 产品、运行形态与 `claw-type`
|
||||
|
||||
`DWS_AGENT_PRODUCT` and `DWS_AGENT_HOST` are caller-declared selection and
|
||||
observation signals. They are not credentials, attestations, or proof of the
|
||||
calling host's identity. DingTalk services may record them for logs/BI and may
|
||||
combine supported values with separately authenticated context for PAT
|
||||
compatibility, PAT identity/source derivation, or Discovery eligibility. A
|
||||
service must allowlist supported values and must never grant access, bypass
|
||||
authentication, or skip authorization solely because either Header claims a
|
||||
particular product or runtime form. They are not used to select ordinary MCP
|
||||
tool endpoints.
|
||||
`DWS_AGENT_PRODUCT` and `DWS_AGENT_HOST` are caller-declared observation
|
||||
signals. They are not credentials, attestations, or proof of the calling
|
||||
host's identity. The CLI validates and emits `x-dws-agent-product` and
|
||||
`x-dws-agent-host`, but does not use either value to derive its authentication,
|
||||
PAT mode, Discovery behaviour, or ordinary MCP endpoint selection. Downstream
|
||||
services own and must document their own contracts for these caller-declared
|
||||
Headers.
|
||||
|
||||
`DWS_AGENT_PRODUCT` controls only the HTTP `claw-type` Header. The similarly
|
||||
named `clawType` tool argument on IM send operations is an independent
|
||||
message-display axis used for the “Send from AI” label. It remains controlled
|
||||
by the active edition's `ClawTypeValue` and `--ai-tag`; changing
|
||||
`DWS_AGENT_PRODUCT` does not change that message label.
|
||||
Service integrators should treat both Headers as untrusted input, allowlist
|
||||
expected values, and should not grant access, bypass authentication, or skip
|
||||
authorization solely because a Header claims a particular Product or Host.
|
||||
|
||||
The HTTP `claw-type` Header is a separate, edition-fixed PAT/routing label:
|
||||
`openClaw` in the open-source build. `DWS_AGENT_PRODUCT` never changes it or
|
||||
PAT `hostControl.clawType`. On IM send/reply operations with `--ai-tag`,
|
||||
however, a valid non-empty Product value is used as the `clawType` tool
|
||||
argument so the delivered message carries the matching “Send from AI” label.
|
||||
Because `--ai-tag` defaults to `true`, this display change is enabled by
|
||||
default for callers that set Product. With `--ai-tag=false`, native
|
||||
`chat message send` / `reply` calls serialize `clawType: ""`, while shortcut
|
||||
calls omit the argument; this client does not assume downstream services treat
|
||||
an empty value and an absent key as equivalent. The display-value precedence
|
||||
when the tag is enabled is valid non-empty `DWS_AGENT_PRODUCT`, then the active
|
||||
edition's `ClawTypeValue`, then `openClaw`.
|
||||
|
||||
Do not set arbitrary Product values that the target downstream and IM services
|
||||
have not explicitly enabled; an unknown value may be ignored or may not render
|
||||
the expected label.
|
||||
|
||||
For QwenWork, report the dimensions separately:
|
||||
|
||||
@@ -39,16 +52,25 @@ DWS_AGENT_PRODUCT=qwenwork
|
||||
DWS_AGENT_HOST=cloud # or desktop
|
||||
```
|
||||
|
||||
Do not set arbitrary product values that the target service has not explicitly
|
||||
enabled. Older combined Host labels such as `qwenwork_cloud` still satisfy the
|
||||
generic syntax for compatibility, but new integrations should use the
|
||||
two-dimensional convention above.
|
||||
Older combined Host labels such as `qwenwork_cloud` still satisfy the generic
|
||||
syntax for compatibility, but new integrations should use the two-dimensional
|
||||
convention above.
|
||||
|
||||
`DWS_AGENT_PRODUCT` 和 `DWS_AGENT_HOST` 均由调用方声明,不是认证凭据,也不能证明
|
||||
真实宿主身份。服务端可以在独立认证上下文中将受支持值用于日志/BI、PAT 兼容策略、
|
||||
PAT 身份/来源派生或 Discovery 准入,但不得仅凭这两个 Header 放权、绕过认证或跳过
|
||||
授权。HTTP `claw-type` 与 IM 消息发送参数 `clawType` 是两个独立维度;后者仅控制
|
||||
“Send from AI”展示,仍由发行版 `ClawTypeValue` 和 `--ai-tag` 决定。
|
||||
真实宿主身份。CLI 只负责校验并发送 `x-dws-agent-product` 与 `x-dws-agent-host`,
|
||||
不会用它们派生本客户端的鉴权、PAT 模式、Discovery 行为或 MCP 端点;下游服务的
|
||||
使用契约由对应服务自行定义和说明。HTTP `claw-type` 是发行版固定的 PAT/路由标签,
|
||||
开源版固定为 `openClaw`,不受 `DWS_AGENT_PRODUCT` 影响。
|
||||
|
||||
服务集成方应将这两个请求头视为不可信输入并对白名单值做校验,不应仅因请求头声明了
|
||||
某个 Product 或 Host 就授予访问、绕过认证或跳过鉴权。
|
||||
|
||||
`--ai-tag` 默认为 `true`,因此配置合法非空 Product 后,默认发送的 IM 工具参数
|
||||
`clawType` 及小尾巴会随之改变。传入 `--ai-tag=false` 时,原生
|
||||
`chat message send` / `reply` 会发送 `clawType: ""`,shortcut 调用则省略该参数;
|
||||
本客户端不假定下游会将空值与键缺失等价处理。启用小尾巴时,展示值优先级依次为
|
||||
`DWS_AGENT_PRODUCT`、当前发行版的 `ClawTypeValue`、`openClaw`。不要传入目标下游及
|
||||
IM 服务未明确支持的 Product 值,否则可能被忽略或无法展示预期标签。
|
||||
|
||||
## Exit Codes / 退出码
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ func init() {
|
||||
configmeta.Register(configmeta.ConfigItem{
|
||||
Name: envDWSAgentHost,
|
||||
Category: configmeta.CategoryExternal,
|
||||
Description: "调用 DWS 的 Agent 运行形态标识;服务端可结合产品用于观测和 PAT 兼容策略",
|
||||
Description: "调用 DWS 的 Agent 运行形态标识;作为 x-dws-agent-host 发送供下游观测,本客户端不使用该值改变 PAT、鉴权或路由",
|
||||
Example: "cloud",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ func init() {
|
||||
configmeta.Register(configmeta.ConfigItem{
|
||||
Name: agentproduct.EnvName,
|
||||
Category: configmeta.CategoryExternal,
|
||||
Description: "调用方声明的 Agent 产品标识;覆盖 HTTP claw-type,但不是认证凭据",
|
||||
DefaultValue: "由当前发行版决定",
|
||||
Description: "调用方声明的 Agent 产品标识;作为 x-dws-agent-product 发送并用于 IM 小尾巴,本客户端不使用该值改变 HTTP claw-type/PAT",
|
||||
DefaultValue: "未设置(请求头省略,IM 使用当前发行版默认值)",
|
||||
Example: "qwenwork",
|
||||
})
|
||||
}
|
||||
@@ -47,25 +47,26 @@ func invalidAgentProductError() error {
|
||||
)
|
||||
}
|
||||
|
||||
// resolveEffectiveAgentProduct resolves the request-header identity with one
|
||||
// shared precedence rule: a valid non-empty runtime override wins, otherwise
|
||||
// the edition's MergeHeaders value wins, otherwise the OSS default is used.
|
||||
// Invalid runtime input falls back here for library callers that bypass root
|
||||
// validation; normal CLI execution rejects it before network access.
|
||||
func resolveEffectiveAgentProduct(headers map[string]string) string {
|
||||
fallback := edition.DefaultOSSClawType
|
||||
if value := headers[agentproduct.HeaderName]; value != "" {
|
||||
fallback = value
|
||||
// resolveEditionClawType resolves the fixed routing/PAT identity supplied by
|
||||
// the active edition. DWS_AGENT_PRODUCT is deliberately not consulted.
|
||||
func resolveEditionClawType(headers map[string]string) string {
|
||||
if value := headers["claw-type"]; value != "" {
|
||||
return value
|
||||
}
|
||||
value, err := agentproduct.ResolveFromEnv(fallback)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
return edition.DefaultOSSClawType
|
||||
}
|
||||
|
||||
func applyAgentProductOverride(headers map[string]string) map[string]string {
|
||||
value := resolveEffectiveAgentProduct(headers)
|
||||
// applyAgentProductHeader injects only a valid, non-empty caller-declared
|
||||
// product. Invalid values are omitted on library paths that bypass root
|
||||
// validation; normal CLI execution rejects them before network access.
|
||||
func applyAgentProductHeader(headers map[string]string) map[string]string {
|
||||
value, err := agentproduct.ResolveFromEnv("")
|
||||
if err != nil || value == "" {
|
||||
if headers != nil {
|
||||
delete(headers, agentproduct.HeaderName)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
if headers == nil {
|
||||
headers = make(map[string]string)
|
||||
}
|
||||
|
||||
@@ -26,13 +26,16 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestUnsetAgentProductKeepsOpenSourceDefault(t *testing.T) {
|
||||
func TestUnsetAgentProductOmitsHeaderAndKeepsOpenSourceClawType(t *testing.T) {
|
||||
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv(agentproduct.EnvName, "")
|
||||
|
||||
headers := resolveIdentityHeaders()
|
||||
if got := headers[agentproduct.HeaderName]; got != edition.DefaultOSSClawType {
|
||||
t.Fatalf("%s = %q, want %q", agentproduct.HeaderName, got, edition.DefaultOSSClawType)
|
||||
if got := headers["claw-type"]; got != edition.DefaultOSSClawType {
|
||||
t.Fatalf("claw-type = %q, want %q", got, edition.DefaultOSSClawType)
|
||||
}
|
||||
if _, ok := headers[agentproduct.HeaderName]; ok {
|
||||
t.Fatalf("unset Product must omit %s", agentproduct.HeaderName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,38 +62,46 @@ func TestParseAgentProductReturnsStableValidationError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveIdentityHeadersAgentProductPrecedence(t *testing.T) {
|
||||
func TestResolveIdentityHeadersSeparatesAgentProductFromClawType(t *testing.T) {
|
||||
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
||||
|
||||
oldEdition := edition.Get()
|
||||
t.Cleanup(func() { edition.Override(oldEdition) })
|
||||
edition.Override(&edition.Hooks{
|
||||
MergeHeaders: func(headers map[string]string) map[string]string {
|
||||
headers[agentproduct.HeaderName] = "wukong"
|
||||
headers["claw-type"] = "wukong"
|
||||
headers[agentproduct.HeaderName] = "merge-product-must-not-win"
|
||||
headers["x-edition-header"] = "preserved"
|
||||
return headers
|
||||
},
|
||||
EnterpriseCredentialHeaders: func(headers map[string]string) map[string]string {
|
||||
headers[agentproduct.HeaderName] = "enterprise-default"
|
||||
headers["claw-type"] = "credential-must-not-win"
|
||||
headers[agentproduct.HeaderName] = "credential-product-must-not-win"
|
||||
headers["x-enterprise-header"] = "preserved"
|
||||
return headers
|
||||
},
|
||||
})
|
||||
|
||||
t.Run("unset keeps edition default", func(t *testing.T) {
|
||||
t.Run("unset omits Product and keeps edition claw-type", func(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "")
|
||||
headers := resolveIdentityHeaders()
|
||||
if got := headers[agentproduct.HeaderName]; got != "wukong" {
|
||||
t.Fatalf("%s = %q, want wukong", agentproduct.HeaderName, got)
|
||||
if got := headers["claw-type"]; got != "wukong" {
|
||||
t.Fatalf("claw-type = %q, want wukong", got)
|
||||
}
|
||||
if _, ok := headers[agentproduct.HeaderName]; ok {
|
||||
t.Fatalf("unset Product must omit %s", agentproduct.HeaderName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid override is final", func(t *testing.T) {
|
||||
t.Run("valid Product is final without changing claw-type", func(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, " qwenwork ")
|
||||
headers := resolveIdentityHeaders()
|
||||
if got := headers[agentproduct.HeaderName]; got != "qwenwork" {
|
||||
t.Fatalf("%s = %q, want qwenwork", agentproduct.HeaderName, got)
|
||||
}
|
||||
if got := headers["claw-type"]; got != "wukong" {
|
||||
t.Fatalf("claw-type = %q, want wukong", got)
|
||||
}
|
||||
if got := headers["x-edition-header"]; got != "preserved" {
|
||||
t.Fatalf("edition header = %q, want preserved", got)
|
||||
}
|
||||
@@ -102,21 +113,48 @@ func TestResolveIdentityHeadersAgentProductPrecedence(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid library input falls back to edition", func(t *testing.T) {
|
||||
t.Run("invalid library input omits Product and keeps edition claw-type", func(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "qwen work")
|
||||
headers := resolveIdentityHeaders()
|
||||
if got := headers[agentproduct.HeaderName]; got != "wukong" {
|
||||
t.Fatalf("%s = %q, want wukong", agentproduct.HeaderName, got)
|
||||
if got := headers["claw-type"]; got != "wukong" {
|
||||
t.Fatalf("claw-type = %q, want wukong", got)
|
||||
}
|
||||
if _, ok := headers[agentproduct.HeaderName]; ok {
|
||||
t.Fatalf("invalid Product must omit %s", agentproduct.HeaderName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplyAgentProductOverrideAllocatesHeaders(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
func TestApplyAgentProductHeader(t *testing.T) {
|
||||
t.Run("valid value allocates headers", func(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
|
||||
headers := applyAgentProductOverride(nil)
|
||||
if got := headers[agentproduct.HeaderName]; got != "qwenwork" {
|
||||
t.Fatalf("%s = %q, want qwenwork", agentproduct.HeaderName, got)
|
||||
headers := applyAgentProductHeader(nil)
|
||||
if got := headers[agentproduct.HeaderName]; got != "qwenwork" {
|
||||
t.Fatalf("%s = %q, want qwenwork", agentproduct.HeaderName, got)
|
||||
}
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "empty value", value: ""},
|
||||
{name: "invalid value", value: "qwen work"},
|
||||
} {
|
||||
t.Run(tc.name+" removes inherited header", func(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, tc.value)
|
||||
headers := applyAgentProductHeader(map[string]string{
|
||||
agentproduct.HeaderName: "must-not-leak",
|
||||
"x-preserved": "yes",
|
||||
})
|
||||
if _, ok := headers[agentproduct.HeaderName]; ok {
|
||||
t.Fatalf("%s must be omitted", agentproduct.HeaderName)
|
||||
}
|
||||
if got := headers["x-preserved"]; got != "yes" {
|
||||
t.Fatalf("x-preserved = %q, want yes", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,17 +205,17 @@ func TestEffectiveClawTypeDoesNotInvokeEnterpriseCredentialHeaders(t *testing.T)
|
||||
hookCalled := false
|
||||
edition.Override(&edition.Hooks{
|
||||
MergeHeaders: func(headers map[string]string) map[string]string {
|
||||
headers[agentproduct.HeaderName] = "wukong"
|
||||
headers["claw-type"] = "wukong"
|
||||
return headers
|
||||
},
|
||||
EnterpriseCredentialHeaders: func(headers map[string]string) map[string]string {
|
||||
hookCalled = true
|
||||
headers[agentproduct.HeaderName] = "enterprise-default"
|
||||
headers["claw-type"] = "enterprise-default"
|
||||
return headers
|
||||
},
|
||||
})
|
||||
|
||||
t.Setenv(agentproduct.EnvName, "")
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
if got := effectiveClawType(); got != "wukong" {
|
||||
t.Fatalf("effectiveClawType() = %q, want wukong", got)
|
||||
}
|
||||
@@ -186,7 +224,7 @@ func TestEffectiveClawTypeDoesNotInvokeEnterpriseCredentialHeaders(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProductHeaderIsSeparateFromMessageClawType(t *testing.T) {
|
||||
func TestAgentProductControlsObservabilityHeaderAndMessageClawTypeOnly(t *testing.T) {
|
||||
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
|
||||
@@ -195,20 +233,24 @@ func TestAgentProductHeaderIsSeparateFromMessageClawType(t *testing.T) {
|
||||
edition.Override(&edition.Hooks{
|
||||
ClawTypeValue: "message-brand",
|
||||
MergeHeaders: func(headers map[string]string) map[string]string {
|
||||
headers[agentproduct.HeaderName] = "wukong"
|
||||
headers["claw-type"] = "wukong"
|
||||
return headers
|
||||
},
|
||||
})
|
||||
|
||||
if got := resolveIdentityHeaders()[agentproduct.HeaderName]; got != "qwenwork" {
|
||||
headers := resolveIdentityHeaders()
|
||||
if got := headers[agentproduct.HeaderName]; got != "qwenwork" {
|
||||
t.Fatalf("HTTP %s = %q, want qwenwork", agentproduct.HeaderName, got)
|
||||
}
|
||||
if got := edition.ClawType(); got != "message-brand" {
|
||||
t.Fatalf("message clawType = %q, want message-brand", got)
|
||||
if got := headers["claw-type"]; got != "wukong" {
|
||||
t.Fatalf("HTTP claw-type = %q, want wukong", got)
|
||||
}
|
||||
if got := edition.ClawType(); got != "qwenwork" {
|
||||
t.Fatalf("message clawType = %q, want qwenwork", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveIdentityHeadersRestoresAgentProductAfterNilCredentialHeaders(t *testing.T) {
|
||||
func TestResolveIdentityHeadersRestoresIdentityAfterNilCredentialHeaders(t *testing.T) {
|
||||
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
|
||||
@@ -218,7 +260,7 @@ func TestResolveIdentityHeadersRestoresAgentProductAfterNilCredentialHeaders(t *
|
||||
credentialHookCalled := false
|
||||
edition.Override(&edition.Hooks{
|
||||
MergeHeaders: func(headers map[string]string) map[string]string {
|
||||
headers[agentproduct.HeaderName] = "wukong"
|
||||
headers["claw-type"] = "wukong"
|
||||
return headers
|
||||
},
|
||||
EnterpriseCredentialHeaders: func(map[string]string) map[string]string {
|
||||
@@ -234,25 +276,49 @@ func TestResolveIdentityHeadersRestoresAgentProductAfterNilCredentialHeaders(t *
|
||||
if got := headers[agentproduct.HeaderName]; got != "qwenwork" {
|
||||
t.Fatalf("%s = %q, want qwenwork", agentproduct.HeaderName, got)
|
||||
}
|
||||
if got := headers["claw-type"]; got != "wukong" {
|
||||
t.Fatalf("claw-type = %q, want wukong", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveClawTypeUsesAgentProductOverride(t *testing.T) {
|
||||
func TestResolveIdentityHeadersRestoresDefaultsAfterNilMergeHeaders(t *testing.T) {
|
||||
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv(agentproduct.EnvName, "")
|
||||
|
||||
oldEdition := edition.Get()
|
||||
t.Cleanup(func() { edition.Override(oldEdition) })
|
||||
edition.Override(&edition.Hooks{
|
||||
MergeHeaders: func(map[string]string) map[string]string {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
headers := resolveIdentityHeaders()
|
||||
if got := headers["claw-type"]; got != edition.DefaultOSSClawType {
|
||||
t.Fatalf("claw-type = %q, want %q", got, edition.DefaultOSSClawType)
|
||||
}
|
||||
if _, ok := headers[agentproduct.HeaderName]; ok {
|
||||
t.Fatalf("unset Product must omit %s", agentproduct.HeaderName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveClawTypeIgnoresAgentProduct(t *testing.T) {
|
||||
oldEdition := edition.Get()
|
||||
t.Cleanup(func() { edition.Override(oldEdition) })
|
||||
edition.Override(&edition.Hooks{
|
||||
MergeHeaders: func(headers map[string]string) map[string]string {
|
||||
headers[agentproduct.HeaderName] = "wukong"
|
||||
headers["claw-type"] = "wukong"
|
||||
return headers
|
||||
},
|
||||
})
|
||||
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
if got := effectiveClawType(); got != "qwenwork" {
|
||||
t.Fatalf("effectiveClawType() = %q, want qwenwork", got)
|
||||
if got := effectiveClawType(); got != "wukong" {
|
||||
t.Fatalf("effectiveClawType() = %q, want wukong", got)
|
||||
}
|
||||
t.Setenv(authpkg.AgentCodeEnv, "agent-code")
|
||||
if got := apperrors.HostControlBlock()["clawType"]; got != "qwenwork" {
|
||||
t.Fatalf("hostControl.clawType = %q, want qwenwork", got)
|
||||
if got := apperrors.HostControlBlock()["clawType"]; got != "wukong" {
|
||||
t.Fatalf("hostControl.clawType = %q, want wukong", got)
|
||||
}
|
||||
|
||||
t.Setenv(agentproduct.EnvName, "")
|
||||
|
||||
@@ -569,9 +569,9 @@ func handlePatAuthCheck(
|
||||
// In host-controlled PAT mode (driven solely by DINGTALK_DWS_AGENTCODE),
|
||||
// or when flowId is absent, the CLI returns machine-readable JSON to
|
||||
// stderr and leaves UI/polling/retry to the host. `claw-type` is NOT
|
||||
// used for this decision — it is only forwarded on the wire via
|
||||
// the edition default / DWS_AGENT_PRODUCT override and surfaced in
|
||||
// hostControl for traceability.
|
||||
// used for this decision — its edition-fixed value is forwarded on the
|
||||
// wire and surfaced in hostControl for traceability. DWS_AGENT_PRODUCT
|
||||
// does not affect this PAT contract.
|
||||
if hostOwnedPAT || patData.Data.FlowID == "" {
|
||||
if hostOwnedPAT {
|
||||
return executor.Result{}, &apperrors.PATError{RawJSON: enrichPATErrorForHostControl(patErr.RawJSON)}
|
||||
|
||||
@@ -1007,11 +1007,11 @@ func TestHandlePatAuthCheck_HostControlledFlowIDPassthrough(t *testing.T) {
|
||||
t.Setenv("DWS_CONFIG_DIR", tmpDir)
|
||||
// Host-owned decision: driven ONLY by DINGTALK_DWS_AGENTCODE.
|
||||
// DINGTALK_AGENT is set to demonstrate it does NOT leak into
|
||||
// hostControl.clawType. With no DWS_AGENT_PRODUCT override the
|
||||
// open-source edition default remains "openClaw".
|
||||
// hostControl.clawType. DWS_AGENT_PRODUCT is also set to demonstrate
|
||||
// that Product does not change the open-source fixed "openClaw" value.
|
||||
t.Setenv(authpkg.AgentCodeEnv, "agt-sales")
|
||||
t.Setenv("DINGTALK_AGENT", "sales-copilot")
|
||||
t.Setenv(agentproduct.EnvName, "")
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
|
||||
mock := &mockRunner{
|
||||
runFunc: func(ctx context.Context, inv executor.Invocation) (executor.Result, error) {
|
||||
|
||||
@@ -29,9 +29,8 @@ import (
|
||||
// - Host-owned is triggered iff DINGTALK_DWS_AGENTCODE is non-empty.
|
||||
// - When triggered, `clawType` in the emitted hostControl block MUST be the
|
||||
// exact value the CLI actually injects on the wire. Each edition supplies
|
||||
// its existing default and an optional valid DWS_AGENT_PRODUCT overrides
|
||||
// it. Invalid input falls back here for library compatibility; root command
|
||||
// execution rejects it before network access.
|
||||
// its fixed value; DWS_AGENT_PRODUCT is a separate observability and IM
|
||||
// message-display signal and never affects this PAT value.
|
||||
// - When DINGTALK_DWS_AGENTCODE is empty the provider returns "" so
|
||||
// HostControlBlock yields nil and no hostControl block is emitted.
|
||||
func init() {
|
||||
@@ -59,5 +58,5 @@ func effectiveClawType() string {
|
||||
headers = h.MergeHeaders(headers)
|
||||
}
|
||||
}
|
||||
return resolveEffectiveAgentProduct(headers)
|
||||
return resolveEditionClawType(headers)
|
||||
}
|
||||
|
||||
+20
-13
@@ -1005,9 +1005,8 @@ func resolveIdentityHeaders() map[string]string {
|
||||
|
||||
// Inject environment variable based headers for MCP gateway tracking.
|
||||
// DINGTALK_AGENT, if set by the caller, is forwarded verbatim as the
|
||||
// x-dingtalk-agent header. It does NOT influence claw-type (which comes
|
||||
// from the edition default plus the explicit DWS_AGENT_PRODUCT override)
|
||||
// and it does NOT influence the host-owned PAT decision (driven solely by
|
||||
// x-dingtalk-agent header. It does NOT influence the edition-fixed
|
||||
// claw-type or the host-owned PAT decision (driven solely by
|
||||
// DINGTALK_DWS_AGENTCODE).
|
||||
sessionID := os.Getenv(envDingtalkSessionID)
|
||||
if sessionID == "" {
|
||||
@@ -1068,22 +1067,30 @@ func resolveIdentityHeaders() map[string]string {
|
||||
if fn := edition.Get().MergeHeaders; fn != nil {
|
||||
headers = fn(headers)
|
||||
}
|
||||
// Resolve the Agent Product before credential injection. The credential
|
||||
// hook has a separate contract and must not be able to replace the
|
||||
// request identity used by PAT hostControl serialization.
|
||||
headers = applyAgentProductOverride(headers)
|
||||
agentProduct := headers[agentproduct.HeaderName]
|
||||
if headers == nil {
|
||||
headers = make(map[string]string)
|
||||
}
|
||||
|
||||
// claw-type is the edition-fixed routing/PAT identity. Agent Product is a
|
||||
// separate caller-declared observability and IM-display dimension.
|
||||
clawType := resolveEditionClawType(headers)
|
||||
headers["claw-type"] = clawType
|
||||
headers = applyAgentProductHeader(headers)
|
||||
agentProduct, hasAgentProduct := headers[agentproduct.HeaderName]
|
||||
if fn := edition.Get().EnterpriseCredentialHeaders; fn != nil {
|
||||
headers = fn(headers)
|
||||
}
|
||||
if headers == nil {
|
||||
headers = make(map[string]string)
|
||||
}
|
||||
// DWS_AGENT_PRODUCT is the explicit caller override for the existing
|
||||
// claw-type wire header. Reassert the resolved product after credential
|
||||
// injection so that hook cannot alter identity. Invalid values are ignored
|
||||
// on this best-effort library path; root execution rejects them earlier.
|
||||
headers[agentproduct.HeaderName] = agentProduct
|
||||
// Credential hooks cannot alter either identity dimension. Restore the
|
||||
// fixed claw-type and the validated Product Header (or its absence).
|
||||
headers["claw-type"] = clawType
|
||||
if hasAgentProduct {
|
||||
headers[agentproduct.HeaderName] = agentProduct
|
||||
} else {
|
||||
delete(headers, agentproduct.HeaderName)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/agentproduct"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
)
|
||||
|
||||
@@ -217,6 +218,73 @@ func TestCrossPlatformCoverageChatSendResolvesUserBeforeDispatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatSendAndReplyDefaultToAgentProductForIMClawType(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "send",
|
||||
args: []string{"message", "send", "--open-dingtalk-id", "D1", "--text", "hello"},
|
||||
},
|
||||
{
|
||||
name: "reply",
|
||||
args: []string{"message", "reply", "--conversation-id", "cid", "--ref-msg-id", "mid", "--ref-sender", "D1", "--text", "hello"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
caller := &chatChangedContractCaller{}
|
||||
if err := executeChatChangedContract(t, caller, tc.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(caller.calls) != 1 || caller.calls[0].toolName != "send_personal_message" {
|
||||
t.Fatalf("calls = %#v", caller.calls)
|
||||
}
|
||||
if got := caller.calls[0].args["clawType"]; got != "qwenwork" {
|
||||
t.Fatalf("clawType = %#v, want qwenwork", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatSendAndReplyDisableAITagWithEmptyClawType(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "send",
|
||||
args: []string{"message", "send", "--open-dingtalk-id", "D1", "--text", "hello", "--ai-tag=false"},
|
||||
},
|
||||
{
|
||||
name: "reply",
|
||||
args: []string{"message", "reply", "--conversation-id", "cid", "--ref-msg-id", "mid", "--ref-sender", "D1", "--text", "hello", "--ai-tag=false"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
caller := &chatChangedContractCaller{}
|
||||
if err := executeChatChangedContract(t, caller, tc.args...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(caller.calls) != 1 || caller.calls[0].toolName != "send_personal_message" {
|
||||
t.Fatalf("calls = %#v", caller.calls)
|
||||
}
|
||||
got, present := caller.calls[0].args["clawType"]
|
||||
if !present || got != "" {
|
||||
t.Fatalf("clawType = %#v, present = %v; want present empty string", got, present)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageChatSendFailsClosedWhenUserCannotResolve(t *testing.T) {
|
||||
caller := &chatChangedContractCaller{}
|
||||
err := executeChatChangedContract(t, caller, "message", "send", "--user", "123", "--text", "hello")
|
||||
|
||||
+9
-6
@@ -55,12 +55,15 @@ Host-owned PAT 开关:
|
||||
由宿主处理全部 UI / 交互 / 回调节奏 / 重试逻辑,
|
||||
CLI 侧不再拉起任何本地浏览器 / 轮询。
|
||||
|
||||
服务端 Agent 产品标签 claw-type:
|
||||
开源构建默认在出站 MCP 请求中注入 claw-type: openClaw。
|
||||
如设置 DWS_AGENT_PRODUCT,则使用经校验的环境变量值覆盖该默认值。
|
||||
hostControl.clawType 会回填请求实际使用的值,避免 PAT 与请求标识漂移。
|
||||
该值由调用方声明,不是认证凭据;服务端不得仅凭它放权或跳过授权。
|
||||
它也不会修改 IM 消息展示使用的 clawType 参数或 --ai-tag 行为。
|
||||
服务端 PAT / 路由标签 claw-type:
|
||||
开源构建固定在出站 MCP 请求中注入 claw-type: openClaw,
|
||||
hostControl.clawType 会回填相同值。DWS_AGENT_PRODUCT 不会修改它。
|
||||
|
||||
Agent 产品标识 DWS_AGENT_PRODUCT:
|
||||
合法非空值作为 x-dws-agent-product 请求头发送,供下游日志 / BI 使用;
|
||||
本客户端不使用该值派生或改变 PAT、鉴权或路由,下游使用契约由对应
|
||||
服务自行定义。同时该值作为启用 --ai-tag 时 IM 消息小尾巴的 clawType
|
||||
参数。未设置或为空时请求头省略,小尾巴回退发行版默认值。
|
||||
|
||||
DINGTALK_AGENT(可选,仅供 x-dingtalk-agent 使用):
|
||||
如设置,将原样注入 HTTP 请求头 x-dingtalk-agent,
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/agentproduct"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -99,6 +100,8 @@ func newPlatformCoverageRoot() *cobra.Command {
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageAIMessageTag(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "qwenwork")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
argv []string
|
||||
@@ -125,8 +128,8 @@ func TestCrossPlatformCoverageAIMessageTag(t *testing.T) {
|
||||
if send.product != "chat" || send.tool != "send_personal_message" {
|
||||
t.Fatalf("last call = %s/%s, want chat/send_personal_message", send.product, send.tool)
|
||||
}
|
||||
if got := send.args["clawType"]; got != edition.ClawType() {
|
||||
t.Fatalf("clawType = %#v, want %q", got, edition.ClawType())
|
||||
if got := send.args["clawType"]; got != "qwenwork" {
|
||||
t.Fatalf("clawType = %#v, want qwenwork", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
// Package agentproduct defines the caller-provided Agent product identity
|
||||
// carried on outbound DWS requests.
|
||||
// used for outbound observability and IM message display.
|
||||
package agentproduct
|
||||
|
||||
import (
|
||||
@@ -25,8 +25,9 @@ import (
|
||||
const (
|
||||
// EnvName is the runtime override for the Agent product identity.
|
||||
EnvName = "DWS_AGENT_PRODUCT"
|
||||
// HeaderName is the existing wire header used for the Agent product.
|
||||
HeaderName = "claw-type"
|
||||
// HeaderName is the observability header used for the Agent product.
|
||||
// It is intentionally separate from the routing/PAT claw-type header.
|
||||
HeaderName = "x-dws-agent-product"
|
||||
// MaxValueBytes bounds the value because it is attached to every outbound
|
||||
// MCP request. Supported values are ASCII, so bytes and characters match.
|
||||
MaxValueBytes = 64
|
||||
|
||||
@@ -103,3 +103,12 @@ func TestResolveFromEnv(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHeaderNameIsSeparateFromClawType(t *testing.T) {
|
||||
if HeaderName != "x-dws-agent-product" {
|
||||
t.Fatalf("HeaderName = %q, want x-dws-agent-product", HeaderName)
|
||||
}
|
||||
if HeaderName == "claw-type" {
|
||||
t.Fatal("Agent Product observability Header must not reuse claw-type")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,18 +15,18 @@ package edition
|
||||
|
||||
import "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/syncdata"
|
||||
|
||||
// DefaultOSSClawType is the default wire value for request header claw-type
|
||||
// in the open-source build. DWS_AGENT_PRODUCT may explicitly override the
|
||||
// request identity; unrelated caller inputs such as DINGTALK_AGENT do not.
|
||||
// DefaultOSSClawType is the fixed wire value for request header claw-type in
|
||||
// the open-source build. It is intentionally independent of caller-provided
|
||||
// environment variables so PAT and routing behaviour stays predictable.
|
||||
const DefaultOSSClawType = "openClaw"
|
||||
|
||||
// defaultHooks returns the open-source edition defaults.
|
||||
//
|
||||
// MergeHeaders is the only hook that ships with behaviour: it supplies
|
||||
// DefaultOSSClawType so every open-source MCP request has a stable default.
|
||||
// The core applies an optional DWS_AGENT_PRODUCT override after edition hooks.
|
||||
// All other fields are nil — the internal code interprets nil as "use
|
||||
// standard open-source behaviour".
|
||||
// DWS_AGENT_PRODUCT is sent through a separate observability Header and never
|
||||
// changes claw-type. All other fields are nil — the internal code interprets
|
||||
// nil as "use standard open-source behaviour".
|
||||
func defaultHooks() *Hooks {
|
||||
return &Hooks{
|
||||
Name: "open",
|
||||
|
||||
+21
-14
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/agentproduct"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -87,10 +88,10 @@ type Hooks struct {
|
||||
|
||||
// ClawTypeValue is the display identity carried only in message-send tool
|
||||
// arguments (parameter clawType) so the IM server can render the
|
||||
// "Send from AI" indicator on delivered messages. It is intentionally
|
||||
// separate from the HTTP claw-type Agent Product header and is not
|
||||
// overridden by DWS_AGENT_PRODUCT. Empty → DefaultOSSClawType; overlays
|
||||
// set their own message-display value (e.g. "wukong").
|
||||
// "Send from AI" indicator on delivered messages. A valid non-empty
|
||||
// DWS_AGENT_PRODUCT overrides this display default, but never the separate
|
||||
// HTTP claw-type routing/PAT header. Empty → DefaultOSSClawType; overlays
|
||||
// set their own message-display default (e.g. "wukong").
|
||||
ClawTypeValue string
|
||||
|
||||
// PersonalEventSourceID identifies the personal-event source channel
|
||||
@@ -115,8 +116,8 @@ type Hooks struct {
|
||||
MergeHeaders func(base map[string]string) map[string]string
|
||||
|
||||
// --- EnterpriseCredential HTTP headers ---
|
||||
// This hook is only for credential material. It must not set claw-type;
|
||||
// the core reasserts that Header after the hook returns.
|
||||
// This hook is only for credential material. It must not set claw-type or
|
||||
// x-dws-agent-product; the core reasserts both after the hook returns.
|
||||
EnterpriseCredentialHeaders func(base map[string]string) map[string]string
|
||||
|
||||
// --- auth ---
|
||||
@@ -206,16 +207,22 @@ func Override(h *Hooks) {
|
||||
current = h
|
||||
}
|
||||
|
||||
// ClawType returns the message-display identity for the active edition,
|
||||
// falling back to DefaultOSSClawType when the overlay does not set one.
|
||||
// Message-send helpers attach this value as the clawType tool argument so the
|
||||
// IM server can label delivered messages as sent via AI. It is a separate axis
|
||||
// from the HTTP claw-type header and is not affected by DWS_AGENT_PRODUCT.
|
||||
// ClawType returns the message-display identity for the active edition.
|
||||
// A valid non-empty DWS_AGENT_PRODUCT wins; otherwise the active edition's
|
||||
// ClawTypeValue (or DefaultOSSClawType) is used. Message-send helpers attach
|
||||
// this value as the clawType tool argument so the IM server can label delivered
|
||||
// messages as sent via AI. It never changes the HTTP claw-type routing/PAT
|
||||
// header.
|
||||
func ClawType() string {
|
||||
if v := Get().ClawTypeValue; v != "" {
|
||||
return v
|
||||
fallback := Get().ClawTypeValue
|
||||
if fallback == "" {
|
||||
fallback = DefaultOSSClawType
|
||||
}
|
||||
return DefaultOSSClawType
|
||||
value, err := agentproduct.ResolveFromEnv(fallback)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// PersonalEventSourceID returns the source channel for user-level events.
|
||||
|
||||
@@ -13,9 +13,14 @@
|
||||
|
||||
package edition
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/agentproduct"
|
||||
)
|
||||
|
||||
func TestClawTypeDefaultsToOSSValue(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "")
|
||||
prev := Get()
|
||||
defer Override(prev)
|
||||
|
||||
@@ -26,6 +31,7 @@ func TestClawTypeDefaultsToOSSValue(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClawTypeUsesOverlayValue(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "")
|
||||
prev := Get()
|
||||
defer Override(prev)
|
||||
|
||||
@@ -35,6 +41,28 @@ func TestClawTypeUsesOverlayValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClawTypeUsesValidAgentProduct(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, " qwenwork ")
|
||||
prev := Get()
|
||||
defer Override(prev)
|
||||
|
||||
Override(&Hooks{Name: "overlay", ClawTypeValue: "wukong"})
|
||||
if got := ClawType(); got != "qwenwork" {
|
||||
t.Fatalf("ClawType() = %q, want qwenwork", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClawTypeInvalidAgentProductFallsBackToOverlay(t *testing.T) {
|
||||
t.Setenv(agentproduct.EnvName, "qwen work")
|
||||
prev := Get()
|
||||
defer Override(prev)
|
||||
|
||||
Override(&Hooks{Name: "overlay", ClawTypeValue: "wukong"})
|
||||
if got := ClawType(); got != "wukong" {
|
||||
t.Fatalf("ClawType() = %q, want overlay fallback wukong", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenStaticServersIncludesCoreProducts(t *testing.T) {
|
||||
servers := openStaticServers()
|
||||
byID := make(map[string]ServerInfo, len(servers))
|
||||
|
||||
Reference in New Issue
Block a user