feat: add AI tool capability permissions and secret masking to CASL layer
Extends AbilityFactory with a risk-stratified capability model for Hermes MCP tools (economic-data/capability-reasoning/legislation-search), ported from Open.Jarvis's plugin permission system: non-owner/admin roles only get low/medium risk tools by default, high/critical stay reserved. Adds maskSensitiveValue as a reusable secret/PII redaction utility for future outbox/audit logging, reinforcing the existing ai_requests hash-only storage principle.
This commit is contained in:
parent
5a1206118a
commit
261bd0dba1
2 changed files with 110 additions and 5 deletions
|
|
@ -1,9 +1,33 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { AbilityBuilder, PureAbility, type AbilityClass } from '@casl/ability';
|
import { AbilityBuilder, createMongoAbility, subject, type ForcedSubject, type MongoAbility } from '@casl/ability';
|
||||||
|
|
||||||
export type Action = 'manage' | 'create' | 'read' | 'update' | 'delete';
|
/**
|
||||||
export type Subject = 'Organization' | 'Transaction' | 'Document' | 'Membership' | 'all';
|
* Nivel de risc pentru unelte AI/agenti (Hermes MCP), portat din modelul de
|
||||||
export type AppAbility = PureAbility<[Action, Subject]>;
|
* capabilitati risk-stratified al Open.Jarvis (github.com/dmrr35/Open.Jarvis,
|
||||||
|
* open_jarvis/plugins/permissions.py) -- roluri non-admin primesc implicit
|
||||||
|
* doar unelte low/medium, high/critical raman rezervate owner/admin.
|
||||||
|
*/
|
||||||
|
export type AgentToolRiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
|
||||||
|
export interface AgentToolDefinition {
|
||||||
|
name: string;
|
||||||
|
riskLevel: AgentToolRiskLevel;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Forma exacta produsa de `subject('AgentTool', toolDefinition)` -- vezi canInvokeAgentTool. */
|
||||||
|
type AgentToolInstance = AgentToolDefinition & ForcedSubject<'AgentTool'>;
|
||||||
|
|
||||||
|
export type Action = 'manage' | 'create' | 'read' | 'update' | 'delete' | 'invoke';
|
||||||
|
export type Subject =
|
||||||
|
| 'Organization'
|
||||||
|
| 'Transaction'
|
||||||
|
| 'Document'
|
||||||
|
| 'Membership'
|
||||||
|
| 'AgentTool'
|
||||||
|
| AgentToolInstance
|
||||||
|
| 'all';
|
||||||
|
export type AppAbility = MongoAbility<[Action, Subject]>;
|
||||||
export type MembershipRole = 'owner' | 'admin' | 'member';
|
export type MembershipRole = 'owner' | 'admin' | 'member';
|
||||||
|
|
||||||
export interface MembershipContext {
|
export interface MembershipContext {
|
||||||
|
|
@ -11,6 +35,31 @@ export interface MembershipContext {
|
||||||
role: MembershipRole;
|
role: MembershipRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registrul uneltelor AI expuse prin Hermes (namespace MetaMCP 'ceo-os-endpoint').
|
||||||
|
* `name` trebuie sa corespunda exact numelui serverului MCP raportat de
|
||||||
|
* `hermes mcp test` -- vezi project_supersrv_new_server.md pentru lista live.
|
||||||
|
*/
|
||||||
|
export const AGENT_TOOL_REGISTRY: readonly AgentToolDefinition[] = [
|
||||||
|
{
|
||||||
|
name: 'economic-data',
|
||||||
|
riskLevel: 'low',
|
||||||
|
description: 'World Bank/ECB/Eurostat/FRED/OECD, stiri, IP/domeniu lookups',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'capability-reasoning',
|
||||||
|
riskLevel: 'low',
|
||||||
|
description: 'Potrivire ocupatii/skill-uri ESCO pentru "ce poate/nu poate face agentul"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'legislation-search',
|
||||||
|
riskLevel: 'medium',
|
||||||
|
description: 'Cautare semantica in legislatia UK/DE/RO -- informativ, nu consultanta juridica',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_ROLE_INVOKABLE_RISK_LEVELS: readonly AgentToolRiskLevel[] = ['low', 'medium'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traduce rolul de membership (blueprint 8.3: memberships.role) intr-un set de
|
* Traduce rolul de membership (blueprint 8.3: memberships.role) intr-un set de
|
||||||
* permisiuni CASL. Regulile sunt intentionat minimale -- se extind per bounded
|
* permisiuni CASL. Regulile sunt intentionat minimale -- se extind per bounded
|
||||||
|
|
@ -19,15 +68,30 @@ export interface MembershipContext {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AbilityFactory {
|
export class AbilityFactory {
|
||||||
createForMembership(membership: MembershipContext): AppAbility {
|
createForMembership(membership: MembershipContext): AppAbility {
|
||||||
const { can, build } = new AbilityBuilder<AppAbility>(PureAbility as AbilityClass<AppAbility>);
|
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
||||||
|
|
||||||
if (membership.role === 'owner' || membership.role === 'admin') {
|
if (membership.role === 'owner' || membership.role === 'admin') {
|
||||||
can('manage', 'all');
|
can('manage', 'all');
|
||||||
} else {
|
} else {
|
||||||
can('read', 'all');
|
can('read', 'all');
|
||||||
can(['create', 'update'], ['Transaction', 'Document']);
|
can(['create', 'update'], ['Transaction', 'Document']);
|
||||||
|
can('invoke', 'AgentTool', { riskLevel: { $in: [...DEFAULT_ROLE_INVOKABLE_RISK_LEVELS] } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return build();
|
return build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica daca o abilitate poate invoca o unealta AI dupa nume. Nume necunoscute
|
||||||
|
* (absente din AGENT_TOOL_REGISTRY) sunt respinse implicit -- acelasi deny-by-default
|
||||||
|
* ca `require_plugin_permission()` din Open.Jarvis.
|
||||||
|
*/
|
||||||
|
export function canInvokeAgentTool(ability: AppAbility, toolName: string): boolean {
|
||||||
|
const toolDefinition = AGENT_TOOL_REGISTRY.find((tool) => tool.name === toolName);
|
||||||
|
if (!toolDefinition) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ability.can('invoke', subject('AgentTool', toolDefinition));
|
||||||
|
}
|
||||||
|
|
|
||||||
41
src/security/mask-sensitive.ts
Normal file
41
src/security/mask-sensitive.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
const MASK = '***';
|
||||||
|
|
||||||
|
const SENSITIVE_KEY_PATTERN = /(key|token|secret|password)/i;
|
||||||
|
const ASSIGNMENT_PATTERN = /([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*)=(\S+)/gi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Masking de secrete/PII portat din Open.Jarvis (github.com/dmrr35/Open.Jarvis,
|
||||||
|
* open_jarvis/memory/privacy_mode.py) -- aplicat inainte de orice scriere in
|
||||||
|
* outbox_events.payload / audit_log, ca strat suplimentar fata de principiul
|
||||||
|
* deja aplicat in ai_requests (se stocheaza doar context_manifest_hash, niciodata
|
||||||
|
* continutul brut).
|
||||||
|
*/
|
||||||
|
export function maskSensitiveValue(value: unknown, key?: string): unknown {
|
||||||
|
if (key !== undefined && SENSITIVE_KEY_PATTERN.test(key)) {
|
||||||
|
return MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.replace(ASSIGNMENT_PATTERN, (_match, name: string) => `${name}=${MASK}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => maskSensitiveValue(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== null && typeof value === 'object') {
|
||||||
|
return maskSensitiveObject(value as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskSensitiveObject(input: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const masked: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(input)) {
|
||||||
|
masked[key] = maskSensitiveValue(value, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return masked;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue