feat(tools): enforce skill capabilities and secret scopes

This commit is contained in:
William Valentin
2026-02-15 10:16:51 -08:00
parent 9900f41057
commit 3451df41b9
11 changed files with 483 additions and 4 deletions
+47
View File
@@ -1,5 +1,6 @@
import type { AutonomyLevel, ToolsConfig, ToolProfile } from '../config/schema.js';
import type { Tool } from './types.js';
import type { SkillPermissions } from '../skills/types.js';
// ── Profile definitions ─────────────────────────────────────────────
@@ -142,6 +143,41 @@ export interface ToolPolicyContext {
tier?: string;
/** Autonomy level for tool execution (affects confirmation requirements). */
autonomyLevel?: AutonomyLevel;
/** Optional active skill name (for capability scoping and audit). */
skillName?: string;
/** Optional active skill permissions (capability declarations). */
skillPermissions?: SkillPermissions;
/** Execution environment for high-risk operations. */
executionEnvironment?: 'host' | 'sandbox';
/** Secret scopes allowed for this context (used by executor). */
allowedSecretScopes?: string[];
/** True when untrusted content has been introduced in this run. */
untrustedContent?: boolean;
}
function resolveSkillAllowedNames(allToolNames: string[], permissions?: SkillPermissions): Set<string> | null {
if (!permissions) {
return null;
}
const explicitTools = Array.isArray(permissions.tools) ? permissions.tools : undefined;
const toolGroups = Array.isArray(permissions.tool_groups) ? permissions.tool_groups : undefined;
const patterns = (explicitTools && explicitTools.length > 0)
? expandGroups(explicitTools)
: (toolGroups && toolGroups.length > 0)
? expandGroups(toolGroups)
: [];
if (patterns.length === 0) {
return null;
}
return new Set(allToolNames.filter((name) => matchesAnyPattern(name, patterns)));
}
// ── ToolPolicy engine ───────────────────────────────────────────────
@@ -225,6 +261,17 @@ export class ToolPolicy {
allowed = intersect(allowed, providerAllowed);
}
// Step 6: Apply skill capability restrictions (intersection)
// Safe-by-default: if a skill is active but has no permissions manifest, deny all tools.
if (context?.skillName && !context.skillPermissions) {
allowed = new Set();
} else {
const skillAllowed = resolveSkillAllowedNames(allToolNames, context?.skillPermissions);
if (skillAllowed) {
allowed = intersect(allowed, skillAllowed);
}
}
return allowed;
}