Skip to content

feat(auth): support multi-profile login#500

Merged
PeterGuy326 merged 24 commits into
DingTalk-Real-AI:mainfrom
shangguanxuan633-lab:codex/dws-multi-profile-login
Jun 29, 2026
Merged

feat(auth): support multi-profile login#500
PeterGuy326 merged 24 commits into
DingTalk-Real-AI:mainfrom
shangguanxuan633-lab:codex/dws-multi-profile-login

Conversation

@shangguanxuan633-lab

Copy link
Copy Markdown
Contributor

Summary

  • Add local profile registry (profiles.json) for primary/current/previous DingTalk org profiles.
  • Store user tokens in corp-scoped keychain slots (auth-token:<corpId>) while preserving the legacy auth-token mirror.
  • Add dws profile list/use/use -, global --profile, and profile-aware auth status/logout/reset.
  • Isolate runtime token cache by profile and add tests for multi-profile save/load/switch/delete and legacy migration.

Server-side impact

No server change is required for this PR. The implementation reuses the existing OAuth/device login responses and the existing TokenData fields (CorpID, CorpName, UserID, UserName, ClientID, refresh/access expiry). All new selection and storage behavior is local to the CLI: profiles.json metadata plus keychain slots.

Server changes would only be needed for future scope outside this PR, such as automatic discovery of every org a user belongs to, cross-org aggregation APIs, or real/embedded host multi-profile hook protocol expansion.

Verification

  • go test ./internal/auth ./internal/app
  • go build ./...
  • git diff --check

Notes

  • go test ./... still has unrelated environment/fixture failures in this checkout: internal/transport stdio initialize timeout, test/cli_compat missing testdata/empty_catalog.json, and test/scripts missing package version/git tag.

@shangguanxuan633-lab

Copy link
Copy Markdown
Contributor Author

Runtime acceptance updated after commit 5dc5b03.

Verification:

  • go test ./internal/auth ./internal/app
  • second org login restored with dws auth login --profile ding32fff839a3e0105d --no-browser --format json
  • profile list shows both orgs with corpName: 钉钉 and 钉钉(中国)信息技术有限公司
  • created + read pre-alidocs test docs under both profiles
  • cross-org node reads fail with forbidden.accessDenied, confirming profile isolation
  • --profile '钉钉(中国)信息技术有限公司' works by org name
  • profile use '钉钉(中国)信息技术有限公司' and profile use - verified current/previous behavior

Test docs:

@shangguanxuan633-lab

Copy link
Copy Markdown
Contributor Author

Verified current PR head 756c7d1.

Passed acceptance for the multi-organization profile loop. Final review found no blocking issues; the last P2 gap was closed by adding root --profile help coverage and auth status --profile no-switch coverage.

Changes:

  • Added prd.json as the Ralph-style product/technical acceptance contract from the three DingTalk docs, with technical naming precedence: auth manages credentials, profile manages organization identity.
  • Completed top-level dws profile list/use behavior: JSON/table/human output includes organization name, profile use <corpId> and profile use - switch current/previous profile, and legacy token mirror stays in sync.
  • Root dws --help now renders Global Flags, including --profile, so terminal dispatch can discover one-shot organization selection.
  • auth status --format table now displays enterprise name and enterprise ID; auth status --profile renders the selected org without changing currentProfile.
  • Tightened logout semantics: primaryProfile cannot be deleted via auth logout; auth logout --all keeps primary and no longer asks the user to re-login while a valid primary remains.

Verification:

  • python3 -m json.tool prd.json
  • go test ./internal/auth -count=1
  • go test ./internal/app -count=1
  • go test ./test/cli -count=1
  • git diff --check

Full go test ./... is not the final gate here because the repo still has known baseline/environment blockers observed earlier:

  • test/cli_compat: missing test/cli_compat/testdata/empty_catalog.json
  • test/scripts: post-goreleaser.sh requires DWS_PACKAGE_VERSION / git tag context

@shangguanxuan633-lab

