Skip to content
Closed
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
8 changes: 6 additions & 2 deletions docs/designs/team-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ teamai env unset GITHUB_TOKEN [--global]
```

- The value is never taken from an argument, so it stays out of shell history. `--stdin` refuses a terminal.
- `env set` accepts a key the scope declares as a secret or, without `--global`, an `env.yaml` variable it receives; `--global` is for secrets only. On Windows the key may be typed in any case: `env set` and `env unset` use the name the scope declares, `env add --secret` and `env remove --secret` the name already declared, and a value stored under another case of the name is the key's. When the declarations or `env.yaml` cannot be read it changes nothing, since it cannot tell. A project config that exists but can't be read makes `env set`, `env unset` and `env list` fail with its path and why, rather than use the user scope, whose team may not be this project's.
- `env set` accepts a key the scope declares as a secret, a `${VAR}` its MCP servers reference that nothing declares or sets (an [implicit secret](#a-variable-only-mcpyaml-names-is-a-secret)), or, without `--global`, an `env.yaml` variable it receives; `--global` is for secrets only. On Windows the key may be typed in any case: `env set` and `env unset` use the name the scope declares, `env add --secret` and `env remove --secret` the name already declared, and a value stored under another case of the name is the key's. When the declarations or `env.yaml` cannot be read it changes nothing, since it cannot tell. A project config that exists but can't be read makes `env set`, `env unset` and `env list` fail with its path and why, rather than use the user scope, whose team may not be this project's.
- Outside any scope (no project here and no user scope), `env set --global` accepts any valid key name and notes that no team declares it yet, so a member can set a token they reuse across teams ahead of time. `env unset` accepts any key that has a value.
- `--from-env` warns when the variable is not set in the current shell. While it is unset, the secret is `missing`: the next source in the [order](#resolution) is not used instead, since that could be another account's token.
- Run `teamai pull` afterwards to update the MCP servers, and `env.sh` for a variable.
Expand Down Expand Up @@ -146,14 +146,18 @@ the member's value for this team teamai env set KEY [--from-env VAR]

**Out of git.** A resolved value lands in a project-scope MCP config only once the clone's `.git/info/exclude` lists the file (#882). An exclude rule does not stop a file git already tracks, so no resolved value, declared secret or not, is written into a project config `git ls-files` tracks: pull leaves that file as it was (an entry an earlier pull wrote stays), and `pull` (a warning), `teamai mcp list` (`withheld:`) and `teamai doctor` (`MCP servers delivered to <tool>` fails) name the file and the fix: `git rm --cached <file>`, and rotate the token if it was ever committed. An exclusion that fails for another reason (`.git/info` or the exclude file not writable, another teamai command holding it, a git error) leaves the file as it was the same way, with that reason and its fix.

## A variable only mcp.yaml names is a secret

A `${VAR}` in `mcp/mcp.yaml` (or an active `mcp/<ns>/mcp.yaml`) that no secrets file declares and no `env.yaml` sets is a member's own token: the team repo cannot hold its value, and a team that has no `env/` at all still references it (#1011). Every consumer treats it as a declared secret, under the file that references it: `teamai env set VAR` stores the member's value; a pull resolves it in the secret [order](#resolution), the member's own environment last; a server whose value a pull cannot see keeps the entry an earlier pull wrote (below), so the session-start pull, which runs without the member's shell exports, no longer removes it; and the [missing line](#a-missing-secret-tells-the-member-what-to-run) names it with the servers that use it. `env list` shows it under `Team secrets` as `used by MCP server <name>`. Declaring it in `env/secrets.yaml` adds a description and a `url`; it changes nothing else. While the secrets files, `env.yaml` or `mcp.yaml` cannot be read, nothing is implied: the command reports that failure.

## A missing secret keeps the MCP entry

`${VAR}` in `mcp/mcp.yaml` can name a declared secret. The session-start pull runs in the agent's environment, which often lacks the member's shell exports (a GUI-launched tool, or a zsh export under `bash -lc`), so a secret can be there for one pull and gone for the next. When a pull finds no value for a server's declared secret:

- A server an earlier pull wrote keeps its entry in each tool's config, as it is, and teamai still manages it: a later pull that finds a value updates it.
- A server no pull has written yet is skipped, as before.
- The entry is removed when its server leaves `mcp.yaml`, and by `teamai mcp remove`, `teamai uninstall`, and `teamai init` when it moves the Claude Code root.
- A server that also misses a variable not declared as a secret is removed, as before. Variables that aren't declared as secrets keep today's behaviour.
- A server that also misses a variable only `mcp.yaml` names is kept the same way: that variable is an [implicit secret](#a-variable-only-mcpyaml-names-is-a-secret). An `env.yaml` variable always has the team's value, so it is never the one missing.

A kept entry holds the value the earlier pull wrote. After a secret is rotated or revoked, the server keeps the old value until a pull finds the new one.

Expand Down
6 changes: 5 additions & 1 deletion docs/designs/team-secrets.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,18 @@ teamai env unset GITHUB_TOKEN [--global]

**不进入 git。** 只有在本地克隆的 `.git/info/exclude` 列出某个项目级 MCP 配置之后,解析后的值才会写入该文件(#882)。exclude 规则挡不住 git 已跟踪的文件,因此无论是否为已声明密钥,解析后的值都不会写入 `git ls-files` 已跟踪的项目配置:pull 保持该文件原样(之前 pull 写入的条目保留),`pull`(警告)、`teamai mcp list`(`withheld:`)和 `teamai doctor`(`MCP servers delivered to <tool>` 失败)会指出该文件和修复方法:`git rm --cached <file>`,如果它曾随 token 一起提交过,还要轮换 token。因其他原因无法排除时(`.git/info` 或 exclude 文件不可写、另一个 teamai 命令占用它、git 出错),同样保持该文件原样,并给出对应的原因与修复方法。

## 只在 mcp.yaml 中出现的变量就是密钥

`mcp/mcp.yaml`(或生效的 `mcp/<ns>/mcp.yaml`)里的 `${VAR}`,若没有任何 secrets 文件声明、也没有任何 `env.yaml` 设置,就是成员自己的 token:团队仓库放不下它的值,而一个根本没有 `env/` 的团队也会引用它(#1011)。所有使用方都把它当作已声明的密钥,归属于引用它的那个文件:`teamai env set VAR` 保存成员的值;pull 按密钥的[解析顺序](#解析顺序)解析它,成员自己的环境排在最后;某次 pull 看不到值时,保留上一次 pull 写入的条目(见下节),因此在没有成员 shell 导出的 session-start pull 中不再删除它;[缺少密钥的提示](#缺少密钥时告诉成员该运行什么)会连同使用它的 server 一起列出它。`env list` 在 `Team secrets` 下把它显示为 `used by MCP server <name>`。在 `env/secrets.yaml` 中声明它只是补充 description 和 `url`,其他都不变。secrets 文件、`env.yaml` 或 `mcp.yaml` 无法读取时不做任何推断:命令会报告那个失败。

## 缺少密钥时保留 MCP 条目

`mcp/mcp.yaml` 中的 `${VAR}` 可以引用已声明的密钥。会话开始时的 pull 运行在 agent 的环境里,而这个环境常常没有成员 shell 中导出的变量(从图形界面启动的工具,或在 `bash -lc` 下读不到的 zsh export),所以一个密钥可能这次 pull 能找到、下次就找不到。pull 找不到某个 server 所需的已声明密钥时:

- 之前某次 pull 写入过的 server 会在每个工具的配置中原样保留,并仍由 teamai 管理:之后某次 pull 找到值时会更新它。
- 还没有任何 pull 写入过的 server 照旧跳过。
- 该 server 从 `mcp.yaml` 中移除时,条目随之删除;`teamai mcp remove`、`teamai uninstall`,以及 `teamai init` 迁移 Claude Code 根目录时,也会删除它。
- 同时缺少某个未声明为密钥的变量的 server 照旧删除。未声明为密钥的变量保持现有行为。
- 同时缺少某个只在 `mcp.yaml` 中出现的变量的 server 同样保留:该变量是[隐式密钥](#只在-mcpyaml-中出现的变量就是密钥)。`env.yaml` 变量总有团队的值,不会是缺少的那个。

保留下来的条目里是之前那次 pull 写入的值。密钥轮换或吊销后,server 会一直使用旧值,直到某次 pull 找到新值。

Expand Down
5 changes: 3 additions & 2 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,8 @@ teamai env set GITHUB_TOKEN --global # for every team on th
teamai env unset GITHUB_TOKEN [--global]
```

