From b4b720601a67e551bb86524be2f2103b302337d9 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 7 Nov 2026 23:00:36 -0600 Subject: [PATCH] fix(agents): hired OpenCode agents inherit the hiring agent's OpenRouter key reference An OpenCode agent hired by another OpenCode agent got no credential, so approving the hire left it failing verification with harness_auth_failed. Credential inheritance covered Claude, Codex and Grok but not opencode_local. Add OPENROUTER_API_KEY, the key AI connections bind for opencode_local. Only secret references are copied; plain values such as OPENCODE_CONFIG_CONTENT still stay with the parent. Co-Authored-By: Claude Opus 5.5 --- ...agent-hire-auth-inheritance-routes.test.ts | 22 +++++++++++++++++++ server/src/routes/agents.ts | 3 +++ 2 files changed, 25 insertions(+) diff --git a/server/src/__tests__/agent-hire-auth-inheritance-routes.test.ts b/server/src/__tests__/agent-hire-auth-inheritance-routes.test.ts index 5159cb27d88..aafcd8095d8 100644 --- a/server/src/__tests__/agent-hire-auth-inheritance-routes.test.ts +++ b/server/src/__tests__/agent-hire-auth-inheritance-routes.test.ts @@ -265,6 +265,28 @@ describeEmbeddedPostgres("hired agent provider credential inheritance", () => { expect(childEnvOf(res).XAI_API_KEY).toMatchObject({ type: "secret_ref", secretId: secret.id }); }); + it("inherits a native OpenCode parent's OPENROUTER_API_KEY reference when the hire inherits the caller runtime", async () => { + const companyId = await seedCompany(); + await db.update(companies).set({ requireBoardApprovalForNewAgents: true }).where(eq(companies.id, companyId)); + const secret = await createCompanySecret(companyId, "openrouter-parent-key"); + const parent = await seedParentAgent(companyId, "paperclip_runner", {}); + await db.update(agents).set({ adapterConfig: { + provider: "opencode", model: "openrouter/deepseek/deepseek-v4.1-flash-1017", opencodePermissionMode: "allow", + env: { OPENROUTER_API_KEY: secretRef(secret.id, { version: "latest" }), OPENCODE_CONFIG_CONTENT: "{\"provider\":{}}" }, + } }).where(eq(agents.id, parent.id)); + + const res = await hire(agentActor(companyId, parent.id), companyId, { + name: "OpenCode Child", role: "qa", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller", + }); + + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(res.body.agent.status).toBe("pending_approval"); + const [child] = await db.select().from(agents).where(eq(agents.id, res.body.agent.id)); + const env = (child!.adapterConfig as { env?: Record }).env ?? {}; + expect(env.OPENROUTER_API_KEY).toMatchObject({ type: "secret_ref", secretId: secret.id, version: "latest" }); + expect(env.OPENCODE_CONFIG_CONTENT).toBeUndefined(); + }); + it.each(["create", "hire"] as const)("blocks native Claude credential redirection before %s inheritance", async mode => { const companyId = await seedCompany(); const secret = await createCompanySecret(companyId, "fixture-parent-credential"); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index fe810eb8049..0a96b861c41 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -2302,6 +2302,9 @@ export function agentRoutes( claude_local: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"], codex_local: ["OPENAI_API_KEY", "CODEX_API_KEY"], grok_local: ["XAI_API_KEY"], + // OpenCode reaches its models through OpenRouter, the provider account + // AI connections bind for opencode_local. + opencode_local: ["OPENROUTER_API_KEY"], }; function isInheritableCredentialReference(value: unknown): value is Record {