Skip to content

feat: add experimental multi-environment tool routing - #4529

Open
7Sageer wants to merge 7 commits into
mainfrom
feat/multi-env-routing-v2
Open

7Sageer wants to merge 7 commits into
mainfrom
feat/multi-env-routing-v2

Conversation

@7Sageer

@7Sageer 7Sageer commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Requirement or Bug

PR #4062 让一个 session 只能绑定一个执行环境(本机、SSH 或 Docker)。这个 PR 在实验 flag 后面,让同一个 session 的文件和 shell 工具可以按调用选择环境,并且在另一个环境连上后,让模型看到那个环境的项目说明(AGENTS.md)和项目 skills。延续 #3856,衔接 #4062。

Bug Reproduction Steps

N/A

Root Cause

N/A

Code Changes

核心思路:环境不再只是工具的路由目标,而是「挂载的工作区」。[environments] 里声明的环境都是候选,仍然在第一次有调用落到它上面时才连接;连上之后,session 会为它打开一次 workdir view,把它的 AGENTS.md 路径通过 roster reminder 告诉模型,把它的项目 skills 以 <skill>@<环境> 加进会话 skill 列表。系统提示仍只描述 primary,flag 关闭时 prompt 和落盘格式都不变。

概念 回答什么问题 本 PR 之后
primary 不传 environment 时调用落在哪 创建 session 时绑定的环境,和 #4062 一致
次环境 还有哪些环境可以用 本机加上声明过且已注册的环境,去掉 primary;ACP 这类会话私有的 primary 不开启
挂载 模型对一个次环境知道多少 次环境首次连上后,session 打开一次它的 workdir view,得到 AGENTS.md 路径和项目 skills
roster 模型怎么知道有哪些环境 environment_roster reminder:首次发完整列表,之后只发变化的环境
flowchart LR
    subgraph AppScope[App]
        Registry[EnvironmentRegistry<br/>连接与 generation]
    end
    subgraph SessionScope[Session]
        Mounts[SessionEnvironmentMounts<br/>每个次环境挂载一次]
        Catalog[SessionSkillCatalog<br/>primary skills + skill@env]
    end
    subgraph AgentScope[Agent]
        Targets[AgentEnvironmentService<br/>targets / acquire]
        Tools[ToolRegistry + ToolExecutor<br/>environment 参数与路由]
        Roster[EnvironmentRoster<br/>完整列表或增量]
    end
    Tools --> Targets --> Registry
    Roster -- 环境已连上 --> Mounts
    Mounts -- Program workdir view --> Registry
    Mounts -- 项目 skills --> Catalog
Loading

连接归 App 层的 registry,跨 session 共享;挂载放在 Session 层,是因为一个 (环境, cwd) 下的 AGENTS.md 和 skills 与是哪个 agent 无关,而 skill catalog 本来就是 Session 级的,子 agent 生成时不会重复扫描。Agent 层只负责路由和渲染 roster:子 agent 和 fork 都继承调用方的 primary,所以同一个 session 里所有 agent 看到的是同一份挂载结果。

