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
76 changes: 37 additions & 39 deletions data-machine.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,8 @@ function () {
// when the registry fires lazily (always after init), so translations are
// already loaded by execution time.
//
// Previously this was wrapped in add_action('init', ...) at priority 10, but
// datamachine_maybe_run_migrations() at init priority 5 triggers the registry
// via ScaffoldAbilities::get_ability() → WP_Abilities_Registry::get_instance(),
// firing wp_abilities_api_init before the hooks were registered.
// Register eagerly so any later lazy WP_Abilities_Registry initialization sees
// every ability hook in this request.
new \DataMachine\Abilities\AuthAbilities();
new \DataMachine\Abilities\AI\InspectRequestAbility();
new \DataMachine\Abilities\File\AgentFileAbilities();
Expand Down Expand Up @@ -647,6 +645,41 @@ function datamachine_create_network_agent_tables() {
function datamachine_activate_for_site() {
datamachine_register_capabilities();

// Ensure every Data Machine database table exists. dbDelta is idempotent.
datamachine_ensure_all_tables();

// Ensure default agent memory files exist.
// During activation the Abilities API is unavailable (init already fired before
// the plugin was included via plugin_sandbox_scrape, so our init callback that
// registers abilities never ran). Set a transient so the scaffold runs on the
// first normal request where the full hook sequence fires in order.
if ( ! datamachine_ensure_default_memory_files() ) {
set_transient( 'datamachine_needs_scaffold', 1, HOUR_IN_SECONDS );
}

// Regenerate every composable memory file (SITE.md, NETWORK.md, AGENTS.md, …)
// from their registered sections.
// Activation-only — composable regeneration is heavy and shouldn't fire on
// every deploy.
\DataMachine\Engine\AI\ComposableFileGenerator::regenerate_all();

// Re-schedule any flows with non-manual scheduling
datamachine_activate_scheduled_flows();

// Track DB schema version so deploy-time migrations auto-run.
update_option( 'datamachine_db_version', DATAMACHINE_VERSION, true );
}

/**
* Create or update every Data Machine database table.
*
* Shared by activation and the deploy-time deferred runtime. dbDelta and
* the per-table column ensures are idempotent, so this is safe to call on
* every version bump.
*
* @return void
*/
function datamachine_ensure_all_tables() {
// Create logs table first — other table migrations log messages during creation.
\DataMachine\Core\Database\Logs\LogRepository::create_table();

Expand Down Expand Up @@ -681,41 +714,6 @@ function datamachine_activate_for_site() {
\DataMachine\Core\Database\Chat\Chat::ensure_transcript_lock_columns();

\DataMachine\Engine\AI\Actions\PendingActionStore::create_table();

// Ensure default agent memory files exist.
// During activation the Abilities API is unavailable (init already fired before
// the plugin was included via plugin_sandbox_scrape, so our init callback that
// registers abilities never ran). Set a transient so the scaffold runs on the
// first normal request where the full hook sequence fires in order.
if ( ! datamachine_ensure_default_memory_files() ) {
set_transient( 'datamachine_needs_scaffold', 1, HOUR_IN_SECONDS );
}

// Run the shared migration chain. Each migration is idempotent and
// option-gated; this same function fires from
// `datamachine_maybe_run_deferred_migrations()` at plugins_loaded:5
// when a deploy advances DATAMACHINE_VERSION past the persisted
// `datamachine_db_version` option (#1301).
datamachine_run_schema_migrations();

// Regenerate every composable memory file (SITE.md, NETWORK.md, AGENTS.md, …)
// from their registered sections, and clean up the legacy SiteContext transient.
// Activation-only — composable regeneration is heavy and shouldn't fire on
// every deploy (the version-gated runtime path is for schema-shape drift,
// not opportunistic content refresh).
\DataMachine\Engine\AI\ComposableFileGenerator::regenerate_all();
delete_transient( 'datamachine_site_context_data' );

// Clean up legacy per-agent-type log level options (idempotent).
foreach ( array( 'pipeline', 'chat', 'system' ) as $legacy_agent_type ) {
delete_option( "datamachine_log_level_{$legacy_agent_type}" );
}

// Re-schedule any flows with non-manual scheduling
datamachine_activate_scheduled_flows();

// Track DB schema version so deploy-time migrations auto-run.
update_option( 'datamachine_db_version', DATAMACHINE_VERSION, true );
}

/**
Expand Down
2 changes: 2 additions & 0 deletions inc/Abilities/Content/EditPostBlocksAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ function ( $content ) use ( $find, $replace ) {
array(
'success' => true,
'is_preview' => true,
'kind' => 'edit_post_blocks',
'preview' => $diff,
'post_id' => $post_id,
'changes_applied' => $changes,
)
Expand Down
4 changes: 4 additions & 0 deletions inc/Abilities/Content/InsertContentAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ public static function getChatTool(): array {
* @return array Result.
*/
public static function handleChatToolCall( array $params, array $tool_def = array() ): array {
unset( $tool_def );

return self::execute( $params );
}

Expand Down Expand Up @@ -317,6 +319,8 @@ public static function execute( array $input ): array {
array(
'success' => true,
'is_preview' => true,
'kind' => 'insert_content',
'preview' => $diff,
'post_id' => $post_id,
'position' => $position,
'insertion_point' => $insertion_point,
Expand Down
2 changes: 2 additions & 0 deletions inc/Abilities/Content/ReplacePostBlocksAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ function ( $c ) {
array(
'success' => true,
'is_preview' => true,
'kind' => 'replace_post_blocks',
'preview' => $diff,
'post_id' => $post_id,
'blocks_replaced' => $clean_changes,
)
Expand Down
4 changes: 0 additions & 4 deletions inc/Api/WebhookAuthResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
* scheduling_config[ 'webhook_auth' ] = <template config>
* scheduling_config[ 'webhook_secrets' ] = [ [...], ... ]
*
* Legacy v1 flows (`webhook_auth_mode = hmac_sha256` + `webhook_signature_*`
* + singular `webhook_secret`) are normalized by the schema migration chain
* before runtime webhook code reads them.
*
* No provider names live in this file.
*
* @package DataMachine\Api
Expand Down
7 changes: 3 additions & 4 deletions inc/Api/WebhookTrigger.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,10 @@ public static function register_routes() {
*
* Flow:
* 1. Load the flow by id.
* 2. Silently migrate legacy v1 HMAC fields into the canonical v2 shape.
* 3. Route to the `authenticate_bearer` or `authenticate_via_verifier`
* 2. Route to the `authenticate_bearer` or `authenticate_via_verifier`
* path based on `webhook_auth_mode`.
* 4. Enforce rate limiting.
* 5. Delegate to the `datamachine/run-flow` ability.
* 3. Enforce rate limiting.
* 4. Delegate to the `datamachine/run-flow` ability.
*
* Returns a generic 401 (or 413 for oversized HMAC payloads) for all auth
* failures to prevent information leakage. The real failure reason is
Expand Down
10 changes: 5 additions & 5 deletions inc/Core/Agents/AgentBundler.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
use DataMachine\Engine\Bundle\AgentBundleArtifactStatus;
use DataMachine\Engine\Bundle\AgentBundleDirectory;
use DataMachine\Engine\Bundle\AgentBundleFlowFile;
use DataMachine\Engine\Bundle\AgentBundleLegacyAdapter;
use DataMachine\Engine\Bundle\AgentBundleArrayAdapter;
use DataMachine\Engine\Bundle\AgentBundleManifest;
use DataMachine\Engine\Bundle\AgentBundleRuntimeDrift;
use DataMachine\Engine\Bundle\AgentBundlePipelineFile;
Expand Down Expand Up @@ -87,7 +87,7 @@ public function export( string $slug ): array {
}

$agent = is_array( $result['agent'] ?? null ) ? $result['agent'] : array();
$bundle = AgentBundleLegacyAdapter::to_legacy_bundle( $directory );
$bundle = AgentBundleArrayAdapter::to_array_bundle( $directory );
$bundle['abilities_manifest'] = $this->collect_abilities_manifest();
$bundle['agent']['site_scope'] = (string) ( $agent['site_scope'] ?? 'site' );

Expand Down Expand Up @@ -255,7 +255,7 @@ public function export_directory_object( string $slug, array $context = array()
* @return \WP_Agent_Package
*/
public static function package_from_bundle( array $bundle ): \WP_Agent_Package {
return AgentPackageProjection::from_legacy_bundle( $bundle );
return AgentPackageProjection::from_array_bundle( $bundle );
}

/**
Expand Down Expand Up @@ -1387,7 +1387,7 @@ public function from_json( string $json ): ?array {
*/
public function to_directory( array $bundle, string $directory ): bool {
try {
AgentBundleLegacyAdapter::from_legacy_bundle( $bundle )->write( $directory );
AgentBundleArrayAdapter::from_array_bundle( $bundle )->write( $directory );
return true;
} catch ( \Throwable $e ) {
return false;
Expand All @@ -1402,7 +1402,7 @@ public function to_directory( array $bundle, string $directory ): bool {
*/
public function from_directory( string $directory ): ?array {
try {
return AgentBundleLegacyAdapter::to_legacy_bundle( AgentBundleDirectory::read( $directory ) );
return AgentBundleArrayAdapter::to_array_bundle( AgentBundleDirectory::read( $directory ) );
} catch ( BundleValidationException $e ) {
unset( $e );
// Fall through to the legacy monolithic manifest reader for old exports.
Expand Down
29 changes: 6 additions & 23 deletions inc/Core/Database/Chat/Chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,7 @@ public static function ensure_agent_id_column(): void {
}

/**
* Ensure the mode column exists, migrating from legacy `context` (or even
* older `agent_type`) columns if present.
*
* Idempotent. Existing rows keep their values under the new `mode` name.
* Ensure the mode column and indexes exist.
*
* @return void
*/
Expand All @@ -161,31 +158,17 @@ public static function ensure_mode_column(): void {
$table_name = self::get_prefixed_table_name();

if ( ! self::column_exists( $table_name, 'mode', $wpdb ) ) {
if ( self::column_exists( $table_name, 'context', $wpdb ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( $wpdb->prepare( 'ALTER TABLE %i CHANGE COLUMN context mode VARCHAR(20) NOT NULL DEFAULT %s', $table_name, 'chat' ) );
} elseif ( self::column_exists( $table_name, 'agent_type', $wpdb ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( $wpdb->prepare( 'ALTER TABLE %i CHANGE COLUMN agent_type mode VARCHAR(20) NOT NULL DEFAULT %s', $table_name, 'chat' ) );
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
// `AFTER <col>` is MySQL-only; SQLite (Studio) rejects it. Column position
// is cosmetic — both engines accept the bare ADD COLUMN form.
$wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ADD COLUMN mode VARCHAR(20) NOT NULL DEFAULT %s', $table_name, 'chat' ) );
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
// `AFTER <col>` is MySQL-only; SQLite (Studio) rejects it. Column position
// is cosmetic — both engines accept the bare ADD COLUMN form.
$wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ADD COLUMN mode VARCHAR(20) NOT NULL DEFAULT %s', $table_name, 'chat' ) );
}

// Idempotent index normalization: drop legacy indexes, add new — only when needed.
// Idempotent index normalization: add current indexes when needed.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
$indexes = $wpdb->get_results( $wpdb->prepare( 'SHOW INDEX FROM %i', $table_name ) );
$existing_keys = array_unique( array_column( $indexes, 'Key_name' ) );

foreach ( array( 'agent_type', 'user_agent', 'context', 'user_context' ) as $legacy_key ) {
if ( in_array( $legacy_key, $existing_keys, true ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( $wpdb->prepare( 'ALTER TABLE %i DROP KEY ' . $legacy_key, $table_name ) );
}
}
if ( ! in_array( 'mode', $existing_keys, true ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ADD KEY mode (mode)', $table_name ) );
Expand Down
28 changes: 7 additions & 21 deletions inc/Core/OAuth/OAuth2Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ public function create_state( string $provider_key, array $payload = array() ):
$serialized_size = strlen( maybe_serialize( $payload ) );
if ( $serialized_size > self::MAX_PAYLOAD_SIZE ) {
throw new \InvalidArgumentException(
sprintf(
'OAuth state payload exceeds maximum size (%d bytes > %d bytes).',
$serialized_size,
self::MAX_PAYLOAD_SIZE
esc_html(
sprintf(
'OAuth state payload exceeds maximum size (%d bytes > %d bytes).',
$serialized_size,
self::MAX_PAYLOAD_SIZE
)
)
);
}
Expand Down Expand Up @@ -104,10 +106,6 @@ public function create_state( string $provider_key, array $payload = array() ):
* Returns the caller-defined payload array on success, or false on failure.
* The transient is consumed (deleted) on successful verification.
*
* Backward-compatible: legacy plain-string transients (from in-flight
* authorizations during the deploy window) are handled gracefully —
* they verify correctly and return an empty payload array.
*
* IMPORTANT: The return type changed from `bool` to `array|false`.
* Callers MUST use `false !== $oauth2->verify_state(...)` instead of
* the previous `if ( $oauth2->verify_state(...) )` pattern, because
Expand All @@ -133,18 +131,6 @@ public function verify_state( string $provider_key, string $state ) {
return false;
}

// Backward compat: legacy plain-string state (pre-payload era).
if ( is_string( $record ) ) {
if ( hash_equals( $record, $state ) ) {
delete_transient( "datamachine_{$provider_key}_oauth_state" );
$this->log_state_verification( $provider_key, true );
return array();
}
$this->log_state_verification( $provider_key, false );
return false;
}

// New structured record format.
if ( ! is_array( $record ) || empty( $record['nonce'] ) ) {
$this->log_state_verification( $provider_key, false );
return false;
Expand Down Expand Up @@ -263,6 +249,7 @@ public function get_pkce_verifier( string $provider_key ): ?string {
* @return string Base64url-encoded string.
*/
private function base64url_encode( string $data ): string {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- OAuth PKCE/state requires RFC 4648 base64url encoding.
return rtrim( strtr( base64_encode( $data ), '+/', '-_' ), '=' );
}

Expand Down Expand Up @@ -775,5 +762,4 @@ private function exchange_token( string $token_url, array $params ) {

return $token_data;
}

}
57 changes: 15 additions & 42 deletions inc/Core/Steps/FlowStepConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -364,70 +364,43 @@ public static function normalizeHandlerShape( array $step_config ): array {
$is_multi = self::isMultiHandler( $step_config );
$step_type = $step_config['step_type'] ?? '';

$legacy_slugs = self::sanitizeSlugList( is_array( $step_config['handler_slugs'] ?? null ) ? $step_config['handler_slugs'] : array() );
$legacy_configs = is_array( $step_config['handler_configs'] ?? null ) ? $step_config['handler_configs'] : array();
$config_slugs = self::sanitizeSlugList( array_keys( $legacy_configs ) );
$scalar_slug = is_string( $step_config['handler_slug'] ?? null ) ? $step_config['handler_slug'] : '';
$scalar_config = is_array( $step_config['handler_config'] ?? null ) ? $step_config['handler_config'] : array();
$scalar_slug = is_string( $step_config['handler_slug'] ?? null ) ? $step_config['handler_slug'] : '';
$scalar_config = is_array( $step_config['handler_config'] ?? null ) ? $step_config['handler_config'] : array();
$handler_slugs = self::sanitizeSlugList( is_array( $step_config['handler_slugs'] ?? null ) ? $step_config['handler_slugs'] : array() );
$handler_configs = is_array( $step_config['handler_configs'] ?? null ) ? $step_config['handler_configs'] : array();

unset( $step_config['handler'], $step_config['handler_slug'], $step_config['handler_slugs'], $step_config['handler_config'], $step_config['handler_configs'] );

if ( ! $uses_handler ) {
$config = $scalar_config;
if ( empty( $config ) && '' !== $step_type && isset( $legacy_configs[ $step_type ] ) && is_array( $legacy_configs[ $step_type ] ) ) {
$config = $legacy_configs[ $step_type ];
}
if ( ! empty( $config ) ) {
$step_config['handler_config'] = $config;
if ( ! empty( $scalar_config ) ) {
$step_config['handler_config'] = $scalar_config;
}
return $step_config;
}

if ( $is_multi ) {
$slugs = $legacy_slugs;
if ( empty( $slugs ) && '' !== $scalar_slug ) {
$slugs = array( $scalar_slug );
}
if ( empty( $slugs ) && ! empty( $config_slugs ) ) {
$slugs = $config_slugs;
}

$configs = $legacy_configs;
if ( empty( $configs ) && ! empty( $scalar_config ) && ! empty( $slugs ) ) {
$configs = array( $slugs[0] => $scalar_config );
if ( ! empty( $handler_slugs ) ) {
$step_config['handler_slugs'] = $handler_slugs;
}

if ( ! empty( $slugs ) ) {
$step_config['handler_slugs'] = $slugs;
}
if ( ! empty( $configs ) ) {
$step_config['handler_configs'] = $configs;
if ( ! empty( $handler_configs ) ) {
$step_config['handler_configs'] = $handler_configs;
}
return $step_config;
}

$slug = '' !== $scalar_slug ? $scalar_slug : ( $legacy_slugs[0] ?? ( $config_slugs[0] ?? '' ) );
if ( '' !== $slug ) {
$step_config['handler_slug'] = $slug;
}

$config = $scalar_config;
if ( empty( $config ) && '' !== $slug && isset( $legacy_configs[ $slug ] ) && is_array( $legacy_configs[ $slug ] ) ) {
$config = $legacy_configs[ $slug ];
if ( '' !== $scalar_slug ) {
$step_config['handler_slug'] = $scalar_slug;
}
if ( ! empty( $config ) || '' !== $slug ) {
$step_config['handler_config'] = $config;
if ( ! empty( $scalar_config ) || '' !== $scalar_slug ) {
$step_config['handler_config'] = $scalar_config;
}
return $step_config;
}

/**
* Get the AI step's enabled tools.
*
* Reads the dedicated `enabled_tools` field. The pre-Phase 2b shape
* (AI tools stored under `handler_slugs`) is migrated on activation
* by inc/migrations/ai-enabled-tools.php — there is no runtime
* fallback to legacy data.
* Reads the dedicated `enabled_tools` field.
*
* @since 0.81.0
*
Expand Down
Loading
Loading