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
72 changes: 68 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ To create an installable ZIP: `cd` to the repo root and run `zip -r digitizer-si
| `Aura_Worker_Redact` | `includes/class-aura-worker-redact.php` | Agent read redaction (2.18.0, #419): detectors (`redact`, `redact_text`), audience (`is_audience`), the `rest_pre_echo_response` read seam (`filter_echo`), the `rest_request_before_callbacks` placeholder guard and unredacted-grant check (`before_callbacks`, `grant_shape`), counters, `status_fragment` |
| `Aura_Worker_Redact_Decode` | `includes/class-aura-worker-redact-decode.php` | Redaction stage 2 (2.18.2, #113), pure: `decode_layers()` (the raw run, then every pass whose result changed, up to `MAX_DECODE_PASSES` layers of HTML5 character references, `%XX` and JSON escapes — `\\`, `\/`, `\uXXXX`; null past the check pass or the `MAX_DECODE_GROWTH` bound), `decode_run()` (its last layer), `decode_pass()`, `html5_code_point()`, `utf8()` |
| `Aura_Worker_Redact_Idna` | `includes/class-aura-worker-redact-idna.php` | Redaction stage 2 view 2 (2.18.3, #116), pure, GENERATED — `map()` applies Unicode 18.0.0's UTS-46 hostname mappings and deletions with one `strtr()`; regenerate with `php bin/generate-idna-map.php` (pinned URL + SHA-256), then run the tests; to move to a new Unicode version, bump `IDNA_VERSION`, `IDNA_URL` and `IDNA_SHA256` in the script, regenerate, then update the three counts AND the MAP content pin in `RedactIdnaTest` |
| `Aura_Worker_Install_Ledger` | `includes/class-aura-worker-install-ledger.php` | The install ledger (2.22.0, Aura spec 2026-09-21 §4): observes `upgrader_package_options` (per-run token in `hook_extra`), `upgrader_pre_download` (`PHP_INT_MAX`: source + context under the token) and `upgrader_install_package_result` (the entry) for plugin/theme runs only; records and never decides. Ring of 200 entries / 90 days in `aura_worker_install_ledger` (+ `_state`), not autoloaded, network options on multisite; `report()` is `audit_agent_code`'s `installs`. `as_siteagent()` wraps the updater's four upgrader calls. `auto_update` = `doing_action( 'wp_maybe_auto_update' )`, never the skin (EMCP Pro installs with `Automatic_Upgrader_Skin`) |
| `Aura_Worker_Install_Ledger` | `includes/class-aura-worker-install-ledger.php` | The install ledger (2.22.0, Aura spec 2026-09-21 §4): observes `upgrader_package_options` (per-run token in `hook_extra`), `upgrader_pre_download` (`PHP_INT_MAX`: source + context under the token) and `upgrader_install_package_result` (the entry) for plugin/theme runs only; records and never decides. Ring of 200 entries / 90 days in `aura_worker_install_ledger` (+ `_state`), not autoloaded, network options on multisite; `report()` is `audit_agent_code`'s `installs`. `as_siteagent()` wraps the updater's three upgrader calls (self-update install, theme upgrade, and `upgrade_plugin_keeping_activation()` — the one generic plugin upgrade since 2.23.1). `auto_update` = `doing_action( 'wp_maybe_auto_update' )`, never the skin (EMCP Pro installs with `Automatic_Upgrader_Skin`) |

### Initialization Flow

Expand Down Expand Up @@ -128,7 +128,7 @@ All routes are under `/wp-json/aura/v1/`.
| Method | Endpoint | Parameters | Description |
|--------|----------|------------|-------------|
| `POST` | `/update/core` | — | Update WordPress core |
| `POST` | `/update/plugin` | `plugin` (required, string) | Update a specific plugin by file path (e.g. `akismet/akismet.php`) |
| `POST` | `/update/plugin` | `plugin` (required, string) | Update a specific plugin by file path (e.g. `akismet/akismet.php`). Keeps an active plugin active (`reactivated`); `409 aura_use_self_update` for SiteAgent itself, `409 aura_no_update_offered`, `500 aura_update_failed`, `500 aura_reactivation_failed` (updated, but left inactive) — see "Generic plugin updates keep the plugin active" |
| `POST` | `/update/theme` | `theme` (required, string) | Update a specific theme by slug |
| `POST` | `/update/translations` | — | Bulk update all translations |
| `POST` | `/update/database` | — | Run `wp_upgrade()` / `dbDelta()` |
Expand Down Expand Up @@ -176,6 +176,70 @@ and `app_password_probe_unproven: { count, at, owner }` (bounded, saturating) wh
could not prove an Application Password gone. Both are always JSON **objects** — the key's
presence is the signal, and the shape must not change with its contents.

### Generic plugin updates keep the plugin active (2.23.1)

`Plugin_Upgrader::upgrade()` hooks core's `deactivate_plugin_before_upgrade` at
`upgrader_pre_install`, which silently deactivates the target whenever
`wp_doing_cron()` is false — a REST request included. wp-admin pairs it with the
skin's re-activation iframe; nothing on the REST path did, so every plugin updated
through `/aura/v1/update/plugin`, `/aura/v2/update/batch` or `update_plugin_safely`
came back **inactive** (on success, and on a failure after pre_install), and an
update of SiteAgent itself removed every `aura/*` route.

- **One call site.** `Aura_Worker_Updater::upgrade_plugin_keeping_activation()` is
the only generic `Plugin_Upgrader::upgrade()` call for plugins; `update_plugin()`
(inside `guarding_self()` + `heartbeat_during()`) and `update_single_plugin()`
(the batch entry) both go through it, under `Aura_Worker_Install_Ledger::as_siteagent()`.
- **Activation.** It records `is_plugin_active()` and, on multisite,
`is_plugin_active_for_network()` before the upgrade; in a `finally` — success,
failure, a `WP_Error`, `null`/`false`, a throw — a plugin that was active and is
not now gets `activate_plugin( $f, '', $was_network, true )` (silent). A plugin
that was inactive is never activated. Results carry `reactivated` (true only when
re-activation was needed and succeeded) and `reactivation_error` (the activation
`WP_Error`'s message) when it failed; batch entries copy both. **A failed
re-activation fails the update** (Codex r2 on #143): after a successful upgrade it
answers `success: false`, `code: aura_reactivation_failed` (REST `500` — the site
needs attention), `updated: true` (the files did change) and an `error` naming the
activation message and "Activate it from wp-admin"; the batch entry is `failed`
with that code and `update_plugin_safely` reports the failure. When the upgrade
itself failed too, the upgrade's code and error stay primary and
`reactivation_error` is attached beside them.
- **SiteAgent is refused.** `SELF_PLUGIN_FILE` answers
`{ success: false, code: aura_use_self_update }` (REST `409`, message pointing at
`POST /aura/v1/self-update`) and never reaches the upgrader or the refresh. The
refusal sits inside the work, so the multisite refusal
(`aura_self_update_multisite_unsupported`), the host refusal and the busy claim
(`in_progress: true`) keep their precedence. A batch refuses only that entry
(`status: failed`, `code`); the other entries run. There is no internal re-route
to `self_update()`. On the batch path the entry is refused in
`batch_update_plugins()`'s guarded work, **before** the recovery helper is built
and before `batch_update_one()`'s backup (Codex r1 on #143) — no backup of
SiteAgent is taken, and an unwritable backup directory still answers
`aura_use_self_update`. The shared helper refuses it again (defence in depth).
The SA#80 lease/heartbeat for a SiteAgent batch entry is therefore unreachable;
the in-phase heartbeat tests drive `self_update()`'s `install()` instead.
- **The result.** `null` is `run()` failing (core's `$result` has no default and
run()'s `WP_Error` is discarded): `aura_update_failed`, `error` = "Update failed:"
plus the skin's reason (`get_errors()` where the skin has it, then the last three
`get_upgrade_messages()`), never "No update available". `false` is
`aura_no_update_offered` (REST `409`, not `500`). A `WP_Error` keeps its code and
message (REST `500`).
- **The offer.** When `update_plugins->response[$f]` is missing, the list is forced
stale — `last_checked = 0` written back with `set_site_transient()`, never deleted
(that would drop every other plugin's offer), because `wp_update_plugins()` returns
early inside its timeout (Codex r1 on #143) — then `wp_update_plugins()`
runs once and the transient is re-read before `upgrade()`. That write is core's
key, acknowledged as a foreign dynamic write in `UninstallCoverageTest` (the GitHub-hosted
plugins' entries are injected at read time, by code that may not be loaded).
`offered_version` (the entry's `new_version`) is reported when there is an entry.
- **Tests.** `UpdatePluginPreservesActivationTest`, `UpdatePluginSelfRefusalTest`,
`UpdatePluginResultMappingTest`, `RestUpdatePluginStatusTest`. Stubs:
`_upgrade_result` (upgrade()'s return; `null` is a failed run), `_upgrade_effect`
(receives the upgrader and the plugin file; models the pre_install deactivation),
`_upgrade_messages`, `_activate_plugin_calls` / `_activate_plugin_result`,
`_site_transients`, `_wp_update_plugins_effect`, and `_updater_calls` (the order of
`wp_update_plugins` and `Plugin_Upgrader::upgrade:<file>`).

### Plugin-file mutations on hosts that block `.php` writes (SA#95)

On some hosts (WP Engine, proven live) PHP gets `Permission denied` creating,
Expand Down Expand Up @@ -661,13 +725,13 @@ All options are cleaned up in `uninstall.php`.
- **Always use `sanitize_text_field()`** or appropriate sanitizer on user input
- **Always use `esc_attr()`, `esc_html()`, `esc_url()`** for output escaping
- **Use `hash_equals()`** for all token/secret comparisons (timing-safe)
- **Validate WordPress Upgrader return values thoroughly** — `Plugin_Upgrader::upgrade()` can return `true`, `false`, `null`, `WP_Error`, or an array depending on the outcome. Always check for `is_wp_error()`, `false === $result`, and `null === $result` before assuming success.
- **Validate WordPress Upgrader return values thoroughly** — `Plugin_Upgrader::upgrade()` can return `true`, `false`, `null`, `WP_Error`, or an array depending on the outcome. Always check for `is_wp_error()`, `false === $result`, and `null === $result` before assuming success. For `upgrade()`, `false` means nothing is offered (no `update_plugins` entry; nothing touched) and `null` means `run()` failed — the reason is only in the skin. `upgrade()` also deactivates its target at pre_install; plugin upgrades go through `upgrade_plugin_keeping_activation()` (2.23.1).
- **Use `wp_unslash()` before `sanitize_*()`** on `$_SERVER` values

### Error Handling

- Return structured arrays from updater methods: `array( 'success' => bool, 'message' => string )` or `array( 'success' => false, 'error' => string )`
- REST handlers wrap results in `WP_REST_Response` with appropriate HTTP status codes (200, 404, 500)
- REST handlers wrap results in `WP_REST_Response` with appropriate HTTP status codes (200, 404, 409, 500) — a designated "nothing to do / wrong path" code is 409, not 500 (e.g. `aura_no_update_offered`, `aura_use_self_update` on `/update/plugin`)
- Use `WP_Error` objects in security/permission callbacks — WordPress REST API will convert these to proper error responses

### Dependency Loading
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</a>
<img src="https://img.shields.io/badge/WordPress-6.2%E2%80%937.1-21759b?logo=wordpress" alt="WordPress" />
<img src="https://img.shields.io/badge/PHP-7.4%2B-777bb4?logo=php" alt="PHP" />
<img src="https://img.shields.io/badge/Stable-2.23.0-green" alt="Stable" />
<img src="https://img.shields.io/badge/Stable-2.23.1-green" alt="Stable" />
</p>

---
Expand Down Expand Up @@ -239,6 +239,12 @@ These plug straight into **Aura's Fleet MCP Gateway**: read tools run on demand,

## Changelog

### 2.23.1

- **A generic plugin update keeps the plugin active.** `Plugin_Upgrader::upgrade()` deactivates its target at `upgrader_pre_install` outside cron, and only wp-admin's skin re-activates it, so every plugin updated through `POST /aura/v1/update/plugin`, `POST /aura/v2/update/batch` or `update_plugin_safely` came back inactive. One helper now records the activation state (network activation included) before the upgrade and re-activates silently in a `finally` — on success, failure and a throw — and never activates a plugin that was inactive. Results carry `reactivated` and, when activation fails, `reactivation_error`.
- **SiteAgent is refused on the generic paths.** Its own file answers `aura_use_self_update` (REST `409`, pointing at `POST /aura/v1/self-update`) after the multisite, host and busy-claim refusals; the upgrader is never reached. A batch refuses only that entry.
- **The upgrader's result is reported truthfully.** `null` (a failed `run()`) is `aura_update_failed` with the skin's reason, never "No update available"; `false` is `aura_no_update_offered` (REST `409`, not `500`); a `WP_Error` keeps its code and message. A plugin missing from `update_plugins` gets one `wp_update_plugins()` before the upgrade, and results carry `offered_version` when an update is offered.

### 2.23.0

- **`publish-document` promoting an autosave is a custom-CSS write** (P7.2, references re-verification 2026-09-28 §4). Elementor 4.3.2's `publish-document` restores the calling user's pending autosave, page settings and CSS included, so a listed promoter (`AUTOSAVE_PROMOTERS`) declares a conservative `custom_css` touch when — and only when — an autosave exists to promote (owner decision). An Elementor without `promote_pending_autosave` promotes nothing; an unresolved target or a missing `wp_get_post_autosave()` fails conservative. (#141)
Expand Down
4 changes: 2 additions & 2 deletions digitizer-site-worker/digitizer-site-worker.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: SiteAgent for Aura
* Plugin URI: https://my-aura.app/siteagent
* Description: Remote site management agent for Aura dashboard. Enables secure updates, health monitoring, and maintenance operations via REST API.
* Version: 2.23.0
* Version: 2.23.1
* Requires at least: 6.2
* Requires PHP: 7.4
* Author: Digitizer
Expand All @@ -18,7 +18,7 @@
exit;
}

define( 'AURA_WORKER_VERSION', '2.23.0' );
define( 'AURA_WORKER_VERSION', '2.23.1' );
define( 'AURA_WORKER_FILE', __FILE__ );
define( 'AURA_WORKER_DIR', plugin_dir_path( __FILE__ ) );

Expand Down
10 changes: 9 additions & 1 deletion digitizer-site-worker/includes/class-aura-worker-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -747,8 +747,16 @@ public function update_plugin( $request ) {
) ), 404 );
}

// 409, not 500, for the two outcomes that are not a broken site
// (2.23.1): nothing is offered to install (`aura_no_update_offered`),
// and SiteAgent's own file, which updates only through POST
// /aura/v1/self-update (`aura_use_self_update`).
$result = $this->updater->update_plugin( $plugin_file );
$status = $result['success'] ? 200 : 500;
$status = 200;
if ( ! $result['success'] ) {
$code = isset( $result['code'] ) ? (string) $result['code'] : '';
$status = in_array( $code, array( 'aura_no_update_offered', 'aura_use_self_update' ), true ) ? 409 : 500;
}
return new WP_REST_Response( Aura_Worker_Rules::with_warnings( $result ), $status );
}

Expand Down
Loading
Loading