fix: Guards laufen auch im permission_callback vor permission() und im try des execute_callback – Lese-Tools antworten bei gesperrter Fähigkeit mit der Ablehnung statt internal_error, werfende Guards ergeben internal_error

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 22:21:48 +02:00
co-authored by Claude Sonnet 5.5
parent 74cf0c54a8
commit b92545a8fe
2 changed files with 141 additions and 6 deletions
+28 -6
View File
@@ -63,7 +63,7 @@ final class AbilityRegistrar
'description' => $ability->description(),
'category' => self::CATEGORY,
'input_schema' => $ability->inputSchema(),
'permission_callback' => static function (mixed $input = null) use ($ability): bool|WP_Error {
'permission_callback' => static function (mixed $input = null) use ($ability, $guards): bool|WP_Error {
if (! Access::currentUserMayUse()) {
return Errors::forbidden('Dein Benutzerkonto ist für LIT MCP nicht freigegeben.');
}
@@ -73,7 +73,12 @@ final class AbilityRegistrar
}
try {
return $ability->permission(is_array($input) ? $input : []);
$input = is_array($input) ? $input : [];
// Before permission(): a read tool whose loader breaks must answer with the guard's refusal
// (e.g. the Divi release lock), not with internal_error from its own permission check.
$refusal = self::guardRefusal($guards, $ability, $input);
return $refusal ?? $ability->permission($input);
} catch (Throwable $e) {
Log::failure($ability->name(), $e);
return Errors::internal();
@@ -81,14 +86,15 @@ final class AbilityRegistrar
},
'execute_callback' => static function (mixed $input = null) use ($ability, $guards): mixed {
$input = is_array($input) ? $input : [];
foreach ($guards as $guard) {
$refusal = $guard->refusal($ability, $input);
try {
// Second line of defence: the permission callback already asked the guards. A refusal there
// stops the call, so a write is logged as refused only once.
$refusal = self::guardRefusal($guards, $ability, $input);
if ($refusal !== null) {
return $refusal;
}
}
try {
$result = $ability->execute($input);
foreach ($guards as $guard) {
$result = $guard->annotate($ability, $result);
@@ -125,6 +131,22 @@ final class AbilityRegistrar
return $args;
}
/**
* @param list<AbilityGuard> $guards
* @param array<string, mixed> $input
*/
private static function guardRefusal(array $guards, Ability $ability, array $input): ?WP_Error
{
foreach ($guards as $guard) {
$refusal = $guard->refusal($ability, $input);
if ($refusal !== null) {
return $refusal;
}
}
return null;
}
/**
* A partial write stays partial_write; otherwise the first ToolRefusal anywhere in the cause chain is the answer
* (e.g. the Divi release lock, spec §5.5); a rolled-back write is write_failed; anything else internal_error.