Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .agents/skills/verify-deepseekbot/features/team-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
- S8 (M3 Host/CLI): `memberDevices { humanId }` is self-scoped without management permission; other targets require `members.manage`. `memberDeviceRevoke { humanId, grantId }` closes only that grant. `current` comes from the authenticated principal.
- S9 (M3 Host/CLI): `ownerTransfer { humanId, password }` verifies the current Owner password; the outgoing Owner gets Admin and the target has no roles. The outgoing Owner's paired devices and pending offers cannot inherit the new Owner, including an A→B→A round trip. Transfers require a credential issued by the current Owner and a completed forced change; `owner-transfer-target-unverified` requires current-Owner reset followed by the target's password change.

- S10 (M14 Host/CLI): `channels member-candidates <id> [--query <name>]` and `channels add-member <id> <username>` enforce `addMembers` as the explicit calling Human. An invitation opens private/roles/public Channels immediately in list, stream and search. A role-based join exits on role revoke; a named invitation remains. Bots in the Channel do not cause invitation refusal. The Add people picker UI is verified separately by its UI owner.

## How to get to it (user POV)

Owner: Bot settings › Team. Member: open the address under Team › Sign-in › Address in a browser on another machine, sign in, choose a new password, then New (+) › Browse channels.
Expand Down Expand Up @@ -46,3 +48,6 @@ M3 canonical verification: `roles catalog`, `roles list|create|update|delete`, `
M3 review regressions: suspended Humans have no effective permissions, role deletion requires CLI `--confirm`, and live roster/Channel gates recheck the current Owner after transfer for baselines, queued frames, new publications and SSE. See the [transfer credential rule](../../../../docs/architecture/team-mode.md#m3-owner-transfer-credential-rule) before qualifying ownership changes; clearing `mustChangePassword` alone does not establish eligibility.

M10 Host/CLI: `usage members --period week`, `models allowed`, and `roles update <roleId> --models all|provider/model,provider/model`, including `--as`. `usageByMember` requires `usage.view`; refusal is `permission-required` with `details.permission`. Route selection and the next turn refuse `model-not-allowed` with `details.route`; `get`/`list` mark `modelAllowed: false` without changing the plan. Members receive no credential readiness. The per-member table, Role Models editor and filtered Client pickers belong to the separate UI half.


M14 canonical verification: launch `m14-logic` and run `doctor`; enable Team mode, create two members and change their initial passwords through `--as`. Create private and roles Channels as Owner. Use Owner and member `channels member-candidates <id> --query <name>` and `channels add-member <id> <username>`; verify `channels`, `channel-messages`, `channel-timeline` and `message-search --query <marker>` as the invited member. Open `followChannel` through the authenticated member carrier. Join one roles Channel with `channels join`; invite the other person by name. Revoke both people's matching role with `members roles <username> --set member`: the role-joined person's list/search and stream lose access, the invited person's retain it. Set `--add-members admins` and prove both candidate search and invitation refuse a regular member with `permission-required` / `details.permission: 'addMembers'`. Exercise `--as` for both successes and refusals. Upgrade/restart classification is covered by `packages/core/test/team-channel-membership-migration.test.ts`; see [M14 runtime notes](../../../../docs/architecture/team-mode.md#m14-runtime-invitations-and-membership-basis). Stop the verified task Host and check its process/port release. This logic journey does not qualify rendered Web, installed Desktop, native mobile or Human acceptance.
11 changes: 11 additions & 0 deletions changes/4188-channel-invitations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
section: Changed
---

## en

- Channel invitations now grant access regardless of visibility; named invitations survive role revocation, while role-based joins end with their access. The CLI can search people available to add ([#4188](https://github.com/BotHarness/DeepSeekBot/issues/4188)).

## zh

- Channel 姓名邀请现在不受可见性限制地授予访问;撤销角色后保留姓名邀请,角色加入则随角色访问撤销而结束。CLI 支持搜索可添加的人([#4188](https://github.com/BotHarness/DeepSeekBot/issues/4188))。
12 changes: 12 additions & 0 deletions docs/architecture/team-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,15 @@ This rule trusts the current Owner's out-of-band delivery to the intended Human.
| M12 | Linux Bot execution confinement (hide other spaces and `DSH_HOME`, network namespace); ends the trial notice | M6 |

Seek Human feedback after M1 before expanding.

## M14 runtime: invitations and membership basis

The 2026-11-15 amendment on spec #4137 supersedes the older Bot audience-containment text above for Channel invitations: there is no Bot co-occupancy refusal. Bot principal inheritance is delivered separately by M7.

M14 stores `channel_human_members.membership_basis` per Channel/Human. `channelJoin` (including the older `publicChannelJoin`) records `role` for browse-and-join; `channelHumanAdd` records `invited` for a named invitation, including promotion of an existing role join. Repeating Join never downgrades an active invitation. Named invitations grant immediate access to private, role-restricted and public Channels regardless of the person's roles or Host permission rank. Removing a role closes only role-dependent memberships that no longer satisfy any allowed role; the role update and membership exit commit together. Restoring the role permits browsing again but does not silently rejoin. Visibility changes, role deletion and Owner-transfer role resets apply the same rule. Named access survives role changes until the person leaves or is removed. Channel creation and explicit ownership transfer establish named access too; transfer does not remove an outgoing active person's named membership.

Application-defined Host bridge methods use the trusted explicit `HumanContext`, never an identity from the payload. `channelHumanAdd { channelId, humanId }` accepts a Human ID or username and returns `null`. `channelMemberCandidates { channelId, query?: string }` returns up to 100 `{ humanId, displayName, username: string | null }` records, sorted by display name and Human ID. Query defaults to empty and is limited to 200 characters; it matches display names and usernames. The picker excludes inactive people and current members. Both require the Host Owner, `channels.manageAll`, a joined Channel owner/admin, or a joined member when `addMembers` is `everyone`; other callers receive `permission-required` with `details.permission: 'addMembers'`. Unknown/non-Group Channels refuse `channel-not-found`; unknown/inactive invitees refuse `member-not-found`. Invalid wire inputs refuse `invalid-input`.

The named `channel-membership-basis` migration is appended after `migration-ledger`. Historic invitation provenance is unavailable. To preserve the former role-revocation boundary, ordinary memberships in role-restricted Groups become `role`, with ineligible members marked left. The Host Owner, Channel owners/admins, DMs and memberships in public/private Groups become `invited`. This conservatively retains existing non-role access and all Owner data; it cannot recover which historic ordinary role-Channel members were invited by name. Re-inviting one by name establishes durable named access. Channel records, messages, read positions and membership history are retained across upgrade and restart.

The existing shared SQL audience predicate governs lists, stream authorization, search and unread queries. Invitation writes publish through the existing membership change log; role revocation refreshes streams and invalidates list cursors. No new cache, timer, polling loop, dependency or Bot execution path is introduced. The additional basis predicate uses the existing membership primary key; a partial index bounds role-membership lookup by Human. Regression guards keep audience/browse checks at one database read independent of Channel count, bound candidate lookup reads and output to 100, and verify stream closure alongside search/list correctness. Runtime timing remains machine-specific; these structure guards do not claim an end-to-end speedup.
12 changes: 12 additions & 0 deletions docs/architecture/team-mode.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ Human 在 2026-11-13 完成 Q1–Q35 并确认的目标设计,不代表已实
## 交付切片

M0 去硬编码 → **M1 首个端到端 tracer**(开启团队模式、创建成员、内网 HTTPS 登录、加入公开 Channel 互发消息、CLI)→ M2 私有 Bot、Tob 与 Activity(M11 之前 Owner 不能查看成员私有内容)→ M3 角色与成员管理 → M4 Channel 可见性与 Human DM → M5 审批 → M6 Workspace → M7 Bot 可见性与角色策略 → M8 移动端 → M9 密码与离职 → M10 用量 → M11 审计 → M12 Linux 执行隔离(结束试用告知)。依赖关系见[英文版](team-mode.md#delivery)。M1 完成后先取得 Human 反馈,再扩展。

## M14 运行时:邀请与成员访问依据

规格 #4137 在 2026-11-15 的修订取代上文旧的 Bot 受众包含限制:邀请 Human 不因 Channel 里有 Bot 而拒绝;Bot 继承其 principal 访问的实现由 M7 单独交付。

`channel_human_members.membership_basis` 按 Channel/Human 保存访问依据。浏览后通过 `channelJoin`(或旧 `publicChannelJoin`)加入记为 `role`;按姓名通过 `channelHumanAdd` 添加记为 `invited`,也可将已有角色加入提升为姓名邀请。重复 Join 不会降低有效邀请。无论 private、roles 或 public,可添加成员的人都能直接授予访问,不比较被邀请者的角色或 Host 权限高低。撤销角色后,不再符合任何允许角色的 `role` 成员在同一事务中退出,流立即关闭,列表和搜索不再显示;恢复角色只恢复浏览资格,不自动重新加入。可见性变更、删除角色和移交 Owner 时的角色重置沿用此规则。姓名邀请一直有效,直到离开或被移除;创建 Channel 和显式移交所有权也授予姓名访问,移交不自动删除原所有者仍有效的姓名成员关系。

应用定义的 Host 方法从受信任的显式 `HumanContext` 取得调用者。`channelHumanAdd { channelId, humanId }` 接受 Human ID 或用户名,返回 `null`。`channelMemberCandidates { channelId, query?: string }` 按显示名和 Human ID 排序,最多返回 100 条 `{ humanId, displayName, username: string | null }`;搜索默认为空,最多 200 字符,匹配显示名和用户名,排除非活跃者与现有成员。两者允许 Host Owner、`channels.manageAll`、已加入的 Channel owner/admin,或 `addMembers: 'everyone'` 时已加入的普通成员。缺权返回 `permission-required`,带 `details.permission: 'addMembers'`;未知或非群组 Channel 返回 `channel-not-found`,未知或非活跃目标返回 `member-not-found`,无效输入返回 `invalid-input`。

追加的具名迁移为 `channel-membership-basis`(位于 `migration-ledger` 之后)。历史记录无法区分邀请来源,因此 roles 群组的普通成员归为 `role`,已不符合角色的记录标记退出;Host Owner、Channel owner/admin、DM 与 public/private 群组旧成员归为 `invited`。这样保留原角色撤销边界及所有 Owner 数据,但无法还原旧角色群组里哪些普通成员曾按姓名邀请;重新按姓名添加即可建立持久邀请。Channel、消息、已读位置与成员历史不会删除,重启保留分类。

列表、流、搜索和未读统一使用既有 SQL 受众检查。邀请沿用成员变更日志;角色撤销刷新流并使列表游标失效。没有新增缓存、计时器、轮询、依赖或 Bot 执行路径。依据检查使用成员主键,按 Human 查询角色成员使用部分索引。回归守卫保持受众/浏览单次数据库读取、候选查询读取次数及最多 100 条结果,并验证搜索、列表和流关闭。结构守卫不代表端到端性能提升。
10 changes: 10 additions & 0 deletions docs/bot-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2112,3 +2112,13 @@ deepseekbot models allowed [--as <username>]
```