决策 选择 没选 理由
何时连接 首次调用时懒连接,连上后挂载;registry 里已就绪的环境直接挂载 会话开始时异步连接所有声明 [environments] 通常是用户级配置,声明只说明「可以用」,不说明这个 session 要用;提前连接会让每个 session 都对所有声明发起 SSH 握手、拉起 docker exec-server
AGENTS.md roster 里列路径,要求模型修改前先读 注入正文 与现有子目录 AGENTS.md 提醒一致,reminder 不会因正文膨胀
skill 命名 次环境 skill 一律叫 <skill>@<环境> 沿用 <环境>:<skill>,或只在重名时加前缀 : 已被 plugin skill 占用;只在重名时加前缀会让同一个 skill 的名字随环境集合变化
工具 schema environment 固定为 string,合法 id 由 roster 说明,执行时校验 在 schema 里动态生成环境 enum 多环境下增删环境不会改变工具定义,避免因此使 system prompt + tools 的前缀缓存失效;roster 仍提供准确候选,未知 id 仍在 preflight 拒绝
roster 重发 历史里已有 roster 时只发变化的环境 每次变化重发完整列表 说明文字只需出现一次;compaction 会丢掉所有 injection,压缩后一定发完整版,不会出现只剩增量的情况
本机 workspace registry 非 local 环境的 view 不读 按路径匹配合并 registry 只描述本机目录,按路径字符串匹配会把本机源目录并进路径碰巧相同的远端环境
sequenceDiagram
    participant Model
    participant Tools as ToolExecutor
    participant Registry as EnvironmentRegistry
    participant Roster as EnvironmentRoster
    participant Mounts as SessionEnvironmentMounts
    Note over Roster: 第 1 步发完整 roster,devbox 标为 not connected yet
    Model->>Tools: Read(path, environment="devbox")
    Tools->>Registry: acquireWhenReady(devbox),首次连接
    Registry-->>Tools: 文件内容
    Roster->>Mounts: 第 2 步开始时 mount(devbox, cwd)
    Mounts-->>Roster: AGENTS.md 路径;deploy@devbox 加入 catalog
    Roster-->>Model: 增量:devbox 的 cwd 和 AGENTS.md 路径
    Model->>Tools: Skill("deploy@devbox")
    Tools-->>Model: skill-loaded environment="devbox"
Loading

阅读指南:

  1. packages/agent-core-v2/src/agent/environmentBinding/agentEnvironment.ts:候选环境怎么算、primary 和次环境的 binding 与 cwd 怎么解析。
  2. packages/agent-core-v2/src/agent/toolRegistry/ 与 src/agent/toolExecutor/:environment 参数何时出现,preflight 怎么把它解析成 EnvironmentTarget 并从工具参数里剥掉。
  3. packages/agent-core-v2/src/session/environmentMounts/environmentMounts.ts:挂载做了什么、只取哪些 skill、环境离开 roster 时如何卸载。
  4. packages/agent-core-v2/src/agent/environmentBinding/environmentRoster.ts:roster 的三种状态,以及完整列表和增量的切换条件。
  5. packages/agent-core-v2/src/features/fileHistory/ 与 src/agent/permissionPolicy/:fileHistory 和会话审批按环境区分。

7 个工具里各自的 resolveExecution 改动、display 和 wire 字段透传、ISessionWorkdir 的接线、测试 stub 补 set/remove、文档可以略读。

roster 示例(真实 CLI 运行,临时目录替换为 <tmp>)

第 1 步:

Execution environments available in this session:
- `local` (primary): working directory <tmp>/work
- `devbox`: working directory <tmp>/devbox; not connected yet
Tools with an `environment` argument run in the environment you name; omit it to use the primary environment. Each environment has its own files, shell, and installed programs, and relative paths resolve against its working directory. A remote environment connects on its first use; if it cannot connect, the call fails instead of running elsewhere. The system prompt's working directory, AGENTS.md instructions, and skills describe the primary environment. Once another environment connects, the AGENTS.md files found there are listed under it; read them before changing files in that environment. Skills found in another environment are named `<skill>@<environment>`.

Read(environment="devbox") 连上之后的第 2 步只追加:

Execution environment updates:
- `devbox`: working directory <tmp>/devbox
  - AGENTS.md: <tmp>/devbox/AGENTS.md
Read the AGENTS.md files listed above before changing files in that environment.

同一步的 skill 变更提醒里出现 - deploy@devbox: Deploy the devbox app,之后 Skill("deploy@devbox") 的加载块为 <skill-loaded name="deploy@devbox" trigger="model-tool" source="project" environment="devbox" dir="<tmp>/devbox/.agents/skills/deploy" args="">。

Behavior Changes and Affected Users

除最后一行外,所有变化只在开启 multi_environment flag(默认关闭)且可用环境 ≥2 时出现。

