pluginDescriptorConflictsWithDistribution and the identity-owner seeding
both treated conference as distribution-owned, so the whole plugin server
was skipped before the replaceable-fallback merge in addPluginCommandsSafe
could run. Skip replaceablePluginFallbacks names in both early gates while
keeping reserved-command protection and plugin-vs-plugin ownership intact.
Address second-round review on PR #555:
- Move CloseAuditSink into the unconditional Execute defer so async remote
forwards are drained on BOTH success and failure paths. Cobra skips
PersistentPostRunE when RunE returns an error, which previously dropped
in-flight forwards for failed commands. Make CloseAuditSink idempotent via
sync.Once so the success-path hook and the defer can both call it.
- CSV export now returns a "文件:行号" error on malformed JSONL instead of
silently skipping the line and exiting 0.
- Add regressions: TestCloseAuditSinkDrainsOnErrorPath (error-path drain),
TestExportCSVFailsOnMalformedJSON (corrupt JSONL visible), and
TestAuditIdentityReresolvesOnProfileSwitch (per-profile Actor via an
injectable token loader seam).
Address PR #555 review:
- chain: derive prev_hash from file tail under cross-process flock, drop the
global .chain sidecar so per-day files stay independently verifiable and
concurrent dws processes cannot fork the chain
- forward: track async forwards with WaitGroup and add bounded Close(ctx) so
in-flight deliveries are not dropped on process exit
- actor: resolve Actor from the active runtime profile (profile-keyed cache)
- observability: BuildSink returns init errors; write/forward failures reported
to file log and to stderr when DWS_AUDIT_DEBUG is set
- cli: reject `audit tail --lines` < 1; check CSV writer/flush errors
- wire CloseAuditSink into PersistentPostRunE
- add regression tests for cross-date/cross-process chain, forwarder
wait/timeout, init-failure, tail validation, CSV export
builtin blank-imports every service + smart package so their registrations run,
exposes Commands(), and provides the zero-side-effect coverage suite
(TestAllShortcutsAssemble / TestAllToolLiteralsAreReal / TestNoDuplicateCommands
/ TestAllHaveIntent). legacy loads user YAML shortcuts then merges built-in
shortcut leaves into the helper command tree; root wraps the tool caller with the
usage recorder and registers dws shortcut.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(auth): support multi-profile login
* fix(auth): complete multi-org profile acceptance
* feat(auth): 完成多组织 profile 验收
* docs(auth): 补充多组织 Ralph 验收材料
* feat(auth): 支持 auth switch TUI 切换 profile
* feat(auth): logout 默认清理所有组织
* feat(auth): login 默认新增组织授权
* feat(profile): 使用 profile switch 切换组织
* docs(ralph): 更新 profile switch 验收材料
* fix(profile): 展示全部可切换组织
* feat(profile): support multi-org switch tui
* chore(install): add branch source installer
* fix(profile): keep global profile out of tool params
* feat(profile): support csv multi-profile runtime
* ci: add multi-profile e2e workflow
* ci: run multi-profile e2e on all branches
* docs: document multi-profile e2e ci gate
* docs: remove multi-profile test cases from pr
* ci: harden multi-profile e2e gates
* fix(auth): serialize profiles.json RMW and harden multi-profile persistence
Wrap all profiles.json read-modify-write paths (profile switch/use/remove,
status marking, token save, logout) in the existing dual-layer lock via a new
withProfilesLock helper. Split each writer into a public (locking) entry point
plus a lock-free *Locked variant so the non-reentrant lock is never re-acquired;
the refresh path (oauth_helpers) and the load-path legacy migration now call the
lock-free saver to avoid self-deadlock.
Also: write profiles.json and the token marker via per-write random temp names
(uuid) to stop concurrent writers from corrupting a fixed .tmp file; quarantine
an unparseable profiles.json and rebuild an empty config so the CLI can
self-heal instead of locking out auth reset/logout; make DeleteAllTokenData
proceed even if profiles.json cannot be read; and stop SyncLegacyTokenMirror
from deleting the legacy mirror on a transient keychain read error.
* fix(auth): do not fall back to a different org's legacy token slot
When no explicit --profile is given, LoadTokenDataForProfile resolves the
current/primary profile and reads its per-corp keychain slot. If that slot
read failed, the code silently fell through to the legacy single token slot,
which after any drift between the legacy mirror and the current profile could
belong to a different organization. The command would then run as the wrong
org with no indication to the user.
Reproduction (conceptual):
- profiles.json currentProfile = corpA
- corpA's keychain slot is unreadable, legacy single slot still holds corpB
- any read command (no --profile) silently used corpB's token
Fix: when a profile is resolved but its slot read fails and no --profile was
given, only fall back to the legacy single slot when its CorpID matches the
resolved profile (same org); otherwise return the original error instead of
acting as a different organization. The no-profile legacy path (pre-migration
installs with no resolved profile) is unchanged.
Tests:
- Covered by the existing internal/auth suite under go test -race; the
same-org fallback preserves the legacy-mirror case while the cross-org
case now surfaces the read error.
* feat(skill): document multi-org profile usage and always ship dws-shared
The skills had no guidance on the multi-profile capability, so an agent would
treat the CLI as single-org: when a lookup missed in the current org it would
give up or ask the user instead of searching other logged-in orgs. The multi
skill set also referenced a `dws-shared` prerequisite that was never actually
installed, and the only multi-org hints lived inline in three product skills.
This adds, in source only:
- A "multi-org / profile" section in the mono SKILL.md (concept, commands,
cross-org rule, aggregation, safety guardrails) plus a decision-tree entry,
trigger conditions, and a corrected logout danger-table row (logout removes
all orgs by default; removing the primary silently re-elects a new primary,
confirm before removing the primary).
- A standalone skills/multi/dingtalk-profile skill mirroring the same content.
- A new skills/multi/dws-shared skill that carries auth, global flags and the
multi-org rule, so every product skill's PREREQUISITE resolves and all
read/search skills inherit the cross-org behavior without per-skill edits.
- Cross-org fallback notes on dingtalk-aisearch / chat / contact.
To guarantee the prerequisite actually ships, multi-mode install now force-
includes dws-shared even when --skill / --exclude narrows the set (no-op when
the source has no dws-shared, preserving older layouts).
Tests:
- internal/app: TestP1SharedAlwaysIncludedWithSkillFilter installs with
`-s aitable` and asserts dws-shared still lands in the destination;
TestP1SharedNoopWhenAbsent guards the older-layout no-op.
- go test -race ./internal/auth/... ./internal/app/... passes.
---------
Co-authored-by: shangguanxuan.sgx <shangguanxuan.sgx@alibaba-inc.com>
Co-authored-by: qinze <audanye@gmail.com>
Override leaves whose backing MCP tool isn't actually deployed rendered in
`dws <svc> --help` but failed at invocation with "tool not found" (43 phantom
commands across 391 overrides; attendance declared 38, only 4 deployed).
BuildDynamicCommands now takes an existingTools oracle (CLI slug -> live tool
set from the tools/ cache). A leaf whose tool is missing from its resolved
server's set is marked Hidden; groups left childless collapse. Safety rails:
acts only when a server's tool set is KNOWN and non-empty (cold cache / overlay
/ plugin paths pass nil and no-op, never blanking the tree); serverOverride
leaves resolve against the target server; pipeline leaves are never hidden.
Adds scripts/dev/check-phantom-overrides.py as a publish-time gate, and
phantom_guard_test.go covering hide / cold-cache-keep / serverOverride / pipeline
/ empty-group-collapse.
Switches the discovery version code bamboo -> cedar and aligns the open edition CLI with dws-wukong across communication (calendar book/acl/attendee, minutes tag, mail folder/template/contact, chat file upload, todo add-attachment, attendance transforms) and structured-office (aitable advperm/view/section/workflow/record, sheet/drive/wiki/doc) domains. Includes output-envelope parity, parse_bool/attendance_class_check_time transforms, --calendar-id support, CHANGELOG 1.0.43 and README command-index refresh. cedar config validated on pre and prod endpoints.
* 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)
- 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.
- 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.
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 会先在新版本上线.
* 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>
* docs(auth): correct login help to reflect actual default + SSH guidance (#226)
The --help long description for `dws auth login` claimed the default mode was
"OAuth 设备流 (默认)", but the actual default starts a 127.0.0.1 loopback
listener (oauth_provider.go:131-136) and only switches to device flow when
--device is passed. SSH-into-headless-Linux users following the docs hit a
dead end because the local browser cannot reach 127.0.0.1 on the remote box.
Rewrite the description so each method is named after its real flag:
- OAuth Loopback 流 (默认)
- OAuth 设备流 (--device)
- 直接提供 Token (--token)
Add an explicit warning callout and a `--device` example for SSH/headless
environments. Also realign two flagErrorWithSuggestions strings in root.go
that propagated the same misconception ("默认使用设备流" → loopback default,
SSH 用户加 --device).
No behavioural change.
* docs(auth): drop emoji from login help warning