feat(security): harden tool provenance and skill isolation

This commit is contained in:
William Valentin
2026-02-15 10:16:55 -08:00
parent 3451df41b9
commit 67058c8719
6 changed files with 102 additions and 17 deletions
+50 -7
View File
@@ -18,6 +18,7 @@ import type { CommandRegistry } from '../commands/index.js';
import type { ComponentRegistry } from '../intents/index.js';
import type { RoutingPolicy } from '../routing/index.js';
import { createClientFromConfig } from './models.js';
import type { SkillRegistry } from '../skills/index.js';
function buildProviderConfigMap(config: Config): Partial<Record<ModelProvider, ModelConfig>> {
const providerConfigs: Partial<Record<ModelProvider, ModelConfig>> = {};
@@ -60,6 +61,7 @@ export function createMessageRouter(deps: {
commandRegistry?: CommandRegistry;
intentRegistry?: ComponentRegistry;
routingPolicy?: RoutingPolicy;
skillRegistry?: SkillRegistry;
}): {
handler: (msg: InboundMessage, reply: (response: OutboundMessage) => Promise<void>) => Promise<void>;
agents: Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }>;
@@ -76,8 +78,9 @@ export function createMessageRouter(deps: {
const tierFromMetadata = metadata?.modelTier as ModelTier | undefined;
// Include agent config name in cache key so different agents aren't shared
const baseSid = agentConfigName
? `${channel}:${senderId}:${agentConfigName}`
const skillOverride = metadata?.skillOverride as string | undefined;
const baseSid = agentConfigName || skillOverride
? `${channel}:${senderId}:${agentConfigName ?? 'default'}:${skillOverride ?? 'none'}`
: `${channel}:${senderId}`;
const session = deps.sessionManager.getSession(channel, senderId);
@@ -97,7 +100,14 @@ export function createMessageRouter(deps: {
let entry = agents.get(sessionId);
if (!entry) {
// Use agent config overrides where available, falling back to global config
const effectiveSystemPrompt = agentConfig?.systemPrompt ?? deps.systemPrompt;
let effectiveSystemPrompt = agentConfig?.systemPrompt ?? deps.systemPrompt;
// If an active skill is specified, annotate the system prompt for clarity.
const activeSkillName = skillOverride;
const activeSkill = activeSkillName ? deps.skillRegistry?.get(activeSkillName) : undefined;
if (activeSkillName) {
effectiveSystemPrompt += `\n\n[Active skill: ${activeSkillName}. Tool access is capability-restricted and may be sandboxed.]`;
}
const modelsConfig = deps.config.models as Record<string, { provider?: string; model?: string; context_window?: number } | undefined>;
const tierConfig = modelsConfig[effectiveTier] ?? deps.config.models.default;
@@ -113,14 +123,24 @@ export function createMessageRouter(deps: {
complex_reasoning: deps.config.agents.delegation.complex_reasoning ?? 'complex',
};
// Clone the tool registry and replace shell tools with sandboxed versions if configured
// Clone the tool registry and replace high-risk tools with sandboxed versions if configured.
let effectiveToolRegistry = deps.toolRegistry;
if (agentConfig?.sandbox && deps.sandboxManager && deps.config.sandbox.enabled) {
const skillEnvPreference = activeSkill?.manifest.permissions?.execution_environment;
const executionEnvironment: 'host' | 'sandbox' = skillOverride
? (skillEnvPreference === 'host'
? 'host'
: (deps.sandboxManager && deps.config.sandbox.enabled ? 'sandbox' : 'host'))
: 'host';
const useSandboxTools = executionEnvironment === 'sandbox' && deps.sandboxManager && deps.config.sandbox.enabled;
if ((agentConfig?.sandbox || Boolean(skillOverride)) && useSandboxTools) {
effectiveToolRegistry = deps.toolRegistry.clone();
// Lazy sandbox: create the sandboxed tools with a deferred sandbox reference
// The sandbox is created on first use via SandboxManager.getOrCreate()
const sandboxSessionId = sessionId;
const sandboxManager = deps.sandboxManager;
const sandboxManager = deps.sandboxManager!;
// Create a proxy sandbox that lazily initializes
const lazySandboxShell: Tool = {
@@ -196,6 +216,10 @@ export function createMessageRouter(deps: {
agent: effectiveTier,
provider: effectiveProvider,
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
skillName: activeSkillName,
skillPermissions: activeSkill?.manifest.permissions,
allowedSecretScopes: activeSkill?.manifest.permissions?.secrets,
executionEnvironment,
},
attachmentCollector: collector,
});
@@ -207,6 +231,7 @@ export function createMessageRouter(deps: {
const handler = async (msg: InboundMessage, reply: (response: OutboundMessage) => Promise<void>): Promise<void> => {
let intentAgentOverride: string | undefined;
let intentSkillOverride: string | undefined;
if (deps.config.intents?.enabled && deps.intentRegistry) {
const intentMatch = deps.intentRegistry.match(msg.text);
@@ -233,9 +258,27 @@ export function createMessageRouter(deps: {
intentAgentOverride = intentMatch.rule.target.name;
}
}
if (intentMatch?.rule.target.type === 'skill') {
let confidence = intentMatch.score;
const decision = deps.routingPolicy
? deps.routingPolicy.decide({ confidence })
: { path: 'fast' as const, reason: 'high_confidence' as const };
console.log(`[routing] intent=${intentMatch.rule.name} confidence=${confidence.toFixed(3)} path=${decision.path} reason=${decision.reason}`);
if (decision.path === 'fast') {
intentSkillOverride = intentMatch.rule.target.name;
}
}
}
const { orchestrator: agent, collector } = getOrCreateAgent(msg.channel, msg.senderId, msg.metadata, intentAgentOverride);
const effectiveMetadata = {
...(msg.metadata ?? {}),
...(intentSkillOverride ? { skillOverride: intentSkillOverride } : {}),
};
const { orchestrator: agent, collector } = getOrCreateAgent(msg.channel, msg.senderId, effectiveMetadata, intentAgentOverride);
const commandInput = msg.metadata?.isCommand && typeof msg.metadata.command === 'string'
? `/${msg.metadata.command}${msg.metadata.commandArgs ? ` ${msg.metadata.commandArgs}` : ''}`