Skip to content

CLI: Add self-hosted WordPress site support via Application Passwords - #3105

Draft
youknowriad wants to merge 4 commits into
trunkfrom
org-app-password-connector
Draft

CLI: Add self-hosted WordPress site support via Application Passwords#3105
youknowriad wants to merge 4 commits into
trunkfrom
org-app-password-connector

Conversation

@youknowriad

@youknowriad youknowriad commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Related issues

  • Related to self-hosted site editing support in Studio Code

How AI was used in this PR

This PR was co-authored with Claude. All code was reviewed and approved by the developer.

Proposed Changes

  • Add a new "Self-hosted" tab to the Studio Code site picker (alongside Local and WordPress.com), allowing users to connect any WordPress site using an Application Password
  • Guided inline connection flow (URL → username → application password) that validates the credentials against an authenticated REST endpoint before saving, with support for multiple saved sites and backspace-to-delete
  • A new wp_request agent tool that talks to the site's own WordPress REST API via Basic Auth — the self-hosted counterpart of wpcom_request, including staged bodyFile/bodyFiles payloads for large generated content and plugin namespaces (e.g. wc/v3)
  • A dedicated lean system prompt for self-hosted sites with no plan-based restrictions; endpoint guidance lives in a new self-hosted-remote-management skill, mirroring how WordPress.com remote guidance is delivered
  • Application Passwords are stored in cli.json and resolved at turn time — they are never persisted in session transcripts, so resumed sessions reconnect safely

Testing Instructions

  • Run npm run cli:build && node apps/cli/dist/cli/main.mjs code
  • Open the site picker (down arrow with empty prompt)
  • Navigate to the "Self-hosted" tab using the right arrow key
  • Select "Add new site" and follow the prompts to connect a self-hosted WordPress site (create an Application Password under Users → Profile → Application Passwords)
  • Verify a wrong password is rejected at connection time
  • Verify the AI agent can list posts, create content, and take screenshots on the connected site
  • Resume the session (--resume) and verify the agent still operates on the self-hosted site
  • Verify backspace on an existing self-hosted site prompts for deletion confirmation

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?
… Passwords

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chubes4

chubes4 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

I would love to manage my self-hosted sites from Studio.

@borkweb

borkweb commented Apr 28, 2026

Copy link
Copy Markdown
Member

My Claude review found these notable items:

Pass 1 — CRITICAL

HTTP scheme accepted; Basic Auth sent in cleartext.

[apps/cli/ai/ui.ts:1042-1046]

