container = $container ?? new Container(); } /** * Shared instance accessor. */ public static function instance(): Plugin { if ( null === self::$instance ) { self::$instance = new Plugin(); } return self::$instance; } /** * Drop the shared instance (test isolation only). */ public static function reset_instance(): void { self::$instance = null; } /** * Register WordPress hooks. */ public function boot(): void { // Ahead of the services, so nothing queries a table the running code // expects but the installed schema has not got yet. add_action( 'plugins_loaded', array( $this, 'maybe_upgrade_schema' ), 0 ); add_action( 'plugins_loaded', array( $this, 'register_services' ) ); } /** * Apply pending schema migrations. * * The activation hook is not enough: WordPress reactivates silently after * an update, so a schema change shipped in a release would otherwise never * reach an existing site. Costs one autoloaded option read per request. */ public function maybe_upgrade_schema(): void { ( new Database\Schema() )->maybe_upgrade(); } /** * Bind services into the container and activate request-time subsystems. * * Subsystems are added here story by story (data layer, auth, * interception, policy, pending, rollback, REST controllers). */ public function register_services(): void { $this->container->set( 'repository.agents', static function (): Repository\AgentRepository { global $wpdb; return new Repository\AgentRepository( $wpdb ); } ); $this->container->set( 'repository.policies', static function (): Repository\PolicyRepository { global $wpdb; return new Repository\PolicyRepository( $wpdb ); } ); $this->container->set( 'repository.ledger', static function (): Repository\LedgerRepository { global $wpdb; return new Repository\LedgerRepository( $wpdb ); } ); $this->container->set( 'repository.pending', static function (): Repository\PendingRepository { global $wpdb; return new Repository\PendingRepository( $wpdb ); } ); $this->container->set( 'repository.snapshots', static function (): Repository\SnapshotRepository { global $wpdb; return new Repository\SnapshotRepository( $wpdb ); } ); $this->container->set( 'ledger.writer', static function ( Container $c ): Ledger\LedgerWriter { $ledger = $c->get( 'repository.ledger' ); assert( $ledger instanceof Repository\LedgerRepository ); return new Ledger\LedgerWriter( $ledger ); } ); $this->container->set( 'auth.tokens', static function ( Container $c ): Auth\TokenService { $agents = $c->get( 'repository.agents' ); assert( $agents instanceof Repository\AgentRepository ); return new Auth\TokenService( $agents ); } ); $this->container->set( 'auth.authenticator', static function ( Container $c ): Auth\AgentAuthenticator { $tokens = $c->get( 'auth.tokens' ); assert( $tokens instanceof Auth\TokenService ); return new Auth\AgentAuthenticator( $tokens ); } ); $this->container->set( 'auth.app_password_binding', static function ( Container $c ): Auth\AppPasswordBinding { $agents = $c->get( 'repository.agents' ); assert( $agents instanceof Repository\AgentRepository ); return new Auth\AppPasswordBinding( $agents ); } ); $this->container->set( 'policy.rate_limiter', static function ( Container $c ): Policy\RateLimiter { $ledger = $c->get( 'repository.ledger' ); assert( $ledger instanceof Repository\LedgerRepository ); return new Policy\RateLimiter( $ledger ); } ); $this->container->set( 'policy.evaluator', static function ( Container $c ): Policy\PolicyEvaluator { $policies = $c->get( 'repository.policies' ); $limiter = $c->get( 'policy.rate_limiter' ); assert( $policies instanceof Repository\PolicyRepository ); assert( $limiter instanceof Policy\RateLimiter ); return new Policy\PolicyEvaluator( $policies, $limiter ); } ); $this->container->set( 'pending.executor', static function (): Pending\ActionExecutor { return new Pending\ActionExecutor(); } ); $this->container->set( 'pending.service', static function ( Container $c ): Pending\PendingActionService { $pending = $c->get( 'repository.pending' ); $ledger = $c->get( 'repository.ledger' ); $executor = $c->get( 'pending.executor' ); assert( $pending instanceof Repository\PendingRepository ); assert( $ledger instanceof Repository\LedgerRepository ); assert( $executor instanceof Pending\ActionExecutor ); return new Pending\PendingActionService( $pending, $ledger, $executor ); } ); $this->container->set( 'rollback.snapshots', static function ( Container $c ): Rollback\SnapshotService { $snapshots = $c->get( 'repository.snapshots' ); assert( $snapshots instanceof Repository\SnapshotRepository ); return new Rollback\SnapshotService( $snapshots ); } ); $this->container->set( 'rollback.service', static function ( Container $c ): Rollback\RollbackService { $ledger = $c->get( 'repository.ledger' ); $snapshots = $c->get( 'repository.snapshots' ); $service = $c->get( 'rollback.snapshots' ); $writer = $c->get( 'ledger.writer' ); assert( $ledger instanceof Repository\LedgerRepository ); assert( $snapshots instanceof Repository\SnapshotRepository ); assert( $service instanceof Rollback\SnapshotService ); assert( $writer instanceof Ledger\LedgerWriter ); return new Rollback\RollbackService( $ledger, $snapshots, $service, $writer ); } ); $this->container->set( 'policy.lockdown', static function (): Policy\Lockdown { return new Policy\Lockdown(); } ); $this->container->set( 'intercept.pipeline', static function ( Container $c ): Intercept\InterceptionPipeline { $evaluator = $c->get( 'policy.evaluator' ); $writer = $c->get( 'ledger.writer' ); $pending = $c->get( 'pending.service' ); $ledger = $c->get( 'repository.ledger' ); $snapshots = $c->get( 'rollback.snapshots' ); $lockdown = $c->get( 'policy.lockdown' ); assert( $evaluator instanceof Policy\PolicyEvaluator ); assert( $writer instanceof Ledger\LedgerWriter ); assert( $pending instanceof Pending\PendingActionService ); assert( $ledger instanceof Repository\LedgerRepository ); assert( $snapshots instanceof Rollback\SnapshotService ); assert( $lockdown instanceof Policy\Lockdown ); return new Intercept\InterceptionPipeline( $evaluator, $writer, $pending, $ledger, $snapshots, $lockdown, new Ledger\EntityLabeler() ); } ); $this->container->set( 'intercept.ability', static function ( Container $c ): Intercept\AbilityInterceptor { $pipeline = $c->get( 'intercept.pipeline' ); assert( $pipeline instanceof Intercept\InterceptionPipeline ); return new Intercept\AbilityInterceptor( $pipeline ); } ); $authenticator = $this->container->get( 'auth.authenticator' ); assert( $authenticator instanceof Auth\AgentAuthenticator ); $authenticator->register(); // Registered alongside the token authenticator: a bound application // password is an agent identity too, and both doors must be open // before rest_pre_dispatch decides whether the caller is an agent. $app_password_binding = $this->container->get( 'auth.app_password_binding' ); assert( $app_password_binding instanceof Auth\AppPasswordBinding ); $app_password_binding->register(); $this->container->set( 'intercept.rest', static function ( Container $c ): Intercept\RestGatekeeper { $pipeline = $c->get( 'intercept.pipeline' ); assert( $pipeline instanceof Intercept\InterceptionPipeline ); return new Intercept\RestGatekeeper( $pipeline ); } ); $ability_interceptor = $this->container->get( 'intercept.ability' ); assert( $ability_interceptor instanceof Intercept\AbilityInterceptor ); $ability_interceptor->register(); $rest_gatekeeper = $this->container->get( 'intercept.rest' ); assert( $rest_gatekeeper instanceof Intercept\RestGatekeeper ); $rest_gatekeeper->register(); $this->container->set( 'intercept.backstop', static function ( Container $c ): Intercept\EffectBackstop { $pipeline = $c->get( 'intercept.pipeline' ); assert( $pipeline instanceof Intercept\InterceptionPipeline ); return new Intercept\EffectBackstop( $pipeline ); } ); $effect_backstop = $this->container->get( 'intercept.backstop' ); assert( $effect_backstop instanceof Intercept\EffectBackstop ); $effect_backstop->register(); $this->container->set( 'ledger.shadow_recorder', static function ( Container $c ): Ledger\ShadowAutomationRecorder { $writer = $c->get( 'ledger.writer' ); assert( $writer instanceof Ledger\LedgerWriter ); return new Ledger\ShadowAutomationRecorder( $writer ); } ); $shadow_recorder = $this->container->get( 'ledger.shadow_recorder' ); assert( $shadow_recorder instanceof Ledger\ShadowAutomationRecorder ); $shadow_recorder->register(); $this->container->set( 'ledger.rejection_recorder', static function ( Container $c ): Ledger\AbilityRejectionRecorder { $writer = $c->get( 'ledger.writer' ); assert( $writer instanceof Ledger\LedgerWriter ); return new Ledger\AbilityRejectionRecorder( $writer ); } ); $rejection_recorder = $this->container->get( 'ledger.rejection_recorder' ); assert( $rejection_recorder instanceof Ledger\AbilityRejectionRecorder ); // Before abilities register, so the pre-7.1 wrapper reaches them. $rejection_recorder->register(); $this->container->set( 'pending.cron', static function ( Container $c ): Pending\ExpiryCron { $service = $c->get( 'pending.service' ); assert( $service instanceof Pending\PendingActionService ); return new Pending\ExpiryCron( $service ); } ); $cron = $this->container->get( 'pending.cron' ); assert( $cron instanceof Pending\ExpiryCron ); $cron->register(); $this->container->set( 'ledger.retention', static function ( Container $c ): Ledger\RetentionService { $ledger = $c->get( 'repository.ledger' ); assert( $ledger instanceof Repository\LedgerRepository ); $pending = $c->get( 'repository.pending' ); assert( $pending instanceof Repository\PendingRepository ); $snapshots = $c->get( 'repository.snapshots' ); assert( $snapshots instanceof Repository\SnapshotRepository ); $writer = $c->get( 'ledger.writer' ); assert( $writer instanceof Ledger\LedgerWriter ); return new Ledger\RetentionService( $ledger, $pending, $snapshots, $writer ); } ); $this->container->set( 'ledger.retention_cron', static function ( Container $c ): Ledger\RetentionCron { $service = $c->get( 'ledger.retention' ); assert( $service instanceof Ledger\RetentionService ); return new Ledger\RetentionCron( $service ); } ); $retention_cron = $this->container->get( 'ledger.retention_cron' ); assert( $retention_cron instanceof Ledger\RetentionCron ); $retention_cron->register(); $this->register_cli(); $this->container->set( 'alert.anomaly', static function (): Alert\AnomalyNotifier { return new Alert\AnomalyNotifier(); } ); $notifier = $this->container->get( 'alert.anomaly' ); assert( $notifier instanceof Alert\AnomalyNotifier ); $notifier->register(); // Its own listener on the same events rather than a branch inside the // mailer: someone who turned email off and a webhook on asked for // exactly that, and neither switch should be able to silence the other. $this->container->set( 'alert.webhook', static function (): Alert\WebhookDispatcher { return new Alert\WebhookDispatcher(); } ); $webhook = $this->container->get( 'alert.webhook' ); assert( $webhook instanceof Alert\WebhookDispatcher ); $webhook->register(); // Registered before the monitor below, so the listener exists by the // time the scan runs on wp_loaded. $this->container->set( 'ledger.drift_recorder', static function ( Container $c ): Ledger\AbilityDriftRecorder { $writer = $c->get( 'ledger.writer' ); assert( $writer instanceof Ledger\LedgerWriter ); return new Ledger\AbilityDriftRecorder( $writer ); } ); $drift_recorder = $this->container->get( 'ledger.drift_recorder' ); assert( $drift_recorder instanceof Ledger\AbilityDriftRecorder ); $drift_recorder->register(); $this->container->set( 'alert.tool_monitor', static function (): Alert\ToolRegistryMonitor { return new Alert\ToolRegistryMonitor(); } ); $tool_monitor = $this->container->get( 'alert.tool_monitor' ); assert( $tool_monitor instanceof Alert\ToolRegistryMonitor ); $tool_monitor->register(); $this->container->set( 'rest.settings', static function ( Container $c ): Rest\SettingsController { $lockdown = $c->get( 'policy.lockdown' ); assert( $lockdown instanceof Policy\Lockdown ); return new Rest\SettingsController( $lockdown ); } ); $this->container->set( 'rest.ledger', static function ( Container $c ): Rest\LedgerController { $ledger = $c->get( 'repository.ledger' ); $snapshots = $c->get( 'rollback.snapshots' ); assert( $ledger instanceof Repository\LedgerRepository ); assert( $snapshots instanceof Rollback\SnapshotService ); $retention = $c->get( 'ledger.retention' ); assert( $retention instanceof Ledger\RetentionService ); return new Rest\LedgerController( $ledger, $snapshots, $retention, new Ledger\EntityLabeler() ); } ); $this->container->set( 'rest.pending', static function ( Container $c ): Rest\PendingController { $pending = $c->get( 'repository.pending' ); $service = $c->get( 'pending.service' ); assert( $pending instanceof Repository\PendingRepository ); assert( $service instanceof Pending\PendingActionService ); return new Rest\PendingController( $pending, $service ); } ); $this->container->set( 'rest.policies', static function ( Container $c ): Rest\PoliciesController { $policies = $c->get( 'repository.policies' ); assert( $policies instanceof Repository\PolicyRepository ); $evaluator = $c->get( 'policy.evaluator' ); assert( $evaluator instanceof Policy\PolicyEvaluator ); $agents = $c->get( 'repository.agents' ); assert( $agents instanceof Repository\AgentRepository ); $ledger = $c->get( 'repository.ledger' ); assert( $ledger instanceof Repository\LedgerRepository ); $retention = $c->get( 'ledger.retention' ); assert( $retention instanceof Ledger\RetentionService ); return new Rest\PoliciesController( $policies, $evaluator, $agents, $ledger, $retention ); } ); $this->container->set( 'rest.agents', static function ( Container $c ): Rest\AgentsController { $agents = $c->get( 'repository.agents' ); $tokens = $c->get( 'auth.tokens' ); assert( $agents instanceof Repository\AgentRepository ); assert( $tokens instanceof Auth\TokenService ); return new Rest\AgentsController( $agents, $tokens ); } ); $this->container->set( 'rest.undo', static function ( Container $c ): Rest\UndoController { $rollback = $c->get( 'rollback.service' ); assert( $rollback instanceof Rollback\RollbackService ); return new Rest\UndoController( $rollback ); } ); $this->container->set( 'scan.checks', static function ( Container $c ): Scan\CheckSet { $agents = $c->get( 'repository.agents' ); assert( $agents instanceof Repository\AgentRepository ); return new Scan\CheckSet( $agents, new Support\AbilityRegistry() ); } ); $this->container->set( 'scan.runner', static function ( Container $c ): Scan\ScanRunner { $checks = $c->get( 'scan.checks' ); assert( $checks instanceof Scan\CheckSet ); return new Scan\ScanRunner( $checks->all() ); } ); $this->container->set( 'rest.scan', static function ( Container $c ): Rest\ScanController { $runner = $c->get( 'scan.runner' ); assert( $runner instanceof Scan\ScanRunner ); return new Rest\ScanController( $runner ); } ); $this->container->set( 'admin.page', static function ( Container $c ): Admin\AdminPage { // Closure, not the repository: the queue is only counted while // the menu is being built, never on plugin load. return new Admin\AdminPage( static function () use ( $c ): int { $pending = $c->get( 'repository.pending' ); assert( $pending instanceof Repository\PendingRepository ); return $pending->count_by_status( 'pending' ); } ); } ); $this->container->set( 'admin.report', static function ( Container $c ): Admin\ReportView { $runner = $c->get( 'scan.runner' ); assert( $runner instanceof Scan\ScanRunner ); return new Admin\ReportView( $runner ); } ); $report_view = $this->container->get( 'admin.report' ); assert( $report_view instanceof Admin\ReportView ); $report_view->register(); $admin_page = $this->container->get( 'admin.page' ); assert( $admin_page instanceof Admin\AdminPage ); $admin_page->register(); add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); } /** * Register admin REST controllers (PRD Epic 8). */ public function register_rest_routes(): void { foreach ( array( 'rest.ledger', 'rest.pending', 'rest.policies', 'rest.agents', 'rest.undo', 'rest.settings', 'rest.scan' ) as $id ) { $controller = $this->container->get( $id ); assert( $controller instanceof \WP_REST_Controller ); $controller->register_routes(); } } /** * The service container. */ public function container(): Container { return $this->container; } /** * Activation callback: install or migrate the database schema. */ public static function activate(): void { ( new Database\Schema() )->install(); } /** * Expose the ledger commands to WP-CLI. * * Guarded so nothing here loads on a web request: WP_CLI does not exist * outside the command line, and the command class references it directly. */ private function register_cli(): void { if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { return; } $this->container->set( 'cli.ledger', static function ( Container $c ): Cli\LedgerCommands { $ledger = $c->get( 'repository.ledger' ); assert( $ledger instanceof Repository\LedgerRepository ); $writer = $c->get( 'ledger.writer' ); assert( $writer instanceof Ledger\LedgerWriter ); $retention = $c->get( 'ledger.retention' ); assert( $retention instanceof Ledger\RetentionService ); $pending = $c->get( 'repository.pending' ); assert( $pending instanceof Repository\PendingRepository ); return new Cli\LedgerCommands( $ledger, $writer, $retention, $pending ); } ); $commands = $this->container->get( 'cli.ledger' ); assert( $commands instanceof Cli\LedgerCommands ); \WP_CLI::add_command( 'agent-firewall', $commands ); } /** * Deactivation callback. * * Only unschedules the cron sweep; deactivation preserves all data * (ADR-6); deletion happens only via opt-in uninstall (AF-9.1). */ public static function deactivate(): void { Pending\ExpiryCron::unschedule(); Ledger\RetentionCron::unschedule(); } }