Tool effect declarations (E1) preserve the existing tool commands and approval outcomes. Full Access, saved rules, Secrets, device targets and Workspace Grants retain their existing checks; no new route or CLI authority is introduced.

### Channel invitations (M14)

```sh
deepseekbot channels member-candidates <channel-id> --query Alice
deepseekbot --as bob channels member-candidates <channel-id> --query alice
deepseekbot --as bob channels add-member <channel-id> alice
```

Candidate listing returns up to 100 active people outside the Channel, with `humanId`, `displayName` and nullable `username`. Search matches names and usernames. Both commands enforce the calling Human's `addMembers` right, including Channel owner/admin and `channels.manageAll`; refusal is `permission-required` with `details.permission: 'addMembers'`. Invitations grant access immediately for every visibility and survive role revocation. Browse-and-join memberships depend on allowed roles and leave when that access is revoked. See [M14 runtime notes](architecture/team-mode.md#m14-runtime-invitations-and-membership-basis) for upgrade classification.
10 changes: 10 additions & 0 deletions docs/bot-cli.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -2053,3 +2053,13 @@ Web 手动检查请求最多等待 25 秒,关闭自检卡片会取消请求。
公网阶段仅对 DNS、网络、超时及 HTTP 429/5xx 短暂失败重试:最多四次,间隔 1/3/6 秒,仍受同一个 20 秒总时限约束。证书或身份失败不重试。每次自检完成后,界面刷新一次账号电脑列表。

工具效果声明(E1)保持既有工具命令与审批结果。Full Access、保存规则、Secrets、设备目标和 Workspace Grant 仍使用原检查;不新增 route 或 CLI 权限。

### Channel 邀请(M14)

```sh
deepseekbot channels member-candidates <channel-id> --query Alice
deepseekbot --as bob channels member-candidates <channel-id> --query alice
deepseekbot --as bob channels add-member <channel-id> alice
```

候选查询最多返回 100 位活跃且未加入该 Channel 的人,包含 `humanId`、`displayName` 和可为空的 `username`,可按显示名或用户名搜索。两个命令都检查调用 Human 的 `addMembers` 权限,包括 Channel owner/admin 和 `channels.manageAll`;缺权返回 `permission-required`,带 `details.permission: 'addMembers'`。姓名邀请对所有可见性立即授予访问,撤销角色后仍保留;浏览后加入的角色成员在角色访问撤销时退出。升级分类见 [M14 运行时说明](architecture/team-mode.zh.md#m14-运行时邀请与成员访问依据)。
2 changes: 1 addition & 1 deletion docs/channel-sidebar/groups.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The **Work** tab lists the Assignments this group asked its Bots for, newest fir

Open the **Members** tab (or expand **Members** in the narrow overlay) to see the Human, participating PersonaBots and the creator marker when present. Click a Bot to open its DM. Its **…** menu offers **Open DM**, **Message attention settings**, and **Remove from group**; removal asks for confirmation.

Click **Invite member** beside the entry title to search for an available Bot and send an invitation. Already-present, paused, or pending-invitation Bots are excluded. The Profile’s **Auto-accept Group invitations** setting determines the result: when enabled an invitation can be accepted immediately; otherwise wait for the Bot to accept or decline. A pending invitation is not membership: check both its result and the actual member list.
Click **Invite a Bot** beside the entry title to search for an available Bot and send an invitation. In Team mode, people who may add members to the Channel also see **Add people**: pick a member and they get access at once, even to a private Channel. Already-present, paused, or pending-invitation Bots are excluded. The Profile’s **Auto-accept Group invitations** setting determines the result: when enabled an invitation can be accepted immediately; otherwise wait for the Bot to accept or decline. A pending invitation is not membership: check both its result and the actual member list.

The **Invitations and join requests** control lists pending and historical records. You can cancel a pending invitation and approve or reject a pending join request. Check the member list afterwards to confirm the resulting membership.

Expand Down
2 changes: 1 addition & 1 deletion docs/channel-sidebar/groups.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

打开 **成员** 标签页(窄窗口覆盖面板中展开 **成员**),查看 Human、参与的 PersonaBot,以及存在时显示的创建者标记。点击 Bot 可打开私聊;它的 **…** 菜单包含 **打开私聊、消息提醒设置、移出群聊**,移出会要求确认。

点击标题旁的 **邀请新群员**,搜索可邀请 Bot 并发送邀请。已在群内、暂停或已有待处理邀请的 Bot 不会列为候选。邀请结果由 Profile 的 **自动接受入群邀请** 设置决定:开启时可以直接显示 **已接受**;关闭时等待 Bot 接受或拒绝。待接受邀请不等于已经入群,应核对邀请状态和实际成员列表。
点击标题旁的 **邀请 Bot**,搜索可邀请的 Bot 并发送邀请。在 Team 模式下,有权向频道添加成员的人还会看到 **添加成员**:选中一位成员后,对方立即获得访问权限,私密频道也一样。已在群内、暂停或已有待处理邀请的 Bot 不会列为候选。邀请结果由 Profile 的 **自动接受入群邀请** 设置决定:开启时可以直接显示 **已接受**;关闭时等待 Bot 接受或拒绝。待接受邀请不等于已经入群,应核对邀请状态和实际成员列表。

**邀请与入群申请** 控件列出待处理和历史记录。可以取消待处理邀请,也可以批准或拒绝待审批入群申请。操作后核对成员列表,确认实际成员关系。

Expand Down
10 changes: 10 additions & 0 deletions packages/browser/lib/client.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/browser/lib/client.js.map

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions packages/client/src/client/bot-main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useTeamMember } from './team-state.js';
import { useTeamMember, useTeamMode } from './team-state.js';
import { AnnouncementGate } from './announcement-gate.js';
import { ReadFailure } from './ds/read-failure.js';
import { humanIdOf, useSelfHumanId, SelfHumanContext } from './self-human.js';
Expand Down Expand Up @@ -1278,6 +1278,7 @@ function ConversationView({
const sidebar = useChannelSidebar(state);
const docked = useChannelDockActive();
const workspaceEnabled = useContext(WorkspaceContext) !== undefined;
const teamMode = useTeamMode(actions.teamCall)?.enabled === true;
const [detail, setDetail] = useScopedState<'files' | 'inbox' | undefined>(
scopeKey,
state.selection?.kind === 'bot' && state.selection.inboxRequest ? 'inbox' : undefined,
Expand Down Expand Up @@ -3936,7 +3937,7 @@ function ConversationView({
(hasChannelBots(channel) ||
(entry.scope !== 'personabot' &&
entry.id !== 'sessions' &&
entry.id !== 'members')) &&
(entry.id !== 'members' || teamMode))) &&
(entry.scope === 'both' || entry.scope === settingsScope) &&
(entry.visible?.(state) ?? true)
}
Expand Down
Loading
Loading