feat(security): wire /elevate to session config
This commit is contained in:
+128
-10
@@ -19,6 +19,8 @@ import type { ComponentRegistry } from '../intents/index.js';
|
|||||||
import type { RoutingPolicy } from '../routing/index.js';
|
import type { RoutingPolicy } from '../routing/index.js';
|
||||||
import { createClientFromConfig } from './models.js';
|
import { createClientFromConfig } from './models.js';
|
||||||
import type { SkillRegistry } from '../skills/index.js';
|
import type { SkillRegistry } from '../skills/index.js';
|
||||||
|
import { auditLogger } from '../audit/index.js';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
function buildProviderConfigMap(config: Config): Partial<Record<ModelProvider, ModelConfig>> {
|
function buildProviderConfigMap(config: Config): Partial<Record<ModelProvider, ModelConfig>> {
|
||||||
const providerConfigs: Partial<Record<ModelProvider, ModelConfig>> = {};
|
const providerConfigs: Partial<Record<ModelProvider, ModelConfig>> = {};
|
||||||
@@ -197,7 +199,7 @@ export function createMessageRouter(deps: {
|
|||||||
effectiveToolRegistry = effectiveToolRegistry.clone();
|
effectiveToolRegistry = effectiveToolRegistry.clone();
|
||||||
effectiveToolRegistry.register(createMediaSendTool(collector));
|
effectiveToolRegistry.register(createMediaSendTool(collector));
|
||||||
|
|
||||||
const orchestrator = new AgentOrchestrator({
|
const orchestrator = new AgentOrchestrator({
|
||||||
modelRouter: deps.modelRouter,
|
modelRouter: deps.modelRouter,
|
||||||
systemPrompt: effectiveSystemPrompt,
|
systemPrompt: effectiveSystemPrompt,
|
||||||
session,
|
session,
|
||||||
@@ -218,15 +220,19 @@ export function createMessageRouter(deps: {
|
|||||||
memoryStore: deps.memoryStore,
|
memoryStore: deps.memoryStore,
|
||||||
memoryInjectionStrategy: deps.config.memory?.injection_strategy,
|
memoryInjectionStrategy: deps.config.memory?.injection_strategy,
|
||||||
memoryMaxInjectionTokens: deps.config.memory?.max_injection_tokens,
|
memoryMaxInjectionTokens: deps.config.memory?.max_injection_tokens,
|
||||||
toolPolicyContext: {
|
toolPolicyContext: {
|
||||||
agent: effectiveTier,
|
agent: effectiveTier,
|
||||||
provider: effectiveProvider,
|
provider: effectiveProvider,
|
||||||
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
|
sessionId: session.id,
|
||||||
skillName: activeSkillName,
|
channel,
|
||||||
skillPermissions: activeSkill?.manifest.permissions,
|
sender: senderId,
|
||||||
allowedSecretScopes: activeSkill?.manifest.permissions?.secrets,
|
tier: effectiveTier,
|
||||||
executionEnvironment,
|
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
|
||||||
},
|
skillName: activeSkillName,
|
||||||
|
skillPermissions: activeSkill?.manifest.permissions,
|
||||||
|
allowedSecretScopes: activeSkill?.manifest.permissions?.secrets,
|
||||||
|
executionEnvironment,
|
||||||
|
},
|
||||||
attachmentCollector: collector,
|
attachmentCollector: collector,
|
||||||
});
|
});
|
||||||
entry = { orchestrator, collector };
|
entry = { orchestrator, collector };
|
||||||
@@ -443,6 +449,118 @@ export function createMessageRouter(deps: {
|
|||||||
session.deleteConfig('modelTier');
|
session.deleteConfig('modelTier');
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getElevation: () => {
|
||||||
|
const untilRaw = session.getConfig('elevation.until_ms');
|
||||||
|
const reason = session.getConfig('elevation.reason') ?? '';
|
||||||
|
const id = session.getConfig('elevation.id') ?? '';
|
||||||
|
if (!untilRaw || !id) {
|
||||||
|
return 'Elevated mode: off';
|
||||||
|
}
|
||||||
|
const untilMs = Number.parseInt(untilRaw, 10);
|
||||||
|
if (!Number.isFinite(untilMs)) {
|
||||||
|
return 'Elevated mode: off';
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
if (untilMs <= now) {
|
||||||
|
session.deleteConfig('elevation.until_ms');
|
||||||
|
session.deleteConfig('elevation.reason');
|
||||||
|
session.deleteConfig('elevation.id');
|
||||||
|
auditLogger?.securityElevationExpired({
|
||||||
|
session_id: session.id,
|
||||||
|
channel: msg.channel,
|
||||||
|
sender: msg.senderId,
|
||||||
|
elevation_id: id,
|
||||||
|
until_ms: untilMs,
|
||||||
|
reason: reason || undefined,
|
||||||
|
});
|
||||||
|
return 'Elevated mode: off (expired)';
|
||||||
|
}
|
||||||
|
const remainingMs = untilMs - now;
|
||||||
|
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||||
|
return `Elevated mode: on (${remainingSec}s remaining)${reason ? ` — ${reason}` : ''}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
setElevation: (input: string) => {
|
||||||
|
const raw = input.trim();
|
||||||
|
const parts = raw.split(/\s+/);
|
||||||
|
const hasYes = parts.includes('--yes') || parts.includes('--confirm');
|
||||||
|
const filtered = parts.filter(p => p !== '--yes' && p !== '--confirm');
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return 'Usage: /elevate <duration> <reason...> --yes | /elevate off --yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtered[0] === 'off') {
|
||||||
|
if (!hasYes) {
|
||||||
|
return 'Refusing to disable elevation without explicit confirmation. Use: /elevate off --yes';
|
||||||
|
}
|
||||||
|
const existingId = session.getConfig('elevation.id') ?? randomUUID();
|
||||||
|
const existingUntil = session.getConfig('elevation.until_ms');
|
||||||
|
const existingReason = session.getConfig('elevation.reason') ?? '';
|
||||||
|
session.deleteConfig('elevation.until_ms');
|
||||||
|
session.deleteConfig('elevation.reason');
|
||||||
|
session.deleteConfig('elevation.id');
|
||||||
|
auditLogger?.securityElevationDisabled({
|
||||||
|
session_id: session.id,
|
||||||
|
channel: msg.channel,
|
||||||
|
sender: msg.senderId,
|
||||||
|
elevation_id: existingId,
|
||||||
|
until_ms: existingUntil ? Number.parseInt(existingUntil, 10) : undefined,
|
||||||
|
reason: existingReason || undefined,
|
||||||
|
});
|
||||||
|
return 'Elevated mode: off';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasYes) {
|
||||||
|
return 'Refusing to enable elevation without explicit confirmation. Use: /elevate <duration> <reason...> --yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dur = filtered[0];
|
||||||
|
const reason = filtered.slice(1).join(' ').trim();
|
||||||
|
const ttlMs = (() => {
|
||||||
|
const m = dur.match(/^(\d+)([smhd])$/i);
|
||||||
|
if (!m) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = Number.parseInt(m[1], 10);
|
||||||
|
if (!Number.isFinite(n) || n <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const unit = m[2].toLowerCase();
|
||||||
|
if (unit === 's') {return n * 1000;}
|
||||||
|
if (unit === 'm') {return n * 60_000;}
|
||||||
|
if (unit === 'h') {return n * 3_600_000;}
|
||||||
|
if (unit === 'd') {return n * 86_400_000;}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!ttlMs) {
|
||||||
|
return 'Invalid duration. Use one of: 30s, 10m, 1h, 1d';
|
||||||
|
}
|
||||||
|
|
||||||
|
const untilMs = Date.now() + ttlMs;
|
||||||
|
const id = randomUUID();
|
||||||
|
session.setConfig('elevation.until_ms', String(untilMs));
|
||||||
|
session.setConfig('elevation.id', id);
|
||||||
|
if (reason) {
|
||||||
|
session.setConfig('elevation.reason', reason);
|
||||||
|
} else {
|
||||||
|
session.deleteConfig('elevation.reason');
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLogger?.securityElevationEnabled({
|
||||||
|
session_id: session.id,
|
||||||
|
channel: msg.channel,
|
||||||
|
sender: msg.senderId,
|
||||||
|
elevation_id: id,
|
||||||
|
until_ms: untilMs,
|
||||||
|
ttl_ms: ttlMs,
|
||||||
|
reason: reason || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `Elevated mode: on until ${new Date(untilMs).toISOString()}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type { Attachment } from '../../channels/types.js';
|
|||||||
import type { SessionManager } from '../../session/manager.js';
|
import type { SessionManager } from '../../session/manager.js';
|
||||||
import type { ModelTier } from '../../models/router.js';
|
import type { ModelTier } from '../../models/router.js';
|
||||||
import type { CommandRegistry } from '../../commands/index.js';
|
import type { CommandRegistry } from '../../commands/index.js';
|
||||||
|
import { auditLogger } from '../../audit/index.js';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
export interface AgentHandlerDeps {
|
export interface AgentHandlerDeps {
|
||||||
sessionBridge: SessionBridge;
|
sessionBridge: SessionBridge;
|
||||||
@@ -129,6 +131,123 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
|
|||||||
}
|
}
|
||||||
return 'Session reset.';
|
return 'Session reset.';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getElevation: () => {
|
||||||
|
if (!sessionId || !deps.sessionManager) {
|
||||||
|
return 'Elevated mode: off';
|
||||||
|
}
|
||||||
|
const untilRaw = deps.sessionManager.getSessionConfig('ws', sessionId, 'elevation.until_ms');
|
||||||
|
const reason = deps.sessionManager.getSessionConfig('ws', sessionId, 'elevation.reason') ?? '';
|
||||||
|
const id = deps.sessionManager.getSessionConfig('ws', sessionId, 'elevation.id') ?? '';
|
||||||
|
if (!untilRaw || !id) {
|
||||||
|
return 'Elevated mode: off';
|
||||||
|
}
|
||||||
|
const untilMs = Number.parseInt(untilRaw, 10);
|
||||||
|
if (!Number.isFinite(untilMs)) {
|
||||||
|
return 'Elevated mode: off';
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
if (untilMs <= now) {
|
||||||
|
deps.sessionManager.deleteSessionConfig('ws', sessionId, 'elevation.until_ms');
|
||||||
|
deps.sessionManager.deleteSessionConfig('ws', sessionId, 'elevation.reason');
|
||||||
|
deps.sessionManager.deleteSessionConfig('ws', sessionId, 'elevation.id');
|
||||||
|
auditLogger?.securityElevationExpired({
|
||||||
|
session_id: `ws:${sessionId}`,
|
||||||
|
channel: 'ws',
|
||||||
|
sender: connectionId,
|
||||||
|
elevation_id: id,
|
||||||
|
until_ms: untilMs,
|
||||||
|
reason: reason || undefined,
|
||||||
|
});
|
||||||
|
return 'Elevated mode: off (expired)';
|
||||||
|
}
|
||||||
|
const remainingMs = untilMs - now;
|
||||||
|
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||||
|
return `Elevated mode: on (${remainingSec}s remaining)${reason ? ` — ${reason}` : ''}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
setElevation: (input: string) => {
|
||||||
|
if (!sessionId || !deps.sessionManager) {
|
||||||
|
return 'Elevate command is not available in this session.';
|
||||||
|
}
|
||||||
|
const raw = input.trim();
|
||||||
|
const parts = raw.split(/\s+/);
|
||||||
|
const hasYes = parts.includes('--yes') || parts.includes('--confirm');
|
||||||
|
const filtered = parts.filter(p => p !== '--yes' && p !== '--confirm');
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return 'Usage: /elevate <duration> <reason...> --yes | /elevate off --yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtered[0] === 'off') {
|
||||||
|
if (!hasYes) {
|
||||||
|
return 'Refusing to disable elevation without explicit confirmation. Use: /elevate off --yes';
|
||||||
|
}
|
||||||
|
const existingId = deps.sessionManager.getSessionConfig('ws', sessionId, 'elevation.id') ?? randomUUID();
|
||||||
|
const existingUntil = deps.sessionManager.getSessionConfig('ws', sessionId, 'elevation.until_ms');
|
||||||
|
const existingReason = deps.sessionManager.getSessionConfig('ws', sessionId, 'elevation.reason') ?? '';
|
||||||
|
deps.sessionManager.deleteSessionConfig('ws', sessionId, 'elevation.until_ms');
|
||||||
|
deps.sessionManager.deleteSessionConfig('ws', sessionId, 'elevation.reason');
|
||||||
|
deps.sessionManager.deleteSessionConfig('ws', sessionId, 'elevation.id');
|
||||||
|
auditLogger?.securityElevationDisabled({
|
||||||
|
session_id: `ws:${sessionId}`,
|
||||||
|
channel: 'ws',
|
||||||
|
sender: connectionId,
|
||||||
|
elevation_id: existingId,
|
||||||
|
until_ms: existingUntil ? Number.parseInt(existingUntil, 10) : undefined,
|
||||||
|
reason: existingReason || undefined,
|
||||||
|
});
|
||||||
|
return 'Elevated mode: off';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasYes) {
|
||||||
|
return 'Refusing to enable elevation without explicit confirmation. Use: /elevate <duration> <reason...> --yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dur = filtered[0];
|
||||||
|
const reason = filtered.slice(1).join(' ').trim();
|
||||||
|
const ttlMs = (() => {
|
||||||
|
const m = dur.match(/^(\d+)([smhd])$/i);
|
||||||
|
if (!m) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = Number.parseInt(m[1], 10);
|
||||||
|
if (!Number.isFinite(n) || n <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const unit = m[2].toLowerCase();
|
||||||
|
if (unit === 's') {return n * 1000;}
|
||||||
|
if (unit === 'm') {return n * 60_000;}
|
||||||
|
if (unit === 'h') {return n * 3_600_000;}
|
||||||
|
if (unit === 'd') {return n * 86_400_000;}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
if (!ttlMs) {
|
||||||
|
return 'Invalid duration. Use one of: 30s, 10m, 1h, 1d';
|
||||||
|
}
|
||||||
|
|
||||||
|
const untilMs = Date.now() + ttlMs;
|
||||||
|
const id = randomUUID();
|
||||||
|
deps.sessionManager.setSessionConfig('ws', sessionId, 'elevation.until_ms', String(untilMs));
|
||||||
|
deps.sessionManager.setSessionConfig('ws', sessionId, 'elevation.id', id);
|
||||||
|
if (reason) {
|
||||||
|
deps.sessionManager.setSessionConfig('ws', sessionId, 'elevation.reason', reason);
|
||||||
|
} else {
|
||||||
|
deps.sessionManager.deleteSessionConfig('ws', sessionId, 'elevation.reason');
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLogger?.securityElevationEnabled({
|
||||||
|
session_id: `ws:${sessionId}`,
|
||||||
|
channel: 'ws',
|
||||||
|
sender: connectionId,
|
||||||
|
elevation_id: id,
|
||||||
|
until_ms: untilMs,
|
||||||
|
ttl_ms: ttlMs,
|
||||||
|
reason: reason || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `Elevated mode: on until ${new Date(untilMs).toISOString()}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user