feat(security): add /elevate command and audit events
This commit is contained in:
@@ -23,6 +23,7 @@ import type {
|
|||||||
HeartbeatRecoverEvent,
|
HeartbeatRecoverEvent,
|
||||||
GmailPollEvent,
|
GmailPollEvent,
|
||||||
GmailNewEmailEvent,
|
GmailNewEmailEvent,
|
||||||
|
SecurityElevationEvent,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { AuditRotator } from './rotation.js';
|
import { AuditRotator } from './rotation.js';
|
||||||
|
|
||||||
@@ -124,6 +125,21 @@ export class AuditLogger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityElevationEnabled(event: SecurityElevationEvent): void {
|
||||||
|
if (!this.shouldLog('tools', 'info')) {return;}
|
||||||
|
this.write({ level: 'info', event_type: 'security.elevation.enabled', event: event as unknown as Record<string, unknown> });
|
||||||
|
}
|
||||||
|
|
||||||
|
securityElevationDisabled(event: SecurityElevationEvent): void {
|
||||||
|
if (!this.shouldLog('tools', 'info')) {return;}
|
||||||
|
this.write({ level: 'info', event_type: 'security.elevation.disabled', event: event as unknown as Record<string, unknown> });
|
||||||
|
}
|
||||||
|
|
||||||
|
securityElevationExpired(event: SecurityElevationEvent): void {
|
||||||
|
if (!this.shouldLog('tools', 'info')) {return;}
|
||||||
|
this.write({ level: 'info', event_type: 'security.elevation.expired', event: event as unknown as Record<string, unknown> });
|
||||||
|
}
|
||||||
|
|
||||||
// ── Session Events ───────────────────────────────────────────
|
// ── Session Events ───────────────────────────────────────────
|
||||||
|
|
||||||
sessionCreate(event: SessionCreateEvent): void {
|
sessionCreate(event: SessionCreateEvent): void {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export type AuditLevel = 'debug' | 'info' | 'warn' | 'error';
|
|||||||
export type AuditEventType =
|
export type AuditEventType =
|
||||||
// Tool execution
|
// Tool execution
|
||||||
| 'tool.start' | 'tool.success' | 'tool.error' | 'tool.denied' | 'tool.approval'
|
| 'tool.start' | 'tool.success' | 'tool.error' | 'tool.denied' | 'tool.approval'
|
||||||
|
// Security
|
||||||
|
| 'security.elevation.enabled' | 'security.elevation.disabled' | 'security.elevation.expired'
|
||||||
// Skills scan
|
// Skills scan
|
||||||
| 'skills.scan.pass' | 'skills.scan.fail'
|
| 'skills.scan.pass' | 'skills.scan.fail'
|
||||||
// Skills installer
|
// Skills installer
|
||||||
@@ -134,6 +136,16 @@ export interface SkillsScanEvent {
|
|||||||
issue_codes: string[];
|
issue_codes: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SecurityElevationEvent {
|
||||||
|
session_id: string;
|
||||||
|
channel: string;
|
||||||
|
sender: string;
|
||||||
|
elevation_id: string;
|
||||||
|
until_ms?: number;
|
||||||
|
ttl_ms?: number;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SessionCreateEvent {
|
export interface SessionCreateEvent {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
frontend: string;
|
frontend: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { createModelCommand } from './index.js';
|
import { createElevateCommand, createModelCommand } from './index.js';
|
||||||
|
|
||||||
describe('builtin /model command', () => {
|
describe('builtin /model command', () => {
|
||||||
it('passes through the full argument string', async () => {
|
it('passes through the full argument string', async () => {
|
||||||
@@ -35,3 +35,35 @@ describe('builtin /model command', () => {
|
|||||||
expect(result).toEqual({ handled: true, text: 'switched' });
|
expect(result).toEqual({ handled: true, text: 'switched' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('builtin /elevate command', () => {
|
||||||
|
it('passes through the full argument string', async () => {
|
||||||
|
const cmd = createElevateCommand();
|
||||||
|
const setElevation = vi.fn(() => 'ok');
|
||||||
|
|
||||||
|
const result = await cmd.execute(['10m', 'reason', '--yes'], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/elevate 10m reason --yes',
|
||||||
|
services: { setElevation },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setElevation).toHaveBeenCalledWith('10m reason --yes');
|
||||||
|
expect(result).toEqual({ handled: true, text: 'ok' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows status when no args are provided', async () => {
|
||||||
|
const cmd = createElevateCommand();
|
||||||
|
const getElevation = vi.fn(() => 'status');
|
||||||
|
const result = await cmd.execute([], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/elevate',
|
||||||
|
services: { getElevation },
|
||||||
|
});
|
||||||
|
expect(getElevation).toHaveBeenCalledOnce();
|
||||||
|
expect(result).toEqual({ handled: true, text: 'status' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -128,6 +128,27 @@ export function createResetCommand(): CommandDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createElevateCommand(): CommandDefinition {
|
||||||
|
return {
|
||||||
|
name: 'elevate',
|
||||||
|
description: 'Enable or disable time-bounded elevated host mode',
|
||||||
|
execute: async (args, ctx) => {
|
||||||
|
if (args.length === 0) {
|
||||||
|
if (!ctx.services?.getElevation) {
|
||||||
|
return notAvailable('Elevate command');
|
||||||
|
}
|
||||||
|
return { handled: true, text: await ctx.services.getElevation() };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.services?.setElevation) {
|
||||||
|
return notAvailable('Elevate command');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { handled: true, text: await ctx.services.setElevation(args.join(' ')) };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
||||||
registry.register(createHelpCommand(registry));
|
registry.register(createHelpCommand(registry));
|
||||||
registry.register(createStatusCommand());
|
registry.register(createStatusCommand());
|
||||||
@@ -135,4 +156,5 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|||||||
registry.register(createModelCommand());
|
registry.register(createModelCommand());
|
||||||
registry.register(createCompactCommand());
|
registry.register(createCompactCommand());
|
||||||
registry.register(createResetCommand());
|
registry.register(createResetCommand());
|
||||||
|
registry.register(createElevateCommand());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,4 +25,7 @@ export interface CommandServices {
|
|||||||
setModel?: (tier: string) => Promise<string> | string;
|
setModel?: (tier: string) => Promise<string> | string;
|
||||||
compact?: () => Promise<string> | string;
|
compact?: () => Promise<string> | string;
|
||||||
reset?: () => Promise<string> | string;
|
reset?: () => Promise<string> | string;
|
||||||
|
|
||||||
|
getElevation?: () => Promise<string> | string;
|
||||||
|
setElevation?: (input: string) => Promise<string> | string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,13 @@ export interface ToolPolicyContext {
|
|||||||
|
|
||||||
/** True when untrusted content has been introduced in this run. */
|
/** True when untrusted content has been introduced in this run. */
|
||||||
untrustedContent?: boolean;
|
untrustedContent?: boolean;
|
||||||
|
|
||||||
|
/** Elevated mode (break-glass): allow host execution for high-risk tools until this epoch millis. */
|
||||||
|
elevatedHostUntilMs?: number;
|
||||||
|
/** User-supplied reason for elevation (audited separately). */
|
||||||
|
elevatedHostReason?: string;
|
||||||
|
/** Correlation id for elevation window. */
|
||||||
|
elevatedHostId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSkillAllowedNames(allToolNames: string[], permissions?: SkillPermissions): Set<string> | null {
|
function resolveSkillAllowedNames(allToolNames: string[], permissions?: SkillPermissions): Set<string> | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user