Copy link
Copy Markdown
Contributor Author

Ralph 验收补充

本轮按技术方案优先收敛了多组织登录能力,并把在线资料、PRD、技术方案、验收评审都落到了 docs/ralph/dws-multi-profile-login/

关键结论

  • 第二/第三个组织不新增特殊命令,继续执行 dws auth login --force --format json,在 OAuth 页选择目标组织。
  • corpId 会新增 profile;已存在 corpId 只刷新 token 和元数据。
  • 查看组织:dws profile list --format json
  • 持久切换:dws profile use <name|corpId|-> --format json
  • 单次命令指定组织:dws --profile <name|corpId> <product> <command> --format json
  • 产品稿里的 auth list/auth switch/--associated/--组织corp ID 不进入 P0,分别由 profile list/use、重复 auth login --force 和全局 --profile 替代。

验证

go test ./internal/auth ./internal/app -run 'Test(MultiProfile|RuntimeProfile|DeleteProfile|UpsertProfile|LoadProfiles|LegacyKeychain|WriteProfile|ProfileList|ProfileUse|AuthStatus|AuthLogout|AuthLogin|ResolveAuthLogin|EnrichAuthLogin|RootHelp|RootShortHelp|RootCommand)'

结果:internal/auth 与多组织相关 internal/app 用例通过。

另已验证本地打包安装:

  • dws versionv1.0.41-SNAPSHOT,commit 756c7d1
  • dws profile list --format json:本机已有两个 profile,一个 primary,一个 current

残余说明

全量 go test ./internal/auth ./internal/appinternal/app 被 upgrade 模块用例 TestValidateNewBinary_RecoversFromUnsignedDarwin 阻塞,错误是测试二进制执行被 macOS kill。该失败不在本次多组织登录改动面内,建议作为独立本机签名/隔离环境问题跟进。

@shangguanxuan633-lab

Copy link
Copy Markdown
Contributor Author

Ralph 验收补充

本轮按技术方案优先收敛了多组织登录能力,并把在线资料、PRD、技术方案、验收评审都落到了 docs/ralph/dws-multi-profile-login/

关键结论

  • 第二/第三个组织不新增特殊命令,继续执行 dws auth login --force --format json,在 OAuth 页选择目标组织。
  • corpId 会新增 profile;已存在 corpId 只刷新 token 和元数据。
  • 查看组织:dws profile list --format json
  • 持久切换:dws profile use <name|corpId|-> --format jsondws auth switch <name|corpId|-> --format json
  • 交互切换:dws auth switchdws profile use 无参数时展示组织选择 TUI。
  • 单次命令指定组织:dws --profile <name|corpId> <product> <command> --format json
  • 产品稿里的 auth list/--associated/--组织corp ID 不进入 P0,分别由 profile list、重复 auth login --force 和全局 --profile 替代;auth switch 作为 profile use 的兼容入口保留。

验证

go test ./internal/auth ./internal/app -run 'Test(MultiProfile|RuntimeProfile|DeleteProfile|UpsertProfile|LoadProfiles|LegacyKeychain|WriteProfile|ProfileList|ProfileUse|ProfileSwitch|AuthSwitch|AuthStatus|AuthLogout|AuthLogin|ResolveAuthLogin|EnrichAuthLogin|RootHelp|RootShortHelp|RootCommand)'

结果:internal/auth 与多组织相关 internal/app 用例通过。

另已验证本地打包安装:

  • dws versionv1.0.41-SNAPSHOT,commit 756c7d1
  • dws profile list --format json:本机已有两个 profile,一个 primary,一个 current

残余说明

全量 go test ./internal/auth ./internal/appinternal/app 被 upgrade 模块用例 TestValidateNewBinary_RecoversFromUnsignedDarwin 阻塞,错误是测试二进制执行被 macOS kill。该失败不在本次多组织登录改动面内,建议作为独立本机签名/隔离环境问题跟进。

@shangguanxuan633-lab

Copy link
Copy Markdown
Contributor Author