Behavior Before After Who relies on the old behavior Escape hatch
环境选择 session / agent 只有一个 primary binding Bash、Read、Write、Edit、Glob、Grep、ReadMediaFile 可按调用传 environment;缺省仍是 primary 未启用 flag 的 TUI、print、desktop、web、VS Code、ACP、SDK 用户 默认关闭
候选环境与连接 只在创建或恢复时连接 primary 声明过且已注册的环境在首次调用时连接;未知或不可用的 id 直接报错,不回落到别的环境 配了 [environments] 的 experimental 用户 删除对应声明或关闭 flag
次环境 cwd 无次环境 使用 defaultCwd;未设置时用首次连接时目标报告的 cwd;primary 的 cwd 和附加目录不变 experimental 多环境用户 在声明中设置 defaultCwd
审批 「本会话允许」按规则共享,审批徽标取 session 环境 会话审批按环境隔离并持久化;徽标取本次调用的环境 在 primary 批准过命令、再到次环境执行同样命令的 experimental 用户 在目标环境重新审批,或使用显式全局规则
次环境的 AGENTS.md 不可见,roster 只提示模型自己去读 连上后 roster 列出用该环境自己的文件系统、从其 cwd 向上找到的项目级 AGENTS.md 路径(与 primary 同路径的文件也照样列出,本机个人 AGENTS.md 不重复),要求修改前先读;次环境调用不再扫描 primary 的文件系统 experimental 用户 关闭 flag
次环境的项目 skills 不可见,Skill 调不到 连上后以 <skill>@<环境> 加入会话 skill 列表,main agent 通过现有 skill 变更提醒看到;只取项目级 skill,本机 user / plugin / builtin skill 不重复;环境离开 roster(声明被删或 flag 关闭)时一并移除 experimental 用户;读取会话 skill 列表的 desktop、web、SDK 关闭 flag
skill 加载块 只有 name / source / dir 等属性 skill 不属于 primary 时多一个 environment 属性;远端 primary 时本机 user 级 skill 标为 local experimental 用户 关闭 flag;单环境会话不输出此属性
roster reminder 系统提示与 reminder 只描述 primary 新增 environment_roster:历史里没有时发完整列表与说明,之后只发变化的环境;环境移除时发 is no longer available;compaction 后重发完整版 开启 flag 的会话 关闭 flag
fileHistory 按 primary 路径记录与读取 次环境的变更在 REST changes 中返回原生 path 与 environment_id;content 接受可选 environment_id;该环境在轮次结束时不可达只跳过它自己的文件 desktop / web 的轮次文件变更视图 关闭 flag;primary 记录格式不变
wire / state 工具 display、审批记录、skill 定义都没有环境字段 新增可选 display.environment(Kap 的 ToolInputDisplaySchema 同步接受)、审批的 environmentId、skill 的 environmentId、workspace 指令快照的 agentsMdProjectPaths;旧数据缺字段时按 primary 处理 code-app desktop / web、VS Code、vis、stream-json 与 kimi export 消费方 忽略未知可选字段即可
外部 hook 参数 tool_input 为工具原始参数 routed call 保留模型给出的 environment 参数,hook 与实际执行看到同一个选择 配了 hooks 且严格校验 tool_input 字段的脚本 接受该可选字段,或不启用 flag
远端环境的 view 合并本机 workspace 目录(与 flag 无关) 远端 cwd 的路径字符串和本机某个已登记 workspace 相同时(如两台 Linux 都有 /home/alice/proj、docker 的 /workspace),本机 workspace 的源目录被并进远端环境的附加目录,#4520 之后还会被拿去扫 skills 非 local 环境的 view 不读本机 workspace registry #4062 的远端 primary 用户中路径恰好重合的人;他们此前拿到的是错误目录 无需退路:这些目录本来就不在远端环境上

Prompt 文本逐句:

  • roster 说明里删掉 "...describe the primary environment only; read a project's AGENTS.md before changing files in another environment.",它原来要求模型自行寻找并阅读次环境的 AGENTS.md。现在改为 "...describe the primary environment." 加 "Once another environment connects, the AGENTS.md files found there are listed under it; read them before changing files in that environment.",由挂载列出的具体路径约束。
  • 新增 "Skills found in another environment are named <skill>@<environment>.",让模型知道 skill 列表里带后缀的条目是什么。
  • 新增增量消息 "Execution environment updates:"、- `<id>` is no longer available.,以及有 AGENTS.md 路径时的 "Read the AGENTS.md files listed above before changing files in that environment."。
  • 以上都只发给开启 flag 且可用环境 ≥2 的会话。

