Compare commits

4 Commits

Author SHA1 Message Date
ealmeida 02b450a5bc Merge PR #1: 18 tools de diagnóstico DB + write tools (sessão 5) 2026-04-07 05:03:08 +01:00
ealmeida d61edd5f2d refactor(diag): rename diag_agents_missing_permissions → diag_agents_without_skip_permissions
Nome anterior era enganador (sugeria RBAC, mas verifica flag Claude CLI dangerouslySkipPermissions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 04:39:56 +01:00
ealmeida 469628cd0a feat(diag): write tools ensure_agent_membership + grant_agent_permission
- ensure_agent_membership: idempotente, fix achado #2 (Reality Checker membership)
- grant_agent_permission: baseline RBAC em principal_permission_grants
- diag_agents_missing_permissions: descrição clarificada (é flag Claude CLI, não RBAC)
- create_routine_trigger: schema cronExpression/timezone alinhado com API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 04:38:11 +01:00
ealmeida 53ab2b99a6 fix(routines): create_routine_trigger schema alinhado com API
API exige cronExpression/timezone (não schedule.cron). Schema do MCP
agora reflecte os campos reais e tem kind como discriminator obrigatório.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 04:31:31 +01:00
2 changed files with 91 additions and 6 deletions
+83 -2
View File
@@ -54,9 +54,9 @@ export const diagnosticsTools: PaperclipTool[] = [
// 3 // 3
{ {
name: 'diag_agents_missing_permissions', name: 'diag_agents_without_skip_permissions',
description: description:
'Agentes sem dangerouslySkipPermissions=true em adapter_config (check 11 health).', 'Agentes sem dangerouslySkipPermissions=true em adapter_config (NOTA: NÃO é RBAC — é flag Claude Code/CLI para correr sem prompts. Aplicar conscientemente por agent, não em massa).',
inputSchema: { type: 'object', properties: {} }, inputSchema: { type: 'object', properties: {} },
handler: async () => { handler: async () => {
const rows = await query( const rows = await query(
@@ -441,4 +441,85 @@ export const diagnosticsTools: PaperclipTool[] = [
return ok({ updated: rows.length, rows }); return ok({ updated: rows.length, rows });
}, },
}, },
// 17 — WRITE
{
name: 'ensure_agent_membership',
description:
'WRITE: garante membership active em company_memberships para um agent (idempotente). Fix achado #2 sessão 5.',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'UUID do agente' },
membership_role: { type: 'string', description: 'Default "member"' },
},
required: ['agent_id'],
},
handler: async (args) => {
const agentId = String(args.agent_id ?? '');
const role = String(args.membership_role ?? 'member');
if (!agentId) throw new Error('agent_id obrigatório');
const existing = await query(
`SELECT id, status, membership_role FROM company_memberships
WHERE company_id = $1 AND principal_type = 'agent' AND principal_id = $2`,
[COMPANY_ID, agentId]
);
if (existing.length > 0) {
const rows = await query(
`UPDATE company_memberships
SET status = 'active', membership_role = $1, updated_at = NOW()
WHERE id = $2
RETURNING id, status, membership_role`,
[role, existing[0].id]
);
return ok({ action: 'updated', row: rows[0] });
}
const rows = await query(
`INSERT INTO company_memberships
(company_id, principal_type, principal_id, status, membership_role)
VALUES ($1, 'agent', $2, 'active', $3)
RETURNING id, status, membership_role`,
[COMPANY_ID, agentId, role]
);
return ok({ action: 'inserted', row: rows[0] });
},
},
// 18 — WRITE
{
name: 'grant_agent_permission',
description:
'WRITE: insere row em principal_permission_grants para um agent (idempotente por permission_key). Baseline RBAC. Fix achado #4.',
inputSchema: {
type: 'object',
properties: {
agent_id: { type: 'string', description: 'UUID do agente' },
permission_key: { type: 'string', description: 'Ex: tasks:assign, agents:create, runs:read' },
scope: { type: 'string', description: 'Scope opcional (default null)' },
},
required: ['agent_id', 'permission_key'],
},
handler: async (args) => {
const agentId = String(args.agent_id ?? '');
const permissionKey = String(args.permission_key ?? '');
const scope = args.scope ? String(args.scope) : null;
if (!agentId || !permissionKey) throw new Error('agent_id e permission_key obrigatórios');
const existing = await query(
`SELECT id FROM principal_permission_grants
WHERE company_id = $1 AND principal_type = 'agent' AND principal_id = $2 AND permission_key = $3`,
[COMPANY_ID, agentId, permissionKey]
);
if (existing.length > 0) {
return ok({ action: 'noop', existing: existing[0] });
}
const rows = await query(
`INSERT INTO principal_permission_grants
(company_id, principal_type, principal_id, permission_key, scope, granted_by_user_id)
VALUES ($1, 'agent', $2, $3, $4, 'mcp-bootstrap')
RETURNING id, permission_key, scope`,
[COMPANY_ID, agentId, permissionKey, scope]
);
return ok({ action: 'inserted', row: rows[0] });
},
},
]; ];
+8 -4
View File
@@ -101,15 +101,19 @@ export const routineTools: PaperclipTool[] = [
}, },
{ {
name: 'create_routine_trigger', name: 'create_routine_trigger',
description: 'Criar um gatilho para uma rotina', description: 'Criar um gatilho para uma rotina. Para schedule passar kind="schedule" + schedule={cron,tz} + enabled + label.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
routine_id: { type: 'string', description: 'ID da rotina' }, routine_id: { type: 'string', description: 'ID da rotina' },
type: { type: 'string', description: 'Tipo de gatilho' }, kind: { type: 'string', description: 'schedule|webhook|api', enum: ['schedule', 'webhook', 'api'] },
config: { type: 'object', description: 'Configuracao do gatilho' }, cronExpression: { type: 'string', description: 'Para kind=schedule: cron 5-field (ex: "0 9 * * 1")' },
timezone: { type: 'string', description: 'Para kind=schedule: ex Europe/Lisbon' },
webhookSecret: { type: 'string', description: 'Para kind=webhook' },
enabled: { type: 'boolean', description: 'Estado activo (default true)' },
label: { type: 'string', description: 'Label legivel' },
}, },
required: ['routine_id'], required: ['routine_id', 'kind'],
}, },
handler: async (args) => { handler: async (args) => {
const { routine_id, ...body } = args as { routine_id: string; [k: string]: unknown }; const { routine_id, ...body } = args as { routine_id: string; [k: string]: unknown };