# 这个 Agent 我调了几天,同事却还得重新教一遍。 | EasyNet Source: https://easynet.run/cases/problems/give-teammate-the-working-agent [easynet.run · 全部问题场景](https://easynet.run/cases#problem-scenarios) # 这个 Agent 我调了几天,同事却还得重新教一遍。 你花了几天把售前助手调好:知道公司的产品口径,会查报价,也知道哪些承诺不能做。你把提示词发给同事,以为他终于不用从头开始。结果他第一次询价就卡住了:报价工具不在他的环境里,资料链接也没有权限。 你想交给他的,其实是一个能完成工作的助手,而不只是角色说明。先决定交付哪一种:让同事使用你持续维护的助手,还是让他在自己的环境重建一份。后者还要重配模型、资料和工具;这里先讨论前者。 如果平台已经支持团队共享助手,直接共享就好。自建助手也可以作为远程 MCP 工具或带鉴权的 HTTP 服务提供给同事,不必为了换一个客户端就引入 EasyNet。只有一个助手、一个固定入口时,先把这条路走通。 值得继续问的是:报价、库存和合同助手分别由不同同事维护,新人来了要找谁拿入口?谁更新了工具,哪些使用者还连着旧版?EasyNet 要解决的是这些能力如何一起被找到和使用,而不是再造一个远程聊天接口。是否值得接入,要看新增同事和更新助手时,实际少了哪些重复配置;同时也要算上 Runtime、身份和服务在线的维护工作。 这里可以共享正在运行的助手,而不是让同事重新安装它。你在自己的 Runtime 中接入维护好的 Agent,将它的对话入口发布出来。同事发来“这批产品怎么报价”,请求交给你维护的助手处理,再把回答返回给同事。模型配置、报价程序和资料更新仍由你在提供端维护,不必每改一次就重新发一份配置。 同事以自己的身份申请并调用这个入口,助手仍在你维护的环境中执行;这不是把他的账号变成你电脑上的运行账号,更不能借给他你的个人密钥。工具访问与知识库访问也要分别处理:能查报价,不意味着能看所有客户资料。入口应当告诉他缺了哪项权限、向谁申请,而不是让他猜为什么助手只会说不知道。 仓库中的测试已覆盖两个独立 Runtime 之间接入外部 Agent、发现对话能力并远程调用的路径,但两端配对的是同一个用户。它说明请求能够到达并返回,还没有验证另一个同事的身份如何获准或被拒绝。测试程序返回确定内容,也不能证明真实售前助手的报价质量。接入实际团队时,还要换成两个不同用户检查授权,再用真实助手核对报价与资料范围。 让同事独立完成一次常见询价,再试一次超出权限的请求。如果前者不再需要你临时帮忙,后者会明确停下,你才真正交出了一部分已经调好的工作,而不是又发了一份配置说明。 01 提供方保持配置 模型、报价工具与资料访问仍由提供方维护 02 发现 chat 入口 取得真实owner和descriptor,核对输入 03 同事提交问题 获准身份发送prompt,不传提供方密钥 04 核对答复与边界 测试常见询价、越权请求和离线 调用同事维护的 Agent,不是复制一份提示词。 先检查下方 starter 的本地配置 先下载下方 agent_entry.py 和 provider.example.json,在提供端另存配置为 provider.json,填写真实路径并完成合成资料审阅。进入下载目录,替换命令中的配置绝对路径;--check 不启动模型。完整注册和同事端接线见下方附件。 本地示例 · bash ``` python3 agent_entry.py --config /absolute/path/to/private/provider.json --check ``` 预期结果:有效配置输出文件与格式已检查;未确认或路径缺失则退出失败。这不是模型结果或独立同事授权的验证。 提供方已经维护好报价说明和工具配置,同事需要的是问它一次,而不是重建整套项目。这个 starter 在提供方机器启动真实 Claude Code,只用于双方共同批准的合成客户和测试规则。 提供方填写实际路径,再检查配置 下载 agent_entry.py 和配置示例,在提供方本机把示例另存为 provider.json。配置中填写 Claude、现有项目和审阅后的 MCP 配置的真实绝对路径;Python 路径只用于下方命令。项目须包含已检查的 CLAUDE.md。审阅完成前保留 false,启动器会拒绝运行。 ``` { "claude_binary": "/REPLACE/with/provider/claude", "workspace": "/REPLACE/with/approved/synthetic/project", "mcp_config": "/REPLACE/with/reviewed/mcp.json", "model": "REPLACE_WITH_PROVIDER_APPROVED_MODEL", "reviewed_for_shared_synthetic_data": false } ``` 检查不会调用模型;注册会发布入口 替换下方所有 /absolute/path。--check 只检查文件与配置。确认提供方 Runtime 已连接并获准注册后,才执行 agent add;使用未占用的 quotation-agent 名称。模型凭据留在提供方,不放进这些参数。 ``` /absolute/path/to/python3 /absolute/path/to/teammate-agent/agent_entry.py \ --config /absolute/path/to/private/provider.json --check easynet agent add quotation-agent --type external \ --command /absolute/path/to/python3 \ --arg /absolute/path/to/teammate-agent/agent_entry.py \ --arg=--config \ --arg /absolute/path/to/private/provider.json \ --label "Shared synthetic quotation agent" easynet agent list ``` 阅读完整启动器 ``` #!/usr/bin/env python3 """Start an inspected provider-side Claude Code project; never export an agent.""" import argparse import json import os from pathlib import Path import sys KEYS = {"claude_binary", "workspace", "mcp_config", "model", "reviewed_for_shared_synthetic_data"} def absolute_path(value, *, directory=False, executable=False): if not isinstance(value, str) or not value or not Path(value).is_absolute(): raise ValueError("configuration paths must be explicit absolute paths") path = Path(value).resolve(strict=True) if not (path.is_dir() if directory else path.is_file()): raise ValueError("configured path has the wrong file type") if executable and not os.access(path, os.X_OK): raise ValueError("Claude binary is not executable") return path def launch_spec(config_path): config = json.loads(Path(config_path).read_text(encoding="utf-8")) if not isinstance(config, dict) or set(config) != KEYS: raise ValueError("configuration must contain exactly the example's five fields") if config["reviewed_for_shared_synthetic_data"] is not True: raise ValueError("provider review of shared synthetic data is required") binary = absolute_path(config["claude_binary"], executable=True) workspace = absolute_path(config["workspace"], directory=True) mcp = absolute_path(config["mcp_config"]) if not (workspace / "CLAUDE.md").is_file(): raise ValueError("the existing project must have an inspected CLAUDE.md") mcp_data = json.loads(mcp.read_text(encoding="utf-8")) if not isinstance(mcp_data, dict) or not isinstance(mcp_data.get("mcpServers"), dict): raise ValueError("MCP configuration must contain an mcpServers object") model = config["model"] if not isinstance(model, str) or not model.strip() or model.startswith("-"): raise ValueError("provider must select an available model explicitly") argv = [str(binary), "-p", "--output-format", "text", "--permission-mode", "dontAsk", "--tools", "Read,Glob,Grep", "--mcp-config", str(mcp), "--strict-mcp-config", "--no-session-persistence", "--max-turns", "8", "--model", model] return workspace, argv def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--config", required=True, help="Provider-owned JSON configuration") parser.add_argument("--check", action="store_true", help="Validate files only; no Claude execution") args = parser.parse_args() try: workspace, argv = launch_spec(args.config) if args.check: print("Configuration paths and shape checked; model, tools and authorization not tested.") return 0 os.chdir(workspace) # No shell, eval, dotenv import, prompt rewriting, resume or credential copying. # exec preserves stdin/stdout/stderr, child exit status and Runtime signal delivery. os.execv(argv[0], argv) except (OSError, ValueError) as error: # Do not print config values: MCP files may contain sensitive settings. print(f"teammate-agent: cannot start ({type(error).__name__}); check paths, JSON and review flag", file=sys.stderr) return 2 if __name__ == "__main__": sys.exit(main()) ``` 启动器不继承原聊天,也不自动恢复共享会话。它保留项目目录,但只开放 Read、Glob、Grep 内建工具;MCP、hooks 和已有权限仍需提供方审阅。这不是把原交互环境完整复制给同事。 让编程 Agent 按两端角色接线 把本页地址与完整 AGENTS.md 一起交给编程 Agent,说明它正在提供端还是同事端。说明中包含实际 chat descriptor 的发现、检查与调用顺序。没有独立同事的授权路径时停在授权步骤,不借用提供方账号。 AGENTS.md 和 prompt.txt 目前是中文原文;界面切换不会翻译或改写下载附件。 [下载 AGENTS.md](https://easynet.run/examples/teammate-agent/AGENTS.md) 阅读完整交接步骤 已通过本地 fake 进程启动测试:参数、目录、输入输出和失败处理。尚未验证真实模型结果或独立用户授权;这些测试不是远端同事使用成功的记录。 ``` # 把提供方已有的报价 Agent 接给同事:合成资料 starter 这里接入的是提供方已经维护的 **Claude Code 项目**:保留该项目的目录、报价说明和经过审阅的 MCP 配置,在原机器启动真实 Claude Code。不是把另一平台的聊天记录导出,也不是用一个新报价函数替代原 Agent。 当前附件能检查启动路径;没有运行真实模型,也没有完成独立同事授权验收。跟进本示例的 Agent 不得自动安装软件、登录、调用模型、注册或授权。先检查文件和本地测试,只有用户明确同意后才执行对应步骤。 ## 提供方先准备什么 提供方选择原项目中**双方已共同批准的合成资料环境**,例如 TEST-TEAM-11 的虚构客户需求、测试价目表、已有的例外规则。目录中必须已有一份审阅过的 `CLAUDE.md`。应能解释:为什么选择某个产品、数量如何影响单价、哪些例外要人工确认。不要仅为通过测试放一条“回答成功”。这份 starter 不附加凭空编造的业务规则。 已有项目如果能访问真实客户或生产写入工具,不适合直接注册。先由提供方准备同一套工作方法的合成测试范围:审阅项目/用户/组织设置、hooks、skills、自动记忆以及每个 MCP 服务的资料和工具权限。需要调用其他进程的 hooks 也在审阅范围内。工作目录不是 OS 沙箱,`dontAsk` 也不会把已有允许规则撤销。不要以提示词替代数据隔离。 将这三个下载文件保存在提供机器的一个固定目录: - `agent_entry.py`:可审阅的启动器,不修改现有项目。 - `provider.example.json`:在本机复制为 `provider.json`,用编辑器填写。 - `prompt.txt`:双方同意后再用于真实模型测试。 `provider.json` 的字段全部由**提供方**填写;不是让同事填写,也不从请求里读取: | 字段 | 填什么 | | --- | --- | | `claude_binary` | 已安装 Claude Code 可执行文件的绝对路径,可用 `command -v claude` 查看;不能填 shell 命令或别名 | | `workspace` | 上述现有合成测试项目的绝对目录,包含 `CLAUDE.md` | | `mcp_config` | 审阅后的 MCP JSON 绝对路径,必须有 `mcpServers` 对象;不用 MCP 时显式提供 `{"mcpServers": {}}` 文件 | | `model` | 提供方账号实际可用、同意承担费用的模型标识;不要照抄占位符 | | `reviewed_for_shared_synthetic_data` | 完成前述审阅后由提供方改为 `true`;这是人为确认,不是程序证明数据安全 | 配置文件不放模型密钥。Runtime 进程的 OS 用户必须已经有获准使用的 Claude 认证。启动器继承该进程环境,不读取 `.env`,不把密钥加入 CLI 参数,不改变 HOME。MCP 文件与现有项目配置由提供方保管,不上传到公共附件或发给同事。 ## 先检查,再注册提供端 以下 `/absolute/...` 全部替换成提供方刚确定的真实路径。Python 也用 `command -v python3` 得到可执行文件的绝对路径,避免 daemon 的 PATH 与终端不同。 ```bash /absolute/path/to/python3 /absolute/path/to/teammate-agent/agent_entry.py \ --config /absolute/path/to/private/provider.json --check easynet agent add quotation-agent --type external \ --command /absolute/path/to/python3 \ --arg /absolute/path/to/teammate-agent/agent_entry.py \ --arg=--config \ --arg /absolute/path/to/private/provider.json \ --label "Shared synthetic quotation agent" easynet agent list ``` `--check` 只检查文件、JSON 和确认字段,不认证、不连接 MCP、不启动 Claude。配置缺失、路径错误或未确认时失败。`agent add` 则是实际 Runtime 写操作,要求提供机器已有兼容、已配对的 EasyNet Runtime;它发布 chat 入口,不等于完成同事授权。 启动器固定只开放 Read/Glob/Grep 这些内建工具,并显式加载所选 MCP 配置;原项目若依赖 Bash、写文件等操作,此 starter 会改变其可用工具,不能宣称完全等同原交互运行。不要为了跑通改成权限绕过。MCP 工具不受内建 `--tools` 列表限制,必须在所选服务和 Claude 有效权限中审定。缺少预先允许的工具调用由 `dontAsk` 拒绝;本例不自动增加 allowedTools。 用户同意模型费用后,提供方可执行下面的本地业务检查。此时会真正调用模型,最多 8 个模型 turn;这不是费用或耗时硬上限。EasyNet 的调用 deadline 另行管理,不应承诺上游费用已取消。 ```bash easynet agent send quotation-agent \ '仅使用双方已批准的合成客户 TEST-TEAM-11 和测试报价规则生成草稿,列出依据和待确认项,不发送或提交。' \ --timeout 180 ``` 人工核对结果是否引用正确的现有规则,而不只判断有没有输出。缺资料和权限拒绝也应清楚返回。启动器不传 `--resume` 或 `--continue`,并关闭 Claude 会话持久化,因此原会话不会自动继承,多个同事也不会自动共用聊天历史。EasyNet 自己的调用记录、Claude 自动记忆/其他现有配置、MCP 服务日志和上游留存是不同的范围,不能把这个 flag 当作“完全不存数据”。 ## 同事端先确认身份,再发现实际入口 同事必须使用自己获准的 Runtime 调用主体。当前两 Runtime harness 的同用户测试不是此步骤的授权证明。部署负责人要先说明如何给这个独立主体开放 chat 入口,并验收允许、拒绝、撤销和提供方离线;没有这套授权操作时停在这里,不借用提供方凭据继续。 提供方交给同事的是其 Runtime 实际公布的 Device URA,不是机器昵称。同事在自己终端设置 `PROVIDER_DEVICE_URA` 后读取目录: ```bash easynet ability list --node "$PROVIDER_DEVICE_URA" --format json ``` 在输出中找到提供方确认的 `quotation-agent.chat`,确认归属和 RPC 模式,复制其真实 `ability_ura` 为 `CHAT_ABILITY_URA`,并保留 RPC `descriptor_ref` 为 `CHAT_DESCRIPTOR_REF`。不要只凭同名条目挑第一个,不要拼接 guessed realm、版本或 hash。再查看完整描述: ```bash easynet ability show "$CHAT_ABILITY_URA" \ --node "$PROVIDER_DEVICE_URA" --format json ``` 核对上一步的 descriptor 与这里的版本/hash/admission action 对应,检查实际输入 schema;将这里真实的 `owner_ura` 记为 `CHAT_OWNER_URA`。如果安装版本没有公开 descriptor ref,先让维护者提供同一当前描述的 canonical ref,不自行拼造,也不绕过 descriptor 检查。 当前源码 harness 用 `prompt` 和 `skills.mode=none` 发起这类请求。只有实际 schema 同样支持时才照下例执行。每次新请求由同事生成新的 nonce;重试未知结果前先查原调用,不自动重跑可能计费的生成。 ```bash FRESH_NONCE_HEX="$(python3 -c 'import secrets; print(secrets.token_hex(16))')" easynet ability invoke "$CHAT_DESCRIPTOR_REF" \ --node "$PROVIDER_DEVICE_URA" \ --subject "$CHAT_OWNER_URA" \ --nonce-hex "$FRESH_NONCE_HEX" --causal-root \ --args '{"prompt":"仅使用共同批准的合成客户 TEST-TEAM-11 和测试规则生成报价草稿,列出依据,不发送或提交。","skills":{"mode":"none"}}' \ --raw ``` `CHAT_OWNER_URA` 是被调用的 Agent,不是把同事伪装成提供方的 caller。入口接受请求后,external 驱动只将 prompt 送给 Claude,不自动把同事的业务身份传播到 MCP;工具仍使用提供方已配置的账号。这就是本例只处理共同批准的合成资料的原因。不要把“我是某客户负责人”这样的提示词当作业务鉴权。 提供方可查看真实调用记录: ```bash easynet invocation list --ability-ura "$CHAT_ABILITY_URA" --format json ``` 记录执行结果不等于草稿正确,草稿正确也不等于独立主体授权完备。给最终交付记录分别写清:本地真实模型结果、实际 caller 身份、远端调用记录、拒绝/撤销/离线结果;没运行的项保持未验证。 ## 本地测试与源码依据 ```bash python3 -m unittest discover -s Frontend/public/examples/teammate-agent -p 'test_*.py' -v ``` 测试自己创建临时 fake 可执行文件,验证 argv、cwd、stdin/stdout/stderr、非零退出及配置失败。fake 不是示例 Agent,不提供报价能力,不证明模型质量、MCP 权限或跨用户授权。测试不安装依赖、不调用 EasyNet 或模型,临时目录由测试回收。 当前源码定位:`EasyNet-Cli/src/daemon/execution/mission/drivers/external.rs`(真实子进程输入输出);`process_runner.rs`(环境/工作目录/退出);`cli/commands/agent/mod.rs`(add 参数);`tools/scripts/docker-two-node-easyremote-cli-e2e.sh:945–978`(目录、描述与远端 chat 调用顺序)。不要以 `ability teach` 的 descriptor 转移代替未验证的 chat 入口授权。 2026-09-07 核对的官方文档:[CLI reference](https://code.claude.com/docs/en/cli-reference)、[Headless](https://code.claude.com/docs/en/headless)、[Permission modes](https://code.claude.com/docs/en/permission-modes)。启动器使用 print/text、dontAsk、内建工具列表、显式 MCP 配置与会话选项;部署前记录实际安装版本并确认支持这些参数。未知 flag 应报错,不做静默兼容回退。 ``` 接入时的检查项 接收者无需重新猜配置即可完成样例任务。 接收者以自己的身份调用,提供方维护执行环境;用不同用户分别验证获准和拒绝,不借用提供方个人凭证。 依赖缺失时具体指出阻碍而不是泛化报错。 源码与接入资料 [设备接入指南](https://easynet.run/docs/connect/runtime) [MCP 接入指南](https://easynet.run/docs/connect/mcp) [函数接入指南](https://easynet.run/docs/connect/function)