v3.3.0
release: v3.3.0 (#81)
Slothlet v3.3.0 Changelog
Release Date: April 22, 2026
Release Type: Minor
Branch: release/3.3.0
Overview
Version 3.3.0 introduces the Permission System — a path-based access control layer that enforces which modules can call which API paths. Rules use the same glob pattern syntax as hooks. Enforcement happens in the applyTrap before hooks or function execution, so denied calls produce zero side effects.
This release also extracts the hook system's pattern matching into a shared utility (pattern-matcher.mjs) for reuse across both hooks and permissions.
No breaking changes. All v3.2.x configuration and API usage is fully compatible.
🚀 New Features
Permission System
Declare glob-based access control rules to restrict which modules can call which API paths. Rules follow the same *, **, ?, {a,b}, !negation syntax as hooks.
Configuration
const api = await slothlet({
dir: "./api",
permissions: {
defaultPolicy: "deny", // "allow" (default) or "deny"
enabled: true, // global toggle
audit: "verbose", // "default" or "verbose"
rules: [
{ caller: "admin.**", target: "**", effect: "allow" },
{ caller: "**", target: "db.read.**", effect: "allow" },
{ caller: "untrusted.**", target: "**", effect: "deny" }
]
}
});Runtime API — api.slothlet.permissions.*
// Add a rule at runtime
const ruleId = api.slothlet.permissions.addRule({
caller: "untrusted.**", target: "admin.**", effect: "deny"
});
// Remove a rule (self-modification blocked)
api.slothlet.permissions.removeRule(ruleId);
// Module self-introspection (always available, cannot be denied)
const canWrite = api.slothlet.permissions.self.access("db.write.insert");
const myRules = api.slothlet.permissions.self.rules();
// Cross-module diagnostics (gatable with a single deny rule)
const allowed = api.slothlet.permissions.global.checkAccess("payments.charge", "db.write");
const rules = api.slothlet.permissions.global.rulesForPath("db.write");
const moduleRules = api.slothlet.permissions.global.rulesByModule("mod_abc123");
// Global toggles (deny-by-default — requires explicit allow rule for caller)
api.slothlet.permissions.control.enable();
api.slothlet.permissions.control.disable();Key Behaviors
- Self-calls bypass: Calls within the same source file always succeed, regardless of deny rules.
- Most-specific-wins: Exact patterns beat single-segment globs, which beat multi-segment globs. Tiebreak is last-registered wins.
- Enforcement before hooks: Permission check fires in
applyTrapbefore anybefore:hooks. A denied call never triggers hooks. - Audit events:
permission:deniedandpermission:self-bypassalways emit via lifecycle.permission:allowedandpermission:defaultemit whenaudit: "verbose". - Built-in control protection: A default rule
{ caller: "**", target: "slothlet.permissions.control.**", effect: "deny" }prevents any module from toggling the permission system unless explicitly allowed.
📋 Full documentation: docs/PERMISSIONS.md
Shared Pattern Matcher
Extracted the hook system's glob-to-regex pattern compilation into src/lib/helpers/pattern-matcher.mjs. Both HookManager and PermissionManager now import compilePattern() from the shared utility. This reduces code duplication and ensures consistent glob behavior across both systems.
🐛 Bug Fixes
LAZY_LIVE context restoration across async boundaries
Fixed a bug where the waiting proxy's async apply (used in lazy mode to materialize modules on-demand) lost the caller's context across the async boundary when using runtime: "live". The live context manager restores state in the outer runInContext's finally block before the Promise settles, which meant the inner permission check could not identify the caller.
Fix: The waiting proxy now captures currentWrapper synchronously at the top of async apply (before any await), and if the context was lost after materialization, re-establishes it via runInContext() with the explicit wrapper.instanceID. This is multi-instance safe because runInContext looks up the context store by the provided instance ID, not the global currentInstanceID.
SlothletError propagation through context managers
Both LiveContextManager.runInContext() and AsyncContextManager.runInContext() now rethrow SlothletError instances directly instead of wrapping them in CONTEXT_EXECUTION_FAILED. This preserves the original error code (e.g., PERMISSION_DENIED) for callers to catch.
🔧 Internal Changes
HookManager refactor
- Pattern compilation logic moved to shared
pattern-matcher.mjs HookManagernow importscompilePatternfrom@cldmv/slothlet/helpers/pattern-matcherinstead of maintaining its own copy
Config normalization
- Added
permissionsconfig key normalization inConfig.normalizeConfig() - Added
permissionstoapi.mutationsdefaults ({ add: true, remove: true, reload: true, permissions: true })
Operation history
- Added
addPermissionRuleandremovePermissionRuleoperation types tooperationHistory - Replay during
slothlet.reload()reconstructs the full permission rule stack with preserved rule IDs
📋 i18n
Added 20 new translation keys across all 11 language files:
- 4 validation keys:
PERM_RULE_NOT_OBJECT,PERM_RULE_CALLER_REQUIRED,PERM_RULE_TARGET_REQUIRED,PERM_RULE_EFFECT_INVALID - 4 error keys:
PERMISSION_DENIED,PERMISSION_SELF_MODIFY,INVALID_PERMISSION_RULE,PERMISSION_MANAGER_NOT_AVAILABLE - 3 hint keys:
HINT_PERMISSION_DENIED,HINT_PERMISSION_SELF_MODIFY,HINT_INVALID_PERMISSION_RULE - 6 debug keys:
DEBUG_PERMISSION_RULE_ADDED,DEBUG_PERMISSION_RULE_REMOVED,DEBUG_PERMISSION_DENIED,DEBUG_PERMISSION_ALLOWED,DEBUG_PERMISSION_DEFAULT,DEBUG_PERMISSION_SELF_BYPASS - 2 pattern-matcher keys:
BRACE_EXPANSION_MAX_DEPTH,HINT_BRACE_EXPANSION_MAX_DEPTH - 1 versioning preparation key:
DEBUG_VERSION_UNREGISTERED
🧪 Tests
Added 39 test files in tests/vitests/suites/permissions/ covering:
- Basic configuration and validation
- Allow/deny rules and default policies
- Self-call bypass
- Glob inheritance and specificity
- Tiebreak (last-registered wins)
- Stacking order (config → api.add → addRule)
- Runtime API (
addRule,removeRule,self.*,global.*,control.*) - Immutability (self-modification blocked)
- Audit events (denied, verbose, self-bypass)
- Enforcement point (before hooks)
- Cache behavior (basic, invalidation)
- Replay and full reload
- Shutdown cleanup
- Error cases (denied, invalid rule, self-modify)
- Multi-instance isolation (rules don't leak between instances)
- Hook manager refactor (existing hook tests still pass with extracted pattern-matcher)
- Versioning integration
All 39 files pass across all 8 matrix configs (458 tests total).
Added 11 test fixture files in api_tests/api_test_permissions/.
📁 New Files
| File | Purpose |
|---|---|
src/lib/handlers/permission-manager.mjs |
Core PermissionManager handler (extends ComponentBase) |
src/lib/helpers/pattern-matcher.mjs |
Shared glob pattern compilation utility |
docs/PERMISSIONS.md |
User-facing permission system documentation |
api_tests/api_test_permissions/ |
Test fixtures (11 files across 6 subdirectories) |
tests/vitests/suites/permissions/ |
Test suite (39 test files) |
📁 Modified Files
| File | Change |
|---|---|
src/lib/builders/api_builder.mjs |
Added api.slothlet.permissions namespace (addRule, removeRule, self, global, control) |
src/lib/handlers/api-manager.mjs |
Permission cache invalidation on add/remove/reload |
src/lib/handlers/context-async.mjs |
SlothletError rethrow in runInContext |
src/lib/handlers/context-live.mjs |
SlothletError rethrow in runInContext |
src/lib/handlers/hook-manager.mjs |
Extracted pattern compilation to shared utility |
src/lib/handlers/unified-wrapper.mjs |
Permission enforcement in applyTrap; LAZY_LIVE context restoration in waiting proxy |
src/lib/helpers/config.mjs |
permissions config normalization; permissions mutation default |
src/lib/i18n/languages/*.json |
20 new keys across all 11 language files |
src/slothlet.mjs |
addPermissionRule / removePermissionRule replay handling |
tests/vitests/setup/vitest-helper.mjs |
API_TEST_PERMISSIONS entry in TEST_DIRS |