feat: central Divi release lock for the Divi tools

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 01:54:36 +02:00
co-authored by Claude Opus 5.5
parent cf3c5dfe76
commit adfe0adfc2
8 changed files with 334 additions and 18 deletions
+37 -11
View File
@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace LIT\MCP\Abilities;
use LIT\MCP\Access;
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
use LIT\MCP\Integrations\Divi\Release\DiviToolGate;
use LIT\MCP\Integrations\IntegrationRegistry;
use LIT\MCP\Log;
use LIT\MCP\Server\RequestContext;
@@ -14,15 +16,18 @@ use WP_Error;
defined('ABSPATH') || exit;
/**
* Registers all abilities of available integrations with the Abilities API and
* wraps them with the role gate and neutral error handling.
* Registers all abilities of available integrations with the Abilities API and wraps them
* with the role gate, the Divi release lock (DiviToolGate) and neutral error handling.
*/
final class AbilityRegistrar
{
public const CATEGORY = 'lit-mcp';
public function __construct(private IntegrationRegistry $registry, private ?ExposurePolicy $policy = null)
{
public function __construct(
private IntegrationRegistry $registry,
private ?ExposurePolicy $policy = null,
private ?DiviToolGate $divi = null
) {
}
public function register(): void
@@ -50,6 +55,7 @@ final class AbilityRegistrar
/** @return array<string, mixed> */
public function argsFor(Ability $ability): array
{
$divi = $this->divi ?? new DiviToolGate($this->registry->groupMap(...));
$args = [
'label' => $ability->label(),
'description' => $ability->description(),
@@ -71,15 +77,18 @@ final class AbilityRegistrar
return Errors::internal();
}
},
'execute_callback' => static function (mixed $input = null) use ($ability): mixed {
'execute_callback' => static function (mixed $input = null) use ($ability, $divi): mixed {
$refusal = $divi->refusal($ability);
if ($refusal !== null) {
return $refusal;
}
try {
return $ability->execute(is_array($input) ? $input : []);
} catch (WriteFailed $e) {
Log::failure($ability->name(), $e);
return $e->toError();
return $divi->annotate($ability, $ability->execute(is_array($input) ? $input : []));
} catch (Throwable $e) {
Log::failure($ability->name(), $e);
return Errors::internal();
return self::errorFor($e);
}
},
'meta' => [
@@ -95,11 +104,28 @@ final class AbilityRegistrar
],
];
$output = $ability->outputSchema();
$output = $divi->outputSchema($ability, $ability->outputSchema());
if ($output !== null) {
$args['output_schema'] = $output;
}
return $args;
}
/**
* A partial write stays partial_write; otherwise a Divi release lock anywhere in the cause chain is its refusal
* (spec §5.5); a rolled-back write is write_failed; anything else internal_error.
*/
private static function errorFor(Throwable $e): WP_Error
{
if ($e instanceof WriteFailed && ! $e->rolledBack) {
return $e->toError();
}
$locked = DiviLocked::in($e);
if ($locked !== null) {
return $locked->toError();
}
return $e instanceof WriteFailed ? $e->toError() : Errors::internal();
}
}