`env set` accepts a key the scope declares as a secret or, without `--global`, an
`env set` accepts a key the scope declares as a secret, a `${VAR}` its MCP servers
reference that nothing declares or sets, or, without `--global`, an
`env.yaml` variable it receives, and stores the value in
`~/.teamai/secrets/teams/<hash>.json` (mode `0600`), one file per team
repo, named by the team repo URL in your `~/.teamai/config.yaml` (not `teamai.yaml`'s `repo:`) so renaming `team:` keeps it; with `--global`, in `~/.teamai/secrets/machine.json`, for every team on the
Expand Down Expand Up @@ -1356,7 +1357,7 @@ An HTTP local-agent update keeps the existing JSON MCP ownership record until th

Copilot uses its native `mcpServers` schema: `stdio` becomes `type: "local"`, remote transports keep `http` or `sse`, and every managed entry gets the required `tools: ["*"]` allowlist. TeamAI honors `COPILOT_HOME`; project configuration uses Copilot CLI's documented `.github/mcp.json` repository location. See [Adding MCP servers for GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers). Codex supports `stdio` and `http`; `sse` is skipped. Qoder supports the Claude-compatible `mcpServers` format in its scope-specific `.qoder/settings.json`. Kiro supports the same `mcpServers` format in its dedicated, mcpServers-only `.kiro/settings/mcp.json` (see [Kiro's MCP configuration docs](https://kiro.dev/docs/mcp/configuration/)). OpenCode supports `stdio` (written as its `type:"local"` shape), `http`, and `sse` (both `type:"remote"`, negotiated by its client); its servers live under the `mcp` key of the shared `opencode.json`. Ownership is tracked in `~/.teamai/managed-mcp.json` — hand-added servers are left alone; name collisions skip unless `--force`.

**Secrets.** Write `${VAR}`, never a literal, in `mcp.yaml`. A key the team declares in `env/secrets.yaml` resolves from your value for this team (`teamai env set`), then your value for the machine (`teamai env set --global`), then your own environment, which leaves out values a teamai `env.sh` exported (see [Team secrets](designs/team-secrets.md#resolution)). Any other variable resolves from your value for this team (`teamai env set KEY`), then from the team env variables this directory receives (`env/env.yaml` and the active `env/<ns>/env.yaml`); the environment fills only a key the team sets nothing for, and no longer overrides a team variable (see [Team secrets](designs/team-secrets.md#variables)). An interactive `pull` and `teamai doctor` say when your export differs from the team's value and is ignored. Unresolved variables skip the server with a hint. A declared secret is different: when a pull can't find it, the entry an earlier pull wrote stays as it is, so it may hold a value that was since rotated, until a pull finds the new one (see [Team secrets](designs/team-secrets.md#a-missing-secret-keeps-the-mcp-entry)). An interactive `pull`, `teamai mcp list`, `teamai env list`, `teamai doctor` and `teamai env exec` name a declared secret with no value, the servers that use it and the command that sets it: `` github: GITHUB_TOKEN is not set. Run `teamai env set GITHUB_TOKEN` (<url>). ``
**Secrets.** Write `${VAR}`, never a literal, in `mcp.yaml`. A key the team declares in `env/secrets.yaml` resolves from your value for this team (`teamai env set`), then your value for the machine (`teamai env set --global`), then your own environment, which leaves out values a teamai `env.sh` exported (see [Team secrets](designs/team-secrets.md#resolution)). Any other variable resolves from your value for this team (`teamai env set KEY`), then from the team env variables this directory receives (`env/env.yaml` and the active `env/<ns>/env.yaml`); the environment fills only a key the team sets nothing for, and no longer overrides a team variable (see [Team secrets](designs/team-secrets.md#variables)). An interactive `pull` and `teamai doctor` say when your export differs from the team's value and is ignored. A `${VAR}` that nothing declares and no `env.yaml` sets is your own token, resolved as a declared secret (see [Team secrets](designs/team-secrets.md#a-variable-only-mcpyaml-names-is-a-secret)). A declared secret is different: when a pull can't find it, the entry an earlier pull wrote stays as it is, so it may hold a value that was since rotated, until a pull finds the new one (see [Team secrets](designs/team-secrets.md#a-missing-secret-keeps-the-mcp-entry)). An interactive `pull`, `teamai mcp list`, `teamai env list`, `teamai doctor` and `teamai env exec` name a declared secret with no value, the servers that use it and the command that sets it: `` github: GITHUB_TOKEN is not set. Run `teamai env set GITHUB_TOKEN` (<url>). ``

teamai **resolves every `${VAR}` to its value and writes it verbatim** into each tool's config, which is then written `0600`, an existing `0644` one included (a config without a resolved value keeps its mode; new files are created `0600`). It does not rely on any tool's own env-var expansion: that expansion is fragile — most decisively, IDEs launched from the GUI (Dock/Launchpad) never inherit your shell's exported variables, so a `${VAR}` placeholder expands to empty and the server 401s. Resolving to plaintext makes the token present no matter how the tool is started.

Expand Down
4 changes: 2 additions & 2 deletions docs/usage-guide.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,7 @@ teamai env set GITHUB_TOKEN --global # 对本机所有团
teamai env unset GITHUB_TOKEN [--global]
```

`env set` 接受已声明的密钥,不加 `--global` 时也接受该目录收到的 `env.yaml` 变量,并把值保存在 `~/.teamai/secrets/teams/<hash>.json`
`env set` 接受已声明的密钥、该 scope 的 MCP server 引用但没有任何声明或设置的 `${VAR}`,不加 `--global` 时也接受该目录收到的 `env.yaml` 变量,并把值保存在 `~/.teamai/secrets/teams/<hash>.json`
(权限 `0600`),每个团队仓库一个文件,按你的 `~/.teamai/config.yaml` 中的团队仓库 URL 命名(不使用 `teamai.yaml` 的 `repo:`),修改 `team:` 不影响它;加 `--global` 时保存在 `~/.teamai/secrets/machine.json`,
对本机所有团队生效,为某个团队设置的值仍然优先。不在任何 scope 中时,`--global` 接受任何合法的 key,
并提示目前还没有团队声明它。值保持设置时该 key 的类型:团队不再声明某个同时在 `env.yaml` 中设置的密钥后,你的值不会用于该变量,`env list` 会提示先运行 `teamai env unset KEY`,再运行 `teamai env set KEY`。`teamai env list` 和 `teamai list env` 会把每个已声明的密钥
Expand Down Expand Up @@ -1209,7 +1209,7 @@ HTTP local-agent 更新 JSON MCP 配置时,先保留原有 ownership 记录,

Copilot 使用原生 `mcpServers` 结构:`stdio` 写成 `type: "local"`,远程传输保留 `http` 或 `sse`,每个 TeamAI 管理的条目都会带上必需的 `tools: ["*"]` 允许列表。TeamAI 遵循 `COPILOT_HOME`,项目配置使用 Copilot CLI 官方文档指定的 `.github/mcp.json` 仓库路径。详见 [GitHub Copilot CLI 添加 MCP Server](https://docs.github.com/zh/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers)。Codex 支持 `stdio` 与 `http`,`sse` 会被跳过。Qoder 使用对应作用域 `.qoder/settings.json` 中与 Claude 兼容的 `mcpServers` 格式。Kiro 在专用的、只含 `mcpServers` 的 `.kiro/settings/mcp.json` 中使用同一格式(见 [Kiro MCP 配置文档](https://kiro.dev/docs/mcp/configuration/))。OpenCode 支持 `stdio`(写成其 `type:"local"` 形态)、`http` 和 `sse`(后两者均为 `type:"remote"`,由客户端协商传输协议),其 server 位于共享 `opencode.json` 的 `mcp` 键下。归属记录在 `~/.teamai/managed-mcp.json`——手动添加的 server 不动;与手写同名则跳过,除非 `--force`。

**密钥**:在 `mcp.yaml` 里写 `${VAR}`,不要写明文。团队在 `env/secrets.yaml` 中声明的 key 优先取你为该团队设置的值(`teamai env set`),其次取你为本机设置的值(`teamai env set --global`),再次取你自己的环境,不包括 teamai `env.sh` 导出的值(见[团队密钥](designs/team-secrets.zh-CN.md#解析顺序))。其他变量优先取你为该团队设置的值(`teamai env set KEY`),其次是该目录收到的团队环境变量(`env/env.yaml` 与活动的 `env/<ns>/env.yaml`);环境只补充团队没有设置的 key,不再覆盖团队变量(见[团队密钥](designs/team-secrets.zh-CN.md#变量))。你导出的值与团队的值不同而被忽略时,交互式 `pull` 和 `teamai doctor` 会指出。变量无法解析则跳过并提示。已声明的密钥不同:pull 找不到它时,之前某次 pull 写入的条目原样保留,因此里面可能是已经轮换掉的旧值,直到某次 pull 找到新值(见[团队密钥](designs/team-secrets.zh-CN.md#缺少密钥时保留-mcp-条目))。交互式 `pull`、`teamai mcp list`、`teamai env list`、`teamai doctor` 和 `teamai env exec` 会指出没有值的已声明密钥、用到它的 server 以及设置它的命令:`` github: GITHUB_TOKEN is not set. Run `teamai env set GITHUB_TOKEN` (<url>). ``
**密钥**:在 `mcp.yaml` 里写 `${VAR}`,不要写明文。团队在 `env/secrets.yaml` 中声明的 key 优先取你为该团队设置的值(`teamai env set`),其次取你为本机设置的值(`teamai env set --global`),再次取你自己的环境,不包括 teamai `env.sh` 导出的值(见[团队密钥](designs/team-secrets.zh-CN.md#解析顺序))。其他变量优先取你为该团队设置的值(`teamai env set KEY`),其次是该目录收到的团队环境变量(`env/env.yaml` 与活动的 `env/<ns>/env.yaml`);环境只补充团队没有设置的 key,不再覆盖团队变量(见[团队密钥](designs/team-secrets.zh-CN.md#变量))。你导出的值与团队的值不同而被忽略时,交互式 `pull` 和 `teamai doctor` 会指出。变量没有任何声明、也没有任何 `env.yaml` 设置的 `${VAR}` 是你自己的 token,按已声明的密钥解析(见[团队密钥](designs/team-secrets.zh-CN.md#只在-mcpyaml-中出现的变量就是密钥))。已声明的密钥不同:pull 找不到它时,之前某次 pull 写入的条目原样保留,因此里面可能是已经轮换掉的旧值,直到某次 pull 找到新值(见[团队密钥](designs/team-secrets.zh-CN.md#缺少密钥时保留-mcp-条目))。交互式 `pull`、`teamai mcp list`、`teamai env list`、`teamai doctor` 和 `teamai env exec` 会指出没有值的已声明密钥、用到它的 server 以及设置它的命令:`` github: GITHUB_TOKEN is not set. Run `teamai env set GITHUB_TOKEN` (<url>). ``

teamai 会**把每个 `${VAR}` 解析成取值后原样写入**各工具的配置文件,并以 `0600` 写入该文件,已有的 `0644` 文件也会收紧(不含已解析值的配置保持原权限;新建文件权限为 `0600`)。它不依赖任何工具自身的环境变量展开——因为那种展开很脆弱:最典型的是,以 GUI 方式(Dock/Launchpad)启动的 IDE 不会继承你 shell 中 `export` 的变量,`${VAR}` 占位符会展开为空、导致服务端 401。解析成明文可以保证无论工具如何启动,token 都在。

Expand Down
Loading
Loading