Release theme: File Manager consolidation + hardening + audit log. Bundles the six-feature series (089 → 094) that turned file-manager/* from a loose collection of read/write primitives into a self-contained subsystem with an admin tab, allowlist-and-content-filter enforcement, and an append-only audit log with pre-image backups. Also cuts the inline .bak.<timestamp> scheme in Delete_File (BREAKING for callers of response.backup — new canonical field is response.backup_path).
Feature 094 — File Manager Audit Log + Backup Harness (partial). Consumes the four Backup & Audit option keys shipped as scaffold in PR #144 and enforced-toggle-only in PR #146. New Audit_Trail utility owns pre-image backups (into wp-content/acrossai-file-manager-backups/<YYYY-MM-DD>/) + append-only log (into wp-content/acrossai-file-manager-logs/acrossai-file-manager.log) + amortised 1-in-10 cleanup + stats. Both storage locations get a Deny from all .htaccess on first creation. All I/O goes through WP_Filesystem.
New ability: file-manager/get-changelog — tails the last N entries (default 100, max 500) via MCP. Honours the read allowlist. Empty log returns a friendly message, not an error. manage_options gated.
Log entry format (blank-line separated, one entry per mutation):
[YYYY-MM-DD HH:MM:SS UTC]
Ability: file-manager/
File:
User: (ID:) IP:
Size: -> bytes
Destination: (COPY / MOVE only)
Backup: <abs path | SKIPPED (…) | FAILED (…) | DISABLED>
Context:
New action hook: do_action('acrossai_file_manager_log_entry', $entry) fires after every log write. Subscribers (Slack, Datadog, SIEM…) receive the parsed entry as an assoc array. Zero cost when no subscribers.
New optional context input field on wired abilities (delete-file, edit-file, create-directory in this PR — more in the follow-up). Schema max 2000 chars; log writer truncates to 500 chars via sanitize_text_field before persisting.
BREAKING — file-manager/delete-file backup response field. The inline <path>.bak.<time> scheme is REPLACED by the centralised backup dir. When backup_enabled=true the response's new canonical field is backup_path; the legacy backup field is populated with the same value for one transition release and will be removed. When backup_enabled=false NO backup is written at all — callers who relied on the inline .bak return value now get null for both fields when the toggle is off. Direct callers to read backup_path instead of backup.
Retention. backup_retention_days deletes backup dirs older than N days; audit_log_retention_days trims log entries older than N days. Both fire probabilistically (1-in-10 per log write) — no WP-Cron dependency.
Scope note (PARTIAL). This PR wires three abilities end-to-end (delete-file, edit-file, create-directory) to prove the design across the backup + log-only paths. The remaining seven mutation abilities (create-file, append-file, copy-file, move-file, delete-directory, edit-wp-config, clear-debug-log) and the BackupAuditPanel scaffold-banner flip + /backup-audit-stats REST endpoint + uninstall extension are DEFERRED to a follow-up PR. Test coverage is scoped accordingly (35 new tests, mostly structural — behavioural I/O tests need a full-WP bootstrap and run in the CI matrix).
Feature 093 — File Manager Hardening (enforcement pass for PR #144 scaffold). The eight Content Filters knobs and the one sensitive-read denylist that PR #144 shipped as UI scaffold are now enforced at runtime by every file-manager write ability and by file-manager/read-file. Persistence layer (Hardening_Settings) unchanged; the new Hardening_Enforcer utility runs each check after the existing File_Mods_Guard + Path_Allowlist_Guard gates and returns the standard {success:false, blocked_reason, path, message, …context} envelope on refusal. Enforcement order (best-cheap-first): dangerous_extensions → block_double_extensions → sanitize_filename_check → strict_filename_filter → mime_type_check → htaccess_directive_scan → write_max_bytes. Empty list / false toggle is a no-op so callers upgrading with defaults get zero behaviour change beyond what defaults dictate.
Six abilities wired — file-manager/{create-file, edit-file, append-file, copy-file, move-file} for the seven content-filter checks; file-manager/read-file for the sensitive-read denylist. Copy/move check the DESTINATION basename; append-file scans only the appended bytes for .htaccess directives and caps size on new_size = existing + appended; copy/move use source file size for the size cap and read source content lazily for the .htaccess scan (only when destination basename is .htaccess). mime_type_check skips append-file (extension didn't change) and always allows {php, txt, log, json, xml, css, js, md, html, htm, htaccess} even with the check on — prevents breaking the mu-plugins deploy use case the write allowlist explicitly permits.
Eight new blocked_reason values — extension_blocked (+extension), double_extension_blocked (+basename), htaccess_directive_blocked (+directive), filename_sanitize_failed (+input, sanitized), write_size_exceeded (+size, max_bytes), filename_strict_blocked (+marker), mime_type_blocked (+extension), sensitive_read_blocked (+basename, matched_pattern). Every affected ability's output_schema declares the union of these context fields. Existing path_not_allowed_for_write / path_not_allowed_for_read / protected_write / file_mods_disabled envelopes are unchanged and take precedence — hardening refusals fire only when all earlier gates pass.
Ordering guarantee — sensitive-read denylist runs AFTER the read allowlist (spec FR-011). If the allowlist would refuse a path, that refusal (path_not_allowed_for_read) is returned; the denylist only fires when the allowlist would permit. This lets an admin safely widen the read allowlist and still keep .env / *.key / id_rsa blocked.
Panel banner update — the Content Filters panel drops its yellow "Scaffold only" notice-warning (added in PR #144) and replaces it with a small notice-info reading "This list now gates create-file / edit-file / append-file / copy-file / move-file." The Backup & Audit panel keeps its scaffold banner but the text now references 094-file-manager-audit-log explicitly. REST GET /acrossai/v1/file-manager-settings/content-filters flips scaffold_only:true → false and follow_up_spec becomes null; /backup-audit unchanged.
Dotfile carve-out for sanitize_filename_check. Legitimate WordPress-adjacent dotfiles (.htaccess, .htpasswd, .user.ini) are exempted from the sanitize-filename roundtrip check because real WP's sanitize_file_name() strips leading dots — applied literally, the check would refuse every valid dotfile and starve the htaccess-directive scanner of any target. Other dotfiles (.gitignore, .env, etc.) are still refused when the check is on; admins who need them disable the check.
Not touched. Delete_File, Delete_Directory, Create_Directory, File_Info, Read_Debug_Log (fixed target), Read_Wp_Config, Get_Wp_Config_Constant, Edit_Wp_Config, all six zip abilities — spec explicitly excludes these.
Feature 092 — File Manager admin tab: per-folder read/write allowlists + configurable secret redactor. New "File Manager" tab at admin.php?page=acrossai-settings gives site admins three per-folder controls over what MCP clients can do via file-manager/* abilities. Also introduces a hardened secret-scrubber that runs on every read response.
Write allowlist. The 8 write-capable file-manager abilities (create-file, edit-file, delete-file, copy-file, move-file, append-file, create-directory, delete-directory) refuse any operation whose target path resolves outside the admin's allowlist. Default on activation: ['wp-content'] (writes only inside wp-content). copy-file and move-file check both source and destination. Refusal returns {success:false, blocked_reason:"path_not_allowed_for_write", allowed_roots:[…]}.
Read allowlist. The 2 content-reading abilities (read-file, read-debug-log) can also be gated. Default on activation: [] — unrestricted (every path readable). Admins can flip a "Restrict reads to specific folders" toggle and pick specific folders. Refusal returns {success:false, blocked_reason:"path_not_allowed_for_read"}. list-directory and file-info remain ungated (metadata only, no content leak).
Secret redactor. Every text response from read-file and read-debug-log is scrubbed before return. Ships one built-in pattern: WordPress credentials (DB_PASSWORD, DB_USER, all 8 auth keys/salts, SECRET_KEY) — value replaced but constant name preserved. Everything else the admin adds via the custom-literals textarea (case-sensitive string match). Third-party API-key regexes are intentionally NOT hardcoded — assumptions about someone else's key format belong in the site's own config, not in this plugin.
Auto-scrub for AI-connector API keys. If the WordPress AI plugin (github.com/WordPress/ai) is installed and any of its provider connectors has an API key configured, that value is added to the redactor's literal list transparently — the admin does not have to copy those keys into the custom-literals textarea. Currently covers OpenAI (connectors_ai_openai_api_key), Anthropic (connectors_ai_anthropic_api_key), and Google (connectors_ai_google_api_key). No configuration required; the redactor reads these options on every scrub call. Responses grow two fields: redacted:bool and redaction_count:int.
REST endpoints. Six new routes under acrossai/v1: GET/POST /file-manager-settings/write-allowlist, /read-allowlist, /redaction. manage_options + X-WP-Nonce required. GET responses include enumeration data (immediate ABSPATH children, get_plugins(), wp_get_themes()) so the React UI renders without a second round-trip.
BREAKING — file-manager/read-file. The previous outright refusal of wp-config.php and .htaccess (blocked_reason:"protected_read") is REMOVED. Those files are now readable; sensitive content is redacted per the secret redactor above. Callers that programmatically handled blocked_reason:"protected_read" should switch to reading the returned content with redacted:true. Write-side refusals on wp-config.php / .htaccess for create-file, edit-file, delete-file, copy-file, move-file, append-file are UNCHANGED.
Not touched. Ability_Definition, File_Mods_Guard, Wp_Filesystem_Init, read-wp-config, edit-wp-config, get-wp-config-constant, list-directory, file-info, all 6 zip-backup abilities, and every ability outside file-manager/*.
= Feature 091 milestone =
Feature 091 — WP_Filesystem migration for file-manager/* abilities. Every filesystem read, write, list, delete, copy, move, and stat performed by 19 file-manager abilities now routes through WordPress's WP_Filesystem transport instead of raw PHP filesystem functions. On the majority of hosts (FS_METHOD='direct') the behaviour is identical. On hosts where WordPress requires FTP / SSH credentials (FS_METHOD='ftpext' / 'ftpsockets' / 'ssh2') the abilities now succeed via the same channel WordPress core's file editor uses instead of silently failing.
Biggest wins — wp-config.php and debug.log: file-manager/read-wp-config, file-manager/edit-wp-config, file-manager/read-debug-log, file-manager/clear-debug-log are the abilities most likely to touch files owned by the SSH user rather than the web-server user. Those calls previously failed on non-direct transports; they now work.
New response value: every migrated ability's blocked_reason enum widens by one value — filesystem_unavailable — returned when WP_Filesystem() initialisation fails (typically missing FTP_HOST / FTP_USER / FTP_PASS on a non-direct host).
BREAKING — file-manager/file-info schema shrink: the response no longer includes ctime or atime fields. WP_Filesystem_Base does not expose these consistently across transports (native stat returns them on direct, FTP/SSH transports don't). Callers programmatically reading .ctime or .atime must switch to .mtime or accept the loss. Every other field on the response is unchanged.
Deferred to feature 092: file-manager/create-zip-backup, file-manager/extract-zip-backup, and file-manager/upload-zip-backup remain on native PHP for now. ZipArchive requires direct filesystem access and has no WP_Filesystem equivalent, and chunked upload uses fopen/fwrite/fclose file-handle APIs that WP_Filesystem does not expose. These three abilities continue to work exactly as before on direct transports and continue to fail as before on FTP/SSH ones. Every file carries a // TODO(feature-092) marker.
Housekeeping: approximately 20 phpcs:ignore WordPress.WP.AlternativeFunctions suppressions removed from the 19 migrated files. Ability_Definition and File_Mods_Guard are unchanged (verified — sibling plugin acrossai-buddyboss continues to extend the former without issue).
Feature 090 — file-manager additions. Four new abilities extend the file-manager/* namespace to cover directory management and metadata: file-manager/append-file (append or prepend to an existing file; refuses missing files and refuses wp-config.php / .htaccess), file-manager/create-directory (recursive-by-default mkdir under ABSPATH; idempotent), file-manager/delete-directory (empty-only by default; opt-in recursive:true; requires confirm:true; refuses nine critical WordPress directories), and file-manager/file-info (read-only stat wrapper with optional POSIX owner/group names). Ability count 385 → 389.
Feature 089 — file abilities consolidation. Every file read / write / list / copy / move for the WordPress installation now flows through the file-manager/* namespace. Three new abilities added; six duplicate theme- and plugin-scoped abilities removed; a pre-existing security gap closed.
Added — three new file-manager/* abilities:
file-manager/list-directory— recursive directory walk under ABSPATH. Bounded bymax_depth(default 5, max 20) andmax_entries(default 1000, max 5000); response setstruncated:truewhen a bound is reached. Symlinks are not followed. Replacesthemes/read-theme-structureandplugins/read-plugin-structure.file-manager/copy-file— copy a file between two paths under ABSPATH. Default refuses when the destination exists; passoverwrite:trueto replace. Refuses copies ontowp-config.phpor.htaccesseven withoverwrite:true. Replaces the copy mode ofplugins/manage-plugin-files.file-manager/move-file— rename/move a file between two paths under ABSPATH. Same overwrite semantics ascopy-file, plus refuses moves fromwp-config.phpor.htaccess. Replaces the move mode ofplugins/manage-plugin-files.
Removed — six duplicate abilities (BREAKING): any MCP client hardcoding these slugs will get an "unknown ability" error. Migrate to the file-manager/* replacement.
| Removed slug | Replacement |
|---|---|
themes/read-theme-code | file-manager/read-file |
themes/edit-theme-file | file-manager/edit-file |
themes/read-theme-structure | file-manager/list-directory |
plugins/read-plugin-code | file-manager/read-file |
plugins/read-plugin-structure | file-manager/list-directory |
plugins/manage-plugin-files | file-manager/copy-file or file-manager/move-file |
Hardened — file-manager/create-file and file-manager/edit-file now refuse wp-config.php and .htaccess. Before this release, these two abilities silently allowed overwriting those files even though read-file and delete-file refused them. This closes the last generic write path to those protected files; the specialized file-manager/edit-wp-config (single-constant edit with secret-key allowlist) remains the only supported way to modify wp-config.php.
Kept as-is (not duplicates): file-manager/read-wp-config, file-manager/edit-wp-config, file-manager/get-wp-config-constant, file-manager/read-debug-log, file-manager/clear-debug-log, recovery/list-recent-fatal-errors, and all theme / plugin lifecycle abilities (install / activate / update / delete-theme / lifecycle-context / checksums / etc.).
Ability count: 388 → 385 (-6 removed, +3 added). Full per-ability inventory refreshed at docs/abilities-inventory.md.