Ralph 验收补充

本轮按技术方案优先收敛了多组织登录能力,并把在线资料、PRD、技术方案、验收评审都落到了 docs/ralph/dws-multi-profile-login/

关键结论

  • 第二/第三个组织不新增特殊命令,继续执行 dws auth login --force --format json,在 OAuth 页选择目标组织。
  • corpId 会新增 profile;已存在 corpId 只刷新 token 和元数据。
  • 查看组织:dws profile list --format json
  • 持久切换:dws profile use <name|corpId|-> --format jsondws auth switch <name|corpId|-> --format json
  • 交互切换:dws auth switchdws profile use 无参数时展示组织选择 TUI。
  • 单次命令指定组织:dws --profile <name|corpId> <product> <command> --format json
  • 登出:dws auth logout 默认清理所有组织登录态;dws auth logout --profile <name|corpId> 只清指定组织;--all 已移除。
  • 产品稿里的 auth list/--associated/--组织corp ID 不进入 P0,分别由 profile list、重复 auth login --force 和全局 --profile 替代;auth switch 作为 profile use 的兼容入口保留。

验证

go test ./internal/auth ./internal/app -run 'Test(MultiProfile|RuntimeProfile|DeleteProfile|UpsertProfile|LoadProfiles|LegacyKeychain|WriteProfile|ProfileList|ProfileUse|ProfileSwitch|AuthSwitch|AuthStatus|AuthLogout|AuthLogin|ResolveAuthLogin|EnrichAuthLogin|RootHelp|RootShortHelp|RootCommand)'

结果:internal/auth 与多组织相关 internal/app 用例通过。

另已验证本地打包安装:

  • dws versionv1.0.41-SNAPSHOT,commit 756c7d1
  • dws profile list --format json:本机已有两个 profile,一个 primary,一个 current

残余说明

全量 go test ./internal/auth ./internal/appinternal/app 被 upgrade 模块用例 TestValidateNewBinary_RecoversFromUnsignedDarwin 阻塞,错误是测试二进制执行被 macOS kill。该失败不在本次多组织登录改动面内,建议作为独立本机签名/隔离环境问题跟进。

@shangguanxuan633-lab

Copy link
Copy Markdown
Contributor Author

Ralph 验收补充

本轮按技术方案优先收敛了多组织登录能力,并把在线资料、PRD、技术方案、验收评审都落到了 docs/ralph/dws-multi-profile-login/

关键结论

  • 第二/第三个组织不新增特殊命令,继续执行 dws auth login --format json,在 OAuth 页选择目标组织。
  • corpId 会新增 profile;已存在 corpId 只刷新 token 和元数据。
  • 查看组织:dws profile list --format json
  • 持久切换:dws profile use <name|corpId|-> --format jsondws auth switch <name|corpId|-> --format json
  • 交互切换:dws auth switchdws profile use 无参数时展示组织选择 TUI。
  • 单次命令指定组织:dws --profile <name|corpId> <product> <command> --format json
  • 登出:dws auth logout 默认清理所有组织登录态;dws auth logout --profile <name|corpId> 只清指定组织;--all 已移除。
  • 产品稿里的 auth list/--associated/--组织corp ID 不进入 P0,分别由 profile list、重复 auth login 和全局 --profile 替代;auth switch 作为 profile use 的兼容入口保留。

验证

go test ./internal/auth ./internal/app -run 'Test(MultiProfile|RuntimeProfile|DeleteProfile|UpsertProfile|LoadProfiles|LegacyKeychain|WriteProfile|ProfileList|ProfileUse|ProfileSwitch|AuthSwitch|AuthStatus|AuthLogout|AuthLogin|ResolveAuthLogin|EnrichAuthLogin|RootHelp|RootShortHelp|RootCommand)'

结果:internal/auth 与多组织相关 internal/app 用例通过。

另已验证本地打包安装:

  • dws versionv1.0.41-SNAPSHOT,commit 756c7d1
  • dws profile list --format json:本机已有两个 profile,一个 primary,一个 current

