Skip to content

v3.3.0

Choose a tag to compare

@cldmv-bot cldmv-bot released this 22 Apr 14:03
· 13 commits to master since this release
v3.3.0
1ef5cba

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 applyTrap before any before: hooks. A denied call never triggers hooks.
  • Audit events: permission:denied and permission:self-bypass always emit via lifecycle. permission:allowed and permission:default emit when audit: "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
  • HookManager now imports compilePattern from @cldmv/slothlet/helpers/pattern-matcher instead of maintaining its own copy

Config normalization

  • Added permissions config key normalization in Config.normalizeConfig()
  • Added permissions to api.mutations defaults ({ add: true, remove: true, reload: true, permissions: true })

Operation history

  • Added addPermissionRule and removePermissionRule operation types to operationHistory
  • 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