Acceptance harness and pre-release hardening - #6
Conversation
…ecurity bumps Findings from a full-codebase quality review, all three tiers approved. Bug fixes: - Chat context truncation could orphan a tool result mid tool-calling loop (unsafe fallback cut), causing provider 400s on the next call. Truncation logic extracted into ContextWindow with a strict user-boundary cut rule that defers truncation when no safe boundary exists. - Caller-cancelled HTTP requests reported as "Request timed out"; now attributed correctly as "Request cancelled". - Keychain delete failures were silent in non-TTY (CI) runs. - Dangling activeProfile now warns instead of silently self-correcting. Security / consistency: - Sensitive-key redaction unified into src/utils/redaction.ts (was 3 divergent implementations; the debug-output one missed compound keys like apiToken). Normalized substring matching over a superset list. - DESTRUCTIVE_PATTERNS broadened (reset-, restore-, rollback-, wipe-, purge-, uninstall-) with documented verb-conservative rationale. - Control-flag strip (dry_run/confirm/user_confirmed) centralized in one buildEffectiveParams path shared by POST body and GET/DELETE query. - undici 7.24 -> 7.28.0 (TLS validation bypass + queue poisoning fixes), @oclif/core 4.0 -> 4.11, plugin-help 6.0 -> 6.2, fast-uri 3.1.3. npm audit now reports 0 vulnerabilities. - login.ts output sanitized with stripControlChars like other paths. - Exit 130 (SIGINT) documented as intentional carve-out in README. Tests: - InputSanitizer enforcement (size/depth/array/key limits, error-message redaction) now actually tested; was zero coverage on the control itself. - jobs/watch.test.ts rewritten against real exports instead of local re-implementations (was tautological). - New ContextWindow unit tests incl. mid-tool-loop orphan regression. - AbortError cancel-vs-timeout attribution regression tests. - Shared createCommandHarness() replaces 4 duplicated e2e factories. Tidiness: - Dead code removed: estimateTokens/getContextStats/maxContextTokens plumbing, ExponentialBackoff.getDelayForAttempt. - doctor + config show migrated to shared formatter helpers (formatDivider/formatSection/formatStatusIcon), output unchanged. - Anthropic/Gemini providers: shared splitSystemMessage(), redundant makeRequest wrappers removed, DEBUG-gated SSE parse-skip logging. - getExecutor/getBatchManager share one cached client config (single keychain lookup per process). Verification: typecheck clean, lint 0 errors, 695 tests passing across unit + process suites (same 6 pre-existing live-Dashboard failures as main), npm audit clean, doctor/config-show/--json output verified. Claude-Session: https://claude.ai/code/session_011xJvnx5BXFSfETmsWgdbKD
…oval-boundary truncation, transport/policy consistency Three findings from a Codex adversarial review of 454a2dc, verified against source before acting. Finding 1 (high) — control-flag strip bypassable via PHP bracket canonicalization. buildEffectiveParams() stripped only exact keys, so a key like `confirm]` survived and, on the GET/DELETE query path, serialized to `input[confirm%5D]=true` which PHP parses back to `input.confirm`. Schema validation doesn't cover it (no removeAdditional; skipped when an ability has no input_schema). Fixed at the real chokepoint both CLI and chat share (InputSanitizer, called by executor.execute on every request): reject any input key containing `[` or `]`. Also assert dry_run/confirm mutual exclusion at the executor boundary, not just the flag layer. Finding 2 (medium) — my earlier truncation fix was incomplete. ChatEngine injects a synthetic `User approved: yes` user message between an assistant tool-call and its confirm result; the "cut only before a user message" rule treated that as a real boundary and could re-orphan the tool result. Refined findSafeCut: a user message immediately followed by a tool message is the synthetic approval, not a genuine turn start, so it's not a cut boundary. Finding 3 (medium) — accepted the consistency half, rejected the removal. Kept the name-based destructive override (deliberate defense-in-depth against a server under-reporting destructiveness). Fixed the real inconsistency: getHttpMethod read raw annotations and could route a destructive-named, server-marked-readonly ability as GET while policy demanded preview+confirm. Extracted isKnownDestructiveName() as the single source of truth and routed HTTP-method selection through the same resolved classification. Corrected the classify() doc comment that falsely claimed "annotations only, no heuristics". Tests: bracket-key rejection (sanitizer), method never GET for destructive-named-readonly + both-flags rejection (executor), approval-boundary defer + catch-up (ContextWindow). Full suite 702 passing (+7), same 6 pre-existing live-Dashboard failures; typecheck + lint clean. Claude-Session: https://claude.ai/code/session_011xJvnx5BXFSfETmsWgdbKD
… with policy - doctor and config show human output now strips terminal escape sequences from error- and config-derived text, matching the --json envelope path (login/keychain/profile-store warnings got the same treatment) - getHttpMethod reads annotations with strict boolean checks so a non-boolean value (readonly: "true") can't diverge transport from SafetyController's validated classification - debug-context redaction now recurses into arrays, with the 300-char string truncation preserved for nested data - drop redundant sensitive-key entries (api_key, authorization) already covered by normalization/substring matching - replace real username in path-redaction test fixture - tests: doctor process test proving escape stripping end-to-end, string-typed annotation method-selection test, base-command redaction unit tests Claude-Session: https://claude.ai/code/session_011xJvnx5BXFSfETmsWgdbKD
… with safety policy Codex review MF1 + MF2 sub-points: - abilities run: a dry_run preview that errors or returns success:false now aborts destructive execution (exit 4, audit-logged declined) instead of continuing to confirm; the successful preview is rendered before the confirmation prompt and included in the JSON envelope. --force skips only the prompt, never the preview. - abilities run --wait: failed/partial terminal batch statuses now exit 4 (previously only timeout did). - system-prompt: ability safety labels and the destructive-warning list now derive from SafetyController.classify() so LLM-facing text matches runtime (destructive-name override included). - abilities-executor: DELETE is only selected when annotations themselves say destructive+idempotent; a name-override-destructive ability goes out as POST since its annotations are distrusted. Claude-Session: https://claude.ai/code/session_01AJRE68wop5Ppj3jPRAqE55
…ormed tool calls
Codex review MF3/MF4/MF5/SF1:
- Provider-safe tool-name aliasing (slash names rejected by all three provider
APIs), collision-checked, resolved back to real ability names in the
envelope parser before ability lookup; pass-through for unaliased names.
- Message carries native assistant toolCalls so continuations serialize valid
OpenAI tool_calls / Anthropic tool_use / Gemini functionCall blocks with
matching ids; destructive-approval resume preserves the original call id
instead of inventing execute_<name>.
- Envelope strictness: unparseable argument JSON, non-object input, multiple
tool calls, answer+tool envelopes, and finishReason length/content_filter
are protocol errors that feed the retry path — never executed as {}.
- Chat path now runs the same AJV schema validation as the CLI before
execution; failures return a tool-result error to the model.
- Replace retired model defaults: claude-sonnet-4-20250514 -> claude-sonnet-4-6,
gemini-1.5-flash -> gemini-3.5-flash; refresh advertised lists.
- --max-context-messages gets min 0; ContextWindow throws on negatives.
Claude-Session: https://claude.ai/code/session_01AJRE68wop5Ppj3jPRAqE55
…gate live tests Codex review MF6/SF2/SF3/SF4 + watchlist: - sanitizeSingleLine() applied to provider-resolution warnings and displayed config paths in config show (raw MAINWP_LLM_PROVIDER / XDG_CONFIG_HOME were rendered unsanitized). - jobs watch: progress suppression uses resolved jsonOutput (settings defaultJsonOutput no longer corrupts the JSON envelope); failed/partial jobs exit 4, SIGINT/SIGTERM exit 130/143 with an error envelope, never success. - Live integration tests excluded from default and process vitest configs and gated on MAINWP_LIVE_TEST=1 (npm test no longer depends on the testbed). - CHANGELOG audit claim corrected to production-only scope (full npm audit has 29 dev-chain advisories; --omit=dev is clean). - engines.node >=20.18.1 (undici@7.28.0 floor); http-client always size-checks the buffered body instead of trusting parseable Content-Length. - audit-logger: document that execution on a declined entry records the fail-closed abort reason. Claude-Session: https://claude.ai/code/session_01AJRE68wop5Ppj3jPRAqE55
…ws, parse errors exit 1 Self-review follow-ups: - --json emits exactly one document on batch timeout/failed/partial: the error envelope carries the partial status in details instead of a success envelope preceding it (human mode still prints results before the error). Timeout process test now asserts the single-document contract. - Preview summaries no longer say "No items would be affected" when the dry_run response shape is unrecognized — the operator sees the raw data with an explicit unrecognized-format warning instead of false reassurance. - oclif flag/arg parse failures (e.g. --dry-run --confirm exclusive validation) exit 1 (user input error) instead of oclif's default 2, which our contract reserves for auth/config errors. - pretest builds the CLI so process tests can never validate a stale bin. Deferred with rationale (not regressions): credentialed provider smoke tests (needs CI secrets), sanitize-on-ingest refactor (output-boundary sanitizing would strip the CLI's own ANSI formatting), dev-dep chain upgrade (separate branch), mainwpctl/mainwpcontrol naming decision (product call). Claude-Session: https://claude.ai/code/session_01AJRE68wop5Ppj3jPRAqE55
Codex adversarial re-review (needs-attention, one medium finding): profile name, dashboard URL, username, and the available-profiles list rendered via stripControlChars, which preserves CR/LF/tab — a crafted profiles.json could inject forged output lines. All profile-derived one-line fields now use sanitizeSingleLine; process regression test covers hostile profile values. Claude-Session: https://claude.ai/code/session_01AJRE68wop5Ppj3jPRAqE55
…fixes - CHANGELOG Unreleased now records the user-visible behavior changes from the review remediation: fail-closed preview, provider-valid chat tool calling, strict tool-call rejection, single JSON envelope on batch failure, exit-code changes (parse errors 1, failed/partial jobs 4, watch signals 130/143), Node 20.18.1 floor, and live-test gating - README: Node requirement corrected to 20.18.1+, exit-130 row covers jobs watch, em dashes removed per house style Claude-Session: https://claude.ai/code/session_01AJRE68wop5Ppj3jPRAqE55
… and single-row sanitization - Carry part-level thoughtSignature through ToolCall, the envelope parser, chat history, and stream accumulation, and re-emit it on Gemini continuation requests (Gemini 3 returns 400 without it) - Context truncation: a user message directly after a tool result is the synthetic approval/decline echo, not a turn boundary; regression tests at maxMessages 1/2/3 with the production message order - Convert remaining single-row output (login, profile/keychain warnings, ability names, table cells, list items, preview labels) from stripControlChars to sanitizeSingleLine Claude-Session: https://claude.ai/code/session_01AJRE68wop5Ppj3jPRAqE55
…asts from CR review
Findings from the CodeRabbit loop (iterations 02-03), all verified against
the code before fixing:
- chat-engine: a non-SchemaValidationError thrown during input validation
propagated out of executeTool after the assistant tool-call message was
already in history, leaving a dangling tool call that corrupted the next
provider request. Such errors now return the same { type: 'error' }
result as the execution catch-all, so a tool message always follows.
- chat-engine: fallback tool-call IDs used a per-turn counter, so call_1,
call_2... repeated across turns in retained history. Replaced with an
engine-lifetime counter.
- abilities run: PREVIEW_FAILED details carried the raw previewFailure
value into --json envelopes; now only the sanitized { reason }.
- keychain: delete() and set() read .message off a bare cast, which threw
a TypeError when keytar rejected with null or undefined. Both now
normalize the rejection value first. New keychain.test.ts pins the
warn-and-continue behavior for non-Error rejections.
One finding rejected: CR asked to convert the Anthropic model list to
dated IDs, but every listed ID is valid and current-generation models are
alias-only. Recorded as anthropic-model-ids-are-current in
REVIEW_DECISIONS.md.
Typecheck clean; 724/724 tests pass (5 new).
Claude-Session: https://claude.ai/code/session_01AJRE68wop5Ppj3jPRAqE55
The Dashboard's json_encode turns empty associative arrays into [], so schemas arrive with inputSchema: [] or properties: []; providers also reject type: ['object', 'null'] at the top level. Sanitize recursively without mutating the input. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
Head-coder audit remediation (four findings): - Chat: typing "no"/"cancel" at the approval prompt was intercepted in chat.ts and only nulled pendingPreview, orphaning the tool_use in history (providers 400 on the next turn) and skipping the declined audit entry. Route all replies through sendMessage() so declines hit handlePreviewResponse; remove the now-dead cancelPendingPreview(). - Safety: add update-site- and activate- to DESTRUCTIVE_NAME_PATTERNS so plugin/theme/core updates and activations on live sites are classified destructive regardless of server annotations. - Redaction: credential scrubbing (Basic/Bearer, user:pass@host URLs, home paths) was only wired to audit-log paths. Extract it to utils/error-sanitizer.ts and apply in errorOutput()/formatError() (message, details, hint) plus the raw streaming error throws in sse-reader and openai-compatible. - Policy: extract executeAbilityWithPolicy() as the shared choke point (flag validation + classification + destructive-requires-flag) for both the abilities run command and chat, matching the documented invariant; run.ts now passes the fetched Ability instead of re-fetching by name. 747 tests pass; typecheck and lint clean. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
…nvelope tests Audit-remainders item 1: prose around a JSON tool call no longer breaks parsing; fence patterns and pure-JSON behavior unchanged. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
…OU truncation window Audit-remainders item 2: chmod dir/file to 0700/0600 on every write (failures swallowed), delete ensureLogFile's check-then-act 'w' open. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
…tored ones at display Audit-remainders item 3: validateUrl rejects user:pass@ on save only (legacy profiles keep loading), maskUrlUserinfo/maskUrlUserinfoInText applied at login, config show, and doctor including JSON and echoed fetch errors. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
…l exit 5 and exit 130 tests Audit-remainders item 5: the e2e harness does produce the real exit code, so the >=1 fallback assertion is gone; exit 5 forced via an unreadable settings.json, exit 130 via TTY-emulated SIGINT (ETX) at the login password prompt. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
…jections don't poison the queue Audit-remainders item 6: pendingPreview and history had no concurrency protection beyond the REPL's incidental serialization; an inFlight promise chain now guards programmatic callers. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
…s wire shape Audit-remainders item 4 resolved without a code fix: the Messages API reference states consecutive same-role turns are combined into a single turn, so the decline-path double-user shape is valid. Tests pin the current conversion so changing it becomes a conscious decision. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
… 4 not 5 Move sanitizeInputSchema to src/validation/sanitize-schema.ts and apply it inside SchemaValidator.getCompiledSchema() so the deterministic path (abilities run) and chat's executeTool both tolerate PHP artifacts (properties: [], inputSchema: [], type: ["object","null"]). Schemas AJV still rejects raise APIError ABILITY_SCHEMA_INVALID (exit 4) — a server-supplied bad schema is not our internal error. CLI bug remainders sprint, item 1. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
login --url with embedded userinfo previously died inside undici's fetch (opaque NetworkError → AuthError) before ProfileStore.save() could reject it. Export validateDashboardUrl from profile-store and call it right after URL normalization so the friendly ConfigError (exit 2) fires with no connection attempt. CLI bug remainders sprint, item 2. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
A 404 during the mandatory destructive preview rendered as bare "Error: Resource not found" with no hint which ability failed. Add an optional tool field to the ChatResponse error variant, populate it at all four producer sites in chat-engine, and render "[tool] Error: ..." in formatResponse when present (engine-level errors stay bare). Export formatResponse for direct test coverage. CLI bug remainders sprint, item 3. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
Contract tests for all three modes: --dry-run preview envelope (mode, preview block, data.data nesting), destructive execute carrying the approved dry_run's preview, direct execute with no preview key. The batch branches spread jobId twice (explicit + ...result); keep only the spread. No envelope shape changes. CLI bug remainders sprint, item 4. Claude-Session: https://claude.ai/code/session_01JcosLpBNAwDRsrkq1ESkXX
…ve targets Pack the tarball into a fresh consumer (or run from source), then drive the installed mainwpcontrol binary through 20 scenarios: read cross-checks against an independent verifier, error contracts, safety flows verified by mock-server request recording (dry-run previews once and never confirms, force skips only the prompt, failed preview fails closed), and guarded live writes (sync, plugin toggle roundtrip). Artifacts land in test-results/ (now gitignored) with per-scenario and per-invocation timings plus a credential-redaction audit. The agent layer (agent-run.ts) is planned but not yet implemented, so test:acceptance:human chains fixture and writes only for now. Claude-Session: https://claude.ai/code/session_017zX6UxnvwCQK7DKBhXr771
Four scenarios drive claude -p with Bash scoped to mainwpcontrol and grade the ordered CLI argv plus final answer against IndependentVerifier ground truth computed before the model runs. Three read scenarios run live; the confirm-delete scenario targets a per-scenario fixture Dashboard because MainWP Child key-locking makes a live victim non-repeatable (the mcp reference scopes it the same way). The child claude gets strict MCP isolation, no Bash sandbox, and a CLI-usage system prompt; grading tolerates failed intermediate attempts since a CLI agent discovers input schemas by trying. Speed telemetry (wall clock, claude/API duration, TTFT, invocation count) lands in results.json and summary.md, closing the gap the mcp harness has. docs/acceptance-testing.md documents the harness. Claude-Session: https://claude.ai/code/session_017zX6UxnvwCQK7DKBhXr771
The agent may pipe CLI output through shell filters (seen live: grep -A4 on the plugin name dropped the slug line), so requiring the slug in captured tool results failed a correctly answered scenario. Accept either the slug or the plugin name when structurally tied to the expected active value; the loose text fallback stays slug-only so a plugin merely listed in an inventory cannot pass.
Implements the accepted findings from the 2026-07-17 Codex review (triage in .mwpdev/reviews/, local-only). Stale findings already fixed on this branch were rejected there with evidence. - Recognize the Dashboard's real queued envelope: normalize snake_case job_id into ExecutionResult.jobId so abilities run --wait actually polls; Dashboard-faithful fixture proves it. Shared job-id validator bounds IDs at both intake points (queued envelope, batch polling). - HTTP client: abort timeout now covers body read, bodies stream with a byte cap, 2xx requires JSON content type and parseable JSON; empty or HTML 2xx is INVALID_RESPONSE, never fabricated success. - Discovery: validate ability entries, cap pagination, warn-and-keep- first on duplicate names, remove ambiguous short aliases. Missing or malformed annotations now classify destructive (fail closed); every real Dashboard ability declares all three annotation keys. - Batch polling: reject unknown statuses, job-ID mismatches, invalid numerics, oversized arrays, and terminal-state regressions; add cancelled status with BATCH_CANCELLED mapping. - Parse-time flag errors under --json emit one JSON envelope on stdout with exit 1; real-SIGINT process tests pin jobs watch at exit 130. - Live tests: TLS-disable and connectivity gated behind MAINWP_LIVE_TEST, password moved off argv to MAINWP_APP_PASSWORD, testbed env path configurable via MAINWP_TESTBED_ENV. - Provider transport: base URLs restricted to http/https, SSE line/ buffer caps with idle and absolute timeouts, error bodies read bounded (16 KiB) before truncation. Malformed streamed tool-call arguments now surface as protocol errors instead of vanishing. - Config hardening: random-suffix O_EXCL atomic writes, keychain store before profile persist on login, honest keychain-delete results in profile delete, audit input bounded at 8 KiB with truncation marker, O_NOFOLLOW append. CI matrix pins the exact Node 20.18.1 floor.
…d chat envelopes
Codex adversarial review found three fail-open paths in this hardening
round, all fixed here:
- Login treated a failed keychain read as "nothing stored", so a later
profile-save failure could roll back by deleting a credential that
still existed. Keychain.getStored() now reports found, not-found, and
read-error distinctly, and login aborts before overwriting when the
previous credential is unreadable.
- errorOutput() piped details through the unbounded sanitizeForTerminal()
before the cycle-guarded sanitizer, so cyclic or deep error details
crashed --json instead of emitting an envelope. Both sanitizers now
bound depth and track the ancestor path: cycles truncate, legitimately
shared references survive, and strip-before-redact order is preserved
so control characters cannot split credential patterns past the
redaction regexes.
- Content that failed JSON parsing but carried envelope keys after prose
('Deleting: {"tool": ...') was accepted as a final answer. It returns
to the retryable protocol-error path; prose with unrelated braces is
still an answer.
The rest of the round: username-only credentialed URLs now redact,
userinfo masking is greedy through the last @ so passwords containing @
mask fully, keychain delete distinguishes notFound from failure and
profile delete reports it as the goal state, oclif parse errors emit
INPUT_ERROR envelopes to match the exit code, jobs watch prints
cancelled status, and the acceptance harness redacts base64 Basic-auth
forms of the credentials.
…rovider boundaries Codex's release audit surfaced eight blockers and ten smaller issues. Triage (with per-claim verification) is in .mwpdev/reviews/ codex-release-audit-triage-2026-07-20.md; this commit fixes everything accepted there. Destructive-confirm outcomes are now fail-closed end to end. A dispatch-stage audit entry is written before every confirm call, and a transport failure after dispatch produces an outcomeUnknown audit entry plus an OUTCOME_UNKNOWN error (exit 3) instead of a generic network failure with no audit trail. Same treatment in chat, which keeps the session alive and history coherent. A process test drops the socket mid-confirm to prove the whole chain. Keychain credentials now store a v1 envelope binding the password to the canonical Dashboard identity. Editing profiles.json to point an existing profile at a different host gets an AuthError instead of the password. Legacy bare-string entries keep working and re-bind on the next login. Profile skipSSLVerification must be strictly boolean; a string "false" no longer disables TLS verification. Provider boundary: tool results are key-redacted before entering provider-bound chat history (local display stays raw), all three provider fetch paths reject redirects with redirect 'manual', hosted providers refuse HTTP base URLs and warn on any override, and the local provider allows HTTP only to loopback and private-range hosts. One-shot chat failures now exit non-zero through the documented JSON envelope instead of printing a raw ChatResponse and exiting 0. Empty provider streams are errors rather than blank successful answers, the REPL error path runs the message sanitizer, and malformed --input JSON reports the parse position instead of echoing the raw payload. Previews with absent or unrecognized data now warn honestly instead of claiming no items would be affected. Dashboard schemas get a recursion depth cap and pattern-length caps. Acceptance runs with unverified scenarios exit 1. Docs: cron guide stores the app password in a chmod-600 env file instead of the crontab, batch-update guide gains backup/canary/rollback/maintenance-window prerequisites and drops the "zero risk" claim, README fixes the PowerShell JSON advice and scopes the global --json claim to exclude help and autocomplete.
Codex's second pass confirmed the main fixes but caught five leftovers. Chat now carries OUTCOME_UNKNOWN as a stable code on the error response, and one-shot chat maps it to UnknownOutcomeError (exit 3) instead of downgrading it to a generic CHAT_ERROR (exit 4). The path is not reachable in one-shot mode today (previews cannot be approved non-interactively), but the code no longer lies if that changes. Legacy keychain entries re-bind opportunistically: an unbound credential read for an authenticated request is rewritten as a v1 envelope bound to that request's Dashboard URL, so existing users get identity protection without a re-login. A failed rewrite leaves the legacy entry untouched. Provider error bodies get key-based redaction: values of sensitive- looking keys in JSON-shaped text (api_key, authToken, ...) become [REDACTED] before the body reaches an error message. The scanner skips object/array values so nested keys are still reached. Schema sanitization traverses contains, propertyNames, dependentSchemas, unevaluatedItems, and unevaluatedProperties, so depth and pattern caps cannot be bypassed through those keywords, and short catastrophic patterns (nested quantifiers like ^(a+)+$) are dropped alongside overlong ones. The incorrect PowerShell backslash-escaping advice is removed from the two remaining workflow guides (input-from-file, plugin-deployment- verification); both now point Windows users at --input-file.
Codex's third verification pass accepted everything except two designs, and it was right about both. Legacy keychain entries are no longer auto-bound on first use. Binding an unknown password to whatever URL the profile holds at that moment just blesses a file that may already be tampered with. An unbound credential read for an authenticated request now gets an AuthError with a one-time-upgrade hint to run login, which binds the credential with the user seeing and providing the URL. Display paths (doctor, config show) that read without a URL still work. The nested-quantifier ReDoS heuristic is replaced with a guaranteed-safe policy: remotely supplied pattern and patternProperties constraints are stripped from Dashboard schemas everywhere in the tree and never reach ajv. No heuristic reliably separates safe regexes from catastrophic ones, and a hostile pattern could stall the CLI before any request is sent. The cost is client-side only; the Dashboard re-validates input.
Codex's fourth pass found a bypass: draft-07 dependencies was not in the traversed keyword list, so a pattern nested under it reached ajv (and the depth cap was equally bypassable there). The sanitizer now walks every object/array value in the remote schema, known keyword or not, deleting pattern/patternProperties at every schema node and enforcing the depth cap on the whole tree. The keyword lists only decide which positions get the PHP empty-array normalization; the scrub no longer depends on them. Data-carrying keys (const, enum, default, examples) are copied verbatim so literal values keep keys named "pattern", and properties maps keep fields literally named pattern. dependencies joins the map keys so a dependency keyed by such a field survives too. Regression tests cover Codex's dependencies probe, an invented future keyword hiding a pattern, depth capping through unknown keywords, and the preserved-verbatim cases (required: [], defaults, examples, properties.pattern).
Codex's fifth pass found two depth bypasses in the sanitizer: nested
arrays recursed without incrementing depth, and data-bearing keys
(const, enum, default, examples) were copied verbatim with no depth
check at all, so 60-level probes survived under both.
Arrays now consume a depth level in every walker, and the walkers check
the cap themselves so an over-deep array chain collapses to {} like any
other subtree. Literal data keeps its preserve-verbatim semantics (no
schema-key deletion inside it) but is measured by an iterative
depth check first; a value nesting past the budget drops the whole
keyword rather than being partially rewritten. The measurement uses an
explicit heap stack so checking an arbitrarily deep hostile structure
cannot itself exhaust the call stack.
Regression tests cover 60-level array chains under an unknown keyword
and 60-level mixed object/array values under each of the four data keys.
…olish pass Structural hardening across the surfaces the release audit touched, with tests for each change: - atomicWriteFile opens the temp file exclusively and fsyncs before the rename, so a crash cannot leave a renamed zero-length file - redactSensitiveKeys gains a 32-level depth cap and ancestor-tracking cycle guard; it and the terminal sanitizer accumulate into null-prototype objects so a crafted __proto__ key cannot pollute prototypes; stripControlChars also drops Unicode bidi/isolate controls - error sanitizer redacts sensitive query-string params and redacts a sensitive key's value outright instead of recursing into it, reusing the shared isSensitiveKey list; keychain set() failures pass through sanitizeKeychainError like the rest of the file; profile-store stops echoing malformed URLs that could carry embedded credentials - audit log free text bounded at 2 KiB with multi-byte-safe truncation and a visible marker; chmod repair failures warn instead of vanishing - ability names are validated case-sensitively at discovery, so a case-variant like Mainwp/Delete-Site-V1 is refused rather than normalized past destructive classification; abilities run --input rejects non-object JSON locally; batch-manager validates jobId before any use - live-test gating requires an explicit MAINWP_LIVE_TEST=1/true (was truthy on "false"), drops a hardcoded testbed path, and refuses to run live with incomplete credentials - acceptance harness: bounded subprocess output buffers, a 15-minute hard kill for claude runs, a PATH-shim guard enforcing per-scenario ability allowlists and confirm policy, TLS-skip conditioned on the write-host allowlist, 0600/0700 artifact permissions, pagination bound - docs: cron fence language tags, a plaintext-credential trade-off note for cron.env, canary-run wording fixes
CHANGELOG.md had not been updated since the ChatEngine serialization commit; everything after it (the CLI bug-remainders sprint, the Codex review triage, the five release-audit rounds, and the polish pass just committed) was undocumented. This backfills the Unreleased section from those commit messages, user-facing changes only. The one entry that must not be lost: the credential-binding change is breaking for existing beta users. Unbound keychain credentials stored by earlier versions are refused for authenticated requests, and each profile needs a one-time re-login. It leads the Changed section.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR adds acceptance-test infrastructure, strengthens destructive execution and provider protocol handling, introduces bounded and sanitized I/O, updates credential and configuration safety, standardizes JSON/error contracts, and documents the resulting CLI, automation, and runtime requirements. ChangesCore safety and execution
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant AcceptanceRunner
participant CLIInvoker
participant MainWPControl
participant MockServer
participant IndependentVerifier
AcceptanceRunner->>CLIInvoker: run scenario command
CLIInvoker->>MainWPControl: execute CLI with isolated environment
MainWPControl->>MockServer: request ability or destructive preview
MockServer-->>MainWPControl: JSON result or preview failure
MainWPControl-->>CLIInvoker: JSON envelope and exit code
AcceptanceRunner->>IndependentVerifier: verify catalog and resource state
IndependentVerifier-->>AcceptanceRunner: independent response
AcceptanceRunner->>AcceptanceRunner: record artifacts and evaluate scenario
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/chat/providers/gemini.ts (1)
227-238: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winStreaming tool-call IDs should preserve Gemini-provided IDs and include an index. The streaming path always synthesizes
fc_${Date.now()}and dropspart.functionCall.id, so multiplefunctionCallparts in the same chunk can collide and laterfunctionResponsematching loses the original correlation ID. Mirror theconvertResponsefallback withpart.functionCall.id ?? \fc_${Date.now()}_${index}``.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chat/providers/gemini.ts` around lines 227 - 238, Update the streaming functionCall handling to preserve the provider-supplied ID and avoid collisions by using part.functionCall.id when present, otherwise falling back to the existing timestamp-based ID with the available part index appended. Keep the rest of the yielded toolCall structure unchanged.
🧹 Nitpick comments (9)
src/__tests__/process/fixtures/cli-runner.ts (1)
108-137: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueAdd an
errorhandler torunCLIWithSignalso spawn failures reject/resolve instead of hanging.Unlike
runWithStdin(which handleschild.on('error')at Line 207), this helper only resolves on'close'. Ifspawnfails (e.g., badBIN_PATH), no'close'fires and the promise stays pending until the Vitest timeout, masking the real failure.♻️ Add error handling
child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk)); child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk)); + child.on('error', (err) => { + clearTimeout(signalTimer); + clearTimeout(timeoutTimer); + resolve({ stdout: '', stderr: err.message, exitCode: 1, duration: Date.now() - start }); + }); child.on('close', (code, closeSignal) => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/__tests__/process/fixtures/cli-runner.ts` around lines 108 - 137, Update runCLIWithSignal to register a child.on('error') handler that settles the promise when spawn fails, matching the existing runWithStdin behavior. Ensure the handler clears both timers and rejects or resolves with the underlying spawn error instead of waiting indefinitely, while preserving the current close-event result handling.src/core/batch-manager.ts (1)
260-267: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate terminal-status predicate across
batch-manager.tsandwatch.ts. Both files independently define an identical "is job status terminal" check, updated in lockstep forcancelledin this PR but with nothing enforcing they stay in sync going forward.
src/core/batch-manager.ts#L260-L267: export the client-facing terminal predicate (equivalent to the current privateisTerminalStatus) as a shared helper alongsideJobStatusType.src/commands/jobs/watch.ts#L40-L43: drop the locally duplicatedisTerminalStatusand import the shared helper frombatch-manager.tsinstead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/batch-manager.ts` around lines 260 - 267, The terminal-status logic is duplicated across the batch manager and watch command. In src/core/batch-manager.ts lines 260-267, export the client-facing predicate equivalent to private isTerminalStatus alongside JobStatusType; in src/commands/jobs/watch.ts lines 40-43, remove the local isTerminalStatus and import and use the shared helper instead.src/lib/base-command.ts (1)
314-343: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winBound
redactDebugValuerecursion likesanitizeErrorValue.No depth limit or cycle (
WeakSet) guard here, unlike the sibling hardening added tosanitizeErrorValuein this same PR. A deeply-nested or self-referential debug-context value would recurse unbounded.♻️ Proposed bounded refactor
- private redactDebugContext(context: Record<string, unknown>): Record<string, unknown> { + private redactDebugContext( + context: Record<string, unknown>, + depth = 0, + seen: WeakSet<object> = new WeakSet() + ): Record<string, unknown> { const redacted: Record<string, unknown> = {}; for (const [key, value] of Object.entries(context)) { - redacted[key] = isSensitiveKey(key) ? '[REDACTED]' : this.redactDebugValue(value); + redacted[key] = isSensitiveKey(key) ? '[REDACTED]' : this.redactDebugValue(value, depth, seen); } return redacted; } - private redactDebugValue(value: unknown): unknown { + private redactDebugValue(value: unknown, depth = 0, seen: WeakSet<object> = new WeakSet()): unknown { if (typeof value === 'string' && value.length > 300) { return `${value.slice(0, 297)}...`; } + if (depth >= MAX_DEBUG_DEPTH) { + return '[TRUNCATED]'; + } + if (Array.isArray(value)) { - return value.map((item) => this.redactDebugValue(item)); + if (seen.has(value)) return '[TRUNCATED]'; + seen.add(value); + return value.map((item) => this.redactDebugValue(item, depth + 1, seen)); } if (value && typeof value === 'object') { - return this.redactDebugContext(value as Record<string, unknown>); + if (seen.has(value)) return '[TRUNCATED]'; + seen.add(value); + return this.redactDebugContext(value as Record<string, unknown>, depth + 1, seen); } return value; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/base-command.ts` around lines 314 - 343, Bound recursion in redactDebugValue and redactDebugContext using the same depth limit and WeakSet cycle guard established by sanitizeErrorValue. Ensure deeply nested values are safely truncated or redacted and self-referential structures do not recurse indefinitely, while preserving existing string truncation and sensitive-key handling.tests/acceptance/lib/artifacts.ts (1)
134-134: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNo-op
.replace('Z', 'Z').This replaces
Zwith itself and does nothing. Either drop it or replace with the intended transform (e.g. stripping the trailingZ).Proposed tidy-up
- const timestamp = startTime.replace(/[-:.]/g, '').replace('Z', 'Z'); + const timestamp = startTime.replace(/[-:.]/g, '');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/acceptance/lib/artifacts.ts` at line 134, Remove the no-op .replace('Z', 'Z') from the timestamp construction in the artifact timestamp logic, while preserving the existing removal of hyphens, colons, and periods.tests/acceptance/scenarios/read.ts (1)
97-112: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a page-count safety cap to the read.ts pagination loops. All four loops terminate only on
items.length >= totalor an empty page. A Dashboard that reports a wrongtotalwhile returning non-empty pages will loop forever and hang the run — exactly the failure modecliListAllSitesintests/acceptance/scenarios/types.ts(L138-181) already guards against withMAX_SITE_PAGES. Mirror that guard so a paging bug fails loudly instead of hanging.
tests/acceptance/scenarios/read.ts#L97-L112: add a max-page guard tocliListAlland throw when exceeded.tests/acceptance/scenarios/read.ts#L114-L127: add the same guard toverifierListAll.tests/acceptance/scenarios/read.ts#L171-L189: add the same guard toverifierListUpdates.tests/acceptance/scenarios/read.ts#L191-L210: add the same guard tocliListUpdates.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/acceptance/scenarios/read.ts` around lines 97 - 112, read.ts pagination can loop indefinitely when the reported total is incorrect; add a shared MAX_SITE_PAGES-style cap and throw once it is exceeded. Apply the guard to cliListAll (tests/acceptance/scenarios/read.ts:97-112), verifierListAll (114-127), verifierListUpdates (171-189), and cliListUpdates (191-210), preserving normal termination when totals are reached or pages are empty.src/chat/tool-envelope.ts (1)
64-110: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSingle-pass
extractFirstJsonObject
extractFirstJsonObjectis quadratic on brace-heavy responses like"{".repeat(n) + "}". ProvidermaxTokensand SSE line-buffer limits bound some inputs, butLLMResponse.contentis still not hard-capped here, so a malformed response can make parsing unnecessarily expensive. A one-pass scan or explicit length cap would avoid that.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chat/tool-envelope.ts` around lines 64 - 110, The nested scanning in extractFirstJsonObject can rescan brace-heavy malformed input quadratically. Refactor this function to scan the text once while tracking JSON string, escape, and brace-depth state, returning the first valid object candidate without restarting from each opening brace; alternatively enforce an explicit maximum scan length before parsing.src/utils/format.test.ts (1)
124-181: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winLGTM! Note: none of these cases cover the malformed/whitespace-normalized-URL edge case raised in the consolidated comment on
src/utils/format.ts#L113-L128— once that's fixed, consider adding a regression test here (e.g. adashboardUrlwith an embedded tab/leading space that still round-trips throughnew URL()with non-empty userinfo).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/format.test.ts` around lines 124 - 181, Add a regression test in the maskUrlUserinfo test suite for a dashboardUrl containing leading or embedded whitespace that new URL() normalizes while retaining non-empty userinfo. Assert that maskUrlUserinfo masks the normalized credentials correctly, covering the malformed/whitespace-normalized URL edge case described for maskUrlUserinfo.src/core/http-client.ts (1)
195-198: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueOptional: cancel the redirect response body before recursing.
The 3xx response body is left unconsumed here before
handleRedirectissues the next request. For undici-backedfetch, an unread body can delay socket release/reuse. Redirect bodies are typically empty so impact is small, but an explicitvoid response.body?.cancel().catch(() => {})keeps connection handling tidy.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/http-client.ts` around lines 195 - 198, In the redirect branch of the HTTP client’s request flow, cancel the unread response body before invoking handleRedirect, safely ignoring any cancellation rejection, then preserve the existing recursive redirect behavior.tests/acceptance/lib/commands.ts (1)
36-42: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueOptional: avoid O(n²) recomputation in
appendBounded.
reduceover all retained chunks runs on every data event, so buffering a large stream in many chunks is quadratic. A running total kept across calls (e.g., module/closure state or a small wrapper object) trims in amortized O(1).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/acceptance/lib/commands.ts` around lines 36 - 42, Optimize appendBounded by avoiding a full chunks.reduce recalculation on every call. Maintain the retained byte total across invocations, decrementing it as chunks are removed and trimming until it is within MAX_STREAM_BYTES; preserve the existing bounded-buffer behavior and function contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/commands/config/show.ts`:
- Around line 274-337: Sanitize the masked secret values before displaying them
in the config output: wrap config.profile.credentialsMasked in
sanitizeSingleLine within the Credentials row and
config.llmProvider.apiKeyMasked in sanitizeSingleLine within the API Key row.
Leave the existing masking and other display behavior unchanged.
In `@src/commands/doctor.ts`:
- Around line 444-466: Update displayReport to sanitize check.message with
sanitizeSingleLine() before colorizing and logging it, preventing embedded line
breaks from creating extra report rows. Leave check.details using
stripControlChars().split('\n') so verbose details continue to support multiline
output.
In `@src/config/keychain.ts`:
- Around line 271-276: Update the docstring near the expectedDashboardUrl
handling to state that legacy unbound entries are readable only when no expected
URL is supplied, while authenticated use with an expected URL throws and
requires a one-time re-login before the credential is rebound. Keep the
identity-bound mismatch behavior and per-invocation environment-variable note
unchanged.
In `@src/utils/format.ts`:
- Around line 113-128: Update maskUrlUserinfo in src/utils/format.ts (lines
113-128) to fail closed: after detecting credentials, apply the redaction and
return a placeholder such as [URL_WITH_CREDENTIALS] if the substitution leaves
the URL unchanged; src/commands/config/show.ts (lines 198-199) and
src/commands/doctor.ts (lines 214-215) require no direct changes because they
inherit this fix.
---
Outside diff comments:
In `@src/chat/providers/gemini.ts`:
- Around line 227-238: Update the streaming functionCall handling to preserve
the provider-supplied ID and avoid collisions by using part.functionCall.id when
present, otherwise falling back to the existing timestamp-based ID with the
available part index appended. Keep the rest of the yielded toolCall structure
unchanged.
---
Nitpick comments:
In `@src/__tests__/process/fixtures/cli-runner.ts`:
- Around line 108-137: Update runCLIWithSignal to register a child.on('error')
handler that settles the promise when spawn fails, matching the existing
runWithStdin behavior. Ensure the handler clears both timers and rejects or
resolves with the underlying spawn error instead of waiting indefinitely, while
preserving the current close-event result handling.
In `@src/chat/tool-envelope.ts`:
- Around line 64-110: The nested scanning in extractFirstJsonObject can rescan
brace-heavy malformed input quadratically. Refactor this function to scan the
text once while tracking JSON string, escape, and brace-depth state, returning
the first valid object candidate without restarting from each opening brace;
alternatively enforce an explicit maximum scan length before parsing.
In `@src/core/batch-manager.ts`:
- Around line 260-267: The terminal-status logic is duplicated across the batch
manager and watch command. In src/core/batch-manager.ts lines 260-267, export
the client-facing predicate equivalent to private isTerminalStatus alongside
JobStatusType; in src/commands/jobs/watch.ts lines 40-43, remove the local
isTerminalStatus and import and use the shared helper instead.
In `@src/core/http-client.ts`:
- Around line 195-198: In the redirect branch of the HTTP client’s request flow,
cancel the unread response body before invoking handleRedirect, safely ignoring
any cancellation rejection, then preserve the existing recursive redirect
behavior.
In `@src/lib/base-command.ts`:
- Around line 314-343: Bound recursion in redactDebugValue and
redactDebugContext using the same depth limit and WeakSet cycle guard
established by sanitizeErrorValue. Ensure deeply nested values are safely
truncated or redacted and self-referential structures do not recurse
indefinitely, while preserving existing string truncation and sensitive-key
handling.
In `@src/utils/format.test.ts`:
- Around line 124-181: Add a regression test in the maskUrlUserinfo test suite
for a dashboardUrl containing leading or embedded whitespace that new URL()
normalizes while retaining non-empty userinfo. Assert that maskUrlUserinfo masks
the normalized credentials correctly, covering the
malformed/whitespace-normalized URL edge case described for maskUrlUserinfo.
In `@tests/acceptance/lib/artifacts.ts`:
- Line 134: Remove the no-op .replace('Z', 'Z') from the timestamp construction
in the artifact timestamp logic, while preserving the existing removal of
hyphens, colons, and periods.
In `@tests/acceptance/lib/commands.ts`:
- Around line 36-42: Optimize appendBounded by avoiding a full chunks.reduce
recalculation on every call. Maintain the retained byte total across
invocations, decrementing it as chunks are removed and trimming until it is
within MAX_STREAM_BYTES; preserve the existing bounded-buffer behavior and
function contract.
In `@tests/acceptance/scenarios/read.ts`:
- Around line 97-112: read.ts pagination can loop indefinitely when the reported
total is incorrect; add a shared MAX_SITE_PAGES-style cap and throw once it is
exceeded. Apply the guard to cliListAll
(tests/acceptance/scenarios/read.ts:97-112), verifierListAll (114-127),
verifierListUpdates (171-189), and cliListUpdates (191-210), preserving normal
termination when totals are reached or pages are empty.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ee99b1e7-b7c4-4f44-a4e2-0926ddc37a70
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (127)
.github/workflows/ci.yml.gitignoreCHANGELOG.mdREADME.mddocs/acceptance-testing.mddocs/workflows/daily-health-check.mddocs/workflows/input-from-file.mddocs/workflows/monthly-batch-updates.mddocs/workflows/plugin-deployment-verification.mdpackage.jsonsrc/__tests__/e2e/batch-polling-flow.test.tssrc/__tests__/e2e/chat-destructive-flow.test.tssrc/__tests__/e2e/command-workflows.test.tssrc/__tests__/e2e/exit-codes.test.tssrc/__tests__/e2e/json-contract.test.tssrc/__tests__/e2e/login-abilities-flow.test.tssrc/__tests__/e2e/non-tty-behavior.test.tssrc/__tests__/e2e/test-helpers.tssrc/__tests__/process/abilities-run.test.tssrc/__tests__/process/auth.test.tssrc/__tests__/process/batch-wait.test.tssrc/__tests__/process/config-show.test.tssrc/__tests__/process/doctor.test.tssrc/__tests__/process/exit-codes.test.tssrc/__tests__/process/fixtures/api-responses.tssrc/__tests__/process/fixtures/cli-runner.tssrc/__tests__/process/live-api.test.tssrc/__tests__/process/live-workflow-docs.test.tssrc/__tests__/process/safety.test.tssrc/__tests__/process/scenarios.test.tssrc/chat/chat-engine.test.tssrc/chat/chat-engine.tssrc/chat/context-window.test.tssrc/chat/context-window.tssrc/chat/providers/anthropic.test.tssrc/chat/providers/anthropic.tssrc/chat/providers/gemini.test.tssrc/chat/providers/gemini.tssrc/chat/providers/openai-compatible.tssrc/chat/providers/provider-fetch.test.tssrc/chat/providers/provider-fetch.tssrc/chat/providers/provider.test.tssrc/chat/providers/provider.tssrc/chat/providers/sse-reader.test.tssrc/chat/providers/sse-reader.tssrc/chat/providers/streamed-tool-arguments.test.tssrc/chat/providers/tool-protocol.test.tssrc/chat/system-prompt.test.tssrc/chat/system-prompt.tssrc/chat/tool-envelope.test.tssrc/chat/tool-envelope.tssrc/commands/abilities/list.tssrc/commands/abilities/run.tssrc/commands/chat.test.tssrc/commands/chat.tssrc/commands/config/show.tssrc/commands/doctor.tssrc/commands/jobs/watch.test.tssrc/commands/jobs/watch.tssrc/commands/login.tssrc/commands/profile/delete.tssrc/config/atomic-write.test.tssrc/config/fs-utils.test.tssrc/config/fs-utils.tssrc/config/keychain.test.tssrc/config/keychain.tssrc/config/profile-store.test.tssrc/config/profile-store.tssrc/core/abilities-executor.test.tssrc/core/abilities-executor.tssrc/core/batch-manager.test.tssrc/core/batch-manager.tssrc/core/execute-ability-with-policy.test.tssrc/core/execute-ability-with-policy.tssrc/core/http-client.test.tssrc/core/http-client.tssrc/core/job-id.tssrc/core/safety-controller.test.tssrc/core/safety-controller.tssrc/lib/base-command.test.tssrc/lib/base-command.tssrc/output/formatter.test.tssrc/output/formatter.tssrc/output/json-envelope.test.tssrc/output/json-envelope.tssrc/utils/audit-logger.permissions.test.tssrc/utils/audit-logger.test.tssrc/utils/audit-logger.tssrc/utils/error-sanitizer.test.tssrc/utils/error-sanitizer.tssrc/utils/errors.tssrc/utils/format.test.tssrc/utils/format.tssrc/utils/prompt.tssrc/utils/redaction.test.tssrc/utils/redaction.tssrc/utils/retry.test.tssrc/utils/retry.tssrc/utils/terminal-sanitizer.test.tssrc/utils/terminal-sanitizer.tssrc/validation/input-sanitizer.test.tssrc/validation/input-sanitizer.tssrc/validation/sanitize-schema.test.tssrc/validation/sanitize-schema.tssrc/validation/schema-validator.test.tssrc/validation/schema-validator.tstests/acceptance/agent-run.tstests/acceptance/fixtures.tstests/acceptance/lib/artifacts.tstests/acceptance/lib/cli.tstests/acceptance/lib/commands.tstests/acceptance/lib/env.tstests/acceptance/lib/guards.tstests/acceptance/lib/local-registry.tstests/acceptance/lib/pack.tstests/acceptance/lib/redact.tstests/acceptance/lib/verify.tstests/acceptance/run.tstests/acceptance/scenarios/configuration.tstests/acceptance/scenarios/errors.tstests/acceptance/scenarios/index.tstests/acceptance/scenarios/read.tstests/acceptance/scenarios/safety.tstests/acceptance/scenarios/types.tstests/acceptance/scenarios/writes.tsvitest.config.tsvitest.process.config.ts
💤 Files with no reviewable changes (2)
- src/utils/retry.ts
- src/utils/retry.test.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/__tests__/process/fixtures/cli-runner.ts`:
- Around line 122-126: Update the child-process handling around the error
listener and close path so child.on('error') records or handles spawn failures
without resolving the run or clearing timeoutTimer after a failed kill. Keep the
close handler as the terminal resolution path, while preserving appropriate
cleanup for genuine spawn failures and the hard-kill fallback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f774e196-ba01-4828-8ff9-6a5085af9cbf
📒 Files selected for processing (13)
src/__tests__/process/fixtures/cli-runner.tssrc/commands/config/show.tssrc/commands/doctor.tssrc/commands/jobs/watch.test.tssrc/commands/jobs/watch.tssrc/config/keychain.tssrc/core/batch-manager.tssrc/core/http-client.tssrc/lib/base-command.tssrc/utils/format.test.tssrc/utils/format.tstests/acceptance/lib/artifacts.tstests/acceptance/scenarios/read.ts
🚧 Files skipped from review as they are similar to previous changes (12)
- src/utils/format.test.ts
- src/utils/format.ts
- tests/acceptance/lib/artifacts.ts
- src/commands/doctor.ts
- src/lib/base-command.ts
- src/commands/jobs/watch.test.ts
- src/commands/jobs/watch.ts
- src/core/http-client.ts
- src/commands/config/show.ts
- tests/acceptance/scenarios/read.ts
- src/config/keychain.ts
- src/core/batch-manager.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/__tests__/process/fixtures/mock-server.ts`:
- Line 44: Update requestWaiters and waitForRequest() so every queued waiter has
timeout or cancellation handling and cannot remain pending indefinitely when its
request is missed. During reset() and shutdown, reject all pending waiters
before clearing the queue, preserving normal resolution when a matching request
arrives. Ensure the runCLIWithSignal readiness gate receives the rejection
rather than hanging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 40458767-b01a-490c-9e73-30b0f0aa4a09
📒 Files selected for processing (3)
src/__tests__/process/batch-wait.test.tssrc/__tests__/process/fixtures/cli-runner.tssrc/__tests__/process/fixtures/mock-server.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/tests/process/fixtures/cli-runner.ts
- src/tests/process/batch-wait.test.ts
| private server: Server | null = null; | ||
| private routes: Route[] = []; | ||
| private recorded: RecordedRequest[] = []; | ||
| private requestWaiters: Array<{ match: (r: RecordedRequest) => boolean; resolve: () => void }> = []; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Settle or time-bound every queued waiter.
waitForRequest() can return a promise that never settles, while reset() simply drops its waiter. If the expected request is missed, the runCLIWithSignal(..., server.waitForRequest(...)) readiness gate can hang indefinitely. Add timeout/cancellation support and reject pending waiters before clearing them during reset or shutdown.
Also applies to: 92-92, 181-187
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/__tests__/process/fixtures/mock-server.ts` at line 44, Update
requestWaiters and waitForRequest() so every queued waiter has timeout or
cancellation handling and cannot remain pending indefinitely when its request is
missed. During reset() and shutdown, reject all pending waiters before clearing
the queue, preserving normal resolution when a matching request arrives. Ensure
the runCLIWithSignal readiness gate receives the rejection rather than hanging.
…tion
The real find: maskUrlUserinfo failed open. new URL() strips tab/newline
before detecting userinfo, but the replace regex excludes whitespace and
is anchored, so a stored URL like https://admin:sec\nret@host passed the
credential check yet came back unmasked. Detected-but-unmaskable URLs
now return [URL_WITH_CREDENTIALS_REDACTED]; regression tests cover
newline, tab, and leading-whitespace variants.
The rest of the review, all accepted:
- config show passes credentialsMasked and apiKeyMasked through
sanitizeSingleLine; the mask keeps literal head/tail bytes of the
secret, which can carry escapes from MAINWP_APP_PASSWORD
- doctor renders check.message with sanitizeSingleLine (single-row
field); verbose details stay multiline via stripControlChars
- keychain docstring reworded: legacy entries are refused for
authenticated use pending a one-time login, not auto-rebound
- isTerminalStatus deduplicated into batch-manager and re-exported from
watch; watch.test builds its mock over importOriginal
- redactDebugValue bounded like the shared sanitizers: 32-level depth
cap, ancestor-tracking WeakSet
- http-client cancels the unread redirect response body before following
- runCLIWithSignal resolves on spawn error instead of hanging; gated on
the 'spawn' event so a failed kill() cannot disarm the SIGKILL
fallback while the child still runs ('close' stays the terminal path)
- acceptance read.ts pagination loops capped at 100 pages; artifacts
drops a no-op replace('Z', 'Z')
Declined two nitpicks: the extractFirstJsonObject single-pass rewrite
(deliberate, test-pinned scanner; provider-response sizes make the
second pass free) and appendBounded's quadratic reduce (buffer is
byte-capped, so the chunk array stays ~100 elements, test-only code).
The advisory (host confusion via a literal backslash authority delimiter) landed against fast-uri 3.0.0-3.1.3 after the last CI run and turned the npm audit gate red on every matrix job. fast-uri reaches production through ajv. Deliberately narrower than `npm audit fix`, which also refreshed five unrelated in-range transitives and pruned orphaned lock entries; this bumps fast-uri alone (`npm update fast-uri`) so the release diff stays reviewable. Most of the lockfile diff is npm 11 rewriting entry order; the only version change is fast-uri, verified by diffing the parsed package list before and after.
7368b43 to
2aa7dae
Compare
The process tests spawn the built CLI as a child per assertion, and the
first full CI runs of this branch surfaced platform gaps. A five-round
isolation probe on a Windows runner (scratch branch, since deleted)
found the root cause and disproved two plausible theories along the way
(Defender scanning and pipe-mode stdio: exclusions changed nothing, and
every stdio mode spawns in ~800ms outside the harness).
The root cause: buildEnv() handed children a hand-built minimal
environment. POSIX children don't care; Windows children lose the OS
plumbing (SystemRoot, TEMP, PATHEXT, APPDATA, ...) and node boots into
multi-second fallback paths — 20-40s per child, measured against ~800ms
with a full env. Under parallel vitest files those children stacked and
starved the runner. On win32 buildEnv now inherits the OS environment,
strips MAINWP*, and applies the same overrides; isolated probe runs
dropped smoke from 121s to 9s, safety from 449s to 20s, and the full
process suite from a 20-minute timeout to 3m15s.
Also in this commit:
- SIGINT delivery was a race: a 750ms timer can fire before the CLI
installs its handler on a slow runner. runCLIWithSignal accepts a
readiness promise, MockServer gains waitForRequest(), and the
jobs-watch SIGINT tests fire the signal only after the first status
poll proves the handler is live.
- Two contracts are POSIX-only and skip on win32: the jobs-watch SIGINT
cancellation flow (child.kill('SIGINT') on Windows terminates without
running handlers) and the audit-log permission self-heal assertions
(Windows has no mode bits; stat reports 0o666 regardless).
- Child and test timeouts keep extra headroom on win32 (60s/90s): free
when healthy, and a slow runner cannot SIGKILL a working child
mid-boot, which reports as empty output.
- The matrix runs with fail-fast off: canceled cells hid whether their
platform actually passes and cost a misdiagnosed round.
2aa7dae to
9d3bc88
Compare
The testbed Dashboard now exposes WordPress-core abilities without a -vN suffix (core/get-site-info, core/get-environment-info). CLI discovery skips unversioned entries by design, so the scenario's raw-catalog expectation failed the live run 16/1/3. The independent expectation now applies the same name rule, and the fixture catalog carries an unversioned entry so the fixture target keeps exercising the contract.
What this is
33 commits preparing @mainwp/control for the next release. Three arcs:
Acceptance harness (
tests/acceptance/). Packs the tarball into a fresh consumer (or runs from source) and drives the installedmainwpcontrolbinary through 20 scenarios against fixture and live Dashboards: read cross-checks against an independent verifier, error contracts, and safety flows proven by mock-server request recording (dry-run previews once and never confirms,--forceskips only the prompt, failed preview fails closed). A graded agent layer drivesclaude -pthrough four scenarios and grades the ordered CLI argv plus final answer against ground truth computed before the model runs. Live-writes baseline: 17 passed / 0 failed / 3 skipped (destructive scenarios are fixture-only).Release-audit remediation. Five fix/verify rounds against Codex's pre-release audit, each round independently re-verified with direct probes. The headline items:
profiles.jsongets an AuthError, not the password.OUTCOME_UNKNOWN) with an audit record instead of a generic network error.pattern/patternPropertiesare stripped structurally from every tree node, and the whole tree (arrays and literal data included) counts against a 32-level depth budget.http://base URLs.Post-audit polish pass (final commit before the changelog). fsync-before-rename atomic writes, prototype-pollution-safe sanitizers, bidi control stripping, bounded audit free text, case-sensitive ability-name validation, explicit live-test gating, and subprocess/artifact bounds in the harness.
Breaking change
Existing beta users must run
mainwpcontrol loginonce per profile: unbound credentials stored by earlier versions are refused for authenticated requests. The changelog leads with this.Not in this PR
Version bump, tag, and npm publish happen after merge.
Verification
npm run typecheck,npm run lint(0 errors),npm testincluding the process suite: 938/938 passingnpm run buildandgit diff --checkcleanSummary by CodeRabbit
--json/envelope contract coverage and deterministic verification tooling.cancelled) and stricter API response validation.jobs watch/batch timeout and improved SIGINT handling to consistently exit with 130.test-results/to.gitignore.