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
34 changes: 32 additions & 2 deletions inc/Engine/AI/Tools/HostToolPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
final class HostToolPolicy {

private const ENV_POLICY_JSON = 'DATAMACHINE_HOST_TOOL_POLICY_JSON';
private const SCHEMA_SANDBOX_TOOL_POLICY = 'datamachine/sandbox-tool-policy/v1';
private const SCHEMA_RUNTIME_TOOL_POLICY = 'agents-api/runtime-tool-policy/v1';

/** @var array<string,mixed> */
Expand Down Expand Up @@ -163,8 +164,8 @@ private static function normalizePolicy( $policy ): ?array {
* @return array<string,mixed>
*/
private static function normalizeTransportPolicy( array $policy ): array {
$schema = is_string( $policy['schema'] ?? null ) ? (string) $policy['schema'] : '';
if ( self::SCHEMA_RUNTIME_TOOL_POLICY !== $schema ) {
$schema = is_string( $policy['schema'] ?? null ) ? trim( (string) $policy['schema'] ) : '';
if ( '' === $schema || ! in_array( $schema, self::transportPolicySchemas(), true ) ) {
return $policy;
}

Expand Down Expand Up @@ -199,6 +200,35 @@ private static function normalizeTransportPolicy( array $policy ): array {
return $policy;
}

/**
* Return list-shaped transport schemas that can be normalized into host policy.
*
* @return array<int,string>
*/
private static function transportPolicySchemas(): array {
$schemas = array(
self::SCHEMA_SANDBOX_TOOL_POLICY,
self::SCHEMA_RUNTIME_TOOL_POLICY,
);

if ( function_exists( 'apply_filters' ) ) {
$schemas = apply_filters( 'datamachine_host_tool_policy_transport_schemas', $schemas );
}

if ( ! is_array( $schemas ) ) {
return array();
}

$normalized = array();
foreach ( $schemas as $schema ) {
if ( is_string( $schema ) && '' !== trim( $schema ) ) {
$normalized[] = trim( $schema );
}
}

return array_values( array_unique( $normalized ) );
}

/**
* Unwrap host policy documents embedded in broader runtime payloads.
*
Expand Down
97 changes: 95 additions & 2 deletions tests/pipeline-tool-policy-snapshot-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public static function get( string $key, $default = null ) {

if ( ! function_exists( 'apply_filters' ) ) {
function apply_filters( string $hook, $value, ...$args ) {
global $datamachine_pipeline_policy_filters;
if ( is_array( $datamachine_pipeline_policy_filters[ $hook ] ?? null ) ) {
foreach ( $datamachine_pipeline_policy_filters[ $hook ] as $callback ) {
$value = $callback( $value, ...$args );
}
return $value;
}

if ( 'datamachine_step_types' === $hook ) {
return array(
'ai' => array( 'uses_handler' => false, 'multi_handler' => false ),
Expand All @@ -47,6 +55,32 @@ function apply_filters( string $hook, $value, ...$args ) {
}
}

if ( ! function_exists( 'add_filter' ) ) {
function add_filter( string $hook, callable $callback ): bool {
global $datamachine_pipeline_policy_filters;
$datamachine_pipeline_policy_filters[ $hook ][] = $callback;
return true;
}
}

if ( ! function_exists( 'remove_filter' ) ) {
function remove_filter( string $hook, callable $callback ): bool {
global $datamachine_pipeline_policy_filters;
if ( ! is_array( $datamachine_pipeline_policy_filters[ $hook ] ?? null ) ) {
return false;
}

foreach ( $datamachine_pipeline_policy_filters[ $hook ] as $index => $registered_callback ) {
if ( $registered_callback === $callback ) {
unset( $datamachine_pipeline_policy_filters[ $hook ][ $index ] );
return true;
}
}

return false;
}
}

if ( ! function_exists( 'do_action' ) ) {
function do_action( string $hook, ...$args ): void {
// no-op for tests.
Expand Down Expand Up @@ -584,7 +618,32 @@ function resolve_policy_tools_with_evidence_for_test( array $flow_step_config, a
assert_policy_equals( 'client', $resolution['alpha_tool']['executor'] ?? null, 'wrapped policy delegates explicit control-plane tool', $failures, $passes );
assert_policy_equals( null, $resolution['beta_tool']['executor'] ?? null, 'wrapped policy leaves runner-default tool local', $failures, $passes );

echo "\n[14] host tool policy accepts generic list-shaped runtime policy payloads:\n";
echo "\n[14] host tool policy accepts neutral list-shaped sandbox policy payloads:\n";
$transport_policy = array(
'schema' => 'datamachine/sandbox-tool-policy/v1',
'default_location' => 'runner',
'tools' => array(
array(
'name' => 'alpha_tool',
'execution_location' => 'control_plane',
),
),
);
$resolution = ( new ToolPolicyResolver( new SnapshotPolicyToolManager() ) )->resolve(
array(
'mode' => ToolPolicyResolver::MODE_PIPELINE,
'pipeline_step_id' => 'ephemeral_pipeline_0',
'engine_data' => array(),
'categories' => array(),
'allow_only_explicit' => true,
'allow_only' => array( 'alpha_tool', 'beta_tool' ),
'host_tool_policy' => $transport_policy,
)
);
assert_policy_equals( 'client', $resolution['alpha_tool']['executor'] ?? null, 'neutral sandbox policy delegates explicit control-plane tool', $failures, $passes );
assert_policy_equals( null, $resolution['beta_tool']['executor'] ?? null, 'neutral sandbox policy leaves runner-default tool local', $failures, $passes );

echo "\n[14b] host tool policy accepts generic list-shaped runtime policy payloads:\n";
$transport_policy = array(
'schema' => 'agents-api/runtime-tool-policy/v1',
'default_location' => 'runner',
Expand Down Expand Up @@ -631,7 +690,37 @@ function resolve_policy_tools_with_evidence_for_test( array $flow_step_config, a
assert_policy_equals( 'client', $resolution['alpha_tool']['executor'] ?? null, 'neutral host policy delegates explicit control-plane tool', $failures, $passes );
assert_policy_equals( null, $resolution['beta_tool']['executor'] ?? null, 'neutral host policy leaves runner-default tool local', $failures, $passes );

echo "\n[16] host tool policy ignores unrecognized list-shaped transport payloads:\n";
echo "\n[16] host tool policy accepts filter-registered list-shaped transport payloads:\n";
$transport_schema_filter = static function ( array $schemas ): array {
$schemas[] = 'vendor/tool-policy/v1';
return $schemas;
};
add_filter( 'datamachine_host_tool_policy_transport_schemas', $transport_schema_filter );
$transport_policy = array(
'schema' => 'vendor/tool-policy/v1',
'tools' => array(
array(
'id' => 'alpha_tool',
'execution_location' => 'control_plane',
),
),
);
$resolution = ( new ToolPolicyResolver( new SnapshotPolicyToolManager() ) )->resolve(
array(
'mode' => ToolPolicyResolver::MODE_PIPELINE,
'pipeline_step_id' => 'ephemeral_pipeline_0',
'engine_data' => array(),
'categories' => array(),
'allow_only_explicit' => true,
'allow_only' => array( 'alpha_tool', 'beta_tool' ),
'host_tool_policy' => $transport_policy,
)
);
remove_filter( 'datamachine_host_tool_policy_transport_schemas', $transport_schema_filter );
assert_policy_equals( 'client', $resolution['alpha_tool']['executor'] ?? null, 'filter-registered transport policy delegates explicit control-plane tool', $failures, $passes );
assert_policy_equals( null, $resolution['beta_tool']['executor'] ?? null, 'filter-registered transport policy does not affect unrelated tools', $failures, $passes );

echo "\n[17] host tool policy ignores unrecognized list-shaped transport payloads:\n";
$transport_policy = array(
'schema' => 'vendor/tool-policy/v1',
'tools' => array(
Expand All @@ -655,6 +744,10 @@ function resolve_policy_tools_with_evidence_for_test( array $flow_step_config, a
assert_policy_equals( null, $resolution['alpha_tool']['executor'] ?? null, 'list-shaped transport policy is not converted into host policy', $failures, $passes );
assert_policy_equals( null, $resolution['beta_tool']['executor'] ?? null, 'list-shaped transport policy does not affect unrelated tools', $failures, $passes );

echo "\n[18] production host policy code has no Codebox sandbox schema special-case:\n";
$host_policy_source = file_get_contents( __DIR__ . '/../inc/Engine/AI/Tools/HostToolPolicy.php' ) ?: '';
assert_policy_equals( false, str_contains( $host_policy_source, 'wp-codebox/sandbox-tool-policy/v1' ), 'HostToolPolicy does not name the Codebox sandbox schema', $failures, $passes );

if ( $failures ) {
echo "\nFAILED: " . count( $failures ) . " pipeline policy assertions failed.\n";
exit( 1 );
Expand Down
Loading