契约影响:docs/wire-manifest.d.ts、docs/state-manifest.d.ts 只有新增可选字段;REST file-history 只多一个可选 environment_id;REST skill 结构、packages/node-sdk/src/index.ts、WS 路由、ACP API 都没改。code-app 的 remote workspaces 按 primary binding 分组,本 PR 不改变 primary,分组与分页不受影响;后续需要在工具卡片和轮次文件变更视图里带上 environment_id。实验版不做次环境 MCP、按环境配置权限、次环境 agent profiles、跨环境文件复制和手动切换 primary。

测试覆盖:

  • 路由、schema 随环境增删保持稳定、未知环境、display,以及经 registry 到真实 exec-server 的懒连接 Read:toolExecutor.test.ts。
  • 候选隔离、cwd 解析、roster 的完整与增量、环境移除、挂载后断线不重发、挂载失败、flag 关闭不挂载:environmentBindingService.test.ts。
  • 挂载服务:项目 skill 改名为 @env、与 primary 同路径的 AGENTS.md 照样列出、每个环境只加载一次、随 session 释放 workdir、打开失败不加 skill、卸载后移除 skill 并可重新挂载、过期的卸载请求不影响新挂载、加载中被卸载不加 skill:environmentBindingService.test.ts;项目级路径与个人路径的区分:context.test.ts。
  • skill 加载块的 environment 属性:prompt.test.ts。非 local view 不读 registry:workspaceDirsService.test.ts。
  • 审批隔离与 replay、fileHistory 按环境记账、7 个工具的重连与 generation 防误用:见折叠块。
  • 用真实 CLI(kimi -p + mock LLM + command 类型环境拉起本机 kimi exec-server)跑通了上面的三步流程。没验证:真实 SSH / Docker 目标。
测试文件与运行结果
  • environmentBindingService.test.ts、toolExecutor.test.ts、environmentGenerationGuard.test.ts
  • permissionPolicyService.test.ts、permissionRules.test.ts:环境级审批隔离、Git 策略(含显式指名 primary)、__proto__ 等环境 id 与 replay。
  • fileHistory.test.ts:跨环境同名路径、备份复用、次环境不可达时不发起连接、按 main 的 primary 判定子 agent 的编辑、环境 id 含 : / / 时 key 不冲突。
  • agentsMdReminder.test.ts、approval.test.ts、prompt.test.ts、workspaceDirsService.test.ts
  • agent-core-v2、kap-server、node-sdk、klient、apps/kimi-code 的 typecheck,check-no-comments 和改动文件的 oxlint 通过;kap-server 的 skills / apiSurface / fileHistory 测试与 node-sdk 全量通过。
  • agent-core-v2 本机全量:7,811 通过、2 skipped、2 失败。tool.test.ts 的 "returns the spawned agent id when a foreground subagent times out" 在本机去掉本次改动后同样超时,上一轮 CI 中通过;fullCompaction.test.ts 的一条用例只在全量并发下失败,单独重跑两次均通过。以 CI 为准。kap-server 全量 1,442 通过。

Checklist

Copilot AI balanced review requested due to automatic review settings October 29, 2026 13:00
@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 475f54f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@7Hanrui

7Hanrui commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T08:59:22.684993Z 475f54f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1772e652c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (!isDangerousCommandGuardEnabled(this.config)) return undefined;
const cwd = this.workspace.workDir;
const environmentId = context.environmentId;
const cwd = environmentId === undefined ? this.workspace.workDir : this.environment.workspaceRoots(environmentId).workDir;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Connect the routed environment before resolving its cwd

When a secondary remote omits defaultCwd, its cwd is intentionally unavailable until the first connection, but a routed Bash call reaches this permission policy before BashTool.execution() performs that lazy connection. With the default-enabled dangerous-command guard, workspaceRoots(environmentId) therefore throws environment.unavailable for ordinary first calls such as pwd, so the documented fallback to the target-reported cwd never runs. Acquire/connect the target before resolving its workspace root, or defer the lookup until a ready lease exists.

Useful? React with 👍 / 👎.

private async capture(path: string, turnId: number): Promise<void> {
const pathKey = this.pathKey(path);
private async capture(path: string, turnId: number, environmentId?: string): Promise<void> {
const pathKey = environmentId === undefined ? this.pathKey(path) : `${environmentId}:${path}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve target case semantics in file-history keys

When the primary environment is Windows and a routed POSIX environment contains distinct files such as /repo/Foo.ts and /repo/foo.ts, both qualified keys are stored here, but contentAt() later passes them through pathKey(), which derives case-insensitivity from the primary environment and returns the first case-insensitive tracked match. Requesting the second path through the file-history API can therefore return the first file's backup, showing incorrect before/after content in desktop or web clients; qualified paths need exact lookup or normalization based on the target environment.

AGENTS.md reference: AGENTS.md:L123-L125

Useful? React with 👍 / 👎.

const planTarget = await this.planService?.resolveFile(args.path);
async resolveExecution(args: WriteInput, context?: ToolResolutionContext): Promise<ToolExecution> {
const environmentId = context?.environmentId;
const planTarget = environmentId === undefined ? await this.planService?.resolveFile(args.path) : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep routed writes behind the plan-file guard

When plan mode is active and the model includes environment on a Write or Edit targeting the advertised plan-file path, this condition skips resolveFile(). The routed tool retains the same absolute path string, while the plan guard checks only that string against the local plan path and calls allow(), so the operation writes or edits that path on the secondary filesystem instead of updating the local plan and does so without plan approval. Plan-file operations must remain local, or the guard must include the execution environment when authorizing them.

AGENTS.md reference: AGENTS.md:L131-L133

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c241a7c635

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +534 to +535
private entryKey(path: string, environmentId: string | undefined): string {
return environmentId === undefined ? this.pathKey(path) : `${environmentId}:${path}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use collision-free file-history keys

When valid environment IDs contain : or /, this concatenation can map distinct environment/path pairs to the same key; for example, environment a with path /b:/x and environment a:/b with path /x both produce a:/b:/x. If both files are edited in one turn, capture() treats the second as already tracked, silently omitting its backup and causing the history API to return content or changes for the wrong target. Encode the pair unambiguously, such as with a tuple or length prefix.

AGENTS.md reference: AGENTS.md:L131-L133

Useful? React with 👍 / 👎.

Comment on lines +69 to +71
const byEnvironment = s.environmentSessionApprovalRulePatterns ?? {};
const patterns = byEnvironment[e.environmentId] ?? [];
if (patterns.includes(pattern)) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid prototype lookups for environment approval buckets

When a configured environment uses an accepted identifier such as __proto__, constructor, or toString, this lookup returns an inherited object or function rather than the fallback array, so the subsequent patterns.includes(...) throws while recording an “allow for this session” approval. The routed operation has already been approved, but its session approval is not persisted and later calls unexpectedly prompt again; use an own-property lookup or a null-prototype map for these buckets.

AGENTS.md reference: AGENTS.md:L131-L133

Useful? React with 👍 / 👎.

}
const lines = targets.map((target) => {
const label = target.primary ? `\`${target.environmentId}\` (primary)` : `\`${target.environmentId}\``;
const cwd = target.primary ? environment.workspaceRoots().workDir : target.cwd;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh lazily resolved cwd in the environment roster

When a secondary environment omits defaultCwd, target.cwd remains undefined even after the first routed call has connected and workspaceRoots(target.environmentId) can resolve the host-reported cwd. Consequently every later reminder still says the directory will be resolved on first use, and because its disclosure never changes no corrected roster is injected, leaving the model with stale environment guidance. Read the connected target's workspace root when available and retain this fallback only before connection.

AGENTS.md reference: AGENTS.md:L119-L121

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0f13a560d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +373 to 374
const current = await this.readCurrent(pathKey, before.environmentId);
if (current === 'unreadable') continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mark disconnected remote history as incomplete

When a routed write succeeds but the target disconnects before TurnEnded, readCurrent() returns unreadable here and the path is omitted from the end checkpoint. readChanges() then enumerates only end-checkpoint entries while turnRecorded() still returns true, so desktop and web file-history consumers silently receive a complete-looking record with the edited remote file missing. Preserve an explicit unknown entry or mark the turn unrecorded when a tracked target cannot be read.

AGENTS.md reference: AGENTS.md:L131-L133

Useful? React with 👍 / 👎.

Comment on lines +37 to +39
if (!resolvedCwds.has(environmentId) && environment.isAvailable([], environmentId)) {
try {
resolvedCwds.set(environmentId, environment.workspaceRoots(environmentId).workDir);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh cached cwd after a secondary reconnects

When a declaration without defaultCwd reconnects the same environment ID to a different host—for example after its launcher configuration is hot-reloaded—this condition never calls workspaceRoots() again because the ID is already cached. The tools use the new target-reported cwd, but the roster and its disclosure retain the old cwd and can direct the model to incorrect absolute paths. The first-connect case from the earlier review is fixed, but this newly added permanent cache is fresh evidence of a remaining stale-roster case; refresh the cached value whenever the target is ready while retaining the last value only during disconnects.

AGENTS.md reference: AGENTS.md:L119-L121

Useful? React with 👍 / 👎.

Comment on lines +84 to +87
async resolveExecution(args: GlobInput, context?: ToolResolutionContext): Promise<ToolExecution> {
const environmentId = context?.environment?.environmentId;
const primary = context?.environment?.primary !== false;
const resolutionLease = await acquireOrWhenReady(this.environment, ['fs'], environmentId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Describe the routed target's path semantics

When the primary and routed target use different path semantics—for example a POSIX remote primary with the local Windows environment as a secondary—this call resolves and executes against the requested target, but Glob.description is still derived solely from the primary environment. A routed local-Windows call therefore omits the Windows note explaining that results use backslashes and must be converted before passing them to Git Bash; the inverse topology incorrectly advertises Windows behavior for a POSIX remote. Include each target's path semantics in the environment guidance or make the tool description valid for every advertised target.

AGENTS.md reference: AGENTS.md:L119-L121

Useful? React with 👍 / 👎.

Comment on lines +148 to +150
const secondary = [LOCAL_ENVIRONMENT_ID, ...declared.keys()]
.filter((environmentId) => environmentId !== current.environmentId && this.resolver.current(environmentId) !== undefined)
.toSorted((left, right) => left.localeCompare(right))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude declarations that failed registration

When a configured declaration ID conflicts with an environment already registered by another provider or temporary SDK connection, the remote provider records a registration error and leaves the pre-existing environment in the registry. This filter checks only that some environment currently has the ID, so the declaration is still advertised and calls are routed to that unrelated environment using the declaration's defaultCwd; a model can consequently read or modify the wrong host instead of failing the call. Exclude IDs with declarations.registrationError(environmentId) or otherwise verify that the registered target belongs to the declaration.

AGENTS.md reference: AGENTS.md:L131-L133

Useful? React with 👍 / 👎.

Comment on lines +534 to +535
private entryKey(path: string, environmentId: string | undefined): string {
return environmentId === undefined ? this.pathKey(path) : JSON.stringify([environmentId, path]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Namespace secondary file-history keys

The JSON tuple removes the earlier collision between two environment/path pairs, but it can still equal a valid primary path key. For example, remote environment gpu with path /x produces ["gpu","/x"], which is also the relative key for a primary file at /workspace/["gpu","/x"]; capturing both in one turn makes the second appear already tracked and silently associates history with only one file. This is fresh evidence that the current encoding is not collision-free across the primary and secondary namespaces; prefix or structurally separate every key, including primary entries.

AGENTS.md reference: AGENTS.md:L131-L133

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 4c390ccc7e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@7Sageer
7Sageer marked this pull request as ready for review October 30, 2026 06:33
@pkg-pr-new

pkg-pr-new Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@475f54f
npx https://pkg.pr.new/@moonshot-ai/kimi-code@475f54f

commit: 475f54f

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c390ccc7e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

name: 'Bash',
domain: 'os/backends',
requiredEnvironmentCapabilities: ['process'],
environmentScoped: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep Bash guidance accurate for routed shells

When the primary and routed target use different shells, this exposes Bash for calls that execute on the secondary, but the BashTool.description getter still names the primary shell while execution uses the selected target's shellPath. The model can therefore generate commands for one shell that fail or behave differently on the routed target. Keep the guidance shell-neutral or make it valid for every advertised target.

AGENTS.md reference: AGENTS.md:L119-L121

Useful? React with 👍 / 👎.

Model the primary as a full environment target: tools and git policies read
workspace roots from the environment service for every target, and only
primary-only features check the target role. Persisted records keep omitting
the environment id for the primary.

File history now exposes the environment as a structured field on its REST
routes, judges routed captures against its own primary, and never connects
an environment while recording; an unreachable environment only skips its
own files.
…r cwd

Encode file-history keys for non-primary environments as an unambiguous
tuple so environment ids containing ':' or '/' cannot collide, look up
per-environment approval buckets by own property so ids such as
'__proto__' keep their approvals, and let the environment roster report
a working directory resolved on first connection without reverting on
disconnect.
Once a secondary environment connects, the session opens its workdir view
once: the roster lists the AGENTS.md files found there and its project
skills join the session catalog as <skill>@<environment>. Skill activations
outside the primary environment name their owning environment, and views of
non-local environments no longer merge local workspace registry dirs that
happen to share a path.
@7Sageer
7Sageer force-pushed the feat/multi-env-routing-v2 branch from 4c390cc to 4dffeea Compare October 30, 2026 07:58
@7Hanrui

7Hanrui commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dffeea860

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

this.catalog.set(`environment:${environmentId}`, { skills }, { priority: SKILL_SOURCE_PRIORITY.workspace });
}
const primaryPaths = new Set(this.instructions.agentsMdPaths ?? []);
const agentsMdPaths = (workdir.instructions.snapshot.agentsMdPaths ?? []).filter((path) => !primaryPaths.has(path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve same-path remote AGENTS.md entries

When the primary and secondary environments use the same absolute layout, such as /workspace/AGENTS.md on both the local host and a container, this string-only deduplication removes the secondary file even though it is a different file on a different filesystem. Because the mount does not inject its contents and the roster only asks the model to read returned paths, the model never learns about the secondary environment's instructions before editing it; keep these paths environment-qualified rather than deduplicating them against the primary.

AGENTS.md reference: AGENTS.md:L131-L133

Useful? React with 👍 / 👎.

createDecorator<ISessionEnvironmentMounts>('sessionEnvironmentMounts');

export function environmentSkillName(name: string, environmentId: string): string {
return `${name}@${environmentId}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make environment-qualified skill names collision-free

When the primary already has a skill named deploy@gpu and environment gpu contributes deploy, both resolve to the same catalog name; distinct (skill, environment) pairs can also collide because both components may contain @. Mounted contributions are merged with replacement enabled, so connecting the secondary silently replaces the primary skill and later activation loads instructions from the wrong host; use an unambiguous namespace or reject collisions.

AGENTS.md reference: AGENTS.md:L131-L133

Useful? React with 👍 / 👎.

environmentId,
}));
if (skills.length > 0) {
this.catalog.set(`environment:${environmentId}`, { skills }, { priority: SKILL_SOURCE_PRIORITY.workspace });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove skills when their environment disappears

When a mounted remote declaration is deleted during a configuration reload, the provider unregisters that environment and the roster reports its removal, but this catalog contribution is never removed. Consequently Skill and slash-command listings continue advertising name@environment, and activating it loads stale instructions from a host that tools can no longer target; remove the contribution when its environment leaves the target roster.

AGENTS.md reference: AGENTS.md:L131-L133

Useful? React with 👍 / 👎.

cwd?: string | undefined;
description?: string | undefined;
language?: 'bash' | undefined;
environment?: string | undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add environment to the Kap display schema

When a consumer parses a routed command or file_io display through the exported ToolInputDisplaySchema, the parallel schema in packages/kap-server/src/protocol/display.ts omits this property, so Zod strips it and the schema's inferred type does not expose it. Kap consumers consequently cannot distinguish the routed target from the primary environment even though the core display contract now promises that information; update both corresponding protocol variants alongside this type.

AGENTS.md reference: AGENTS.md:L123-L125

Useful? React with 👍 / 👎.

List the AGENTS.md files a mounted environment found from its own working
directory instead of deduplicating paths against the primary, which hid
same-path files on another filesystem. Drop a mounted environment's skills
and workdir once it leaves the roster, and accept the routed environment in
the Kap tool display schema.
@7Hanrui

7Hanrui commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82b6d6deab

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +64 to +68
if (pending === undefined && environment.isAvailable([], target.environmentId)) {
const cwd = connectedCwd(target.environmentId);
if (cwd !== undefined) {
pending = { cwd, mount: mounts.mount(target.environmentId, cwd) };
mounted.set(target.environmentId, pending);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mount instructions before allowing the first remote mutation

When a disconnected secondary's first call is Write, Edit, or a mutating Bash, the pre-call roster only says that the environment is not connected. This mount occurs only when the roster reminder is rendered again after tool resolution has connected the target, while the already-issued mutation executes in the same turn; the model therefore changes the remote project before it can see or read that project's AGENTS.md. Mount the context and deliver its instructions before permitting the first mutating call, or reject that call and require a retry after the roster update.

AGENTS.md reference: AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 475f54f4a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


private recordedEnvironmentId(environmentId: string | undefined): string | undefined {
const primary = this.environment.targets().find((target) => target.primary);
return environmentId === primary?.environmentId ? undefined : environmentId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve file-history keys across environment rebinding

When a file is edited in a secondary environment and that environment is later rebound as the agent's primary, capture() stored it under a key containing the environment ID, but contentAt(..., environment_id) now normalizes that ID to undefined based on the current target list and searches a different key. The saved before/after content is therefore returned as null even though the changes endpoint still lists the edit; keep history keying independent of the current binding. 【AGENTS.md†L133-L133】

Useful? React with 👍 / 👎.

const declared = new Map(
this.declarations.snapshot().entries.map((declaration) => [declaration.id, declaration.entry.defaultCwd]),
);
if (current.environmentId !== LOCAL_ENVIRONMENT_ID && !declared.has(current.environmentId)) return [primary];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve other targets for an undeclared primary

When the main agent is started on a process-registered but undeclared environment (supported by session creation when environmentCwd is supplied), this early return hides every configured target even with multi_environment enabled. routeEnvironment() then has only one target, so the tool schema omits environment and calls cannot reach the configured remotes or local host from that session; keep the declared targets and filter only the primary ID.

Useful? React with 👍 / 👎.

execution.isError !== true &&
(execution.display?.kind === 'command' || execution.display?.kind === 'file_io')
) {
execution = { ...execution, display: { ...execution.display, environment: call.environment.environmentId } };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Carry routed environments into VS Code displays

When a VS Code session with multi-environment routing enabled approves or reviews a routed Bash, Read, Write, Edit, Glob, or Grep call, this annotation is dropped by apps/vscode/src/runtime/tool-display.ts (describeToolDisplay/toLegacyDisplay emit only the command or path), so the approval and tool-call card do not identify the target host. Fresh evidence beyond the earlier Kap-schema issue is that the VS Code adapter still converts the newly annotated display to a legacy payload without an environment field; preserve the target in that projection. 【AGENTS.md†L125-L125】

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
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