go:embed the skills/ tree (mono + multi) into the binary and make
`dws skill setup` install from the embedded bundle by default. Upgrading
the binary now refreshes the installed skill, instead of silently reusing
a stale copy probed from the current working directory. An explicit
--source / DWS_SKILL_SOURCE still overrides for development.
Replants the change from #441 onto main: #441 was stacked on the
abandoned feat/chat-bot-provisioning-openclaw base (PRs #407/#397 closed),
which is why its CI was red. The embedded-bundle test asserted a
connect.md doc that only existed on that dead base; retargeted it to a
reference doc that exists on main.
dws upgrade registered no --dry-run flag and never read the global one,
so --dry-run fell through to runUpgrade and performed a real, irreversible
upgrade (download + replace binary). This contradicts the flag's documented
contract (预览操作内容,不实际执行).
Resolve the target release and platform asset (so a missing build / 'already
latest' is still reported), then render the planned 1-5 steps and return
before any side effect: no backup, no download, no replace. Advertise
--dry-run in the command examples.
Fixes#364
Tag each MCP request with which agent host is driving dws (agent_code) and a
per-(machine × agent_code) instance id, so usage can be sliced by channel and
instance in the data warehouse. Root cause it fixes: agent_code was only sent
when the host injected DINGTALK_DWS_AGENTCODE (~99.98% empty), so the gateway
logged none.
Detection ladder (every signature observed on a real host / official docs, not
guessed; unknown -> custom):
T0 explicit DINGTALK_DWS_AGENTCODE
T1 verified env signatures: claudecode (CLAUDECODE), codex (CODEX_SANDBOX),
openclaw (OPENCLAW_BUNDLE_ROOT), hermes (HERMES_HOME)
T2 VSCODE_BRAND value (covers the whole VS Code fork family)
T3 macOS __CFBundleIdentifier map (qoder/cursor/vscode/workbuddy)
T4 custom fallback
identity.json v2 (machineId + per-agent_code agents map), deterministic
dwsa_<base62> derivation, transparent v1 migration. Backward-compatible wiring:
x-dws-agent-id stays machine-level; new x-dws-agent-instance-id carries the
per-channel id; X-Cli-Version emitted so old/new clients are distinguishable.
Trust boundary (docs/agent-code.md): agent_code and the ids are self-reported
and spoofable — fit for statistics ONLY, never for auth/limit/billing.
Includes unit tests for every tier and the integration doc.
* fix(cli): guard canonical mcp tree against poisoned-cache flag collisions
The canonical 'dws mcp' tree is built from cached catalog data before the
legacy command build and before Cobra dispatches anything, so a pflag
panic there (a tool schema property named after the reserved --params
flag, as cached during the 1.0.32 incident) aborted every invocation --
including 'dws cache refresh' and 'dws upgrade' -- and sat outside all
three poisoned-cache guards (#447/#449/#452).
Two layers, mirroring the existing guards:
- applyFlagSpecs now skips reserved (--json/--params), duplicate and
alias-colliding flag names and sanitizes shorthands instead of letting
pflag panic; the skipped property stays reachable through the reserved
JSON payload flags (same degradation semantics as #449).
- newMCPCommand wraps the build in the #452 recover -> quarantine ->
retry-once -> degrade-to-stub sequence, so even an unforeseen panic
class no longer locks the CLI out.
* docs: amend 1.0.36 changelog for the re-cut with the canonical tree guard (#454)
A panic during the envelope-driven command build no longer just degrades
to helper commands (#447): the partition's discovery cache is first moved
aside to <partition>.quarantined (kept for inspection, previous quarantine
replaced) and the build retried once against a fresh fetch. Any path that
delivers a fixed binary -- dws upgrade or a reinstall -- now escapes the
lock-out with zero manual cache surgery; only a second panic (remote
envelope still poisoned, or offline) falls back to the degraded helper
set with the 'dws cache refresh' hint.
dws upgrade additionally clears the discovery-derived caches (market /
tools / detail across all partitions, leaving the downloads dir alone)
after the binary swap, so the upgraded binary rebuilds its command tree
from fresh data instead of inheriting snapshots written by the old
version.
The dynamic command tree is built from cached discovery data before
Cobra dispatches any command. A panic during that build (e.g. a
duplicate pflag registration fed by a poisoned cache, as seen before
1.0.32: "chat_permission_grant flag redefined: params") aborted every
invocation — including 'dws cache refresh', the very command that
repairs the cache. The only way out was manually deleting
~/.dws/cache/<partition>/tools/*.
Wrap the envelope-driven build in a local recover: on panic the CLI now
logs the failure, prints a stderr hint pointing at 'dws cache refresh',
and falls back to the hardcoded helper commands so utility commands
stay alive and users can self-heal.
- printExecutionError: in JSON mode emit the structured error to stdout
(not stderr) so machine consumers parsing stdout get a parseable result;
update the 3 root_execute tests to assert the new contract.
- requireYesForDelete prompt: when stdin is not a TTY (piped/scripted/JSON
pipelines), do not show an interactive confirm that emits non-JSON and
blocks on stdin; return a structured validation error requiring --yes.
Extract the server-list endpoint path into a single discoveryAPIPath constant and move it from /cli/discovery/apis to /cli/discovery/apis/bamboo so future version bumps touch one place.
Only the path changes: the MCP base host stays on production https://mcp.dingtalk.com and the auth / skill / doctor endpoints are untouched. The edition DiscoveryURL hook (full-URL FetchServersFromURL) is unaffected. All mock/test fixtures are updated to the new path and a CHANGELOG 1.0.34 entry is added.
The conference-local stdio plugin runs an Initialize/ListTools handshake
during command-tree construction. When the DingTalk desktop client isn't
running it returns "本地服务未就绪", which was logged at Warn and printed
to stderr on every invocation.
Because discovery happens in NewRootCommandWithEngine — before
PersistentPreRunE applies --debug/--verbose via configureLogLevel — this
Warn showed regardless of flags, polluting output and misleading callers
(and LLMs) into treating it as the cause of an unrelated command error
(e.g. an auth error or PARAM_ERROR from a different server).
This is an expected, benign outcome for an optional local plugin:
commands that ship toolOverrides still register up-front via
registerStdioServerFromOverlay, so availability is unaffected. Downgrade
both discovery handshake failures to Debug so normal output stays clean.
* fix(drive): drop client-side Content-Type fallback on presigned OSS PUT
The drive helper used to set req.Header["Content-Type"] = fallbackMIME
whenever the prepare_upload response returned an empty headers map.
DingTalk drive uses OSS presigned URLs whose StringToSign is computed
against an empty Content-Type at signing time, so any client-side
Content-Type makes the signature OSS recomputes at PUT time differ
from the server's presignature → HTTP 403 SignatureDoesNotMatch.
This broke every `dws drive upload` for any file whose mime detects
to a non-empty value (image/png, application/pdf, etc.), which is
basically everything in practice.
Reproduction (against DingTalk drive):
dws drive upload --file any.png
→ [1/3] 获取上传凭证 any.png (X 字节, image/png)...
→ [2/3] 上传文件到 OSS...
→ OSS 上传失败 HTTP 403: SignatureDoesNotMatch
Fix: trust the server's headers map as authoritative. Empty map means
"no client-side headers needed" — do not infer and add Content-Type.
Removes the hasContentType / fallbackMIME path entirely; httpPutDriveFile
signature loses the fallbackMIME parameter.
Manual verification:
curl -X PUT -H "Content-Type:" --data-binary @file <same-presigned-url>
→ HTTP 200 (proves the only difference was the client-side Content-Type)
Note: aitable.go's upload-file helper deliberately keeps its
Set("Content-Type", mimeType) call because its OSS endpoint uses a
different signing mode (server includes the client-declared mime in
its signature computation, verified by running aitable upload-file
across 12 file types — all succeed). The two helpers must not be
unified without re-validating both endpoints.
Tests:
- TestHttpPutDriveFile_NoContentTypeWhenServerHeadersEmpty: guards
that an empty server headers map results in no Content-Type on PUT.
- TestHttpPutDriveFile_PassthroughServerHeaders: guards that server
headers (Content-Type, x-oss-*) are forwarded verbatim.
* feat(aitable): unhide attachment upload-file + clarify prepare-only command
AI agents that discover commands only via --help (e.g. Lobster, generic
LLM agents) currently hit a dead end when trying to upload an attachment
to an AITable:
- `dws aitable attachment` exposes only `upload` (prepare-only),
which returns uploadUrl + fileToken but does not actually upload.
- The real one-shot command `attachment upload-file` (which performs
prepare + HTTP PUT + return fileToken automatically) is marked
Hidden:true, so it is invisible in --help output.
Agents that don't read skills/references/products/aitable.md therefore
get stuck after step 1 — they call `upload`, receive an uploadUrl they
have no idea how to consume, attempt various wrong things (e.g. write
the uploadUrl into the record's attachment field as if it were a token),
fail, and fall back to "please use the UI to upload" messages, which
makes dws look broken even though the capability is fully implemented.
This is the same UX gap that lark-cli avoids with its highly visible
`base +record-upload-attachment` command — discoverability via --help
is the difference between "works for any agent" and "only works if the
agent reads our skill docs".
Changes:
- newAITableUploadFileCommand: remove Hidden:true so the command
appears in `dws aitable attachment --help`. Tighten Short to
explicitly mention the 3 steps it bundles. Long now also calls
out the prepare-only sibling and recommends this command as the
default for AI agents.
- newAITableAttachmentUploadCommand (prepare-only): add a Long
description that explicitly states this command is only step 1
of a 3-step flow, lists what an agent must do after (HTTP PUT,
then write fileToken into record attachment field with the exact
[{"fileToken":"ft_xxx"}] shape), and points to upload-file as
the recommended one-shot alternative. Updated Short to flag that
no file is uploaded by this command.
- aitable_upload_file_test: add TestAITableUploadFileCommandIsDiscoverable
to guard against re-introducing Hidden:true. The test includes a
rationale comment explaining the agent-discoverability gap.
The drive Content-Type fix in the preceding commit and this aitable
discoverability fix together restore end-to-end attachment upload
functionality for both human operators and AI agents that only read
--help.
* feat(root): show 'dws upgrade' guidance in dws --help when commands are missing
AI agents (and users) reading `dws --help` see only the discovered MCP
service list and utility command list, with no hint about what to do if
none of the listed commands fit their task. The natural failure mode is
to give up or hack around — but in many cases the right action is just
`dws upgrade`, because new capabilities and bugfixes ship continuously
and a missing command is usually a stale binary issue.
Two changes restore visibility of the root command's Long description so
this guidance can be surfaced:
- internal/app/root.go: set root.Long to a one-paragraph hint —
"if you hit a missing command, an error, or cannot complete the
task, try `dws upgrade` first; both the DingTalk OpenAPI surface
and dws CLI evolve continuously."
- internal/app/root_help.go: the custom SetHelpFunc that renders the
root help (renderRootHelp) replaces cobra's default template, which
had been silently dropping root.Long from --help output. Restore
rendering by appending root.Long (when non-empty) after the command
list, separated by a blank line. This matches cobra's default
behavior for the Long field while preserving the custom services /
utilities sections renderRootHelp introduced.
- internal/app/visibility_test.go: new TestRenderRootHelpIncludesLong
guards against re-introducing the regression. Uses a sentinel Long
string and asserts renderRootHelp output contains it verbatim. If a
future change rewrites the help renderer without preserving Long
rendering, this test fails immediately and reminds the author the
upgrade hint must stay visible.
Verified locally:
$ ./dws --help | tail -3
Use "dws <service> --help" for more information about a discovered MCP
service or "dws <command> --help" for utility commands.
提示: 如果遇到能力缺失、命令报错、新功能未注册、或无法完成任务,
请先用 'dws upgrade' 升级到最新版本后再试. 钉钉 OpenAPI 和 dws CLI
持续迭代, 新能力和 bugfix 会先在新版本上线.
GoReleaser cross-compiles darwin/arm64 binaries on ubuntu-latest with no
codesign step. macOS 11+ on Apple Silicon requires at least an ad-hoc
signature; unsigned arm64 binaries are SIGKILL'd by amfid on first exec,
which surfaces as `signal: killed` and aborts `dws upgrade` at the
"解压并验证" step.
Two layers of fix:
1. Release-side: post-goreleaser.sh now unpacks each dws-darwin-*.tar.gz,
applies an ad-hoc signature (codesign locally, rcodesign in CI),
deterministically repacks, and rewrites the matching line in
checksums.txt. release.yml installs rcodesign 0.27.0 before
GoReleaser runs.
2. Client-side self-heal: validateNewBinary detects `signal: killed` on
darwin and retries once after running `codesign --force --sign -` and
clearing com.apple.quarantine. Future releases stay functional even
if the signing step is ever skipped.
Verified end-to-end: stripped a real dws binary → exec exits 137 →
validateNewBinary recovers → final binary shows Signature=adhoc and runs.
* fix(auth): partition app.json filename by edition to isolate credentials
Two dws binaries sharing the same config directory (typically ~/.dws or
DWS_CONFIG_DIR) previously read and wrote a single app.json. Editions
that pin AuthClientID via hooks still go through the open-core
post-login persistence path, which records a bare ClientID without a
paired ClientSecret. The sibling edition reading the same path would
then adopt that foreign clientID via ResolveAppCredentials.
Mirror the strategy already used by the cache loader
(pkg/config.EditionPartition): GetAppConfigPath returns a filename
suffixed with the active edition name. Open-source keeps "app.json" for
backwards compatibility; sibling editions land on "app-<edition>.json".
LoadAppConfig / SaveAppConfig / HasAppConfig / DeleteAppConfig all
route through GetAppConfigPath, so this single change physically
isolates credential files end-to-end without any read-time heuristics.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(auth): address app config partition review
* fix(auth): clean legacy sibling app config
* test(auth): cover legacy app config cleanup guards
* fix(auth): close app config review follow-ups
---------
Co-authored-by: shangguanxuan.sgx <shangguanxuan.sgx@alibaba-inc.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(app): prevent command field from overwriting existing endpoint
In AppendDynamicServer, when a plugin declares command != id, the command
endpoint is written unconditionally, overwriting any previously registered entry.
Fix: use first-writer-wins - only write if the key is not yet present.
id registration and dynamicProducts remain unconditional (unaffected).
* test(app): add regression test for command endpoint first-writer-wins guard
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* 1.0.19 changelog
* stick opt
* fix(cli): utf-8 safe sticky suffix guard + changelog (#272)
SuffixLooksLikeValue used to read suffix[0] (a single byte) for both the
uuid format branch and the fallback "is the first rune a letter?" check.
For multi-byte UTF-8 leading runes — common in dws because value text is
often Chinese — this picked up only the first byte (0xE0..0xF4 lead),
which is not a letter and not a hex digit, so the function silently
returned true and let glued tokens like --name<CJK> get split into
--name <CJK>... This switches both branches to utf8.DecodeRuneInString
and adds a utf8.RuneError guard so invalid UTF-8 input is rejected too.
Also locks down the new behaviour in CHANGELOG ## [Unreleased]:
- Changed: schema-aware sticky flag splitting
- Added: available_flags field on unknown-flag errors
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(output): add -f ndjson (implemented) and scaffold -f csv (#252)
`larksuite/cli` exposes --format ndjson / csv; dws only had json/table/raw/
pretty. This adds both as recognised global formats:
- ndjson: fully implemented (internal/output/ndjson.go) — top-level arrays
and well-known list wrappers ({items|results|data|records|...}) emit one
compact JSON document per line; anything else degrades to a single line.
Streaming-friendly counterpart to `-f json`.
- csv: scaffolded (internal/output/csv.go) — writeCSV currently returns a
clear "not implemented (#252)" error rather than silently degrading to
JSON. A TODO block spells out the planned implementation (reuse the table
renderer's extractRowsFromMap/rowsFromSlice flattening + encoding/csv).
Wired through Write() and normalizeFormat(); --format help string now lists
ndjson (csv to be added when writeCSV lands). Tests cover ndjson rendering
(array / wrapped-list / scalar) and pin the csv not-implemented contract.
Skeleton PR for #252 — ndjson is shippable as-is; csv is left for a
follow-up commit on this branch.
* feat(output): implement -f csv (#252)
Completes the CSV half of the format work. writeCSV mirrors the shape
decisions `-f table` already makes (reuses normalizePayload /
unwrapPrimaryObject / extractRowsFromMap / rowsFromSlice / formatValue) so
columns and value flattening are consistent between the two formats:
- a list of objects (bare or wrapped under items/results/data/records/...) →
header row + one row per element; union of keys sorted; missing values are
empty cells; nested objects/arrays render as compact JSON in the cell;
sibling metadata of the list (total, hasMore, ...) is dropped.
- a single object → two-column key,value CSV (keys sorted).
- a non-uniform list / scalar → single-column `value` CSV.
- empty / nil → empty document.
encoding/csv.Writer handles RFC-4180 quoting (commas, quotes, newlines);
cells go through formatValue (also strips terminal control sequences, same
as the table renderer). `--fields` projection composes for free since
WriteFiltered applies SelectFields before Write.
--format help now lists csv; FormatCSV doc comment dropped the WIP marker;
the not-implemented test is replaced with real coverage (list with
comma/CJK/nested-array-as-JSON, wrapped-list-with-metadata, single object,
scalar) plus a --fields composition test.
* feat(output): broadcast list metadata as trailing columns in -f csv (#252)
Per review preference: instead of dropping the list's sibling metadata
(total, hasMore, ...) when rendering {records:[...], total:N} as CSV, append
each meta key as a trailing column repeated on every row, so a CSV consumer
never silently loses it (CSV has no "footer table" the way the table
renderer does). Meta keys colliding with a data column are skipped; an empty
list still emits the header (data + meta) plus one row of empty data cells
carrying the meta values. New broadcastMeta helper; doc comment + tests
updated (incl. an empty-list-with-metadata case).
* feat(output): recognise real DingTalk envelope keys in -f csv/ndjson/table
之前 -f csv 和 -f ndjson 的 list 检测白名单只认 items/results/data/list/
records/tools/servers/products,但真实钉钉响应用的是 result(单数直接数组
或一层包裹)/documents/emailAccounts/todoCards/events/messages,导致大
部分 list 命令的 csv 输出退化成 key,value 二列、ndjson 退化成整包一行。
修复点:
1. preferredListKeys 扩展加上真实 envelope key(result/documents/
emailAccounts/todoCards/events/messages),并把它升级为 csv/table/
ndjson 共享的"单一事实源"——filter.go 的 findDataList 不再维护自己
的本地副本,直接复用这个列表。
2. extractRowsFromMap 改为委托 findDataList,自动获得"一层深度"的
wrapper 支持({result: {todoCards: [...]}} 这种 envelope 现在能识
别)。meta 合并:outer + inner 双层 sibling 拉通,outer 同名 key
优先(避免 inner 把外层 success/total 等覆盖掉)。
3. writeTableish 和 writeCSV 调整 unwrapPrimaryObject 和
extractRowsFromMap 的优先级——先试 list 检测,没命中再走 unwrap,
避免 {result: {todoCards: [...]}} 被 unwrap 剥掉外层后直接走
key,value 分支。
4. findDataList 允许"空数组+preferred key" 命中,保留原来"空 list +
metadata 仍渲染为表格 + meta 广播一行"的行为。
新增 TestTabularDetectsRealDingTalkEnvelopes:四个真实 envelope 形态
(contact user search 的 result 直接数组、doc search 的 documents 顶层、
mail mailbox list 的 emailAccounts、todo task list 的 result.todoCards
一层深度)验证 ndjson 行数和 csv 表头都符合预期。
真接口回归(已登录态跑过):
- dws contact user search -f csv → name/userId 列正常出表
- dws todo task list -f ndjson → 20 行一条任务,可 jq -r .subject 直接管
- dws doc search -f csv → 10 行 + nextPageToken 等 meta 广播尾列
- dws schema -f csv → 无回归
Closes part of #252 follow-up.