Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions prisma/migrations/20251127123756_ag_credits/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "user" ADD COLUMN "creditBalance" DOUBLE PRECISION NOT NULL DEFAULT 100,
ADD COLUMN "creditBalanceLastUpdated" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP;
4 changes: 3 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ model user {
email String @unique
emailVerified Boolean
image String?
nonce String?
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
nonce String?
agent agent[]
apikey apikey[]
walletAddress walletAddress?
creditBalance Float @default(100)
creditBalanceLastUpdated DateTime @db.Timestamptz(6) @default(now())
}

model walletAddress {
Expand Down
22 changes: 17 additions & 5 deletions src/controllers/agent.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,28 @@ export class AgentController {
}
}

public static readonly verifyAgent = async(c: Context) => {
public static readonly runAgent = async (c: Context) => {
try {
const { deployedUrl, default_agent_name } =(await c.req.json()) as IAgentNameVerification;
const agent_id = c.req.param('id');
const { message } = await c.req.json();
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation for required parameters. The agent_id and message parameters are not validated before use. Add validation to ensure these required fields exist and are non-empty strings, similar to other endpoints like deleteAgent:

if (!agent_id || typeof agent_id !== 'string' || agent_id.trim().length === 0) {
    return c.json(
        api_response({
            message: "agent_id is required and must be a non-empty string",
            is_error: true
        }),
        400
    );
}

if (!message || typeof message !== 'string' || message.trim().length === 0) {
    return c.json(
        api_response({
            message: "message is required and must be a non-empty string",
            is_error: true
        }),
        400
    );
}
Suggested change
const { message } = await c.req.json();
const { message } = await c.req.json();
// Validate agent_id
if (!agent_id || typeof agent_id !== 'string' || agent_id.trim().length === 0) {
return c.json(
api_response({
message: "agent_id is required and must be a non-empty string",
is_error: true
}),
400
);
}
// Validate message
if (!message || typeof message !== 'string' || message.trim().length === 0) {
return c.json(
api_response({
message: "message is required and must be a non-empty string",
is_error: true
}),
400
);
}

Copilot uses AI. Check for mistakes.
const user = await c.get('user');
const agent = await AgentService.runAgent(agent_id.trim(), message.trim(), user?.id);
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing user authentication check. The user object retrieved from context may be undefined, but there's no validation before accessing user?.id. This could result in undefined being passed to AgentService.runAgent(). Add a check similar to other endpoints (e.g., createAgent, deleteAgent) that verifies the user is authenticated before proceeding:

const user = await c.get('user');
if (!user || !user.id) {
    return c.json(
        api_response({
            message: "User authentication required",
            is_error: true
        }),
        401
    );
}
Suggested change
const agent = await AgentService.runAgent(agent_id.trim(), message.trim(), user?.id);
if (!user || !user.id) {
return c.json(
api_response({
message: "User authentication required",
is_error: true
}),
401
);
}
const agent = await AgentService.runAgent(agent_id.trim(), message.trim(), user.id);

Copilot uses AI. Check for mistakes.
return c.json(api_response({ message: "Agent response", data: agent }));
} catch (error) {
return c.json(api_response({ message: `Failed to run agent: ${getErrorMessage(error)}`, is_error: true }), 500);
}
}

public static readonly verifyAgent = async (c: Context) => {
try {
const { deployedUrl, default_agent_name } = (await c.req.json()) as IAgentNameVerification;
if (!deployedUrl && !default_agent_name) {
return c.json(api_response({ message: 'deployedUrl and default_agent_name are required' }), 400);
}
await AgentService.verifyAgent(deployedUrl, default_agent_name);
return c.json(api_response({ message: 'Agent URL verified successfully', data: true }), 200);
} catch (error) {
if(error instanceof Error) {
if (error instanceof Error) {
return c.json(api_response({ message: error.message, data: false, is_error: true }), 500);
}
}
Expand Down Expand Up @@ -245,7 +257,7 @@ export class AgentController {
}

// Validate numeric fields
const numericCost = parseFloat(agentCost);
const numericCost = parseFloat(agentCost.toString());
if (isNaN(numericCost) || numericCost < 0) {
return c.json(
api_response({
Expand All @@ -272,7 +284,7 @@ export class AgentController {
const agent = await AgentService.createAgent(user.id, {
name: name.trim(),
description: description.trim(),
agentCost,
agentCost: agentCost.toString(),
deployedUrl: deployedUrl.trim(),
llmProvider: llmProvider.trim(),
skills: skills.map(skill => skill.trim()),
Expand Down
1 change: 1 addition & 0 deletions src/routes/agent.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ app.post("/", verifyApiKey, AgentController.primary);
app.post("/verify", AgentController.verifyAgent);
app.post("/create", AgentController.createAgent);
app.get("/all", AgentController.getAllAgents);
app.post("/:id/run", AgentController.runAgent);
app.get("/:id", AgentController.getAgent);
app.put("/:id", AgentController.updateAgent);
app.delete("/:id", AgentController.deleteAgent);
Expand Down
47 changes: 44 additions & 3 deletions src/services/agent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class AgentService {
matched_agents[0].deployedUrl,
matched_agents[0].default_agent_name || "",
(matched_agents[0].framework_used as AgentFrameWorks) ||
AgentFrameWorks.google_adk,
AgentFrameWorks.google_adk,
data.message,
session_id,
api_key.userId
Expand Down Expand Up @@ -91,6 +91,47 @@ export class AgentService {
}
};

public static readonly runAgent = async (agent_id: string, message: string, user_id: string) => {
const agent = await prisma.agent.findUnique({
where: { id: agent_id },
});
if (!agent) {
throw new Error("Agent not found");
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing authorization check for agent ownership. The method doesn't verify that the user is authorized to run this agent. If the agent is private (not public), only the owner should be able to run it, or there should be proper access control. Add a check similar to deleteAgent:

const agent = await prisma.agent.findUnique({
    where: { id: agent_id },
});
if (!agent) {
    throw new Error("Agent not found");
}

// Check if user can access this agent
if (!agent.isPublic && agent.userId !== user_id) {
    throw new Error("Unauthorized to run this agent");
}
Suggested change
}
}
// Check if user can access this agent
if (!agent.isPublic && agent.userId !== user_id) {
throw new Error("Unauthorized to run this agent");
}

Copilot uses AI. Check for mistakes.
const session_id = crypto.randomUUID();

const response = await callProxiedAgent(
agent.deployedUrl,
agent.default_agent_name || "",
(agent.framework_used as AgentFrameWorks) || AgentFrameWorks.google_adk,
message,
session_id,
user_id
);
Comment on lines +94 to +110
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No check for sufficient credit balance before running the agent. The service will decrement the user's credit balance without first verifying that they have sufficient credits. This could result in negative credit balances. Add a check to ensure the user has enough credits before calling the agent:

const user = await prisma.user.findUnique({
    where: { id: user_id },
});
if (!user) {
    throw new Error("User not found");
}

const estimatedCost = Number(agent.agentCost) + 
    (agent.inputTokenCost > 0 ? Number(agent.inputTokenCost) * 1000 : 0) + // Estimate max tokens
    (agent.outputTokenCost > 0 ? Number(agent.outputTokenCost) * 1000 : 0);

if (user.creditBalance < estimatedCost) {
    throw new Error("Insufficient credit balance");
}

Copilot uses AI. Check for mistakes.

await prisma.user.update({
where: { id: user_id },
data: {
creditBalance: {
decrement: Number(agent.agentCost) + (agent.inputTokenCost > 0 ? Number(agent.inputTokenCost) * (response.input_tokens || 0) : 0) + (agent.outputTokenCost > 0 ? Number(agent.outputTokenCost) * (response.output_tokens || 0) : 0),
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complex credit calculation embedded in update statement reduces maintainability. The inline credit calculation is difficult to read, debug, and test. Extract this calculation into a separate, testable function:

const calculateAgentCost = (
    agent: { agentCost: string; inputTokenCost: number; outputTokenCost: number },
    response: { input_tokens: number | null; output_tokens: number | null }
): number => {
    const baseCost = Number(agent.agentCost);
    const inputCost = agent.inputTokenCost > 0 
        ? Number(agent.inputTokenCost) * (response.input_tokens || 0) 
        : 0;
    const outputCost = agent.outputTokenCost > 0 
        ? Number(agent.outputTokenCost) * (response.output_tokens || 0) 
        : 0;
    return baseCost + inputCost + outputCost;
};

// Then use it in the update:
const totalCost = calculateAgentCost(agent, response);
await prisma.user.update({
    where: { id: user_id },
    data: {
        creditBalance: { decrement: totalCost },
        creditBalanceLastUpdated: new Date(),
    },
});

Copilot uses AI. Check for mistakes.
},
creditBalanceLastUpdated: new Date(),
},
});

const user = await prisma.user.findUnique({
where: { id: user_id },
});
if (!user) {
throw new Error("User not found");
}

return {
response: response.response_content,
creditBalance: user.creditBalance,
};
Comment on lines +112 to +132
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk of race condition in credit balance update. The credit balance is read after the agent call and update operation, which could lead to stale data if multiple concurrent requests are made. The balance could be decremented multiple times before the user query returns the updated value. Consider using an atomic transaction or returning the updated user data from the update operation:

const updatedUser = await prisma.user.update({
    where: { id: user_id },
    data: {
        creditBalance: {
            decrement: /* credit calculation */,
        },
        creditBalanceLastUpdated: new Date(),
    },
});

return {
    response: response.response_content,
    creditBalance: updatedUser.creditBalance,
};

Copilot uses AI. Check for mistakes.
}

public static readonly verifyAgent = async (
uri: string,
agent_name: string
Expand Down Expand Up @@ -268,7 +309,7 @@ export class AgentService {
) => {
// First check if the agent exists and user has permission
const existingAgent = await prisma.agent.findUnique({
where: { id: agent_id },
where: { id: agent_id, userId: user_id },
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid Prisma where clause. Prisma's where clause on findUnique requires a unique constraint, but the agent model only has id as a unique field, not a compound (id, userId) constraint. This will cause a runtime error. The authorization check on line 319 is already sufficient, so the where clause should only use id:

const existingAgent = await prisma.agent.findUnique({
    where: { id: agent_id },
});

Copilot uses AI. Check for mistakes.
});

if (!existingAgent) {
Expand All @@ -280,7 +321,7 @@ export class AgentService {
}

const agent = await prisma.agent.delete({
where: { id: agent_id },
where: { id: agent_id, userId: user_id },
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid Prisma where clause. Same issue as in findUnique above - Prisma's delete requires a unique constraint, but (id, userId) is not a compound unique key. Use only id:

const agent = await prisma.agent.delete({
    where: { id: agent_id },
});

Copilot uses AI. Check for mistakes.
});

return agent;
Expand Down