maybe_upgrade(); // Feature 020 — per-server tool selection. Presence-based storage; the // empty-table state is the correct initial state for a server the seeder // does not touch. Moved above the seeder in 0.3.6 — see the note above. // Co-commit invariant with the Main.php request-time boot below // (DEC-BERLINDB-TABLE-REQUEST-BOOT). MCPServerToolTable::instance()->maybe_upgrade(); CliAuthLogTable::instance()->maybe_upgrade(); // F096 — the activity log. Empty-table state is the correct initial // state: both enable toggles ship off, so nothing writes until an // operator opts in. ActivityLogTable::instance()->maybe_upgrade(); // Feature 017 — per-server ability exposure overrides. No seeder call — // the empty-table state IS the correct backwards-compatible initial state. MCPServerAbilityTable::instance()->maybe_upgrade(); // F037 — per-server key-value meta. This was the only BerlinDB table the // activator did not create: it relied on admin_init@3 // (Main::reconcile_database_schemas) to appear, so between activation and // the first wp-admin request every read hit "table doesn't exist". Any // front-end or REST request in that window — exactly where the Embeds // feature reads `_embeds_enabled` — ran against a missing table. MCPServerMetaTable::instance()->maybe_upgrade(); // F095 — OAuth tables. Created here for exactly the reason the F037 note // above records: a table that waits for admin_init@3 does not exist // between activation and the first wp-admin request, and every read in // that window hits "table doesn't exist". For OAuth that window is the // token endpoint, so the failure would be connected clients unable to // authenticate. Co-commit invariant with the Main.php request-time boot // (DEC-BERLINDB-TABLE-REQUEST-BOOT). // // Order is Tokens -> AuthCodes -> Clients per D31 (F032). OAuthTokensTable::instance()->maybe_upgrade(); OAuthAuthCodesTable::instance()->maybe_upgrade(); OAuthClientsTable::instance()->maybe_upgrade(); ConnectorApprovedUsersTable::instance()->maybe_upgrade(); // F091 — heal any column a migration could not deliver, THEN give the // just-created columns their values, and only then seed. All three // orderings are load-bearing: the backfill consumes the reconciler's // created-column report, and the seeder writes columns both of them // repair. $created = SchemaReconciler::maybe_reconcile( array( MCPServerTable::instance(), CliAuthLogTable::instance(), ActivityLogTable::instance(), MCPServerAbilityTable::instance(), MCPServerToolTable::instance(), MCPServerMetaTable::instance(), ) ); CreatedColumnBackfill::apply( $created['acrossai_mcp_servers'] ?? array() ); DefaultServerSeeder::seed(); // F095 — copy OAuth rows from the companion. After the seeder for the // same reason as in Main::reconcile_database_schemas(): destinations must // be present and current first. OAuthDataMigration::maybe_migrate(); // F095 — daily expiry sweep for OAuth tokens and auth codes. Hook name is // a contract preserved from before the F040 split, and Deactivator already // clears this exact name. if ( ! wp_next_scheduled( 'acrossai_mcp_manager_oauth_cleanup' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'acrossai_mcp_manager_oauth_cleanup' ); } // F096 — daily retention + ceiling sweep for the activity log. Scheduled // only while a bound is configured; `sync_schedule()` also unschedules // when both retention periods and the ceiling are 0, so an operator who // turns everything off does not keep a daily event that deletes nothing. ActivityLogCleanup::sync_schedule(); // Feature 015 — Access Control v2 adoption. Create the // {$wpdb->prefix}mcp_access_control table via the vendor-owned // RuleTable BerlinDB subclass. SEC-015-001 defense-in-depth: the // class_exists guard protects against a vendor-uninstall-then-reactivate // race where the Composer package was removed since the last activation. if ( class_exists( WPB_AccessControl_RuleTable::class ) ) { global $wpdb; $ac_table = $wpdb->prefix . AcrossAI_MCP_Access_Control::TABLE_SLUG . '_access_control'; $version_op = 'wpb_ac_' . AcrossAI_MCP_Access_Control::TABLE_SLUG . '_db_version'; // Phantom-version guard (WORKLOG 2026-07-02 lesson). The vendor's // RuleTable does NOT ship this guard (unlike our F011 subclasses), // so if the operator manually dropped the table, BerlinDB's // needs_upgrade() would return false (version option still stamped) // and silently skip the CREATE. Wiping the option first forces a // fresh install. SILENT — no error_log, no admin notice. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $exists = (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $ac_table ) ); if ( '' === $exists ) { delete_option( $version_op ); } ( new WPB_AccessControl_RuleTable( AcrossAI_MCP_Access_Control::TABLE_SLUG ) )->maybe_upgrade(); } // FrontendAuth — Phase 6.0 absorbs the full class. Activator delegates // rewrite-rule registration so the pattern definition lives in // exactly one place (loader contract). if ( class_exists( FrontendAuth::class ) ) { FrontendAuth::instance()->register_rewrite_rule(); } // npm / CLI login is enabled by default on fresh installs. // add_option() is idempotent — it fails silently if the row already // exists, so operators who explicitly stored `false` (unchecked // + saved) keep their choice; only sites with no stored row pick // up the new default. register_setting()'s metadata default in // SettingsMenu.php stays `false` so the PHPUnit assertion at // tests/phpunit/Admin/SettingsMenuTest.php:81 still holds — the // row-is-always-present-after-activation invariant is what makes // the runtime behaviour "enabled by default". add_option( 'acrossai_mcp_npm_login_enabled', 1 ); flush_rewrite_rules(); } }