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 fd80ddf876..ef03ac91eb 100644 --- a/server/src/__tests__/agent-hire-auth-inheritance-routes.test.ts +++ b/server/src/__tests__/agent-hire-auth-inheritance-routes.test.ts @@ -333,6 +333,66 @@ describeEmbeddedPostgres("hired agent provider credential inheritance", () => { expect(Object.keys(childEnvOf(res))).toHaveLength(0); }); + it("does not inherit a codex_local credential from a hiring agent in another company", async () => { + const parentCompanyId = await seedCompany(); + const targetCompanyId = await seedCompany(); + const secret = await createCompanySecret(parentCompanyId, "sk-openai-other-company"); + const parent = await seedParentAgent(parentCompanyId, "codex_local", { + OPENAI_API_KEY: secretRef(secret.id), + }); + + // The actor claims the target company, but the named hiring agent belongs + // to a different company. The route must reject the request and must + // never copy the other company's credential reference. + const res = await hire(agentActor(targetCompanyId, parent.id), targetCompanyId, { + name: "Codex Cross-Company Child", + role: "engineer", + adapterType: "codex_local", + }); + + expect(res.status).toBe(403); + const children = await db.select().from(agents).where(eq(agents.companyId, targetCompanyId)); + expect(children).toHaveLength(0); + }); + + it("does not inherit a grok_local credential from a hiring agent in another company", async () => { + const parentCompanyId = await seedCompany(); + const targetCompanyId = await seedCompany(); + const secret = await createCompanySecret(parentCompanyId, "xai-other-company"); + const parent = await seedParentAgent(parentCompanyId, "grok_local", { + XAI_API_KEY: secretRef(secret.id), + }); + + const res = await hire(agentActor(targetCompanyId, parent.id), targetCompanyId, { + name: "Grok Cross-Company Child", + role: "engineer", + adapterType: "grok_local", + }); + + expect(res.status).toBe(403); + const children = await db.select().from(agents).where(eq(agents.companyId, targetCompanyId)); + expect(children).toHaveLength(0); + }); + + it("does not inherit a claude_local credential, including the fixed OAuth binding, from a hiring agent in another company", async () => { + const parentCompanyId = await seedCompany(); + const targetCompanyId = await seedCompany(); + const parent = await seedParentAgent(parentCompanyId, "claude_local", { + ANTHROPIC_API_KEY: secretRef((await createCompanySecret(parentCompanyId, "ant-other-company")).id), + CLAUDE_CODE_OAUTH_TOKEN: { ...FIXED_CLAUDE_OAUTH_BINDING, version: 1 }, + }); + + const res = await hire(agentActor(targetCompanyId, parent.id), targetCompanyId, { + name: "Claude Cross-Company Child", + role: "engineer", + adapterType: "claude_local", + }); + + expect(res.status).toBe(403); + const children = await db.select().from(agents).where(eq(agents.companyId, targetCompanyId)); + expect(children).toHaveLength(0); + }); + it("carves out a per-agent CODEX_HOME when the child inherits OPENAI_API_KEY", async () => { const companyId = await seedCompany(); const secret = await createCompanySecret(companyId, "sk-openai-isolated"); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index b739ee7356..baa8a39388 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -2472,8 +2472,13 @@ export function agentRoutes( // key inherits no Claude credential key at all. That keeps child-wins // precedence and rules out the forbidden pairing of the fixed OAuth binding // with an ANTHROPIC_API_KEY. + // + // The hiring agent must belong to the target company. Without that check, an + // agent that can create agents in another company could copy its own + // company's credential reference into that other company. async function applyHiringAgentAuthInheritance( req: Request, + companyId: string, adapterType: string | null | undefined, adapterConfig: Record, ): Promise<{ adapterConfig: Record; inheritedFixedClaudeOAuthBinding: boolean }> { @@ -2483,7 +2488,7 @@ export function agentRoutes( if (!credentialKeys) return noInheritance; const parent = await svc.getById(req.actor.agentId); - if (!parent || parent.adapterType !== adapterType) return noInheritance; + if (!parent || parent.companyId !== companyId || parent.adapterType !== adapterType) return noInheritance; const parentEnv = asRecord(asRecord(parent.adapterConfig)?.env); if (!parentEnv) return noInheritance; @@ -4263,6 +4268,7 @@ export function agentRoutes( const hiredAgentId = randomUUID(); const authInheritance = await applyHiringAgentAuthInheritance( req, + companyId, hireInput.adapterType, applyCreateDefaultsByAdapterType( hireInput.adapterType,