* fix release upload of signed macOS assets
* ci: sign macOS releases with Developer ID
* fix release publication atomicity
* harden Developer ID release verification
* fix: run release script tests in CI
---------
Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.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.
Resolve-LatestVersion referenced $LatestUrl (lines 185/197) but the
variable was never defined, so on the default GitHub path both
Invoke-WebRequest calls failed with a null Uri. With
$ErrorActionPreference = "Stop" the script then hit Write-Err and
exit 1 — closing freshly-launched PowerShell windows instantly
(the reported "闪退"). Every user on the default `latest` path was
affected; the Bash installer was unaffected because it inlines the URL.
Define $LatestUrl = "https://github.com/$Repo/releases/latest", mirroring
the Bash installer. Verified end-to-end with pwsh 7.5: the script now
resolves the latest tag, downloads, checksum-verifies and installs.
* fix(devapp): point dev installer at DingTalk-Real-AI, drop the fork
install-devapp.sh / .ps1 and the robot quickstart still pulled the dev
binary + dingtalk-dev skill from wxianfeng/dingtalk-workspace-cli's
feat/dws-devapp fork branch. The dev-app work has since landed on main and
shipped in stable v1.0.42 under DingTalk-Real-AI, so the fork dependency is
obsolete.
- DEVAPP_REPO default: wxianfeng/... → DingTalk-Real-AI/...
- Bootstrap URLs in headers + quickstart: fork feat/dws-devapp → main.
- Drop "preview/prerelease" wording — releases are now stable; the
newest-release resolution still works either way.
- Quickstart China note now points at the standard install.sh Gitee
mirror (which carries dws dev in v1.0.42); install-devapp.sh pulls its
binary from github.com, so a gitee-raw script alone would not help China.
Verified: releases?per_page=1 on DingTalk-Real-AI resolves v1.0.42 and the
darwin/skills assets are present.
Note: docs/devapp-agent-install-guide.md is separately stale (describes the
old source-build flow + the pre-rename `dws devapp` command) and needs its
own rewrite — left out of this change.
* docs(devapp): rewrite agent install guide for binary install + `dws dev`
The guide was stale on two axes:
- It described the old source-build flow (clone the fork branch + go/make,
env vars DEVAPP_REPO_URL / DEVAPP_BRANCH / DEVAPP_SOURCE_DIR), but
install-devapp.sh now downloads a pre-built binary (curl + tar, no
git/go/make) from DingTalk-Real-AI.
- Every command used the pre-rename `dws devapp ...`; the command is now
`dws dev app ...`.
Rewrite against the real `dws dev` tree (verified from the binary):
- install: DingTalk-Real-AI binary installer + correct env (DEVAPP_REPO /
DEVAPP_VERSION / DWS_INSTALL_DIR / DWS_NO_SKILLS) + a China Gitee note.
- skill name corrected to `dingtalk-dev`.
- commands: `dws dev app {list,get,create,update,enable,disable,delete,
credentials,permission,member,robot,security,version,webapp,event}` with
real flags (--confirm-name, --scope-values, --user-ids, --redirect-urls,
--version-id/--confirmed-sensitive), async robot create via submit/result,
version publish gated by check-approval.
The verify-replace mirror listed attachments via /releases/{id}, whose
"assets" array omits the attach id. DELETE /attach_files/{id} was therefore
called with an empty id and silently no-op'd, so a stale asset was never
removed — instead a second (correct) copy was uploaded. Gitee then serves the
OLDER attachment by name, so the stale darwin binaries kept winning and failed
install.sh's checksums.txt verification on macOS (国内 install broken).
Fix:
- List attachments via the dedicated /attach_files endpoint, which DOES
return the numeric id needed for deletion.
- Treat duplicates: collect every attach id carrying a given name; when >1,
delete them all and upload exactly one fresh, correct file. count==1 still
does the byte-identical skip / stale-replace; count==0 uploads new.
Self-heals the existing v1.0.42 darwin duplicates on the next mirror run.
The v1.0.42 Gitee release served darwin-amd64/arm64 binaries that did NOT
match checksums.txt (the macOS binaries are ad-hoc signed and differed
between the GitHub release and the earlier mirror run), so install.sh's
checksum verification failed for China macOS users. The previous skip-if-name-
present logic could not repair this — it skipped the stale assets.
sync-to-gitee.sh now verifies by content: for each artifact it compares the
sha256 of the asset already on Gitee against the local file (downloaded from
the GitHub release), and
• skips it when byte-identical,
• deletes + re-uploads it when present but stale,
• uploads it when missing,
bringing the Gitee release into byte-for-byte agreement with the GitHub
release that checksums.txt describes. Re-running the Sync-release-to-gitee
workflow now self-heals a mismatched mirror.
bash -n + sha256 helper validated locally.
The v1.0.42 Release job hit timeout-minutes: 30 mid-upload while mirroring
release assets to Gitee, so the Gitee release ended up missing
dws-windows-arm64.zip and checksums.txt. Root cause + fixes:
- sync-to-gitee.sh now skips assets already attached to the Gitee release,
so a re-run only uploads what is missing (instead of re-uploading every
artifact and creating duplicates). It no longer fails when everything is
already present.
- New workflow sync-release-to-gitee.yml (workflow_dispatch, version input)
mirrors a published GitHub release's assets to Gitee on its own — it
downloads the assets from the GitHub release and runs the idempotent sync,
without running GoReleaser or touching the GitHub release (no outage). Use
it to repair an incomplete Gitee mirror.
- Bump the Release job timeout 30 -> 60 so a full Gitee upload has room.
bash -n + YAML validated.
Probe GitHub Releases on startup; if it is unreachable (typical in mainland
China), automatically switch to the Gitee mirror — so a plain
`curl … | sh` (or .ps1 / install-skills.sh) works everywhere with no
DWS_GITEE_REPO env var. Explicit DWS_GITEE_REPO still wins; DWS_NO_FALLBACK=1
disables the probe; local source-checkout installs skip it.
Add scripts/release/sync-to-oss.sh and wire it into release.yml. After
the GitHub release, artifacts (binaries, checksums, dws-skills.zip) plus
the install scripts are pushed to an OSS bucket in the
<base>/<version>/<file> layout the install.sh DWS_RELEASE_BASE switch
expects, with a latest.txt pointer.
Gated on OSS_* repo secrets: skips cleanly when unset, so forks without
the secrets keep releasing to GitHub only.
Parameterize the hardcoded GitHub Releases domain in install.sh,
install.ps1 and install-skills.sh so domestic users can point asset
downloads (binary tarball, checksums, skills zip) and latest-version
resolution at a China-accessible mirror (e.g. OSS+CDN / dingtalk domain).
Defaults stay on GitHub — fully backward compatible.
Unauthenticated curl is limited to 60 req/h, and once exhausted the API
returns 403. The previous `curl -fsSL ... | grep tag_name` swallowed the
403 silently, leaving DEVAPP_VERSION empty and surfacing a misleading
"No release found" error even though the fork has plenty of releases.
- Prefer `gh api` when available (authenticated, 5 000 req/h).
- Fall back to curl while capturing the HTTP status; on 403/429 fail
with an actionable message instead of pretending no release exists.
Two distribution defects left installed skills stale (issue PeterGuy326#8):
1. skill setup resolved its source from cwd / exe-adjacent checkouts, so
an upgraded binary happily re-installed whatever stale checkout it was
run from, and agents kept routing 'create robot' into the chat dead end.
The skills/mono and skills/multi trees are now embedded (go:embed) and
setup defaults to that copy, materialized into ~/.dws/skills/<mode> —
upgrading the binary upgrades what setup installs. --source and
DWS_SKILL_SOURCE stay as explicit overrides, and an override that has
no skill root errors out instead of silently falling back.
2. dws upgrade's skill refresh (knownSkillDirs) skipped ~/.qoder and
~/.qoderwork, so Qoder/QoderWork hosts never got refreshed routing.
Added both to the dir table and its seven mirrors (npm install.js,
install.sh/ps1, install-skills.sh, homebrew template, package test,
verify-package-managers.sh).
setup-side detection and --target qoder/qoderwork already work on this
branch; this closes the remaining upgrade-side and source-side gaps.
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(install): add .hermes/skills to AGENT_DIRS (#188)
dws 安装后未自动复制 skill 到 .hermes/skills/。在 4 份硬编码的
AGENT_DIRS 清单(build/npm/install.js、scripts/install.sh、
scripts/install.ps1、scripts/install-skills.sh)末尾追加
.hermes/skills,与现有'父目录守卫'逻辑天然兼容:
- 已安装 Hermes 的用户:自动获得 ~/.hermes/skills/dws
- 未安装 Hermes 的用户:父目录不存在则跳过,零副作用
Closes#188
* fix(install): cover remaining 4 AGENT_DIRS sources for .hermes/skills (review feedback)
Address review feedback from #221: the AGENT_DIRS list actually has 8 sources
in the repo, not 4. Without this commit, dws upgrade users and Homebrew users
would still NOT get .hermes/skills/dws populated even after the previous
4-script fix landed.
Fixes (functional):
- internal/upgrade/paths.go (knownSkillDirs):
Append .hermes/skills so 'dws upgrade' refreshes ~/.hermes/skills/dws/
for users who have Hermes installed.
- build/homebrew.rb.tmpl (post_install targets):
Append .hermes/skills/dws so brew users also get the skill copied to
~/.hermes/skills/dws on post_install.
Fixes (test / smoke coverage):
- test/scripts/package_script_test.go (expectedPackagedSkillTargets):
Append .hermes/skills/dws so future regressions (e.g. someone removes
.hermes from install.js) are caught by CI.
- scripts/release/verify-package-managers.sh (HOME_AGENT_PARENTS / HOME_SKILL_TARGETS):
Append .hermes to both lists so the release-time npm/brew smoke test
actually asserts ~/.hermes/skills/dws/SKILL.md exists.
Documentation:
- internal/upgrade/paths.go: expanded the 'Kept in sync with' comment from
a single file reference (build/npm/install.js) to all 7 in-sync sources,
so future maintainers don't have to rediscover this list.
Verification:
- go vet ./internal/upgrade/... ./test/scripts/... PASS
- go build ./... PASS
- sh -n scripts/install.sh PASS
- sh -n scripts/install-skills.sh PASS
- sh -n scripts/release/verify-package-managers.sh PASS
- node --check build/npm/install.js PASS
---------
Co-authored-by: 猷诺 <bicheng.ybc@alibaba-inc.com>