Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Agents API sits between tool/action discovery and product-specific automation. I
- Generic multi-turn conversation loop sequencing around caller-owned adapters.
- Iteration budget primitives for bounded execution across configurable dimensions.
- Tool-call mediation contracts and runtime tool declaration value objects.
- Generic tool visibility policy and action policy resolver contracts.
- Conversation transcript store contracts.
- Consent policy contracts for memory, transcripts, sharing, and escalation.
- Tool source registration, parameter normalization, tool-call mediation, and execution result contracts.
Expand Down Expand Up @@ -143,6 +144,11 @@ wp_register_agent(
- `AgentsAPI\AI\Context\ContextConflictResolution`
- `AgentsAPI\AI\Context\ContextConflictResolverInterface`
- `AgentsAPI\AI\Context\DefaultContextConflictResolver`
- `WP_Agent_Tool_Policy`
- `WP_Agent_Tool_Policy_Filter`
- `WP_Agent_Tool_Access_Policy_Interface`
- `WP_Agent_Action_Policy_Resolver`
- `WP_Agent_Action_Policy_Provider_Interface`
- `AgentsAPI\Core\Workspace\AgentWorkspaceScope`
- `AgentsAPI\Core\Database\Chat\ConversationTranscriptStoreInterface`
- `AgentsAPI\Core\FilesRepository\AgentMemoryStoreInterface` and memory value objects, including provenance/trust metadata contracts
Expand Down Expand Up @@ -223,6 +229,67 @@ final class RepoMemoryValidator implements AgentsAPI\Core\FilesRepository\AgentM

Agents API defines the contracts only. Concrete stores decide how metadata is physically materialized, how ranking is executed, and which workspace facts are supplied to validators.

## Tool Visibility And Action Policy

Agents API owns generic policy contracts for deciding which tools are visible to an agent and how a called tool is allowed to execute. Consumers still own concrete tool sources, concrete execution adapters, approval storage, UI, workflows, and any product-specific mandatory tools.

Tool visibility is resolved by `WP_Agent_Tool_Policy` over an already-gathered tool map. The resolver applies generic layers only:

- Tool-declared runtime modes through `mode` or `modes`.
- Caller-owned access checks through `tool_access_checker`.
- Registered agent or runtime `tool_policy` config with `allow` / `deny`, `tools`, and `categories`.
- Host-provided `WP_Agent_Tool_Access_Policy_Interface` policy fragments.
- Runtime `categories`, `allow_only`, and explicit `deny` lists.

Mandatory tools are not hardcoded by Agents API. A consumer that needs mandatory runtime plumbing can return `mandatory_tools` or `mandatory_categories` from a policy provider, and explicit deny still wins.

```php
$visible_tools = ( new WP_Agent_Tool_Policy() )->resolve(
$all_tools,
array(
'mode' => 'chat',
'agent_config' => array(
'tool_policy' => array(
'mode' => 'allow',
'categories' => array( 'read' ),
),
),
'tool_policy_providers' => array( $consumer_policy_provider ),
)
);
```

Action policy is resolved by `WP_Agent_Action_Policy_Resolver` and always returns one of the canonical values from `AgentsAPI\AI\Tools\ActionPolicy`: `direct`, `preview`, or `forbidden`.

Resolution order is:

1. Explicit runtime `deny` list resolves a tool to `forbidden`.
2. Registered agent or runtime `action_policy.tools[tool_name]`.
3. Registered agent or runtime `action_policy.categories[category]`.
4. Host-provided `WP_Agent_Action_Policy_Provider_Interface` providers.
5. Tool-declared `action_policy` default.
6. Tool-declared mode-specific `action_policy_<mode>` default.
7. Global default `direct`.
8. Final `agents_api_tool_action_policy` filter, if present.

```php
$policy = ( new WP_Agent_Action_Policy_Resolver() )->resolve_for_tool(
array(
'tool_name' => 'example/publish',
'tool_def' => $visible_tools['example/publish'],
'mode' => 'chat',
'agent_config' => array(
'action_policy' => array(
'categories' => array(
'publishing' => 'preview',
),
),
),
'action_policy_providers' => array( $consumer_action_policy_provider ),
)
);
```

## Workspace Scope

`AgentsAPI\Core\Workspace\AgentWorkspaceScope` is the generic workspace identity shared by memory, transcript, persistence, and audit adapters. It is deliberately broader than a WordPress site ID:
Expand Down
7 changes: 6 additions & 1 deletion agents-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
require_once AGENTS_API_PATH . 'src/Runtime/AgentExecutionPrincipal.php';
require_once AGENTS_API_PATH . 'src/Runtime/AgentCompactionItem.php';
require_once AGENTS_API_PATH . 'src/Tools/RuntimeToolDeclaration.php';
require_once AGENTS_API_PATH . 'src/Tools/ActionPolicy.php';
require_once AGENTS_API_PATH . 'src/Tools/class-wp-agent-tool-access-policy-interface.php';
require_once AGENTS_API_PATH . 'src/Tools/class-wp-agent-action-policy-provider-interface.php';
require_once AGENTS_API_PATH . 'src/Tools/class-wp-agent-tool-policy-filter.php';
require_once AGENTS_API_PATH . 'src/Tools/class-wp-agent-tool-policy.php';
require_once AGENTS_API_PATH . 'src/Tools/class-wp-agent-action-policy-resolver.php';
require_once AGENTS_API_PATH . 'src/Runtime/AgentConversationRequest.php';
require_once AGENTS_API_PATH . 'src/Runtime/AgentConversationRunnerInterface.php';
require_once AGENTS_API_PATH . 'src/Runtime/AgentConversationCompletionDecision.php';
Expand All @@ -79,7 +85,6 @@
require_once AGENTS_API_PATH . 'src/Runtime/AgentConversationResult.php';
require_once AGENTS_API_PATH . 'src/Runtime/AgentConversationLoop.php';
require_once AGENTS_API_PATH . 'src/Tools/ToolCall.php';
require_once AGENTS_API_PATH . 'src/Tools/ActionPolicy.php';
require_once AGENTS_API_PATH . 'src/Tools/ToolParameters.php';
require_once AGENTS_API_PATH . 'src/Tools/ToolExecutionResult.php';
require_once AGENTS_API_PATH . 'src/Tools/ToolExecutorInterface.php';
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"php tests/authorization-smoke.php",
"php tests/action-policy-values-smoke.php",
"php tests/consent-policy-smoke.php",
"php tests/tool-policy-contracts-smoke.php",
"php tests/action-policy-resolver-smoke.php",
"php tests/tool-runtime-smoke.php",
"php tests/pending-action-store-contract-smoke.php",
"php tests/approval-resolver-contract-smoke.php",
Expand Down
26 changes: 26 additions & 0 deletions src/Tools/class-wp-agent-action-policy-provider-interface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* Agent action policy provider contract.
*
* @package AgentsAPI
*/

defined( 'ABSPATH' ) || exit;

if ( ! interface_exists( 'WP_Agent_Action_Policy_Provider_Interface' ) ) {
/**
* Provides host-specific execution policy for a tool invocation.
*/
interface WP_Agent_Action_Policy_Provider_Interface {

/**
* Resolve a policy override for a tool invocation.
*
* Return one of direct, preview, forbidden, or null for no opinion.
*
* @param array<string, mixed> $context Action policy context.
* @return string|null Action policy value, or null for no opinion.
*/
public function get_action_policy( array $context ): ?string;
}
}
235 changes: 235 additions & 0 deletions src/Tools/class-wp-agent-action-policy-resolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<?php
/**
* Generic tool action policy resolver.
*
* @package AgentsAPI
*/

use AgentsAPI\AI\Tools\ActionPolicy;

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WP_Agent_Action_Policy_Resolver' ) ) {
/**
* Resolves whether a tool call runs directly, previews, or is forbidden.
*/
class WP_Agent_Action_Policy_Resolver {

/**
* @var WP_Agent_Action_Policy_Provider_Interface[]
*/
private array $policy_providers;

/**
* @var WP_Agent_Tool_Policy_Filter
*/
private WP_Agent_Tool_Policy_Filter $tool_filter;

/**
* Constructor.
*
* @param WP_Agent_Action_Policy_Provider_Interface[]|null $policy_providers Host policy providers.
* @param WP_Agent_Tool_Policy_Filter|null $tool_filter Shared tool filter.
*/
public function __construct( ?array $policy_providers = null, ?WP_Agent_Tool_Policy_Filter $tool_filter = null ) {
$this->policy_providers = is_array( $policy_providers ) ? $policy_providers : array();
$this->tool_filter = $tool_filter ?? new WP_Agent_Tool_Policy_Filter();
}

/**
* Resolve action policy for one tool invocation.
*
* @param array<string, mixed> $context Resolution context.
* @return string One of direct, preview, forbidden.
*/
public function resolve_for_tool( array $context ): string {
$tool_name = (string) ( $context['tool_name'] ?? '' );
$mode = (string) ( $context['mode'] ?? WP_Agent_Tool_Policy::RUNTIME_CHAT );
$tool_def = is_array( $context['tool_def'] ?? null ) ? $context['tool_def'] : array();

if ( '' === $tool_name ) {
return ActionPolicy::DIRECT;
}

if ( in_array( $tool_name, $this->string_list( $context['deny'] ?? array() ), true ) ) {
return $this->apply_filter( ActionPolicy::FORBIDDEN, $tool_name, $mode, $context );
}

$agent_policy = $this->agent_action_policy_from_context( $context );
$agent_tool = $this->agent_tool_override( $agent_policy, $tool_name );
if ( null !== $agent_tool ) {
return $this->apply_filter( $agent_tool, $tool_name, $mode, $context );
}

$agent_category = $this->agent_category_override( $agent_policy, $tool_def );
if ( null !== $agent_category ) {
return $this->apply_filter( $agent_category, $tool_name, $mode, $context );
}

foreach ( $this->get_policy_providers( $context ) as $provider ) {
$provided = ActionPolicy::normalize( $provider->get_action_policy( $context ) );
if ( null !== $provided ) {
return $this->apply_filter( $provided, $tool_name, $mode, $context );
}
}

$tool_default = $this->tool_declared_default( $tool_def );
if ( null !== $tool_default ) {
return $this->apply_filter( $tool_default, $tool_name, $mode, $context );
}

$mode_default = $this->mode_declared_default( $tool_def, $mode );
if ( null !== $mode_default ) {
return $this->apply_filter( $mode_default, $tool_name, $mode, $context );
}

return $this->apply_filter( ActionPolicy::DIRECT, $tool_name, $mode, $context );
}

/**
* Return policy providers from constructor, context, and filters.
*
* @param array<string, mixed> $context Runtime context.
* @return WP_Agent_Action_Policy_Provider_Interface[] Providers.
*/
private function get_policy_providers( array $context ): array {
$providers = $this->policy_providers;
if ( is_array( $context['action_policy_providers'] ?? null ) ) {
$providers = array_merge( $providers, $context['action_policy_providers'] );
}

if ( function_exists( 'apply_filters' ) ) {
$providers = apply_filters( 'agents_api_action_policy_providers', $providers, $context, $this );
}

return array_values(
array_filter(
is_array( $providers ) ? $providers : array(),
static fn( $provider ): bool => $provider instanceof WP_Agent_Action_Policy_Provider_Interface
)
);
}

/**
* Return action policy from runtime or registered agent config.
*
* @param array<string, mixed> $context Runtime context.
* @return array<string, mixed> Policy map.
*/
private function agent_action_policy_from_context( array $context ): array {
if ( is_array( $context['action_policy'] ?? null ) ) {
return $context['action_policy'];
}

$agent_config = array();
if ( is_array( $context['agent_config'] ?? null ) ) {
$agent_config = $context['agent_config'];
} elseif ( ( $context['agent'] ?? null ) instanceof WP_Agent ) {
$agent_config = $context['agent']->get_default_config();
} else {
$agent_slug = (string) ( $context['agent_slug'] ?? ( $context['agent_id'] ?? '' ) );
if ( '' !== $agent_slug && function_exists( 'wp_get_agent' ) ) {
$agent = wp_get_agent( $agent_slug );
if ( $agent instanceof WP_Agent ) {
$agent_config = $agent->get_default_config();
}
}
}

return is_array( $agent_config['action_policy'] ?? null ) ? $agent_config['action_policy'] : array();
}

/**
* Resolve per-tool agent override.
*
* @param array<string, mixed> $policy Agent policy.
* @param string $tool_name Tool name.
* @return string|null Normalized policy or null.
*/
private function agent_tool_override( array $policy, string $tool_name ): ?string {
$tools = is_array( $policy['tools'] ?? null ) ? $policy['tools'] : array();
return ActionPolicy::normalize( $tools[ $tool_name ] ?? null );
}

/**
* Resolve per-category agent override.
*
* @param array<string, mixed> $policy Agent policy.
* @param array<string, mixed> $tool_def Tool definition.
* @return string|null Normalized policy or null.
*/
private function agent_category_override( array $policy, array $tool_def ): ?string {
$categories = is_array( $policy['categories'] ?? null ) ? $policy['categories'] : array();
foreach ( $categories as $category => $raw_policy ) {
if ( ! is_string( $category ) || ! $this->tool_filter->tool_matches_categories( $tool_def, array( $category ) ) ) {
continue;
}

$policy_value = ActionPolicy::normalize( $raw_policy );
if ( null !== $policy_value ) {
return $policy_value;
}
}

return null;
}

/**
* Return tool-declared default action policy.
*
* @param array<string, mixed> $tool_def Tool definition.
* @return string|null Normalized policy or null.
*/
private function tool_declared_default( array $tool_def ): ?string {
return ActionPolicy::normalize( $tool_def['action_policy'] ?? null );
}

/**
* Return mode-specific tool-declared action policy.
*
* @param array<string, mixed> $tool_def Tool definition.
* @param string $mode Runtime mode.
* @return string|null Normalized policy or null.
*/
private function mode_declared_default( array $tool_def, string $mode ): ?string {
return ActionPolicy::normalize( $tool_def[ 'action_policy_' . $mode ] ?? null );
}

/**
* Apply final WordPress filter and keep only canonical values.
*
* @param string $policy Computed policy.
* @param string $tool_name Tool name.
* @param string $mode Runtime mode.
* @param array<string, mixed> $context Resolution context.
* @return string Filtered policy.
*/
private function apply_filter( string $policy, string $tool_name, string $mode, array $context ): string {
if ( ! function_exists( 'apply_filters' ) ) {
return $policy;
}

$filtered = apply_filters( 'agents_api_tool_action_policy', $policy, $tool_name, $mode, $context, $this );
return ActionPolicy::normalize( $filtered ) ?? $policy;
}

/**
* Normalize a list of strings.
*
* @param mixed $values Raw list.
* @return string[] Non-empty strings.
*/
private function string_list( $values ): array {
$values = is_array( $values ) ? $values : array( $values );
$values = array_filter(
array_map(
static fn( $value ) => is_string( $value ) ? trim( $value ) : '',
$values
),
static fn( string $value ): bool => '' !== $value
);

return array_values( array_unique( $values ) );
}
}
}
Loading
Loading