fix(agents): confirm the hiring parent's company before credential inheritance
The hire route copied provider credential references from the hiring agent to the hired agent, but the merge checked only the adapter type, not the hiring agent's company. Add a check that the hiring agent belongs to the target company, so a copy can never cross a company boundary. Add regression tests for the codex_local, grok_local, and claude_local credential paths. Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
c2e517d948
commit
65e3780f3e
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>,
|
||||
): Promise<{ adapterConfig: Record<string, unknown>; 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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue