Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
454a2dc
Code review remediation: truncation bug, redaction unification, dep s…
dennisdornon Jul 10, 2026
9397a3f
Address Codex adversarial review: control-flag canonicalization, appr…
dennisdornon Jul 10, 2026
1f7a3d5
Pre-release head-coder pass: close sanitization gaps, align transport…
dennisdornon Jul 10, 2026
9d974b8
Fail closed on destructive preview; align prompt labels and transport…
dennisdornon Jul 12, 2026
5c76c20
Make chat tool calling protocol-valid for real providers; reject malf…
dennisdornon Jul 12, 2026
126addf
CLI/infra hardening: sanitize env-derived output, honest exit codes, …
dennisdornon Jul 12, 2026
2bb0db3
Head-coder pass: single JSON envelope on batch failure, honest previe…
dennisdornon Jul 12, 2026
134e1b2
Sanitize profile-derived config show fields to a single line
dennisdornon Jul 12, 2026
fd21c02
Docs pass: changelog entries for review remediation, README accuracy …
dennisdornon Jul 12, 2026
3e767af
Preserve Gemini 3 thought signatures; harden approval-echo truncation…
dennisdornon Jul 12, 2026
30e44db
Fix chat-engine history corruption, ID collisions, and unsafe error c…
dennisdornon Jul 12, 2026
bf293e6
Normalize PHP-serialized ability input schemas for LLM tool APIs
dennisdornon Jul 13, 2026
5362426
Fix decline-path history corruption; close safety-net and redaction gaps
dennisdornon Jul 13, 2026
26be835
Replace greedy raw-JSON fallback with brace-depth scanner; add tool-e…
dennisdornon Jul 14, 2026
d8524c2
Self-heal audit log permissions and open in append mode to close TOCT…
dennisdornon Jul 14, 2026
f9f34de
Reject dashboard URLs with embedded userinfo at intake; mask legacy s…
dennisdornon Jul 14, 2026
6ca4ca8
Pin exit-code contract: config error asserts exit 2, add process-leve…
dennisdornon Jul 14, 2026
d12c209
Serialize ChatEngine.sendMessage: concurrent calls queue in order, re…
dennisdornon Jul 14, 2026
ce0c38c
Document Anthropic consecutive-user-turn behavior; pin convertMessage…
dennisdornon Jul 14, 2026
80297ee
Sanitize malformed Dashboard schemas centrally; irreparable ones exit…
dennisdornon Jul 16, 2026
0b5179e
Validate dashboard URL at login intake, before the connection test
dennisdornon Jul 16, 2026
b06c315
Name the failing ability in chat error responses
dennisdornon Jul 16, 2026
dadd145
Pin abilities run --json envelope shapes; drop redundant jobId spread
dennisdornon Jul 16, 2026
b0fef2b
Add acceptance harness: packed/source CLI runs against fixture and li…
dennisdornon Jul 17, 2026
de71d35
Add agent acceptance layer: graded claude-driven CLI scenarios
dennisdornon Jul 17, 2026
327978d
Grade agent plugin evidence by name or slug tied to active state
dennisdornon Jul 17, 2026
19bf613
Harden transport, discovery, and batch surfaces per Codex review triage
dennisdornon Jul 17, 2026
aa5e25f
Fail closed on keychain rollback, hostile error details, and malforme…
dennisdornon Jul 21, 2026
15c7826
Close the release-audit gaps: credential binding, unknown outcomes, p…
dennisdornon Jul 21, 2026
e99ec44
Close residual gaps from Codex re-verification
dennisdornon Jul 21, 2026
83d05e3
Refuse unbound legacy credentials and stop compiling remote regexes
dennisdornon Jul 21, 2026
de22b99
Make the no-remote-regex guarantee structural, not keyword-list-based
dennisdornon Jul 21, 2026
8e4ab76
Count arrays and literal data against the schema depth budget
dennisdornon Jul 21, 2026
d455d54
Tighten sanitizer, atomic-write, and harness bounds in a post-audit p…
dennisdornon Jul 21, 2026
3e98dd0
Add changelog entries for the audit remediation and hardening rounds
dennisdornon Jul 21, 2026
4d349ed
Apply CodeRabbit review: fail-closed URL masking and display sanitiza…
dennisdornon Jul 22, 2026
5cf6a85
Update fast-uri to 3.1.4 for GHSA-v2hh-gcrm-f6hx
dennisdornon Jul 22, 2026
9d3bc88
Make the process suite deterministic on CI platforms
dennisdornon Jul 22, 2026
bced629
Apply the discovery name rule to the abilities-list cross-check
dennisdornon Jul 22, 2026
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ on:
jobs:
test:
strategy:
# Let every matrix cell report: a canceled job hides whether its
# platform actually passes, which has already cost a debugging round.
fail-fast: false
matrix:
node: [20, 22]
node: ['20.18.1', 22]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Thumbs.db

# Test coverage
coverage/
test-results/

# Cache
.cache/
Expand Down
72 changes: 72 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Concurrent `ChatEngine.sendMessage` calls now queue and run in call order instead of interleaving shared history and preview state; the interactive REPL already serialized calls, so this protects programmatic callers
- Chat responses that wrap a JSON tool call in prose (text before or after the object, braces inside string values) now parse correctly: the greedy first-`{`-to-last-`}` fallback was replaced with a brace-depth scanner that respects string literals and escapes; fenced-block parsing and pure-JSON responses are unchanged
- Destructive execution now fails closed: if the automatic `dry_run` preview errors or returns an unsuccessful result, the command exits 4 without sending a confirm request. The successful preview is shown before the confirmation prompt (and included in the `--json` envelope); `--force` skips only the prompt, never the preview
- Chat tool calling now works against the real OpenAI, Anthropic, and Gemini APIs: ability names are aliased to provider-safe tool names (all three reject `/`), assistant tool-call blocks are preserved across turns so continuations pair correctly with their results, and the destructive-approval flow keeps the original tool-call id
- Malformed chat tool calls are rejected instead of executing with empty input: unparseable argument JSON, non-object input, multiple tool calls in one response, responses containing both an answer and a tool call, and responses truncated by `length` or `content_filter` all return a protocol error to the model
- Chat now validates tool input against the ability's JSON schema before execution, matching the CLI path; validation failures go back to the model as tool errors
- Replaced retired LLM model defaults: Anthropic `claude-sonnet-4-20250514` (retired June 2026) with `claude-sonnet-4-6`, Gemini `gemini-1.5-flash` (shut down 2025) with `gemini-3.5-flash`
- Preview summaries no longer claim "No items would be affected" when the dry-run response is in an unrecognized format; the raw response is shown with a warning instead
- `--max-context-messages` rejects negative values; `0` remains the only way to disable truncation
- Chat context truncation no longer orphans tool results mid tool-calling loop or at the destructive-action approval step, which could cause provider API errors on the next message
- Caller-cancelled requests now report "Request cancelled" instead of "Request timed out"
- Gemini 3 thought signatures on function calls are preserved through parsing, chat history, and streaming, and echoed back on the continuation request; previously they were discarded, which Gemini 3 models (including the default `gemini-3.5-flash`) reject with HTTP 400
- Context truncation no longer treats the synthetic approval echo after a destructive action as a turn boundary, which with a small `--max-context-messages` could erase the executed action and its result from history right after approval
- HTTP method selection now resolves destructiveness the same way the safety classifier does, so a destructive-named ability is never sent as a read-only GET even if the server mislabels it; non-boolean annotation values (e.g. `readonly: "true"` as a string) are likewise ignored. When the destructive classification comes from the name override rather than the annotations, the request uses POST instead of trusting the annotations' `idempotent` flag for DELETE
- Keychain credential-removal failures now warn in non-interactive (CI) runs instead of only when attached to a terminal
- Warning shown when the active profile no longer exists and the CLI falls back to another profile
- Chat error responses name the failing ability (`[mainwp/update-site-v1] Error: ...`) so a failed preview or tool call is attributable; engine-level errors stay bare
- `login --url` with embedded credentials fails at intake with the friendly configuration error (exit 2) instead of an opaque network error from the connection test
- Malformed Dashboard ability schemas (PHP artifacts such as `properties: []` or `"inputSchema": []`) are repaired centrally for both `abilities run` and chat tool execution; schemas that remain invalid exit 4 (Dashboard error), not 5
- The Dashboard's queued-job envelope (`job_id`) is recognized, so `abilities run --wait` polls to completion instead of returning immediately
- One-shot chat failures exit non-zero through the documented JSON error envelope instead of printing a raw response object and exiting 0; an empty provider stream is an error, not a blank successful answer
- Malformed `--input` JSON reports the parse position instead of echoing the raw payload, and JSON input that is not an object (array, string, number) is rejected locally instead of being sent to the Dashboard
- Parse-time flag errors under `--json` emit a single JSON error envelope on stdout (exit code 1 unchanged)
- `jobs watch` prints the `cancelled` batch status, mapped to `BATCH_CANCELLED`
- Batch polling rejects unknown statuses, job-ID mismatches, invalid numeric fields, oversized arrays, and terminal-state regressions instead of trusting them
- A keychain read error during login aborts before overwriting, instead of being treated as "nothing stored" and later rolling back a credential that still existed; keychain delete distinguishes not-found from failure, and profile delete reports not-found as the goal state
- Cyclic or over-deep error details no longer crash `--json` output: both sanitizers bound depth and truncate cycles while legitimately shared references survive
- Atomic config writes sync file contents to disk before the rename, so a crash at the wrong moment cannot leave a truncated file behind
- Failures to repair config and audit file permissions now warn instead of being silently ignored

### Changed

- **Breaking:** keychain credentials are now stored bound to the profile's canonical Dashboard identity, and unbound credentials stored by earlier versions are refused for authenticated requests. Each existing profile needs a one-time `mainwpcontrol login` to re-bind its credential; the error message says so. Repointing a profile at a different host by editing `profiles.json` now gets an authentication error instead of the stored password
- A transport failure after a destructive confirm was dispatched exits 3 (`OUTCOME_UNKNOWN`) with audit entries for both the dispatch and the unknown outcome, instead of a generic network error that implied nothing ran; chat reports the same stable code and keeps the session alive
- 2xx Dashboard responses must be parseable JSON with a JSON content type; empty or HTML responses are `INVALID_RESPONSE` errors, never treated as success
- Ability discovery validates entries, caps pagination, warns and keeps the first entry on duplicate names, and no longer generates ambiguous short aliases
- `--json` now emits exactly one JSON document when a batch job times out, fails, or completes partially: an error envelope with the job status in `error.details` (previously a success envelope was printed before the error envelope)
- `jobs watch` and `abilities run --wait` exit 4 when the job ends `failed` or `partial`; `jobs watch` exits 130/143 with an error envelope when interrupted by SIGINT/SIGTERM (previously all of these exited 0 with a success envelope)
- Flag and argument parse errors (for example passing `--dry-run` with `--confirm`) exit 1 (user input error) instead of 2
- Minimum Node.js version is 20.18.1 (required by the bundled undici)
- `npm test` no longer requires a reachable MainWP Dashboard; live integration tests run only via `npm run test:live` with `MAINWP_LIVE_TEST=1`
- Unified sensitive-key redaction into one shared utility covering compound keys (`apiToken`, `appPassword`) across error output, debug logging, and input sanitization
- Broader destructive-ability name patterns (`reset-`, `restore-`, `rollback-`, `wipe-`, `purge-`, `uninstall-`) in the defense-in-depth safety classification
- Exit code 130 on Ctrl-C at prompts documented as the intentional SIGINT convention

### Security

- Dashboard URLs with embedded credentials (`https://user:pass@host`) are rejected at login with a hint to use `--username` and the password prompt; profiles stored before this fix have the userinfo masked as `***:***@` in `login`, `config show`, and `doctor` output (human, JSON, and echoed error messages)
- The audit log directory and file permissions now self-heal to `0700`/`0600` on every write, and the log is opened atomically in append mode, removing a check-then-act window that could truncate the log
- `config show` sanitizes every untrusted value in human-readable output to a single safe line: environment-derived provider names and paths (`MAINWP_LLM_PROVIDER`, `XDG_CONFIG_HOME`) and profile-derived fields (profile name, dashboard URL, username), closing line-injection via a crafted `profiles.json` or hostile environment
- `doctor` and `config show` human-readable output now strips terminal escape sequences from error- and config-derived text, matching the sanitization the `--json` path already applied
- HTTP responses are size-checked after buffering even when the server sends a parseable `Content-Length`, so an inaccurate header can no longer bypass the response size limit
- All remaining single-row terminal output (login summary, profile fallback and keychain warnings, ability names, table cells, list items, preview labels) collapses untrusted values to a single line instead of only stripping non-CR/LF control characters
- Input keys containing `[` or `]` are now rejected; they could canonicalize server-side (PHP query parsing) to alias a control flag like `confirm` past the executor's flag-stripping guard
- Mutual exclusion of `dry_run` and `confirm` is now also asserted at the executor boundary, not only at the flag layer
- Updated `undici` to 7.28.0, resolving TLS certificate validation bypass and response queue poisoning advisories
- Updated `fast-uri` (transitive, via `ajv`) to 3.1.4, resolving a high-severity host-confusion advisory (GHSA-v2hh-gcrm-f6hx)
- Updated `@oclif/core`, `@oclif/plugin-help`, `@oclif/plugin-autocomplete`, and transitive dependencies; `npm audit --omit=dev` reports zero production vulnerabilities, dev-chain advisories are tracked separately
- Abilities with missing or malformed safety annotations are classified destructive (fail closed) instead of defaulting to read-only; every real Dashboard ability declares all three annotation keys
- Case-variant ability names (`Mainwp/Delete-Site-V1`) are refused at discovery, so a case variant can never evade destructive-name classification or alias a cache key
- `pattern` and `patternProperties` from Dashboard schemas are stripped from every node of the tree and never compiled, so a hostile Dashboard cannot stall the CLI with a catastrophic regex; the whole tree, including arrays and literal data values, counts against a 32-level depth budget
- Tool results are key-redacted before entering provider-bound chat history (local display stays raw); provider requests refuse redirects; hosted providers refuse `http://` base URLs; the local provider allows HTTP only to loopback and private-range hosts
- Profile `skipSSLVerification` must be strictly boolean; the string `"false"` no longer disables TLS verification
- SSE streams are bounded (line and buffer caps, idle and absolute timeouts), and provider error bodies are read bounded and key-redacted before they can reach an error message
- Dashboard response bodies stream against a byte cap and the request timeout covers the body read, so an unbounded or stalled body cannot hang the process
- Config files write via random-suffix `O_EXCL` temporary files; the audit log opens with `O_NOFOLLOW`; audit-log input is bounded at 8 KiB and free text at 2 KiB with visible truncation markers
- URLs with username-only credentials now redact, and userinfo masking is greedy through the last `@` so passwords containing `@` mask fully; malformed URLs are no longer echoed in profile-store error messages
- Sensitive query-string parameters (`access_token=...`) are redacted in error output, and keychain store errors pass through the same sanitizer as the rest of the keychain surface
- Redaction and terminal sanitization build results on null-prototype objects, so a crafted `__proto__` key cannot pollute prototypes; terminal output also strips Unicode bidirectional and isolate controls to prevent right-to-left display spoofing

## [1.1.0-beta.1] - 2026-03-26

### Added
Expand Down
21 changes: 8 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A CLI for managing your MainWP Dashboard from the terminal. List sites, push upd