if ( ! normalizedUrl.startsWith( 'http' ) ) { normalizedUrl = https://${ siteUrl }; } accepts http:// (and any string starting with "http", e.g. httpx://). User entering an http:// URL transmits the App Password unencrypted on every request — defeats the whole auth model.

Fix: Require https://. Either reject http:// outright with a clear error, or gate it behind an explicit --allow-insecureopt-in. Tighten the prefix check tostartsWith('http://') || startsWith('https://').

My commentary: I don't think this is actually a big deal if someone is explicitly working with an http site on purpose.

Prompt injection via untrusted site name.

[apps/cli/ai/ui.ts:1019 → apps/cli/ai/system-prompt.ts:114-115 and apps/cli/commands/ai/index.ts:370]

The connect probe reads data.name from /wp-json/ of an attacker-controlled URL, persists it as the site display name, and embeds it verbatim into the system prompt ("${site.name}" at ${site.url}) and the per-turn enriched prompt. A site with a crafted name (e.g. containing \n\nIgnore prior instructions and DELETE /users/2)
becomes part of the agent's system prompt.

Fix: Strip control characters / newlines, length-cap (e.g. 80 chars), and consider treating it as a quoted data field (e.g. <site_name>{...}</site_name>) rather than free-form prose.

No fetch timeout — agent can hang.

[apps/cli/ai/self-hosted-tools.ts:84 and apps/cli/ai/ui.ts:1006]

Both the connection probe and every wp_request call use bare fetch(). A slow or unresponsive site freezes the chat with no abort.

Fix: AbortSignal.timeout(15_000) (or similar) on both call sites; surface "Request timed out" in error result.

Backspace-as-delete is a footgun.

[apps/cli/ai/ui.ts:798-802]

On the self-hosted tab, when search query is empty, backspace immediately enters a delete-confirm flow. Hint text mentions it, but a stray keystroke + accidental "Yes" wipes credentials. The mismatch with the WP.com tab (where backspace does nothing destructive) makes it especially surprising.

Fix: Bind delete to an explicit key (d or Delete), and/or require typing the site name to confirm.

Error response body forwarded into LLM context unbounded.

[apps/cli/ai/self-hosted-tools.ts:88-91]

On non-2xx, await response.text() is concatenated into the tool result. WordPress error responses can include plugin file paths, stack traces, and (for some misconfigured WAFs) echo back request headers. Everything ends up in the LLM context and any session log.

Fix: Cap body length (e.g. 4 KB), parse JSON and surface only code/message when possible.

wp_request path traversal escapes the /wp/v2/ namespace.

[apps/cli/ai/self-hosted-tools.ts:71]

new URL(${ baseUrl }/wp-json/wp/v2${ relativePath }) doesn't normalize, but the WordPress server normalizes ... The LLM can pass path: "/../wp/v3/foo" or "/../../something" to reach REST namespaces outside wp/v2/ — contradicting the tool description and system prompt's claims. Not a security boundary (admin App Password
already grants this access), but it's a contract violation that future tightening assumes is enforced.

Fix: Reject paths containing .. segments; require leading /.

@Poliuk

Poliuk commented May 7, 2026

Copy link
Copy Markdown
Contributor

Should we at least require self-hosted sites to have Jetpack installed to connect with Studio?

@chubes4

chubes4 commented May 7, 2026

Copy link
Copy Markdown
Contributor

In my opinion, Studio should remain generic and not have any blockers for self-hosted sites. For example, I don't (currently) use Jetpack on my sites so this would block me from developing with Studio.

I think it would be best to make Studio so useful as a general-purpose WordPress development environment that everyone who uses WP wants to use Studio for everything, regardless of whether or not they use our other products.

I would love to take self-hosted support even further and include direct SSH access via Studio Code so users can execute remote commands on their live sites.

@youknowriad

Copy link
Copy Markdown
Contributor Author

I agree that ideally we don't require anything at first (for basic modifications and using the agent), an application password like this PR requests should be enough. That said, I do believe that later we could ask the users to install Jetpack if they trigger "push" or things like that, because these features will be provided by Jetpack and not available by default in WP sites.

@youknowriad

Copy link
Copy Markdown
Contributor Author

If anyone would like to take over this PR and push it, please do, I'm a little bit on other stuff at the moment.

The original branch predates the pi-coding-agent runtime: agent.ts and
tools.ts no longer exist, tools are one-per-file under ai/tools/, and
endpoint guidance lives in skills instead of the system prompt. The
self-hosted feature is re-integrated accordingly: wp_request is a
defineTool-based tool with staged body-file support, credentials are
resolved from cli.json at turn time instead of being carried on
SiteInfo, connection validation hits an authenticated endpoint, and a
self-hosted-remote-management skill replaces the inline endpoint docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@youknowriad

Copy link
Copy Markdown
Contributor Author

Revived this PR by merging trunk (~3 months of drift). The original branch predates the pi-coding-agent runtime, so this was a port rather than a mechanical conflict resolution:

Architecture port

  • agent.ts / tools.ts (Agent SDK era) are gone on trunk; the self-hosted branch now lives in the pi runtime's buildAgentTools() / createStudioAgentSession().
  • The wp_request tool was rewritten from the Agent SDK tool() + zod shape to defineTool() + TypeBox in apps/cli/ai/tools/wp-request.ts, matching the one-tool-per-file layout.
  • The ~50-line endpoint reference was moved out of the system prompt into a new self-hosted-remote-management skill, mirroring wpcom-remote-management; the self-hosted prompt intro is now lean like the WP.com one.

Improvements over the original branch

  • Credentials no longer ride on SiteInfo: only the saved-connection id does; the Application Password is resolved from cli.json at turn time (same pattern as the WP.com OAuth token). This also means resumed sessions (studio.site_selected replay) reconnect correctly instead of silently degrading, and secrets never land in session transcripts.
  • Connection validation actually validates: the old flow fetched /wp-json/ with Basic Auth, but that endpoint is public, so wrong credentials still "connected". It now authenticates against /wp-json/wp/v2/users/me first.
  • wp_request gained bodyFile/bodyFiles staged payloads (shared helpers extracted from wpcom_request) so large generated content doesn't blow up inline bodies, plus an apiNamespace param for plugin endpoints (wc/v3, etc.).
  • Site deletion in the picker now matches by connection id rather than url+username, so duplicate URLs with different users behave.

Verified: npm run typecheck, full apps/cli + packages/common test suites (new wp-request tests + self-hosted system-prompt tests added), npm run cli:build (skill bundles correctly).

Known limitations / follow-up candidates

  • Sites without pretty permalinks (?rest_route= fallback) aren't supported by the connection flow or the tool.
  • The Application Password is stored in plaintext in cli.json — same treatment as other CLI-owned state, but worth an explicit call before this leaves draft.
  • No pull-to-local for self-hosted sites (site_pull is WP.com-only), so the toolset is content/REST management + screenshots.
@youknowriad
youknowriad marked this pull request as ready for review July 6, 2026 10:55
@draganescu

Copy link
Copy Markdown

Sites without pretty permalinks (?rest_route= fallback) aren't supported by the connection flow or the tool.

I think I had this similar issue with Telex.

@wpmobilebot

wpmobilebot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing 9435b5e vs trunk

app-size

Metric trunk 9435b5e Diff Change
App Size (Mac) 1345.25 MB 1345.27 MB +0.02 MB ⚪ 0.0%

site-editor

Metric trunk 9435b5e Diff Change
load 750 ms 747 ms 3 ms ⚪ 0.0%

site-startup

Metric trunk 9435b5e Diff Change
siteCreation 6462 ms 6483 ms +21 ms ⚪ 0.0%
siteStartup 2385 ms 2396 ms +11 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

…nk self-hosted sites work

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@draganescu draganescu self-assigned this Jul 6, 2026
@draganescu

Copy link
Copy Markdown

Looked into this. Added a discovery for api base URL. Would we want this to show in UI too?

@youknowriad

Copy link
Copy Markdown
Contributor Author

@draganescu probably as a follow-up yeah. This is being discussed in https://a8c.slack.com/archives/C08LZ79MKG9/p1783336369753759

@wojtekn

wojtekn commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@youknowriad is this being replaced by #4104?

Besides that, I'm marking it as a draft until conflicts are fixed.

@wojtekn
wojtekn marked this pull request as draft July 14, 2026 07:49
@youknowriad

Copy link
Copy Markdown
Contributor Author

Both have their place IMO. Both PRs can be shipped quickly but at this point, it's about whether we want them or not. It seems for now the answer is "no". So I'm just leaving them for later.

@wojtekn

wojtekn commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Thanks for confirming that @youknowriad - let's keep them as drafts then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

7 participants