*/ private static array $door_targets = array(); /** * The agent this state is for; another agent starts afresh. */ private static ?int $agent = null; /** * Forget this request's decisions (tests and request teardown only). */ public static function reset(): void { self::$decided = false; self::$door = false; self::$door_row = null; self::$door_targets = array(); self::$agent = null; } /** * Constructor. * * @param InterceptionPipeline $pipeline Shared interception pipeline. * @param Registry $extensions Extensions, for their replay providers. */ public function __construct( private readonly InterceptionPipeline $pipeline, private readonly Registry $extensions ) { } /** * Decide a write caught at an effect hook. * * A hold stores the envelope with the provider id, to be replayed by * that provider on approval. When approval could not safely run it - no * such provider, no entity provider to drift-check the target, or an * envelope that does not survive storage as it was given - the write is * decided unqueueable, as the backstop decides, so a hold becomes a * denial rather than a hold nobody can approve, or one approved blind. * * @param ActionDescriptor $action The write, as the extension describes it. * @param array $envelope What its replay provider needs to run it. * @param string $provider_id The replay provider's id. * @param bool $independent Decide it even inside a request a * door evaluated (AF-25.9). */ public function hold( ActionDescriptor $action, array $envelope, string $provider_id, bool $independent = false ): EffectOutcome { // The approval re-running this very write meets the same hook. if ( ActionExecutor::is_replaying_effect( $provider_id, $envelope ) ) { return new EffectOutcome( null, 'replaying' ); } $context = AgentContext::current(); if ( null === $context ) { return new EffectOutcome( null, 'not_an_agent' ); } if ( $context->agent->id !== self::$agent ) { self::reset(); self::$agent = $context->agent->id; } if ( ! self::$decided ) { self::$door = InterceptionPipeline::request_already_evaluated(); self::$door_row = InterceptionPipeline::first_ledger_id( $context->agent->id ); self::$door_targets = InterceptionPipeline::decided_targets( $context->agent->id ); } // Independent or not, a write the door itself decided - the agent // asked for this very refund - is one write: one row, one spend. if ( self::$door && ( ! $independent || self::covered( $action ) ) ) { return new EffectOutcome( null, 'already_evaluated' ); } // Holdable only if approval could replay it and drift-check it: a // hold whose target no entity provider describes would run blind. $replayable = null !== $this->extensions->replay_provider( $provider_id ) && null !== $this->extensions->entity_provider( $action->entity_type ) && self::storable( $envelope ); if ( $replayable ) { $action = $action->with_replay( array( 'effect_provider' => $provider_id, 'envelope' => $envelope, ) ); } self::$decided = true; try { // The extension built this descriptor: its type is the // extension's statement (AF-25.15). $outcome = $this->pipeline->process( $context->agent, $action, null, $replayable, self::$door ? self::$door_row : null, true ); } catch ( \Throwable $failure ) { // Never an exception the add-on might swallow and so let the // write through. return new EffectOutcome( 'DENY', 'unavailable' ); } return new EffectOutcome( $outcome->decision->value, $outcome->reason, $outcome->ledger_id, $outcome->pending_id ); } /** * Whether the door decided this write already: the same entity type and * category, and the same id where both name one. * * @param ActionDescriptor $action The effect. */ private static function covered( ActionDescriptor $action ): bool { foreach ( self::$door_targets as list( $type, $category, $id ) ) { if ( $type === $action->entity_type && $category === $action->action_cat && ( null === $id || null === $action->entity_id || $id === $action->entity_id ) ) { return true; } } return false; } /** * Whether an envelope comes back from storage as it went in: plain data, * valid UTF-8, no objects. * * @param array $envelope The envelope. */ private static function storable( array $envelope ): bool { $encoded = wp_json_encode( $envelope ); return is_string( $encoded ) && json_decode( $encoded, true ) === $envelope; } }