$envelope What the replay provider needs. * @param string $provider_id The replay provider's id. * @param bool $independent Decide it whatever decided the * request around it. */ public static function hold_at_effect( \AgentFirewall\Policy\ActionDescriptor $action, array $envelope, string $provider_id, bool $independent = false ): EffectOutcome { $container = \AgentFirewall\Plugin::instance()->container(); $holds = $container->has( 'intercept.effect_holds' ) ? $container->get( 'intercept.effect_holds' ) : null; if ( ! $holds instanceof \AgentFirewall\Intercept\EffectHolds ) { // Called before the firewall is wired: nothing can be decided, // so nothing may run. return new EffectOutcome( 'DENY', 'unavailable' ); } return $holds->hold( $action, $envelope, $provider_id, $independent ); } /** * The agent acting in this request, for an extension's attribution - * labeling a job the agent scheduled, say (AF-25.12). Never a * decision: whether the agent may act is the pipeline's call. * * @return array{id: int, key: string, name: string}|null Null outside an agent's request. */ public static function current_agent(): ?array { $context = \AgentFirewall\Auth\AgentContext::current(); if ( null === $context ) { return null; } return array( 'id' => $context->agent->id, 'key' => $context->agent->agent_key, 'name' => $context->agent->name, ); } /** * The first ledger row this request recorded for the acting agent, * for attribution (AF-25.12); null when there is no agent or nothing * recorded yet. */ public static function request_ledger_id(): ?int { $agent = self::current_agent(); return null === $agent ? null : \AgentFirewall\Intercept\InterceptionPipeline::first_ledger_id( $agent['id'] ); } /** * Give the current request an agent identity from a credential the * firewall does not know - a WooCommerce REST API key - as it binds an * application password (AF-25.13). The extension maps its credential * to an agent; the same checks then guard the binding: * * - the agent exists, is active, and is linked to the user the * credential authenticated, so a mapping that points at someone else's * agent binds nothing; * - no agent context exists yet, and nothing in the request has been * evaluated: a late binding would make the request's earlier writes * untracked and its later ones the agent's; * - an agent whose user holds manage_options is bound only once an * administrator confirmed it runs as one (AF-25.15, ADR-18 §9), so an * agent linked before AF-12.14, or whose user was promoted since, is * `privileged`. A confirmed one is guarded where no rule can loosen. * * Call it while authenticating, before the request is dispatched. A * refusal binds nothing; report `mismatch`, `inactive` and `privileged` * as a 401, as the parent does for a password. * * @param int $agent_id The agent the credential is bound to. * @return string bound | unknown | mismatch | inactive | privileged | already | too_late */ public static function bind_agent( int $agent_id ): string { if ( \AgentFirewall\Auth\AgentContext::is_agent_request() ) { return 'already'; } if ( \AgentFirewall\Intercept\InterceptionPipeline::request_already_evaluated() ) { return 'too_late'; } $agent = self::agents()->find_by_id( $agent_id ); if ( null === $agent ) { return 'unknown'; } if ( get_current_user_id() !== $agent->wp_user_id ) { return 'mismatch'; } if ( 'active' !== $agent->status ) { return 'inactive'; } // An agent with administrator rights is bound only once an // administrator confirmed it runs as one (AF-25.15): not one linked // before AF-12.14, nor one whose user was promoted since. if ( ! $agent->runs_as_admin() && user_can( $agent->wp_user_id, 'manage_options' ) ) { return 'privileged'; } \AgentFirewall\Auth\AgentContext::establish( $agent ); return 'bound'; } /** * An agent, described for an extension's binding screen and checks * (AF-25.13): never a credential. Do not store `user_id` anywhere the * public can read it (job arguments, logs). * * @param int $agent_id Agent. * `runs_as_admin`: an administrator confirmed the agent runs as one * (AF-25.15), so it may be bound to a credential that authenticates as * an administrator. * * @return array{id: int, key: string, name: string, user_id: int, status: string, runs_as_admin: bool}|null */ public static function agent( int $agent_id ): ?array { $agent = self::agents()->find_by_id( $agent_id ); return null === $agent ? null : array( 'id' => $agent->id, 'key' => $agent->agent_key, 'name' => $agent->name, 'user_id' => $agent->wp_user_id, 'status' => $agent->status, 'runs_as_admin' => $agent->runs_as_admin(), ); } /** * A link to the Activity Ledger narrowed to one entity type and, when * given, one entity (AF-25.16): from an order screen to the rows about * that order. Escape it where it is printed (`esc_url()`). A type that is * not a slug links to the whole ledger; an id that is not positive, to * the type's rows. * * @param string $entity_type Entity type, as the ledger records it. * @param int|null $entity_id Entity id, or null for every one. */ public static function ledger_url( string $entity_type, ?int $entity_id = null ): string { $url = \AgentFirewall\Admin\Sections::url( 'ledger' ); if ( 1 !== preg_match( Registry::ENTITY_PATTERN, $entity_type ) ) { return $url; } $url .= '&entity_type=' . $entity_type; return null !== $entity_id && $entity_id > 0 ? $url . '&entity_id=' . $entity_id : $url; } /** * The agent repository. */ private static function agents(): \AgentFirewall\Repository\AgentRepository { $agents = \AgentFirewall\Plugin::instance()->container()->get( 'repository.agents' ); assert( $agents instanceof \AgentFirewall\Repository\AgentRepository ); return $agents; } }