残余说明

全量 go test ./internal/auth ./internal/appinternal/app 被 upgrade 模块用例 TestValidateNewBinary_RecoversFromUnsignedDarwin 阻塞,错误是测试二进制执行被 macOS kill。该失败不在本次多组织登录改动面内,建议作为独立本机签名/隔离环境问题跟进。

@shangguanxuan633-lab

Copy link
Copy Markdown
Contributor Author

Ralph 验收补充

本轮按技术方案优先收敛了多组织登录能力,并把在线资料、PRD、技术方案、验收评审都落到了 docs/ralph/dws-multi-profile-login/

关键结论

  • 第二/第三个组织不新增特殊命令,继续执行 dws auth login --format json,在 OAuth 页选择目标组织。
  • corpId 会新增 profile;已存在 corpId 只刷新 token 和元数据。
  • 查看组织:dws profile list --format json
  • 持久切换:dws profile switch <name|corpId|-> --format json;目标可以是主组织,选中主组织即可切回。
  • 交互切换:dws profile switch 无参数时展示组织选择 TUI,列表包含主组织、当前组织和已登录附属组织。
  • 单次命令指定组织:dws --profile <name|corpId> <product> <command> --format json
  • 登出:dws auth logout 默认清理所有组织登录态;dws auth logout --profile <name|corpId> 只清指定组织;--all 已移除。
  • 产品稿里的 auth list/--associated/--组织corp ID/auth switch 不进入 P0,分别由 profile list、重复 auth login、全局 --profileprofile switch 替代;auth 命令组不暴露 switch。

验证

go test ./internal/auth ./internal/app -run 'Test(MultiProfile|RuntimeProfile|DeleteProfile|UpsertProfile|LoadProfiles|LegacyKeychain|WriteProfile|ProfileList|ProfileUse|ProfileSwitch|AuthCommandDoesNotExposeSwitch|AuthStatus|AuthLogout|AuthLogin|ResolveAuthLogin|EnrichAuthLogin|RootHelp|RootShortHelp|RootCommand)'
go test ./internal/auth ./internal/app

结果:internal/authinternal/app 均通过。

另已验证本地打包安装,本机 dws 已指向本 PR 最新构建。

@shangguanxuan633-lab

Copy link
Copy Markdown
Contributor Author

Profile switch TUI fix

Updated current PR head to c5ae1c2.

What changed:

  • dws profile switch now builds TUI options from every profile in profiles.json, so all logged-in profiles are selectable.
  • The switch TUI label was changed from a wide table row to a compact single-line label to avoid narrow terminal wrapping hiding options.
  • Added TestProfileSwitchOptionsIncludeAllLoggedProfiles to assert all profiles are passed into the selector and labels do not contain newlines.

Verification:

  • go test ./internal/app -run 'TestProfileSwitch|TestProfileUse|TestAuthCommandDoesNotExposeSwitch|TestWriteProfile|TestProfileList' -count=1\n- go test ./internal/auth ./internal/app\n- local dws version => v1.0.41-SNAPSHOT, commit c5ae1c2

shangguanxuan.sgx and others added 11 commits June 26, 2026 19:13
…stence

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.
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.
audanye-sudo and others added 2 commits June 29, 2026 17:33
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.
…file-login

# Conflicts:
#	.github/workflows/auto-dev-release.yml

@PeterGuy326 PeterGuy326 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

看过了,CI 全绿,本地 go test ./internal/auth ./internal/app、多组织 profile 切换和 --profile 都验过,没问题。

一个小建议(不卡合入):Verification 段补一下多组织手测流程——升级迁移、第二个组织登录、profile use - 切换、auth logout 清槽,每步贴预期 vs 实际;再点一句 keychain 不随 DWS_CONFIG_DIR 隔离这个坑,方便别人复现。

@PeterGuy326 PeterGuy326 merged commit e32fa15 into DingTalk-Real-AI:main Jun 29, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants