Composio CLI 无头 Agent 登录契约解析:`composio login --agent` 端到端测试如何验证无人值守鉴权
2026/9/12 4:35:49 网站建设 项目流程

Composio CLI 无头 Agent 登录契约解析:composio login --agent端到端测试如何验证无人值守鉴权

【免费下载链接】composioComposio powers 1000+ toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action.项目地址: https://gitcode.com/GitHub_Trending/co/composio

本文基于仓库中ts/e2e-tests/cli/agent-signin/README.md及其配套测试ts/e2e-tests/cli/agent-signin/e2e.test.ts展开,讲解 Composio CLI 针对"无人值守 Agent"设计的三条登录契约:composio login --agent如何把一个全新环境带到已认证状态、管道里运行composio login时为何绝不自动注册账号、以及一份已存储的 READY 状态agent.json如何让普通composio login在 headless 环境下静默复用完成登录。读完后你将理解该 e2e 测试的断言设计、mock 服务端实现,以及 CLI 源码中对应流程(login.cmd.ts、agents.ts)的落地细节。

一、契约背景:为什么需要专门的 Agent 签入 e2e

Composio CLI 的默认登录是"人在浏览器里点一下"的 OAuth 流程,但 CI 容器、Agent 运行环境里没有人。README(标题为CLI agent sign-in e2e (PRDE-1138))明确给出这个 e2e 要断言的三条"headless agent onboarding contract":

  1. composio login --agent能把一个无人的 Agent 从零带到已认证 CLI。服务端agents.composio.dev通过环境变量COMPOSIO_AGENTS_BASE_URL指向 mock 服务,登录完成后再跟一条composio whoami验证身份确实落盘生效。
  2. 管道里裸跑composio login时只打印 OAuth URL 和轮询指令,并且主动提示无值守 Agent 应该走composio login --agent;关键是 guardrail:绝不为管道里的人类自动创建账号(原文表述:"humans in pipes are not signed up")。
  3. 已存储的 READY 状态 Agent 身份(agent.json)让普通 headlesscomposio login可以复用身份无人值守地完成登录,同样不触发注册。

README 最后强调:所有网络面都由 mock-agents-server.ts 模拟,"no real accounts are created"——整个测试不碰真实账号。下面按"测试环境 → 三个场景 → 源码实现"逐层拆解。

二、测试环境与 mock 服务端:全部网络面本地可控

2.1 测试入口与运行方式

e2e 用例位于 e2e.test.ts,使用bun:test与仓库自研的e2e()测试脚手架(来自@e2e-tests/utils,声明于 package.json):

# 在 ts/e2e-tests/cli/agent-signin/ 目录下 bun test e2e.test.ts

用例只针对 CLI 的current版本运行(versions: { cli: ['current'] }),意味着契约跟随最新构建验证。

2.2 三个独立 mock 实例与统一环境变量

beforeAll里启动了三台独立的 mock 服务,分别对应三个场景,互不污染:

signupServer = await startMockAgentsServer(); // 场景一:无值守注册 guardrailServer = await startMockAgentsServer(); // 场景二:headless 人类登录 reuseServer = await startMockAgentsServer(); // 场景三:复用已存身份

三条命令统一通过envPrefix注入环境变量(e2e.test.ts#L22-L27):

const envPrefix = (server: MockAgentsServer): string => [ `COMPOSIO_BASE_URL=${server.dockerBaseUrl}`, `COMPOSIO_AGENTS_BASE_URL=${server.dockerBaseUrl}`, `COMPOSIO_CACHE_DIR=${CACHE_DIR}`, ].join(' ');
  • COMPOSIO_AGENTS_BASE_URL把 Agent 面(POST /api/signupGET /api/whoami)指向 mock,对应源码 agents.ts#L87-L90 中APP_CONFIG.AGENTS_BASE_URL覆盖DEFAULT_AGENTS_BASE_URLhttps://agents.composio.dev)的逻辑;
  • COMPOSIO_BASE_URL把 Composio 平台面(浏览器登录的create-session等)一并 mock 掉;
  • COMPOSIO_CACHE_DIR=/tmp/composio-agent-signin隔离凭据缓存目录;
  • dockerBaseUrl形如http://host.docker.internal:<port>,说明测试在 Docker 容器内执行 CLI,通过该主机名回连宿主机上的 mock 服务。

2.3 mock 端点清单与身份 fixture

mock-agents-server.ts 用node:http实现,只做两件事:记录每个请求requests.push(\${method} ${url.pathname}`)`,供断言"注册是否被触发")和按路由返回固定响应。它 mock 的路由有四条(mock-agents-server.ts#L44-L85):

方法 + 路径角色响应
POST /api/signupagents.composio.dev 面,注册新 Agent返回AGENT_IDENTITY
GET /api/whoami校验 Agent key请求头必须是Bearer <composio_agent_key>,否则 401
POST /api/v3.1/cli/create-session默认登录路径的 Apollo 面返回 10 分钟过期的 pending session(id/code/expiresAt/status
POST /api/v3/cli/analytics分析上报204 空体

未 mock 的路由统一返回 404——注释说明这些 404 恰好能"exercise their degrade paths"(演练各流程的降级路径),即测试同时覆盖了部分接口不可用时的容错。

核心 fixture 是一份写死的 READY 身份(mock-agents-server.ts#L10-L21):

const AGENT_IDENTITY = { status: 'READY', slug: 'e2e-agent', email: 'e2e-agent@agent.composio.ai', composio_agent_key: 'cak_e2e_agent', composio: { member_id: 'mem_e2e_agent', org_id: 'org_e2e_agent', project_id: 'proj_e2e_agent', user_api_key: 'uak_e2e_agent', }, } as const;

这份结构就是源码里AgentIdentitySchema 的样例(agents.ts#L20-L40):顶层是 Agent 账号属性(statusslugemailcomposio_agent_key),内嵌composio对象携带真正用于调 API 的凭据(user_api_keyorg_id等)。"READY" 的判定 =status归一化后为READYcomposio.user_api_keycomposio.org_id非空(见 agents.ts#L406-L409),这正是后两个场景能成立的前提。

三、场景一:composio login --agent无值守引导(从 0 到已认证)

3.1 测试命令与断言

beforeAll中执行的命令(e2e.test.ts#L46-L48):

COMPOSIO_BASE_URL=... COMPOSIO_AGENTS_BASE_URL=... COMPOSIO_CACHE_DIR=/tmp/composio-agent-signin \ composio login --agent --no-skill-install && composio whoami

--no-skill-install关闭登录成功后的 Claude Code skill 安装步骤,保证输出干净。随后四组断言逐条验证 README 第一条契约:

it('exits successfully', () => { expect(unattendedSignup.exitCode).toBe(0); }); it('signs up via agents.composio.dev', () => { expect(signupServer.requests).toContain('POST /api/signup'); }); it('prints the agent login summary on stdout', () => { expect(unattendedSignup.stdout).toContain('"account_type":"agent"'); expect(unattendedSignup.stdout).toContain('"logged_in":true'); expect(unattendedSignup.stdout).toContain('"status":"READY"'); }); it('leaves the CLI authenticated as the agent (whoami)', () => { const whoami = lastJsonLine(unattendedSignup.stdout); expect(whoami.account_type).toBe('agent'); });

注意最后一条:whoami&&拼接在登录命令之后的独立进程,它从 stdout 的最后一行 JSON解析身份——这验证的不是"登录命令自己声称成功",而是"凭据真的写进了缓存目录、且下一条命令能以此身份工作"。

3.2 源码侧:--agent的调用链

在 login.cmd.ts 中,--agent是一个布尔选项(login.cmd.ts#L78-L81):

const agentOpt = Options.boolean('agent').pipe( Options.withDefault(false), Options.withDescription('Sign up or log in using a Composio agent identity') );

选项互斥被显式校验:--agent不能与--no-browser/--no-wait/--key/--user-api-key组合(login.cmd.ts#L915-L921),因为它们是互斥的登录路径。命中--agent分支后的主流程(login.cmd.ts#L963-L974):

if (agent) { return yield* handleAgentAuthError( Effect.gen(function* () { yield* ensureAgentSignupAllowed; const identity = yield* getOrSignupReadyAgent(); yield* completeAgentLogin(identity); if (!noSkillInstall && canPrompt) { yield* installSkillSafe({ channel: inferSkillReleaseChannel(APP_VERSION) }); } }) ); }

三个关键步骤在 agents.ts 中:

  1. ensureAgentSignupAllowed(agents.ts#L318-L327):如果当前 CLI 已以普通人类账号登录(存在 API key 且该 key 不属于任何已存 Agent 身份),直接报错并给出composio logout后的切换步骤——防止--agent在人类会话上"偷换"身份。
  2. getOrSignupReadyAgent()(agents.ts#L414-L431):先读本地agent.json,若存在则用composio_agent_keyGET /api/whoami刷新;刷新结果status === 'READY'直接复用。否则走signupAgent()——POST /api/signup(agents.ts#L267-L276)——并把返回身份原子写入缓存目录下的agent.json这就是场景一断言POST /api/signup出现的来源:全新环境没有本地身份,必然注册。
  3. completeAgentLogin(identity)(login.cmd.ts#L305-L312):调loginWithAgentIdentitycomposio.user_api_key+composio.org_id完成真正的 CLI 登录(agents.ts#L433-L446),然后输出测试断言的那段 JSON:
yield* ui.log.success(`Logged in as Composio agent ${summary.email ?? summary.slug ?? ''}`); yield* ui.output(JSON.stringify({ ...summary, logged_in: true }));

其中summary = safeAgentSummary(identity)(agents.ts#L154-L164),字段固定为account_type: 'agent'statusslugemailorg_idproject_idmember_idclaimed_byclaimed_at——与测试断言的"account_type":"agent""status":"READY"一一对应。

四、场景二:管道里的composio login——guardrail 验证

4.1 测试命令与断言

第二条命令只有一行(e2e.test.ts#L50):

composio login # 在容器/管道环境中运行,stdin 非 TTY

e2e 框架在 Docker 容器中执行 CLI,canPrompt能力探测为 false,于是进入非交互路径。断言(e2e.test.ts#L89-L106)分三层:

it('exits successfully with instructions instead of hanging', () => { expect(headlessHumanLogin.exitCode).toBe(0); expect(headlessHumanLogin.stdout).toContain('Open this URL in your browser to log in:'); expect(headlessHumanLogin.stdout).toContain('composio login --poll'); }); it('offers the unattended agent path', () => { expect(headlessHumanLogin.stdout).toContain('For unattended agents'); expect(headlessHumanLogin.stdout).toContain('composio login --agent'); }); it('never auto-signs-up an account', () => { expect(guardrailServer.requests.filter(request => request.includes('/api/signup'))).toEqual([]); });

三条断言恰好对应 README 的第二条契约:打印 OAuth URL + 轮询指令推荐--agent路径零注册请求。最后一条是整个 guardrail 的核心——通过检查 mock 端requests数组中没有任何/api/signup,从服务端视角证明"管道里的人类没被自动签入"。

4.2 源码侧:非交互输出与"只复用、不注册"

当终端不可交互(effectiveNoWait为真),browserLogin走纯打印分支(login.cmd.ts#L703-L721),先createSession拿到 mock 的 pending 会话,再把指令文本交给ui.output。指令文本由formatNonInteractiveLoginInstructions生成(login.cmd.ts#L195-L208),测试断言的四段字样全部出自这里:

Open this URL in your browser to log in: <loginUrl> Then run this command to complete login: composio login --poll hint: For agents: Show the URL above to the user to click, then run the command above. ... hint: For unattended agents: If no human is available to open the URL, run `composio login --agent` instead — it signs the CLI in with a Composio agent account (creating one if needed) without a browser. Never use it when a human is present to log in with their own account.

这段 hint 本身就是一份写给 Agent 的"操作说明书":明确告知轮询命令用缓存的 login key、最长轮询 10 分钟、不要询问用户是否轮询——与--poll的常量定义(LOGIN_POLL_TIMEOUT_SECONDS = 10 * 60,login.cmd.ts#L84-L87)一致。

guardrail 的真正实现在login命令末尾的复用检查(login.cmd.ts#L1009-L1027):

// Reuse-only by design: headless login may complete with an existing // agent identity but must never auto-create one for a human in a pipe. const requestedExplicitLoginFlow = noBrowser || noWait; if (!canPrompt && !requestedExplicitLoginFlow) { const storedAgent = yield* getStoredReadyAgent; if (Option.isSome(storedAgent)) { const identity = storedAgent.value; const activeApiKey = ctx.data.apiKey; if ( Option.isNone(activeApiKey) || isAgentIdentityForApiKey(identity, activeApiKey.value) ) { return yield* handleAgentAuthError(completeAgentLogin(identity)); } } }

注释把设计意图写得很直白:"Reuse-only by design... must never auto-create one for a human in a pipe"。注意分支条件:只有无 TTY 且未显式要求--no-browser/--no-wait输出契约时才尝试复用——后两者承诺了确定性的输出格式(打印 URL、会话 JSON),静默复用 Agent 会破坏该契约。场景二中没有任何已存agent.jsongetStoredReadyAgent返回Option.none,流程落到browserLogin的非交互打印分支,于是:退出码 0、打印 URL 与--poll指令、且POST /api/signup请求数为零。

五、场景三:READY 的agent.json让普通composio login无人值守完成

5.1 测试命令与断言

第三条命令先落盘身份、再跑登录(e2e.test.ts#L52-L59):

mkdir -p /tmp/composio-agent-signin printf '%s' '<reuseServer.agentIdentity 的 JSON>' > /tmp/composio-agent-signin/agent.json composio login && composio whoami

注意这里写的是composio login(不带--agent,写入的内容就是 mock 服务导出的reuseServer.agentIdentity(即第二节 2.3 的 fixture)。断言(e2e.test.ts#L108-L124):

it('completes login unattended by reusing the stored identity', () => { expect(storedIdentityLogin.exitCode).toBe(0); expect(storedIdentityLogin.stdout).toContain('"logged_in":true'); expect(storedIdentityLogin.stdout).not.toContain('Open this URL in your browser'); }); it('refreshes the identity instead of signing up', () => { expect(reuseServer.requests).toContain('GET /api/whoami'); expect(reuseServer.requests.filter(request => request.includes('/api/signup'))).toEqual([]); }); it('leaves the CLI authenticated as the agent (whoami)', () => { const whoami = lastJsonLine(storedIdentityLogin.stdout); expect(whoami.account_type).toBe('agent'); });

三条断言合起来精确刻画 README 第三条契约:无 URL 提示、退出 0(静默完成);GET /api/whoami刷新而非POST /api/signup注册whoami确认最终身份仍是 agent 账号。

5.2 源码侧:getStoredReadyAgent的刷新语义

该场景走的是第四节末尾的复用分支,核心是 agents.ts#L387-L412 的getStoredReadyAgent。注释称其为 "Reuse-only counterpart ofgetOrSignupReadyAgent"——它刷新并返回已存的 READY 身份,从不注册新 Agent。两个实现细节值得注意:

1. 401/403 与网络失败的区分处理

const remote = yield* fetchAgentWhoami(agentKey).pipe(Effect.either); // An auth rejection means the API examined and refused this key — the stored // identity is revoked, not unreachable. Only transport-shaped failures may // fall back to the on-disk identity. if (Either.isLeft(remote) && isAgentKeyRejection(remote.left)) { return Option.none<AgentIdentity>(); }

isAgentKeyRejection只认 401/403(agents.ts#L380-L381):服务器明确拒绝了 key,说明身份已被吊销,不可回退到磁盘身份;而 DNS 超时之类的传输层故障则可回退使用本地副本(identity = ... : stored.value)。这个语义保证了吊销的 Agent 不会在离线环境下"诈尸"。mock 服务的whoami路由正是用Bearer ${composio_agent_key}校验(mock-agents-server.ts#L55-L64),使该分支在测试中真实可触发。

2. READY 门槛:刷新(或回退)后的身份必须满足status === 'READY' && composio.user_api_key && composio.org_id才会被返回(agents.ts#L406-L411)。fixture 恰好是 READY 且凭据齐全,所以场景三通过。

身份随后由completeAgentLogin落盘为 CLI 凭据并输出logged_in: true的 JSON 摘要——与场景一共用同一出口,但注册请求数为零,这就是"refresh the identity instead of signing up"断言的实现依据。

六、本地运行这份 e2e 与相关路径速查

  • 测试目录:ts/e2e-tests/cli/agent-signin/,脚本test:e2e: bun test e2e.test.ts(package.json);依赖 e2e 通用脚手架@e2e-tests/utils(workspace 内部包)。
  • mock 服务可直接由bun ts/packages/cli/scripts/mock-agents-server.ts风格调用(startMockAgentsServer(),支持host/port参数,port: 0自动分配随机端口并返回hostBaseUrldockerBaseUrl两个回连地址,mock-agents-server.ts#L36-L43)。
  • CLI 命令面:--agent选项定义与互斥校验在 login.cmd.ts#L78-L81 与 login.cmd.ts#L915-L921;headless 复用 guardrail 在 login.cmd.ts#L1009-L1027。
  • Agent 身份服务层:身份 Schema 与agent.json文件位置(缓存目录下,AGENT_CONFIG_FILE_NAME)见 agents.ts#L13-L40;注册/刷新/登录入口signupAgent/fetchAgentWhoami/loginWithAgentIdentity见 agents.ts#L267-L286 与 agents.ts#L433-L453。

七、小结:三条契约、一套可复制的验证模式

这份 e2e 的价值不在"跑通了登录",而在于用服务端请求日志 + stdout 契约把三个易被破坏的行为钉死:

场景命令关键断言对应实现
无值守引导composio login --agent && whoami出现POST /api/signup;输出"logged_in":true;whoami 为 agentlogin.cmd.ts#L963-L974、agents.ts#L414-L431
管道人类登录composio login(非 TTY)打印 URL +--poll指令;推荐--agent/api/signup请求数为 0login.cmd.ts#L195-L208、login.cmd.ts#L1009-L1027
复用已存身份agent.jsoncomposio login && whoami无 URL 提示;GET /api/whoami出现且无 signup;whoami 为 agentagents.ts#L387-L412

对 Agent 开发者而言,可落地的结论是:CI 或无人值守环境应显式使用composio login --agent完成首登(其输出是可解析的 JSON 摘要);已有 READY 身份时,裸composio login会静默复用;而把composio login交给人类在管道里执行,只会得到一段包含 OAuth URL 与composio login --poll的说明文本,不会产生任何账号——这三条边界正是 README 所定义、并由 e2e.test.ts 持续守护的契约。

【免费下载链接】composioComposio powers 1000+ toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action.项目地址: https://gitcode.com/GitHub_Trending/co/composio

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询