> **On Windows?** Use [Git Bash](https://gitforwindows.org/) and every example below works without changes. For scheduled workflows (cron), see [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).

You need Node.js 20+ and a MainWP Dashboard (v6+) with an [Application Password](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/).
You need Node.js 20.18.1+ and a MainWP Dashboard (v6+) with an [Application Password](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/).

```bash
npm install -g @mainwp/control
Expand Down Expand Up @@ -70,7 +70,7 @@ mainwpcontrol abilities run list-updates-v1 --json
mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json
```

> **Windows?** This works as-is in [Git Bash](https://gitforwindows.org/). In PowerShell, escape the inner quotes: `'{\"site_id_or_domain\": 1}'`. Or skip quoting entirely with `--input-file` ([details](docs/workflows/input-from-file.md)).
> **Windows?** This works as-is in [Git Bash](https://gitforwindows.org/). In PowerShell, use `--input-file` instead of inline JSON: how PowerShell passes quoted arguments to native commands varies by version ([details](docs/workflows/input-from-file.md)).

**Preview a destructive action before running it:**

Expand Down Expand Up @@ -136,7 +136,7 @@ A terminal is where you type commands instead of clicking buttons. You'll see it

**How to open it:**
- **macOS**: Open **Terminal** (search in Spotlight, or look in Applications > Utilities)
- **Windows**: Open **Git Bash** (installed with [Git for Windows](https://gitforwindows.org/)). If you don't have it, PowerShell works too see the [quoting notes](#json-quoting-on-the-command-line) below.
- **Windows**: Open **Git Bash** (installed with [Git for Windows](https://gitforwindows.org/)). If you don't have it, PowerShell works too; see the [quoting notes](#json-quoting-on-the-command-line) below.
- **Linux**: Open your distribution's **Terminal** app (usually in the applications menu)

### What does `npm install -g` do?
Expand Down Expand Up @@ -178,14 +178,11 @@ When you pass JSON with `--input`, quoting depends on your shell:
```bash
# macOS / Linux / Git Bash on Windows
mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json

# Windows PowerShell
mainwpcontrol abilities run get-site-v1 --input '{\"site_id_or_domain\": 1}' --json
```

**Git Bash on Windows** (comes with [Git for Windows](https://gitforwindows.org/)) handles quoting the same way macOS and Linux do. If you use Git Bash, all the examples in this documentation work without changes.

PowerShell strips the inner double quotes unless you escape them with backslashes. If this gets annoying, put your parameters in a file and use `--input-file`:
**Windows PowerShell** quoting of inline JSON is unreliable: whether backslash-escaped quotes inside a single-quoted string reach the command intact depends on your PowerShell version. Don't fight it, put your parameters in a file and use `--input-file`:

```bash
mainwpcontrol abilities run get-site-v1 --input-file params.json --json
Expand Down Expand Up @@ -219,10 +216,7 @@ mainwpcontrol abilities run list-sites-v1 --json
# Run with input parameters
mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json

# Windows PowerShell: escape inner quotes (Git Bash doesn't need this)
mainwpcontrol abilities run get-site-v1 --input '{\"site_id_or_domain\": 1}' --json

# Or use a file (works everywhere)
# Or use a file (works everywhere, and is the reliable option on Windows PowerShell)
mainwpcontrol abilities run get-site-v1 --input-file params.json --json
```

Expand Down Expand Up @@ -277,7 +271,7 @@ See [Chat Mode Configuration](#chat-mode-configuration) for all supported provid

### Global Flags

These flags work on every command.
These flags work on every `mainwpcontrol` command except the built-in `help` and `autocomplete` commands.

| Flag | Description |
|------|-------------|
Expand Down Expand Up @@ -477,6 +471,7 @@ Step-by-step guides for common automation patterns:
| 3 | Network error | Retry or check connectivity |
| 4 | API error | Check ability parameters |
| 5 | Internal error | Report bug |
| 130 | Interrupted (SIGINT) | Ctrl-C during a prompt or `jobs watch`; standard Unix 128+SIGINT convention, outside the 0-5 contract |

### Environment Variables

Expand Down Expand Up @@ -606,7 +601,7 @@ GPL-3.0-or-later

## Requirements

- Node.js 20 LTS or later
- Node.js 20.18.1 or later
- MainWP Dashboard 6+ with Abilities API
- WordPress Application Password

Expand Down
Loading
Loading