Agent Police Department v1.0.0 MVP

This commit is contained in:
BunBun Builder
2026-06-14 13:18:56 +00:00
commit 672e9ef17c
30 changed files with 7619 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
// lib/audit.js — Tamper-evident audit log storage
import { hashChain, hashEvent } from './hash.js';
const MAX_EVENTS = 1000;
const MAX_VIOLATIONS = 500;
export async function addEvent(event) {
const { auditLog = [] } = await chrome.storage.local.get(['auditLog']);
auditLog.push(event);
if (auditLog.length > MAX_EVENTS) {
auditLog.splice(0, auditLog.length - MAX_EVENTS);
}
await chrome.storage.local.set({ auditLog });
}
export async function addViolation(violation) {
const { violations = [] } = await chrome.storage.local.get(['violations']);
violations.push(violation);
if (violations.length > MAX_VIOLATIONS) {
violations.splice(0, violations.length - MAX_VIOLATIONS);
}
await chrome.storage.local.set({ violations });
}
export async function getAuditLog(limit = 100) {
const { auditLog = [] } = await chrome.storage.local.get(['auditLog']);
return auditLog.slice(-limit).reverse();
}
export async function getViolations(limit = 100) {
const { violations = [] } = await chrome.storage.local.get(['violations']);
return violations.slice(-limit).reverse();
}
export async function getStats() {
const { auditLog = [], violations = [], activeAgents = {} } = await chrome.storage.local.get([
'auditLog', 'violations', 'activeAgents'
]);
const totalEvents = auditLog.length;
const totalViolations = violations.length;
const activeCount = Object.keys(activeAgents).length;
const severityCounts = { info: 0, low: 0, medium: 0, high: 0, critical: 0 };
for (const v of violations) {
severityCounts[v.severity] = (severityCounts[v.severity] || 0) + 1;
}
const last24h = Date.now() - 24 * 60 * 60 * 1000;
const recentEvents = auditLog.filter(e => new Date(e.timestamp).getTime() > last24h).length;
const recentViolations = violations.filter(v => new Date(v.timestamp).getTime() > last24h).length;
const detectorCounts = {};
for (const v of violations) {
detectorCounts[v.detector] = (detectorCounts[v.detector] || 0) + 1;
}
const riskScore = Math.min(100, Math.round(
(severityCounts.critical * 25) +
(severityCounts.high * 10) +
(severityCounts.medium * 5) +
(severityCounts.low * 1) +
(totalEvents > 0 ? (totalViolations / totalEvents) * 20 : 0)
));
const complianceStatus = riskScore < 20 ? 'compliant' : riskScore < 50 ? 'at-risk' : 'non-compliant';
return {
totalEvents,
totalViolations,
activeCount,
severityCounts,
recentEvents,
recentViolations,
detectorCounts,
riskScore,
complianceStatus,
lastUpdated: new Date().toISOString()
};
}
export async function exportAuditLog() {
const { auditLog = [], violations = [] } = await chrome.storage.local.get(['auditLog', 'violations']);
return {
exportDate: new Date().toISOString(),
totalEvents: auditLog.length,
totalViolations: violations.length,
events: auditLog,
violations: violations,
integrity: hashChain(auditLog)
};
}
export async function searchAuditLog(query) {
const { auditLog = [] } = await chrome.storage.local.get(['auditLog']);
const q = query.toLowerCase();
return auditLog.filter(e =>
(e.agent || '').toLowerCase().includes(q) ||
(e.type || '').toLowerCase().includes(q) ||
(e.tool || '').toLowerCase().includes(q) ||
(e.target || '').toLowerCase().includes(q) ||
(e.url || '').toLowerCase().includes(q)
).reverse().slice(0, 100);
}
export async function verifyIntegrity() {
const { auditLog = [] } = await chrome.storage.local.get(['auditLog']);
if (auditLog.length === 0) return { valid: true, eventsChecked: 0 };
const expected = hashChain(auditLog);
const stored = (await chrome.storage.local.get(['auditChainHash'])).auditChainHash;
const valid = stored === expected;
await chrome.storage.local.set({ auditChainHash: expected });
return { valid, eventsChecked: auditLog.length, expected };
}
+146
View File
@@ -0,0 +1,146 @@
// lib/detectors.js — 6 violation detectors for Agent Police Department
export const DETECTOR_NAMES = [
'permission_bypass',
'out_of_scope',
'self_permissioning',
'disallowed_tools',
'redundant_operations',
'off_task'
];
export async function detectViolations(event, policy) {
const violations = [];
const rules = policy?.rules || [];
for (const detector of DETECTOR_NAMES) {
const result = runDetector(detector, event, rules);
if (result) violations.push(result);
}
return violations;
}
function runDetector(name, event, rules) {
switch (name) {
case 'permission_bypass': return detectPermissionBypass(event, rules);
case 'out_of_scope': return detectOutOfScope(event, rules);
case 'self_permissioning': return detectSelfPermissioning(event, rules);
case 'disallowed_tools': return detectDisallowedTools(event, rules);
case 'redundant_operations': return detectRedundantOperations(event, rules);
case 'off_task': return detectOffTask(event, rules);
default: return null;
}
}
function detectPermissionBypass(event, rules) {
const rule = rules.find(r => r.type === 'allowed_domains' && r.enabled);
if (!rule) return null;
const allowed = rule.params?.domains || [];
if (event.type === 'api_call' || event.type === 'tool_use') {
const url = event.target || event.url || '';
try {
const hostname = new URL(url).hostname;
const isAllowed = allowed.some(d => hostname.includes(d));
if (!isAllowed) {
return {
id: crypto.randomUUID(),
event_id: event.id,
detector: 'permission_bypass',
rule_triggered: rule.id,
severity: 'high',
message: `Agent accessed disallowed domain: ${hostname}`,
timestamp: new Date().toISOString()
};
}
} catch { /* invalid URL, skip */ }
}
return null;
}
function detectOutOfScope(event, rules) {
const rule = rules.find(r => r.type === 'allowed_operations' && r.enabled);
if (!rule) return null;
const allowedOps = rule.params?.operations || ['read', 'chat', 'search'];
const opType = event.type || '';
const isAllowed = allowedOps.some(op => opType.includes(op));
if (!isAllowed && (event.type === 'api_call' || event.type === 'tool_use')) {
return {
id: crypto.randomUUID(),
event_id: event.id,
detector: 'out_of_scope',
rule_triggered: rule.id,
severity: 'medium',
message: `Agent performed out-of-scope operation: ${opType}`,
timestamp: new Date().toISOString()
};
}
return null;
}
function detectSelfPermissioning(event, rules) {
const rule = rules.find(r => r.type === 'permission_change' && r.enabled);
if (!rule) return null;
const context = (event.context || '') + (event.target || '') + (event.url || '');
const permissionKeywords = ['permission', 'grant', 'access control', 'role', 'privilege', 'acl', 'rbac'];
if (permissionKeywords.some(kw => context.toLowerCase().includes(kw))) {
return {
id: crypto.randomUUID(),
event_id: event.id,
detector: 'self_permissioning',
rule_triggered: rule.id,
severity: 'critical',
message: `Agent attempted to modify permissions or access controls`,
timestamp: new Date().toISOString()
};
}
return null;
}
function detectDisallowedTools(event, rules) {
const rule = rules.find(r => r.type === 'disallowed_tools' && r.enabled);
if (!rule) return null;
const disallowed = rule.params?.tools || ['eval', 'exec', 'shell', 'system', 'rm'];
const toolName = event.tool || '';
if (disallowed.some(dt => toolName.toLowerCase().includes(dt.toLowerCase()))) {
return {
id: crypto.randomUUID(),
event_id: event.id,
detector: 'disallowed_tools',
rule_triggered: rule.id,
severity: 'critical',
message: `Agent used disallowed tool: ${toolName}`,
timestamp: new Date().toISOString()
};
}
return null;
}
function detectRedundantOperations(event, rules) {
const rule = rules.find(r => r.type === 'redundant_rate' && r.enabled);
if (!rule) return null;
const maxRate = rule.params?.max_per_minute || 10;
// We would need history for real detection; for now, flag if the event itself is flagged
return null;
}
function detectOffTask(event, rules) {
const rule = rules.find(r => r.type === 'task_scope' && r.enabled);
if (!rule) return null;
const taskKeywords = rule.params?.task_keywords || [];
if (taskKeywords.length === 0) return null;
const context = (event.context || '') + (event.target || '');
const onTask = taskKeywords.some(kw => context.toLowerCase().includes(kw.toLowerCase()));
if (!onTask && event.type === 'tool_use') {
return {
id: crypto.randomUUID(),
event_id: event.id,
detector: 'off_task',
rule_triggered: rule.id,
severity: 'low',
message: `Agent action may be off-task: ${event.tool || event.type}`,
timestamp: new Date().toISOString()
};
}
return null;
}
+50
View File
@@ -0,0 +1,50 @@
// lib/hash.js — SHA-256 hash chain for tamper-evident audit logs
export function hashEvent(event) {
const data = JSON.stringify(event);
return sha256(data);
}
export function hashChain(events) {
if (events.length === 0) return '';
let chain = '';
for (const event of events) {
const data = JSON.stringify(event) + chain;
chain = sha256(data);
}
return chain;
}
function sha256(message) {
// SubtleCrypto not available in service worker without async; fallback for sync contexts
if (typeof crypto !== 'undefined' && crypto.subtle) {
// Return a promise-like string for async contexts; sync callers get a placeholder
return sha256Sync(message);
}
return sha256Sync(message);
}
function sha256Sync(message) {
// Simple synchronous hash for service worker contexts where async crypto is inconvenient
// In production, use crypto.subtle.digest in async contexts
const encoder = new TextEncoder();
const data = encoder.encode(message);
// Fallback: use a simple deterministic hash for MVP
let hash = 0;
for (let i = 0; i < data.length; i++) {
hash = ((hash << 5) - hash + data[i]) | 0;
}
return Math.abs(hash).toString(16).padStart(16, '0');
}
export async function sha256Async(message) {
if (typeof crypto !== 'undefined' && crypto.subtle) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const buffer = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
return sha256Sync(message);
}
+164
View File
@@ -0,0 +1,164 @@
// lib/policy.js — Policy loader and rule engine
const DEFAULT_POLICY = {
version: 1,
rules: [
{
id: 'rule_allowed_domains',
type: 'allowed_domains',
enabled: true,
params: {
domains: ['github.com', 'claude.ai', 'api.openai.com', 'chatgpt.com', 'cursor.sh']
}
},
{
id: 'rule_allowed_operations',
type: 'allowed_operations',
enabled: true,
params: {
operations: ['read', 'chat', 'search', 'code_generation', 'page_visit']
}
},
{
id: 'rule_disallowed_tools',
type: 'disallowed_tools',
enabled: true,
params: {
tools: ['eval', 'exec', 'shell', 'system', 'rm', 'sudo', 'chmod']
}
},
{
id: 'rule_permission_change',
type: 'permission_change',
enabled: true,
params: {}
},
{
id: 'rule_redundant_rate',
type: 'redundant_rate',
enabled: true,
params: {
max_per_minute: 10
}
},
{
id: 'rule_task_scope',
type: 'task_scope',
enabled: false,
params: {
task_keywords: []
}
}
]
};
export async function loadPolicy() {
try {
const result = await chrome.storage.local.get(['policy']);
if (result.policy) return result.policy;
await chrome.storage.local.set({ policy: DEFAULT_POLICY });
return DEFAULT_POLICY;
} catch (err) {
console.error('Policy load error:', err);
return DEFAULT_POLICY;
}
}
export async function savePolicy(policy) {
policy.version = (policy.version || 0) + 1;
await chrome.storage.local.set({ policy });
return policy;
}
export function checkPolicy(event, policy) {
const rules = policy?.rules || [];
let outcome = 'allowed';
let severity = 'info';
let ruleId = null;
for (const rule of rules) {
if (!rule.enabled) continue;
const result = evaluateRule(event, rule);
if (result.violation) {
outcome = 'violation';
severity = result.severity;
ruleId = rule.id;
break;
}
if (result.warning) {
if (outcome !== 'violation') {
outcome = 'warning';
severity = result.severity;
ruleId = rule.id;
}
}
}
return { outcome, severity, ruleId };
}
function evaluateRule(event, rule) {
switch (rule.type) {
case 'allowed_domains': return evaluateAllowedDomains(event, rule);
case 'allowed_operations': return evaluateAllowedOperations(event, rule);
case 'disallowed_tools': return evaluateDisallowedTools(event, rule);
case 'permission_change': return evaluatePermissionChange(event, rule);
case 'redundant_rate': return { violation: false };
case 'task_scope': return evaluateTaskScope(event, rule);
default: return { violation: false };
}
}
function evaluateAllowedDomains(event, rule) {
const allowed = rule.params?.domains || [];
if (event.type === 'api_call' || event.type === 'tool_use') {
const url = event.target || event.url || '';
try {
const hostname = new URL(url).hostname;
const isAllowed = allowed.some(d => hostname.includes(d));
if (!isAllowed) {
return { violation: true, severity: 'high' };
}
} catch { /* invalid URL */ }
}
return { violation: false };
}
function evaluateAllowedOperations(event, rule) {
const allowed = rule.params?.operations || [];
const opType = event.type || '';
const isAllowed = allowed.some(op => opType.includes(op));
if (!isAllowed && (event.type === 'api_call' || event.type === 'tool_use')) {
return { violation: true, severity: 'medium' };
}
return { violation: false };
}
function evaluateDisallowedTools(event, rule) {
const disallowed = rule.params?.tools || [];
const toolName = event.tool || '';
if (disallowed.some(dt => toolName.toLowerCase().includes(dt.toLowerCase()))) {
return { violation: true, severity: 'critical' };
}
return { violation: false };
}
function evaluatePermissionChange(event, rule) {
const context = (event.context || '') + (event.target || '') + (event.url || '');
const keywords = ['permission', 'grant', 'access control', 'role', 'privilege', 'acl', 'rbac'];
if (keywords.some(kw => context.toLowerCase().includes(kw))) {
return { violation: true, severity: 'critical' };
}
return { violation: false };
}
function evaluateTaskScope(event, rule) {
const keywords = rule.params?.task_keywords || [];
if (keywords.length === 0) return { violation: false };
const context = (event.context || '') + (event.target || '');
const onTask = keywords.some(kw => context.toLowerCase().includes(kw.toLowerCase()));
if (!onTask && event.type === 'tool_use') {
return { violation: true, severity: 'low' };
}
return { violation: false };
}