FEAT - CODEX API: replace workspace project grants
This commit is contained in:
parent
f52a8345a1
commit
948893bc47
|
|
@ -127,6 +127,14 @@ export type UpsertGrantInput = {
|
|||
createdByUserId: string;
|
||||
};
|
||||
|
||||
export type ReplaceWorkspaceGrantsInput = {
|
||||
workspaceSlug: string;
|
||||
projectIds: string[];
|
||||
scopes: AgentScope[];
|
||||
mode: AgentGrantMode;
|
||||
actorUserId: string;
|
||||
};
|
||||
|
||||
export type CreateTokenInput = {
|
||||
token: string;
|
||||
name: string;
|
||||
|
|
@ -274,6 +282,71 @@ export class AgentsRepository {
|
|||
return result.rows.map(mapGrant);
|
||||
}
|
||||
|
||||
async replaceWorkspaceProjectGrants(agentId: string, input: ReplaceWorkspaceGrantsInput): Promise<AgentGrantRecord[]> {
|
||||
assertAllowedScopes(input.scopes);
|
||||
|
||||
const projectIds = [...new Set(input.projectIds.map((projectId) => projectId.trim()).filter(Boolean))];
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
await client.query(
|
||||
`
|
||||
DELETE FROM agent_grants
|
||||
WHERE agent_id = $1
|
||||
AND workspace_slug = $2
|
||||
AND NOT (project_id = ANY($3::text[]))
|
||||
`,
|
||||
[agentId, input.workspaceSlug, projectIds]
|
||||
);
|
||||
|
||||
const grants: AgentGrantRecord[] = [];
|
||||
for (const projectId of projectIds) {
|
||||
const result = await client.query<GrantRow>(
|
||||
`
|
||||
INSERT INTO agent_grants(agent_id, workspace_slug, project_id, scopes, mode, created_by_user_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (agent_id, workspace_slug, project_id)
|
||||
DO UPDATE SET
|
||||
scopes = EXCLUDED.scopes,
|
||||
mode = EXCLUDED.mode,
|
||||
created_by_user_id = EXCLUDED.created_by_user_id,
|
||||
updated_at = now()
|
||||
RETURNING *
|
||||
`,
|
||||
[agentId, input.workspaceSlug, projectId, input.scopes, input.mode, input.actorUserId]
|
||||
);
|
||||
grants.push(mapGrant(result.rows[0]));
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO agent_audit_events(agent_id, event_type, actor_user_id, metadata)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`,
|
||||
[
|
||||
agentId,
|
||||
"agent.grants.replaced",
|
||||
input.actorUserId,
|
||||
{
|
||||
workspaceSlug: input.workspaceSlug,
|
||||
projectIds,
|
||||
scopes: input.scopes,
|
||||
mode: input.mode,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return grants;
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async createToken(agentId: string, input: CreateTokenInput): Promise<AgentTokenRecord> {
|
||||
const result = await this.pool.query<TokenRow>(
|
||||
`
|
||||
|
|
|
|||
|
|
@ -87,6 +87,14 @@ const upsertGrantBodySchema = z.object({
|
|||
created_by_user_id: z.string().min(1),
|
||||
});
|
||||
|
||||
const replaceWorkspaceGrantsBodySchema = z.object({
|
||||
workspace_slug: z.string().min(1),
|
||||
project_ids: z.array(z.string().min(1)).min(1),
|
||||
scopes: z.array(z.enum(allowedAgentScopes)).min(1),
|
||||
mode: z.enum(["voluntary", "reporting"]).default("voluntary"),
|
||||
created_by_user_id: z.string().min(1),
|
||||
});
|
||||
|
||||
const createTokenBodySchema = z.object({
|
||||
name: z.string().min(1).max(120).default("Local Codex token"),
|
||||
expires_at: z.string().datetime().nullish(),
|
||||
|
|
@ -230,6 +238,31 @@ export async function registerAgentRoutes(app: FastifyInstance, deps: AgentRoute
|
|||
};
|
||||
});
|
||||
|
||||
app.post("/api/v1/agents/:agentId/grants/replace", async (request, reply) => {
|
||||
requireLifecycleAccess(request, deps);
|
||||
const repository = requireRepository(deps);
|
||||
const { agentId } = agentParamsSchema.parse(request.params);
|
||||
const body = replaceWorkspaceGrantsBodySchema.parse(request.body);
|
||||
const agent = await repository.getAgent(agentId);
|
||||
|
||||
if (!agent) {
|
||||
return sendNotFound(reply, "agent_not_found");
|
||||
}
|
||||
|
||||
const grants = await repository.replaceWorkspaceProjectGrants(agentId, {
|
||||
workspaceSlug: body.workspace_slug,
|
||||
projectIds: body.project_ids,
|
||||
scopes: body.scopes,
|
||||
mode: body.mode,
|
||||
actorUserId: body.created_by_user_id,
|
||||
});
|
||||
|
||||
return reply.status(200).send({
|
||||
ok: true,
|
||||
grants: grants.map(serializeGrant),
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/v1/agents/:agentId/tokens", async (request, reply) => {
|
||||
requireLifecycleAccess(request, deps);
|
||||
const repository = requireRepository(deps);
|
||||
|
|
@ -430,6 +463,31 @@ export async function registerAgentRoutes(app: FastifyInstance, deps: AgentRoute
|
|||
});
|
||||
});
|
||||
|
||||
app.post("/api/internal/v1/owners/:ownerUserId/agents/:agentId/grants/replace", async (request, reply) => {
|
||||
requireLifecycleAccess(request, deps);
|
||||
const repository = requireRepository(deps);
|
||||
const { ownerUserId, agentId } = ownerAgentParamsSchema.parse(request.params);
|
||||
const body = replaceWorkspaceGrantsBodySchema.omit({ created_by_user_id: true }).parse(request.body);
|
||||
const agent = await repository.getAgent(agentId);
|
||||
|
||||
if (!agent || agent.ownerUserId !== ownerUserId) {
|
||||
return sendNotFound(reply, "agent_not_found");
|
||||
}
|
||||
|
||||
const grants = await repository.replaceWorkspaceProjectGrants(agentId, {
|
||||
workspaceSlug: body.workspace_slug,
|
||||
projectIds: body.project_ids,
|
||||
scopes: body.scopes,
|
||||
mode: body.mode,
|
||||
actorUserId: ownerUserId,
|
||||
});
|
||||
|
||||
return reply.status(200).send({
|
||||
ok: true,
|
||||
grants: grants.map(serializeGrant),
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/internal/v1/owners/:ownerUserId/agents/:agentId/tokens", async (request, reply) => {
|
||||
requireLifecycleAccess(request, deps);
|
||||
const repository = requireRepository(deps);
|
||||
|
|
|
|||
Loading…
Reference in New Issue