Skip to content
Open
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
41 changes: 6 additions & 35 deletions data-machine.php
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,11 @@ function () {
// ActionPolicy + unified pending-action resolver. Content abilities register
// themselves on `datamachine_pending_action_handlers` via
// inc/Abilities/Content/ContentActionHandlers.php (required above).
if ( interface_exists( '\AgentsAPI\AI\Approvals\WP_Agent_Pending_Action_Observer' ) ) {
if (
\DataMachine\Core\Bootstrap\DependencyChecker::has(
\DataMachine\Core\Bootstrap\DependencyChecker::CHECK_PENDING_ACTION_OBSERVER
)
) {
// @phpstan-ignore-next-line Scoped analysis sees the observer implementation before the conditional interface load.
\DataMachine\Engine\AI\Actions\PendingActionObservers::register( new \DataMachine\Engine\AI\Actions\WordPressActionDispatchObserver() );
}
Expand Down Expand Up @@ -387,40 +391,7 @@ function ( $post_id ) {
* @return bool True when full runtime registration should run.
*/
function datamachine_should_load_full_runtime(): bool {
// WordPress PHPUnit loads plugins through a frontend-shaped request. Keep
// the full runtime available so ability/tool registration tests exercise
// the same surfaces as CLI, REST, admin, cron, and Ajax entry points.
// Any of these constants is sufficient to identify a WP test environment.
if (
defined( 'WP_TESTS_DOMAIN' )
|| defined( 'WP_TESTS_CONFIG_FILE_PATH' )
|| defined( 'WP_TESTS_EMAIL' )
|| defined( 'WP_TESTS_TITLE' )
) {
return true;
}

// @phpstan-ignore-next-line Runtime constant may be defined false outside PHPStan's configured CLI context.
if ( defined( 'WP_CLI' ) && (bool) constant( 'WP_CLI' ) ) {
return true;
}

if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
return true;
}

$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
$path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );

if ( str_starts_with( $path, '/wp-json/' ) || str_starts_with( $path, '/datamachine-auth/' ) ) {
return true;
}

if ( isset( $_GET['rest_route'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Request-shape detection only.
return true;
}

return (bool) apply_filters( 'datamachine_should_load_full_runtime', false );
return \DataMachine\Core\Bootstrap\RuntimeEnvironment::should_load_full_runtime();
}


Expand Down
114 changes: 114 additions & 0 deletions inc/Core/Bootstrap/DependencyChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php
/**
* Central dependency and capability checks.
*
* @package DataMachine\Core\Bootstrap
* @since 0.138.0
*/

namespace DataMachine\Core\Bootstrap;

defined( 'ABSPATH' ) || exit;

/**
* Provides named bootstrap checks for host/runtime capabilities.
*/
class DependencyChecker {

public const CHECK_AGENTS_API_ACCESS_STORE = 'agents_api_access_store';
public const CHECK_ACTION_SCHEDULER = 'action_scheduler';
public const CHECK_FILESYSTEM_WRITES = 'filesystem_writes';
public const CHECK_IMAP = 'imap';
public const CHECK_PENDING_ACTION_OBSERVER = 'pending_action_observer';
public const CHECK_WORDPRESS_ABILITIES = 'wordpress_abilities';
public const CHECK_ZIP_ARCHIVE = 'zip_archive';

/**
* Run a named dependency/capability check.
*
* @param string $check Check name.
* @return bool True when the named dependency/capability is available.
*/
public static function has( string $check ): bool {
return match ( $check ) {
self::CHECK_AGENTS_API_ACCESS_STORE => self::has_agents_api_access_store_contracts(),
self::CHECK_ACTION_SCHEDULER => self::has_action_scheduler(),
self::CHECK_FILESYSTEM_WRITES => self::has_filesystem_writes(),
self::CHECK_IMAP => self::has_imap(),
self::CHECK_PENDING_ACTION_OBSERVER => self::has_pending_action_observer_contract(),
self::CHECK_WORDPRESS_ABILITIES => self::has_wordpress_abilities(),
self::CHECK_ZIP_ARCHIVE => self::has_zip_archive(),
default => false,
};
}

/**
* Determine whether the Agents API access-store contracts are available.
*
* @return bool True when the adapter can safely register.
*/
public static function has_agents_api_access_store_contracts(): bool {
return interface_exists( 'WP_Agent_Access_Store' ) && interface_exists( 'WP_Agent_Principal_Access_Store' );
}

/**
* Determine whether Action Scheduler is available.
*
* @return bool True when Action Scheduler is loaded.
*/
public static function has_action_scheduler(): bool {
return class_exists( 'ActionScheduler' ) || function_exists( 'as_enqueue_async_action' );
}

/**
* Determine whether Data Machine can write to its plugin directory by default.
*
* @param string|null $path Optional path to check.
* @return bool True when the path is writable.
*/
public static function has_filesystem_writes( ?string $path = null ): bool {
$path ??= defined( 'DATAMACHINE_PATH' ) ? DATAMACHINE_PATH : dirname( __DIR__, 3 );

if ( function_exists( 'wp_is_writable' ) ) {
return wp_is_writable( $path );
}

return false;
}

/**
* Determine whether IMAP support is available.
*
* @return bool True when the PHP IMAP extension functions are available.
*/
public static function has_imap(): bool {
return function_exists( 'imap_open' );
}

/**
* Determine whether the Agents API pending-action observer contract is available.
*
* @return bool True when observer implementations can safely register.
*/
public static function has_pending_action_observer_contract(): bool {
return interface_exists( '\AgentsAPI\AI\Approvals\WP_Agent_Pending_Action_Observer' );
}

/**
* Determine whether the WordPress Abilities API is available.
*
* @return bool True when WordPress abilities can be registered/resolved.
*/
public static function has_wordpress_abilities(): bool {
return class_exists( 'WP_Ability' ) && class_exists( 'WP_Abilities_Registry' );
}

/**
* Determine whether ZipArchive support is available.
*
* @return bool True when the PHP Zip extension is available.
*/
public static function has_zip_archive(): bool {
return class_exists( 'ZipArchive' );
}
}
67 changes: 67 additions & 0 deletions inc/Core/Bootstrap/RuntimeEnvironment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* Bootstrap-time request and dependency checks.
*
* @package DataMachine\Core\Bootstrap
* @since 0.138.0
*/

namespace DataMachine\Core\Bootstrap;

defined( 'ABSPATH' ) || exit;

/**
* Centralizes cheap bootstrap decisions so the plugin entry point stays thin.
*/
class RuntimeEnvironment {

/**
* Determine whether the current request is a WordPress test bootstrap.
*
* @return bool True when the request is running under WordPress tests.
*/
public static function is_wordpress_tests(): bool {
return defined( 'WP_TESTS_DOMAIN' )
|| defined( 'WP_TESTS_CONFIG_FILE_PATH' )
|| defined( 'WP_TESTS_EMAIL' )
|| defined( 'WP_TESTS_TITLE' );
}

/**
* Determine whether the current request is WP-CLI.
*
* @return bool True when WP-CLI is active.
*/
public static function is_wp_cli(): bool {
// @phpstan-ignore-next-line Runtime constant may be defined false outside PHPStan's configured CLI context.
return defined( 'WP_CLI' ) && (bool) constant( 'WP_CLI' );
}

/**
* Determine whether the full Data Machine runtime is needed for this request.
*
* @return bool True when full runtime registration should run.
*/
public static function should_load_full_runtime(): bool {
if ( self::is_wordpress_tests() || self::is_wp_cli() ) {
return true;
}

if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
return true;
}

$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
$path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );

if ( str_starts_with( $path, '/wp-json/' ) || str_starts_with( $path, '/datamachine-auth/' ) ) {
return true;
}

if ( isset( $_GET['rest_route'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Request-shape detection only.
return true;
}

return (bool) apply_filters( 'datamachine_should_load_full_runtime', false );
}
}
1 change: 0 additions & 1 deletion inc/Core/FilesRepository/MediaValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,6 @@ protected function detect_mime_type( string $file_path ): ?string {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
if ( $finfo ) {
$mime = finfo_file( $finfo, $file_path );
finfo_close( $finfo );
if ( $mime && strpos( $mime, '/' ) !== false ) {
return $mime;
}
Expand Down
2 changes: 1 addition & 1 deletion inc/Engine/AI/ConversationManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ private static function hasSuccessfulToolResultAfter( array $conversation_messag
continue;
}

if ( $tool_name !== ( $message['payload']['tool_name'] ?? null ) ) {
if ( ( $message['payload']['tool_name'] ?? null ) !== $tool_name ) {
continue;
}

Expand Down
3 changes: 2 additions & 1 deletion inc/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
use DataMachine\Core\Content\ContentFormat;
use DataMachine\Core\Database\Chat\ConversationStoreFactory;
use DataMachine\Core\Auth\AgentAccessStoreAdapter;
use DataMachine\Core\Bootstrap\DependencyChecker;
use DataMachine\Core\OAuth\HttpBasicAuthProvider;
use DataMachine\Core\PluginSettings;

Expand Down Expand Up @@ -101,7 +102,7 @@ static function ( array $fields, string $provider_slug ): array {
2
);

if ( interface_exists( 'WP_Agent_Access_Store' ) && interface_exists( 'WP_Agent_Principal_Access_Store' ) ) {
if ( DependencyChecker::has( DependencyChecker::CHECK_AGENTS_API_ACCESS_STORE ) ) {
AgentAccessStoreAdapter::register();
}

Expand Down
103 changes: 103 additions & 0 deletions tests/bootstrap-runtime-environment-smoke.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php
/**
* Pure-PHP smoke test for bootstrap dependency and request-shape checks.
*
* Run with: php tests/bootstrap-runtime-environment-smoke.php
*
* @package DataMachine\Tests
*/

declare( strict_types = 1 );

namespace {
use DataMachine\Core\Bootstrap\DependencyChecker;

if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', '/tmp/' );
}

require_once dirname( __DIR__ ) . '/inc/Core/Bootstrap/RuntimeEnvironment.php';
require_once dirname( __DIR__ ) . '/inc/Core/Bootstrap/DependencyChecker.php';

$failed = 0;
$total = 0;

$assert = static function ( string $name, bool $condition ) use ( &$failed, &$total ): void {
++$total;
if ( $condition ) {
echo " PASS: {$name}\n";
return;
}

echo " FAIL: {$name}\n";
++$failed;
};

$plugin_root = dirname( __DIR__ );
$bootstrap = file_get_contents( $plugin_root . '/inc/bootstrap.php' );
$plugin_file = file_get_contents( $plugin_root . '/data-machine.php' );

if ( false === $bootstrap || false === $plugin_file ) {
fwrite( STDERR, "FAIL: bootstrap source is not readable\n" );
exit( 1 );
}

$assert(
'access-store adapter registration uses centralized contract check',
str_contains( $bootstrap, 'DependencyChecker::CHECK_AGENTS_API_ACCESS_STORE' )
);

$assert(
'pending-action observer registration uses centralized contract check',
str_contains( $plugin_file, 'DependencyChecker::CHECK_PENDING_ACTION_OBSERVER' )
);

$assert(
'core named dependency checks exist',
DependencyChecker::CHECK_ACTION_SCHEDULER === 'action_scheduler'
&& DependencyChecker::CHECK_FILESYSTEM_WRITES === 'filesystem_writes'
&& DependencyChecker::CHECK_IMAP === 'imap'
&& DependencyChecker::CHECK_WORDPRESS_ABILITIES === 'wordpress_abilities'
&& DependencyChecker::CHECK_ZIP_ARCHIVE === 'zip_archive'
);

if ( ! interface_exists( 'WP_Agent_Access_Store' ) ) {
interface WP_Agent_Access_Store {}
}

if ( ! interface_exists( 'WP_Agent_Principal_Access_Store' ) ) {
interface WP_Agent_Principal_Access_Store {}
}
}

namespace AgentsAPI\AI\Approvals {
if ( ! interface_exists( WP_Agent_Pending_Action_Observer::class ) ) {
interface WP_Agent_Pending_Action_Observer {}
}
}

namespace {
use DataMachine\Core\Bootstrap\DependencyChecker;

$assert(
'access-store contracts are detected after stubs load',
DependencyChecker::has( DependencyChecker::CHECK_AGENTS_API_ACCESS_STORE )
);

$assert(
'pending-action observer contract is detected after stub loads',
DependencyChecker::has( DependencyChecker::CHECK_PENDING_ACTION_OBSERVER )
);

$assert(
'unknown dependency checks fail closed',
! DependencyChecker::has( 'missing-check' )
);

if ( $failed > 0 ) {
fwrite( STDERR, "bootstrap runtime environment smoke failed: {$failed}/{$total}\n" );
exit( 1 );
}

echo "Bootstrap runtime environment smoke passed: {$total} assertions.\n";
}
Loading
Loading