Skip to content

feat(web): expose Playbook Exchange to mobile/web - #947

Merged
pedramamini merged 9 commits into
RunMaestro:rcfrom
chr1syy:feat/web-playbook-exchange
May 10, 2026
Merged

feat(web): expose Playbook Exchange to mobile/web#947
pedramamini merged 9 commits into
RunMaestro:rcfrom
chr1syy:feat/web-playbook-exchange

Conversation

@chr1syy

@chr1syy chr1syy commented May 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Mobile users can now browse, preview, and import community playbooks from inside the AutoRun setup sheet (Playbook Exchange parity with desktop's MarketplaceModal).
  • Marketplace cache / fetch / import logic is extracted into a shared service so the IPC handler and the web-server use a single code path (no renderer round-trip).
  • SSH-remote sessions on mobile import to the correct host — the server resolves autoRunFolderPath and SSH config from the session itself, so mobile clients can't override them.

What changed

Server

  • src/main/services/marketplace-service.ts (new) — pure helpers for cache/manifest/document/import.
  • src/main/ipc/handlers/marketplace.ts — slimmed to thin wrappers around the service. Existing IPC behavior preserved (45 IPC tests still pass).
  • src/main/web-server/handlers/messageHandlers.ts — four new WebSocket message types: marketplace_get_manifest (with refresh: true), marketplace_get_document, marketplace_get_readme, marketplace_import_playbook. Path-traversal + required-field validation at the boundary.
  • src/main/web-server/web-server-factory.ts — wires callbacks; importMarketplacePlaybook resolves autoRunFolderPath and SSH config from the session (mobile clients only send sessionId, playbookId, targetFolderName).
  • WebServer.ts, CallbackRegistry.ts, types.ts — extended for the four new callbacks.

Mobile UI

  • src/web/mobile/MarketplaceSheet.tsx (new) — 90vh bottom sheet, list → detail → import flow with category chips, search, README + document preview via MobileMarkdownRenderer, and target-folder input.
  • src/web/mobile/AutoRunSetupSheet.tsx — optional onOpenMarketplace prop and a "Browse Playbook Exchange" entry point at the top of the scrollable area.
  • src/web/mobile/App.tsx — adds showMarketplaceSheet state, marketplace handlers, wires the entry point, and auto-refreshes AutoRun docs after a successful import.

Test plan

  • npm run lint (TypeScript, all configs)
  • npm run lint:eslint
  • npx prettier --check for all changed files
  • vitest run src/__tests__/main/ipc/handlers/marketplace.test.ts (45 tests pass — IPC handler refactor preserves behavior)
  • vitest run src/__tests__/main/web-server/handlers/messageHandlers.test.ts (113 tests pass — includes 7 new tests for marketplace WS handlers + traversal rejection + unconfigured-callback paths)
  • vitest run src/__tests__/web/mobile/App.test.tsx (95 tests pass — adds mock for new MarketplaceSheet)
  • Manual smoke on mobile: open AutoRun setup → tap Playbook Exchange → browse, search, preview README, preview document, import to a folder name → verify the imported playbook appears in the AutoRun document list (AR-PARITY-01)
  • Manual smoke with SSH remote session: import lands on the remote host (parity with desktop)

Notes

  • No browse-folder picker on mobile (no native folder picker available); users type the folder name. Same constraint the desktop modal already has when remote.
  • Marketplace cache TTL, file watcher hot-reload, and local-manifest behavior are unchanged.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Playbook Exchange (Marketplace) on mobile: browse/search by category, preview README/docs, edit import folder name, import playbooks with progress and haptic feedback; integrates with Auto Run.
  • Bug Fixes / Safety

    • Stronger input validation and traversal protections; clearer errors when SSH remote is missing/disabled. Imports tolerate per-document failures and record only successfully imported docs/assets.
  • Tests

    • Expanded tests covering marketplace messaging, IPC, import edge cases, and mobile flows.

Mobile users can now browse, preview, and import community playbooks
from inside the AutoRun setup sheet — closing the AR-PARITY-01 gap
between desktop and the mobile interface.

- Extracted marketplace cache/fetch/import logic into
  src/main/services/marketplace-service.ts so the IPC handler and the
  web-server share one code path (no renderer round-trip).
- Added four WebSocket message types: marketplace_get_manifest (with
  refresh:true), marketplace_get_document, marketplace_get_readme,
  marketplace_import_playbook. Filename traversal + missing-field
  validation at the boundary.
- Wired callbacks in web-server-factory; the import callback resolves
  autoRunFolderPath and SSH config from the session itself, so mobile
  clients can't override them — SSH remote sessions import to the
  correct host the same way as desktop.
- Created MarketplaceSheet (mobile bottom sheet with list → detail →
  import flow, category chips, search, README/document preview) and
  added a "Browse Playbook Exchange" entry point at the top of
  AutoRunSetupSheet. After import, AutoRun docs auto-refresh so
  freshly imported docs appear in the selector.

Tests: new marketplace block in messageHandlers.test.ts covers all
four message types + traversal rejection + unconfigured-callback
paths. AutoRun App.test mocks the new sheet. All scoped suites pass
(marketplace IPC: 45, message handlers: 113, mobile App: 95).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Playbook Exchange (Marketplace): a new marketplace service (manifest caching/merge, document/asset fetch, import with local/SSH targets, local-manifest watcher), thin IPC/WebSocket handlers and CallbackRegistry/WebServer wiring, a mobile MarketplaceSheet UI and AutoRun entry, and updated tests/mocks.

Changes

Marketplace Feature Implementation

Layer / File(s) Summary
Types
src/main/web-server/types.ts
Adds marketplace types, result interfaces, and callback type aliases (MarketplaceManifestResult, MarketplaceImportResult, GetMarketplaceManifestCallback, GetMarketplaceDocumentCallback, GetMarketplaceReadmeCallback, ImportMarketplacePlaybookCallback).
Service / Data Shape
src/main/services/marketplace-service.ts
New shared service: cache/local path utilities, local-path detection and target-name validation, local manifest loader + merge with official GitHub manifest, 6‑hour TTL cache read/write, fetch helpers for documents/README/assets (with traversal protections), and exported service interfaces/types.
Core Import Logic
src/main/services/marketplace-service.ts
Implements importMarketplacePlaybook (resolve merged manifest, create local/SSH destinations, import documents/assets with per-item failure tolerance, write session-scoped playbook JSON) and createLocalManifestWatcher.
IPC Layer
src/main/ipc/handlers/marketplace.ts
Replaces prior in-file logic with thin IPC handlers delegating to service functions, manages createLocalManifestWatcher lifecycle, resolves optional sshRemoteId to sshConfig, and forwards import calls.
WebSocket / Message Handlers
src/main/web-server/handlers/messageHandlers.ts
Adds marketplace_get_manifest, marketplace_get_document, marketplace_get_readme, marketplace_import_playbook routes; performs input validation (rejects local/untrusted paths, filename/target traversal), centralized failure frames, and structured success *_result responses.
Callback Registry & Wiring
src/main/web-server/managers/CallbackRegistry.ts, src/main/web-server/WebServer.ts, src/main/web-server/web-server-factory.ts
CallbackRegistry gains marketplace callback slots with getters/setters; WebServer exposes setter methods; factory registers main-process callbacks that call service functions, resolves session SSH config, validates session/autoRun folder, and returns structured error/success results.
Mobile UI & Integration
src/web/mobile/MarketplaceSheet.tsx, src/web/mobile/AutoRunSetupSheet.tsx, src/web/mobile/App.tsx
Adds MarketplaceSheet UI (manifest list, search, detail/README/document preview, import flow with editable target folder), wires AutoRunSetup entry and App state to open/close sheet and refresh AutoRun docs after import.
Tests & Mocks
src/__tests__/main/web-server/handlers/messageHandlers.test.ts, src/__tests__/main/ipc/handlers/marketplace.test.ts, src/__tests__/web/mobile/App.test.tsx, src/__tests__/main/web-server/web-server-factory.test.ts
Adds/updates tests and mocks: marketplace message-handler suite and mock callbacks, tightened IPC import tests (tilde path handling, partial-document failure persistence, SSH-not-found/disabled now fail early), and a MarketplaceSheet mock for mobile tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Mobile Client
    participant Server as WebServer<br/>MessageHandler
    participant Registry as CallbackRegistry
    participant Service as Marketplace<br/>Service
    participant GitHub as GitHub API
    participant Disk as Disk Cache<br/>& Local FS

    rect rgba(100,150,200,0.5)
    Client->>Server: marketplace_get_manifest {refresh?}
    Server->>Registry: getMarketplaceManifest(opts)
    Registry->>Service: getMarketplaceManifest(app)
    alt Cache valid
        Service->>Disk: read cache
        Disk-->>Service: cached manifest
    else Cache miss/expired
        Service->>GitHub: fetch official manifest
        GitHub-->>Service: manifest.json
        Service->>Disk: write cache
    end
    Service->>Disk: read local manifest
    Service->>Service: merge official + local
    Service-->>Registry: {manifest, fromCache, cacheAge}
    Registry-->>Server: {manifest, fromCache, cacheAge}
    Server->>Client: marketplace_get_manifest_result
    end
Loading
sequenceDiagram
    participant Client as Mobile Client
    participant Server as WebServer<br/>MessageHandler
    participant Registry as CallbackRegistry
    participant Service as Marketplace<br/>Service
    participant GitHub as GitHub API
    participant LocalFS as Local/Remote FS

    rect rgba(100,150,200,0.5)
    Client->>Server: marketplace_import_playbook {sessionId, playbookId, targetFolderName}
    Server->>Registry: importMarketplacePlaybook(...)
    Registry->>Service: importMarketplacePlaybook(opts)
    Service->>Service: resolve manifest & playbook
    Service->>LocalFS: create target directory (local or remote)
    loop documents
        Service->>GitHub: fetch document.md (or local read)
        GitHub-->>Service: content
        Service->>LocalFS: write document.md
    end
    alt local playbook path
        Service->>LocalFS: discover local assets/
        Service->>Service: merge assets list
    end
    loop assets
        Service->>GitHub: fetch asset (or local read)
        GitHub-->>Service: binary
        Service->>LocalFS: write asset to assets/
    end
    Service->>LocalFS: append session playbook JSON (filter only successful docs)
    Service-->>Registry: {playbook, importedDocs, importedAssets}
    Registry-->>Server: result
    Server->>Client: marketplace_import_playbook_result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • RunMaestro/Maestro#686 — touches WebServer/CallbackRegistry/messageHandlers surfaces; related callback additions and tests.
  • RunMaestro/Maestro#898 — also adds WebSocket handlers and CallbackRegistry wiring in the same modules.

Suggested labels

ready to merge

Poem

🐰 I hopped through manifests, cached and bright,
Merged local playbooks into the light.
Docs and assets fetched with care,
Imported gardens here and there.
Happy rabbit—new playbooks take flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely describes the main change: exposing Playbook Exchange functionality to mobile/web users. It accurately summarizes the primary objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR exposes the Playbook Exchange to mobile/web clients via four new WebSocket message types, backed by a new marketplace-service.ts that the IPC handlers and WS server share. The server-side importMarketplacePlaybook callback resolves autoRunFolderPath and SSH config from the session store rather than accepting them from the client, and defence-in-depth path-traversal guards (isUntrustedLocalPath, assertSafeTargetFolderName) are applied at the WS handler boundary and again inside the service.

  • importError in MarketplaceSheet is a shared state for both README/document preview failures and actual import failures; preview errors appear in the import footer next to the "Import Playbook" button, misleading users into thinking an import operation failed when only the preview fetch did.

Confidence Score: 4/5

Safe to merge after resolving the importError state conflation in MarketplaceSheet; all server-side security boundaries are correctly enforced.

The server-side guards (local-path rejection, folder-name traversal, session-resolved SSH config) are well-implemented and the IPC refactor preserves existing behaviour. The one concrete defect is in MarketplaceSheet.tsx: importError doubles as a preview-fetch error channel and surfaces README/document load failures in the import footer, where users can reasonably interpret them as failed import attempts.

src/web/mobile/MarketplaceSheet.tsx — importError state conflation and the setIsImporting(false) timing after successful close.

Important Files Changed

Filename Overview
src/main/services/marketplace-service.ts New shared service for marketplace cache/fetch/import logic; assertSafeTargetFolderName and path-traversal guards are correct; fetchDocument/fetchAsset check for .. before writing so traversal via manifest filenames is blocked; static os import is consistent with ESM.
src/main/web-server/handlers/messageHandlers.ts Four new WS marketplace handlers with isUntrustedLocalPath guard blocking local filesystem reads from web clients; targetFolderName traversal check added at handler boundary; defence-in-depth is solid.
src/main/web-server/web-server-factory.ts Wires four new marketplace callbacks; importMarketplacePlaybook callback correctly resolves autoRunFolderPath and SSH config from the server-side session store, preventing mobile clients from overriding them.
src/web/mobile/MarketplaceSheet.tsx New 90vh bottom sheet for browsing/importing playbooks; importError state is reused for both preview-fetch failures and actual import failures, causing misleading errors in the import footer; setIsImporting(false) runs during the 300ms close animation after a successful import.
src/web/mobile/App.tsx Adds showMarketplaceSheet state, marketplace open/close/imported handlers, and wires MarketplaceSheet and AutoRunSetupSheet.onOpenMarketplace; auto-refreshes AutoRun docs after import.
src/web/mobile/AutoRunSetupSheet.tsx Adds optional onOpenMarketplace prop and a Browse Playbook Exchange entry point; changes are additive and non-breaking.
src/main/ipc/handlers/marketplace.ts Slimmed to thin wrappers around the new service; IPC behaviour is preserved per the 45-test pass.
src/main/web-server/WebServer.ts Extended with four new marketplace setter methods and callback registrations; consistent with existing callback patterns.

Sequence Diagram

sequenceDiagram
    participant Mobile as MarketplaceSheet (mobile)
    participant WS as WebServer WS Handler
    participant Factory as WebServerFactory callback
    participant Service as MarketplaceService
    participant GitHub as GitHub Raw CDN
    participant FS as Local Filesystem

    Mobile->>WS: marketplace_get_manifest
    WS->>Factory: getMarketplaceManifest()
    Factory->>Service: getMarketplaceManifest(app)
    Service->>FS: read cache (6h TTL)
    alt cache miss / expired
        Service->>GitHub: fetch manifest.json
        Service->>FS: write cache
    end
    Service->>FS: read local-manifest.json (optional)
    Service-->>Factory: merged manifest
    Factory-->>WS: result
    WS-->>Mobile: marketplace_get_manifest_result

    Mobile->>WS: marketplace_get_document
    WS->>WS: isUntrustedLocalPath check
    WS->>WS: filename traversal check
    WS->>Factory: getMarketplaceDocument(playbookPath, filename)
    Factory->>Service: getMarketplaceDocument(...)
    Service->>GitHub: fetch .md file
    Service-->>WS: content
    WS-->>Mobile: marketplace_get_document_result

    Mobile->>WS: marketplace_import_playbook
    WS->>WS: validate sessionId / playbookId / targetFolderName
    WS->>Factory: importMarketplacePlaybook(sessionId, playbookId, folder)
    Factory->>Factory: lookup session -> autoRunFolderPath + sshConfig
    Factory->>Service: importMarketplacePlaybook(opts)
    Service->>Service: assertSafeTargetFolderName
    Service->>GitHub: fetch docs + assets
    alt local session
        Service->>FS: mkdir + writeFile
    else SSH remote
        Service->>FS: mkdirRemote + writeFileRemote
    end
    Service->>FS: update playbooks/sessionId.json
    Service-->>Factory: result
    WS-->>Mobile: marketplace_import_playbook_result
Loading

Reviews (2): Last reviewed commit: "fix(web): single-segment slug for mobile..." | Re-trigger Greptile

Comment on lines +3434 to +3437
if (typeof targetFolderName !== 'string' || targetFolderName.trim() === '') {
this.sendError(client, 'Missing targetFolderName', { requestId: message.requestId });
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Path traversal in targetFolderName is not blocked

The handler checks that targetFolderName is non-empty but never validates it against path traversal sequences. The service then calls path.join(autoRunFolderPath, targetFolderName) (local) or string-concatenates it (SSH). A targetFolderName of "../../sensitive" resolves cleanly through path.join to a directory outside autoRunFolderPath, letting an authenticated mobile client write playbook files anywhere the desktop user has write access.

The existing isValidFilename helper (used for Auto Run document names at line 1802) already rejects .., /, and \ — the same check should be applied here before the callback is invoked.

Comment on lines +3324 to +3354
private handleMarketplaceGetDocument(client: WebClient, message: WebClientMessage): void {
const playbookPath = message.playbookPath;
const filename = message.filename;

if (typeof playbookPath !== 'string' || playbookPath.trim() === '') {
this.sendError(client, 'Missing or invalid playbookPath', {
requestId: message.requestId,
});
return;
}
if (typeof filename !== 'string' || filename.trim() === '') {
this.sendError(client, 'Missing or invalid filename', { requestId: message.requestId });
return;
}
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
this.sendError(client, 'Invalid filename', { requestId: message.requestId });
return;
}

if (!this.callbacks.getMarketplaceDocument) {
this.send(client, {
type: 'marketplace_get_document_result',
success: false,
error: 'Marketplace not configured',
requestId: message.requestId,
});
return;
}

this.callbacks
.getMarketplaceDocument(playbookPath, filename)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Client-controlled playbookPath enables local filesystem read

marketplace_get_document and marketplace_get_readme accept playbookPath from the client without restricting it to GitHub-hosted paths. The service's isLocalPath() helper returns true for any absolute path or ~/… path, so an authenticated mobile client can send { playbookPath: "/home/user/my-project", filename: "SECRETS" } to read /home/user/my-project/SECRETS.md. The validateSafePath guard inside fetchDocument only ensures the resolved file stays under the caller-supplied playbookPath — it does nothing to prevent the caller from choosing a sensitive base directory.

Mitigation: reject playbookPath values where isLocalPath() is true at the WS handler boundary (local-manifest lookups should be handled entirely server-side using the session's known paths).

Comment on lines +49 to +55
function resolveTildePath(pathStr: string): string {
if (pathStr.startsWith('~/') || pathStr.startsWith('~\\')) {
const homedir = require('os').homedir();
return path.join(homedir, pathStr.slice(2));
}
return pathStr;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 require('os') is a CommonJS dynamic require inside a function body. Since the file already uses ES module import statements at the top, os should be imported statically to stay consistent and avoid any bundler surprises.

Suggested change
function resolveTildePath(pathStr: string): string {
if (pathStr.startsWith('~/') || pathStr.startsWith('~\\')) {
const homedir = require('os').homedir();
return path.join(homedir, pathStr.slice(2));
}
return pathStr;
}
import os from 'os';
function resolveTildePath(pathStr: string): string {
if (pathStr.startsWith('~/') || pathStr.startsWith('~\\')) {
const homedir = os.homedir();
return path.join(homedir, pathStr.slice(2));
}
return pathStr;
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/web-server/handlers/messageHandlers.ts (1)

103-103: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Move the AGENT_IDS import back into the import block.

This import sits after executable declarations, so the module will not parse.

Suggested fix
 import { logger } from '../../utils/logger';
+import { AGENT_IDS } from '../../../shared/agentIds';
 
 // Logger context for all message handler logs
 const LOG_CONTEXT = 'WebServer';
@@
-const EXTERNAL_TOAST_MAX_DURATION_SECONDS = 60;
-import { AGENT_IDS } from '../../../shared/agentIds';
+const EXTERNAL_TOAST_MAX_DURATION_SECONDS = 60;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/web-server/handlers/messageHandlers.ts` at line 103, The standalone
import of AGENT_IDS is located after executable code and breaks module parsing;
move the "AGENT_IDS" import statement back into the top import block with the
other imports in messageHandlers.ts so all imports are declared before any
executable code, ensuring the symbol AGENT_IDS is imported alongside the
existing imports at the file header.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/ipc/handlers/marketplace.ts`:
- Around line 132-140: When sshRemoteId is provided but
getSshRemoteById(sshRemoteId) returns undefined the code currently falls back to
a local import; instead, guard in marketplace handler by checking if sshRemoteId
is truthy and sshConfig is undefined and fail fast (throw or return a rejected
error response) so importMarketplacePlaybook is not called with the wrong
destination; update the logic around sshRemoteId / getSshRemoteById / sshConfig
to validate resolution and surface a clear error to the caller before calling
importMarketplacePlaybook.

In `@src/main/services/marketplace-service.ts`:
- Around line 464-483: The loop currently records successes in importedDocs but
newPlaybook.documents is still persisted with the full
marketplacePlaybook.documents list, causing playbooks to reference missing
files; update the import logic so you only add documents to the playbook when
their import succeeds (either build a newDocuments array and push the doc object
on successful fetch/write, or filter marketplacePlaybook.documents by
importedDocs before assigning to newPlaybook.documents), and apply the same
pattern to the analogous asset import block (the code using
writeFileRemote/fs.writeFile and variables fetchDocument, importedDocs,
marketplacePlaybook.documents, and newPlaybook.documents).
- Around line 448-452: Validate and sanitize targetFolderName before
constructing targetPath: ensure targetFolderName is not an absolute path, does
not contain path separators ('/' or '\\'), and does not include traversal tokens
like '..' (e.g., reject or normalize if path.isAbsolute(targetFolderName) ||
targetFolderName.includes(path.sep) || targetFolderName.includes('/') ||
targetFolderName.includes('..')). Alternatively enforce a whitelist regex (e.g.,
/^[A-Za-z0-9._-]+$/) or use path.basename to strip directories, then use the
sanitized value in the existing targetPath construction (the code around
targetFolderName, autoRunFolderPath and isRemote).
- Around line 275-299: In fetchReadme, don't swallow unexpected I/O/network
errors — only convert ENOENT (from fs.readFile) and 404 (from fetch response) to
null; for all other errors re-throw so they surface to Sentry. Specifically, in
the local-path branch (resolveTildePath/validateSafePath + fs.readFile) catch
and return null only when (error as NodeJS.ErrnoException).code === 'ENOENT' and
otherwise re-throw the error; in the remote branch preserve the existing
MarketplaceFetchError handling but in the outer catch re-throw any
non-MarketplaceFetchError (i.e., network/other unexpected errors) instead of
returning null. Ensure function signature and thrown types remain unchanged.
- Around line 565-574: The current try/catch silently drops existing playbooks
on any read/parse error then overwrites the file; change this so only a missing
file (ENOENT) initializes playbooks = [], while JSON.parse errors or other I/O
errors are re-thrown so they surface to Sentry. Specifically, around
fs.readFile(playbooksFilePath) and JSON.parse(content) handle errors explicitly:
if the caught error has code === 'ENOENT' set playbooks = []; otherwise re-throw
the error (and treat JSON parsing failures as unexpected by not swallowing
SyntaxError). Ensure you still push newPlaybook and write only after a
successful read-or-confirmed-missing case so you don't silently overwrite on
transient failures.

In `@src/main/web-server/handlers/messageHandlers.ts`:
- Around line 3353-3361: The marketplace callback handlers (e.g.,
callbacks.getMarketplaceDocument used to send 'marketplace_get_document_result')
must check for a null result before constructing the success response; currently
a null is treated as success or causes a dereference crash (see the import
handler reading result.success). Update each handler that calls
callbacks.getMarketplaceDocument, callbacks.getMarketplaceReadme, and
callbacks.getMarketplaceImport (or similarly named methods in CallbackRegistry)
to first if (!result) send a structured failure via this.send with success:
false, an explanatory error string, and the original requestId, otherwise send
the normal success payload using result properties; also avoid unconditional
dereferences like result.success and use the guarded result fields when building
responses.
- Around line 3434-3437: The current validation for targetFolderName only checks
for empty strings but must also reject path traversal/separators; update the
check around the targetFolderName variable in messageHandlers.ts (the same block
that calls this.sendError) to fail if targetFolderName contains '/' or '\' or
the sequence '..' (or any path.sep), and return this.sendError(client, 'Invalid
targetFolderName') so importMarketplacePlaybook cannot be given a value that
escapes the Auto Run folder; keep the existing empty-string check and use the
same requestId in the error payload.

In `@src/web/mobile/AutoRunSetupSheet.tsx`:
- Around line 255-275: The button in AutoRunSetupSheet removes the default
outline (outline: 'none') which hides keyboard focus; restore a visible focus
indicator by adding a focus/focus-visible replacement to the button's styles
used in this component (the element with onClick that calls triggerHaptic and
onOpenMarketplace), e.g. provide a clear outline or boxShadow and a contrasting
border color for :focus/:focus-visible states and keep
touchAction/WebkitTapHighlightColor as-is so keyboard users can see focus
without changing touch behavior.

In `@src/web/mobile/MarketplaceSheet.tsx`:
- Around line 174-215: The async preview handlers handleSelectPlaybook and
handleSelectDocument must guard against stale responses and stop silently
swallowing errors: add a per-request token (e.g., incrementing requestId in a
React ref or use AbortController) before calling sendRequest, capture the token
in the closure, and verify the token still matches before calling
setReadmeContent/setDocumentContent or toggling setIsLoadingDocument so late
responses don't overwrite a newer selection; when filename === null clear
setIsLoadingDocument immediately; in catch blocks do not swallow errors—either
rethrow the caught error or forward it to your error-reporting helper so
exceptions propagate to Sentry while still ensuring local state is guarded by
the request token/abort check.
- Around line 436-439: Replace the truthy-fallback logic for previewContent so
empty README or document strings are preserved: in the ternary that sets
previewContent (which checks selectedDocFilename), use the nullish coalescing
operator (readmeContent ?? '*No README available*' and documentContent ??
'*Document not found*') instead of the logical ORs so only null/undefined
trigger the placeholders; update the expressions around readmeContent and
documentContent accordingly.

---

Outside diff comments:
In `@src/main/web-server/handlers/messageHandlers.ts`:
- Line 103: The standalone import of AGENT_IDS is located after executable code
and breaks module parsing; move the "AGENT_IDS" import statement back into the
top import block with the other imports in messageHandlers.ts so all imports are
declared before any executable code, ensuring the symbol AGENT_IDS is imported
alongside the existing imports at the file header.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3fc7b37c-dcf5-4b14-a768-7c0e05a9015d

📥 Commits

Reviewing files that changed from the base of the PR and between 1338860 and ba21e49.

📒 Files selected for processing (12)
  • src/__tests__/main/web-server/handlers/messageHandlers.test.ts
  • src/__tests__/web/mobile/App.test.tsx
  • src/main/ipc/handlers/marketplace.ts
  • src/main/services/marketplace-service.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/managers/CallbackRegistry.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/web/mobile/App.tsx
  • src/web/mobile/AutoRunSetupSheet.tsx
  • src/web/mobile/MarketplaceSheet.tsx

Comment thread src/main/ipc/handlers/marketplace.ts
Comment thread src/main/services/marketplace-service.ts
Comment thread src/main/services/marketplace-service.ts
Comment thread src/main/services/marketplace-service.ts
Comment thread src/main/services/marketplace-service.ts
Comment thread src/main/web-server/handlers/messageHandlers.ts
Comment thread src/main/web-server/handlers/messageHandlers.ts
Comment thread src/web/mobile/AutoRunSetupSheet.tsx
Comment thread src/web/mobile/MarketplaceSheet.tsx Outdated
Comment thread src/web/mobile/MarketplaceSheet.tsx Outdated
Resolves merge conflict in src/main/ipc/handlers/marketplace.ts by
keeping the new thin-wrapper refactor; ports the upstream watcher
error-handler addition (Windows UNKNOWN / EPERM / ENOENT
classification) into createLocalManifestWatcher in the new
src/main/services/marketplace-service.ts.

Also addresses PR RunMaestro#947 review feedback from greptile + coderabbit:

Security
- WS handlers reject `playbookPath` values that point at the local
  filesystem (absolute or `~`-prefixed). Local paths remain reachable
  via desktop IPC where the renderer is trusted, but mobile/web
  clients can no longer coerce the server into reading arbitrary
  files via the marketplace fetch helpers.
- WS `marketplace_import_playbook` rejects `targetFolderName` values
  containing path separators or traversal tokens. Service layer adds
  `assertSafeTargetFolderName` as the source-of-truth guard.
- IPC `marketplace:importPlaybook` fails loudly when `sshRemoteId` is
  provided but cannot be resolved, instead of silently downgrading to
  a local import (matches CLAUDE.md SSH-spawn pattern: never silently
  downgrade).

Correctness
- Persisted `newPlaybook.documents` only references docs that
  actually wrote to disk, so a partial import never produces a
  playbook pointing at missing files.
- Reading the per-session playbooks file no longer silently resets
  to `[]` on non-ENOENT errors. Corrupt JSON / EACCES now throws
  rather than overwriting existing user data.
- `fetchReadme` re-throws unexpected I/O / network failures (only
  ENOENT and 404 still map to "no README"), so production faults
  surface in Sentry.
- WS document/readme handlers return a structured failure when the
  underlying callback resolves to `null`, instead of treating
  unconfigured wiring as a successful empty response.

Code quality
- Replaced dynamic `require('os')` in marketplace-service with a
  static `import os from 'os'` (consistent with the rest of the
  module).

UI polish
- AutoRunSetupSheet entry button gets a focus ring on `:focus` so
  keyboard users still see focus state after `outline: none`.
- MarketplaceSheet swaps `||` to `??` for README/document fallback
  text so empty files render instead of showing the placeholder.
- MarketplaceSheet adds a monotonic preview-request id; stale
  README/document responses are discarded if the user has already
  picked another playbook or document. Error responses now surface
  in the import-error banner instead of being swallowed.

Tests
- Updated marketplace IPC tests for the new SSH fail-loud behavior
  (replaces "fall back to local fs" cases with "fail loudly").
- Added missing `mockRejectedValueOnce({ code: 'ENOENT' })` for the
  per-session playbooks file read in 7 tests that previously relied
  on the bare-catch swallowing JSON.parse errors.
- Fixed `os` mock to expose `default` for the new static import.
- Asserts that partially-imported playbooks only persist the
  successful docs.

All scoped suites pass (marketplace IPC: 45, message handlers: 113,
mobile App: 95).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 3, 2026

Copy link
Copy Markdown
Contributor Author

Merged upstream/rc and addressed all review feedback from greptile and coderabbitai. Pushed f36de1fd.

Conflict resolution

The only conflict was src/main/ipc/handlers/marketplace.ts — kept this PR's thin-wrapper refactor (logic now in src/main/services/marketplace-service.ts) and ported the upstream watcher error handler (Windows UNKNOWN / EPERM / ENOENT classification) into createLocalManifestWatcher.

Review feedback addressed

Security (P1)

  • WS handlers reject playbookPath values that point at the local filesystem (absolute or ~-prefixed). Local paths remain reachable via desktop IPC where the renderer is trusted; mobile/web clients cannot coerce arbitrary file reads via the marketplace fetch helpers. (greptile)
  • WS marketplace_import_playbook rejects targetFolderName containing .., /, \\, ~, or absolute paths. New assertSafeTargetFolderName in the service layer is the source of truth so the IPC path gets the same guard. (greptile + coderabbit)
  • IPC marketplace:importPlaybook fails loudly when sshRemoteId is provided but cannot be resolved, rather than silently downgrading to a local import. Matches CLAUDE.md's SSH-spawn pattern. (coderabbit)

Correctness (Major)

  • Persisted newPlaybook.documents is filtered against importedDocs, so partial imports no longer save references to missing files. (coderabbit)
  • Reading the per-session playbooks file no longer silently resets to [] on non-ENOENT errors — corrupt JSON / EACCES now throws rather than overwriting existing data. (coderabbit)
  • fetchReadme re-throws unexpected I/O and network failures (only ENOENT/404 map to "no README"), so faults surface in Sentry. (coderabbit)
  • WS document/readme handlers return a structured failure when the callback resolves to null (unconfigured wiring), instead of treating it as a successful empty response. (coderabbit)

Code quality (P2)

  • Replaced dynamic require('os') with import os from 'os' in marketplace-service. (greptile)

UI polish (Minor)

  • AutoRunSetupSheet entry button: focus ring rendered via onFocus/onBlur so keyboard users still see focus after outline: none. (coderabbit)
  • MarketplaceSheet preview: ?? instead of || so empty README/doc renders rather than the fallback placeholder. (coderabbit)
  • MarketplaceSheet: monotonic previewRequestIdRef discards stale README/document responses when the user picks another playbook/file mid-flight; error responses surface in the import-error banner instead of being swallowed. (coderabbit)

Test updates

  • Replaced two SSH "fall back to local fs" tests with "fail loudly" assertions matching the new behavior.
  • Added the missing mockRejectedValueOnce({ code: 'ENOENT' }) for the per-session playbooks file in 7 tests that previously relied on the bare-catch silently swallowing JSON.parse(undefined) errors.
  • Fixed the os mock in the tilde-path test to expose default for the new static import.
  • Asserts that partial-import playbooks persist only successfully-imported docs.

Verification

  • npm run lint clean (TypeScript, all configs).
  • npm run lint:eslint clean.
  • prettier --check clean.
  • Scoped suites pass: marketplace IPC (45), message handlers (113), mobile App (95).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/web-server/handlers/messageHandlers.ts`:
- Around line 3344-3363: Validation branches currently call this.sendError(...)
which emits a generic error frame; change these to send the request-scoped typed
failure frames (e.g., marketplace_get_document_result,
marketplace_get_readme_result, marketplace_import_playbook_result) so callers
receive the expected typed response. For each branch referencing playbookPath,
filename, this.isUntrustedLocalPath, etc., construct and send the appropriate
marketplace_*_result failure object (including requestId: message.requestId and
an error message) to the same client instead of calling this.sendError, and
return immediately after sending; keep usage of sendError only for
non-request-scoped/global errors. Ensure you update all similar validation
blocks noted (around the shown block and at the other ranges: 3409-3420,
3472-3499) and reuse the same result shape the success path uses.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 474810d1-e411-4678-a94f-3857fc41b994

📥 Commits

Reviewing files that changed from the base of the PR and between ba21e49 and f36de1f.

📒 Files selected for processing (8)
  • src/__tests__/main/ipc/handlers/marketplace.test.ts
  • src/main/ipc/handlers/marketplace.ts
  • src/main/services/marketplace-service.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/web/mobile/AutoRunSetupSheet.tsx
  • src/web/mobile/MarketplaceSheet.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/web/mobile/AutoRunSetupSheet.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/web-server/web-server-factory.ts
  • src/web/mobile/MarketplaceSheet.tsx
  • src/main/web-server/types.ts
  • src/main/services/marketplace-service.ts

Comment thread src/main/web-server/handlers/messageHandlers.ts
Resolves 6 conflicts that came in from upstream/rc's PR2 of the CLI
surface refactor (list_desktop_sessions / get_session_history) plus
the AutoRunSetupSheet playbook-CRUD redesign and FolderPickerSheet:

- WebServer.ts, types.ts, CallbackRegistry.ts: combined the marketplace
  callback registrations with upstream's listDesktopSessions and
  getSessionHistory callbacks (both sets coexist; ordering preserves
  marketplace-first since that was already on the branch).
- handlers/messageHandlers.ts: combined the four marketplace WS
  handlers (get_manifest, get_document, get_readme, import_playbook)
  with the new list_desktop_sessions and get_session_history handlers.
- web/mobile/App.tsx: kept both the MarketplaceSheet wiring and the
  new FolderPickerSheet + handleAutoRunFolderConfirm flow; passed
  sendRequest, send, currentDocument and onOpenMarketplace through to
  the redesigned AutoRunSetupSheet.
- web/mobile/AutoRunSetupSheet.tsx: kept the upstream playbook-CRUD
  refactor (sendRequest/send/currentDocument props, playbooks panel,
  inline name-prompt / delete-confirm modals) and re-anchored the
  Playbook Exchange entry button above the new Playbooks section so
  marketplace import remains discoverable.
- __tests__/main/web-server/handlers/messageHandlers.test.ts: kept
  the marketplace mocks/tests alongside upstream's
  listDesktopSessions / getSessionHistory mocks and tests.

Also addresses CI failures and a new coderabbit comment from the last
push:

CI fix
- web-server-factory.test.ts MockWebServer was missing the four
  setMarketplaceManifestCallback / setMarketplaceDocumentCallback /
  setMarketplaceReadmeCallback / setImportMarketplacePlaybookCallback
  stubs, causing 36 test failures with `TypeError: ... is not a
  function`. Added the stubs (combined with upstream's new
  setListDesktopSessionsCallback / setGetSessionHistoryCallback).

Coderabbit P3 (Major)
- Marketplace WS validation branches were emitting generic
  `type: 'error'` frames rather than the request-scoped
  `marketplace_*_result` types. Clients waiting on the typed result
  would miss the failure or hang. Introduced
  `sendMarketplaceFailure(client, type, error, message, extra?)`
  helper and routed all validation-failure paths through it (missing
  fields, untrusted local paths, traversal in filename, separators in
  targetFolderName, callback unconfigured). Updated the relevant
  tests (assert typed-result failures and added two new cases for
  absolute playbookPath rejection and targetFolderName separator
  rejection).

Verification
- `npm run lint` clean (TypeScript, all configs).
- `npm run lint:eslint` clean.
- `prettier --check` clean across all touched files.
- Scoped tests pass: marketplace IPC (45), message handlers (123 — 7
  new), web-server-factory (95), mobile App (95). 319 total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 3, 2026

Copy link
Copy Markdown
Contributor Author

Merged `upstream/rc` again, fixed CI failures, and addressed the new coderabbit comment. Pushed `1fb98426`.

Conflicts resolved (6 files)

Upstream's PR2 of the CLI surface refactor (`list_desktop_sessions` / `get_session_history`), AutoRunSetupSheet playbook-CRUD redesign, and FolderPickerSheet all landed on `rc` after the previous merge. Combined both sides:

  • `WebServer.ts`, `types.ts`, `CallbackRegistry.ts` — kept marketplace callback registrations alongside upstream's `listDesktopSessions` / `getSessionHistory`.
  • `handlers/messageHandlers.ts` — kept four marketplace WS handlers alongside the new `list_desktop_sessions` / `get_session_history` handlers.
  • `web/mobile/App.tsx` — kept both `MarketplaceSheet` wiring and the new `FolderPickerSheet` + `handleAutoRunFolderConfirm` flow; threaded `sendRequest` / `send` / `currentDocument` / `onOpenMarketplace` into the redesigned `AutoRunSetupSheet`.
  • `web/mobile/AutoRunSetupSheet.tsx` — kept the upstream playbook-CRUD refactor and re-anchored the Playbook Exchange entry button above the Playbooks section so import remains discoverable.
  • `tests/main/web-server/handlers/messageHandlers.test.ts` — kept marketplace mocks/tests alongside upstream's CLI-surface mocks/tests.

CI fix

  • `web-server-factory.test.ts` MockWebServer was missing the four marketplace setter stubs, producing 36 `TypeError: ... is not a function` failures. Added them to the mock (combined with upstream's two new CLI setters).

New coderabbit comment (Major) — typed `marketplace_*_result` failures

Marketplace WS validation branches were emitting generic `type: 'error'` frames rather than the request-scoped `marketplace_*_result` types, which would make typed clients hang waiting for the success type.

Introduced a small `sendMarketplaceFailure(client, type, error, message, extra?)` helper and routed every validation-failure path through it (missing fields, untrusted local paths, traversal in filename, separators in `targetFolderName`, callback unconfigured). Tests updated to assert typed-result failures, plus two new cases:

  • `marketplace_get_document` with an absolute `playbookPath` returns `marketplace_get_document_result { success: false, error: 'Local filesystem paths are not allowed…' }`.
  • `marketplace_import_playbook` with separators in `targetFolderName` returns `marketplace_import_playbook_result { success: false, error: '… separators…' }`.

Verification

  • `npm run lint` clean (TypeScript, all configs).
  • `npm run lint:eslint` clean.
  • `prettier --check` clean.
  • Scoped suites pass: marketplace IPC (45), message handlers (123, +7 new tests), web-server-factory (95), mobile App (95). 319 total.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/main/web-server/handlers/messageHandlers.ts (1)

4065-4072: 💤 Low value

Misleading error message when callback returns null.

At this point the callback is confirmed to exist (checked at line 4052), so a null result means the document wasn't found or fetching failed—not that marketplace is unconfigured.

Suggested clarification
 .then((result) => {
 	if (!result) {
 		this.sendMarketplaceFailure(
 			client,
 			'marketplace_get_document_result',
-			'Marketplace not configured',
+			'Document not found',
 			message
 		);
 		return;
 	}

Same applies to handleMarketplaceGetReadme at line 4131.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/web-server/handlers/messageHandlers.ts` around lines 4065 - 4072,
The current error message 'Marketplace not configured' is misleading when the
callback exists but returns null; update the sendMarketplaceFailure call in the
block that checks if (!result) inside the handler that invokes the marketplace
callback (refer to the sendMarketplaceFailure call with event
'marketplace_get_document_result') to use a clear message like 'Document not
found or failed to fetch' (or similar), and make the analogous change in
handleMarketplaceGetReadme where the same null-result handling occurs so both
handlers report that the document/readme was not found or could not be fetched
rather than saying the marketplace is unconfigured.
src/web/mobile/App.tsx (1)

1535-1537: 💤 Low value

Consider resetting showMarketplaceSheet when the setup sheet closes.

handleAutoRunCloseSetup resets showAutoRunSetup but not showMarketplaceSheet. In practice the marketplace's full-screen backdrop prevents clicking the setup sheet's close button, but keyboard-driven dismissal (e.g., an Escape handler in AutoRunSetupSheet on desktop browsers) could close the setup sheet while leaving the marketplace sheet floating. Adding the reset is a one-liner:

🛡️ Proposed fix
 const handleAutoRunCloseSetup = useCallback(() => {
 	setShowAutoRunSetup(false);
+	setShowMarketplaceSheet(false);
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/mobile/App.tsx` around lines 1535 - 1537, The close handler
handleAutoRunCloseSetup currently only calls setShowAutoRunSetup(false); update
it to also reset the marketplace state by calling setShowMarketplaceSheet(false)
so closing the AutoRunSetupSheet (including via keyboard) won't leave the
marketplace sheet/backdrop open — locate handleAutoRunCloseSetup in App.tsx and
add the call to setShowMarketplaceSheet(false) alongside the existing
setShowAutoRunSetup(false).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/web-server/handlers/messageHandlers.ts`:
- Around line 4065-4072: The current error message 'Marketplace not configured'
is misleading when the callback exists but returns null; update the
sendMarketplaceFailure call in the block that checks if (!result) inside the
handler that invokes the marketplace callback (refer to the
sendMarketplaceFailure call with event 'marketplace_get_document_result') to use
a clear message like 'Document not found or failed to fetch' (or similar), and
make the analogous change in handleMarketplaceGetReadme where the same
null-result handling occurs so both handlers report that the document/readme was
not found or could not be fetched rather than saying the marketplace is
unconfigured.

In `@src/web/mobile/App.tsx`:
- Around line 1535-1537: The close handler handleAutoRunCloseSetup currently
only calls setShowAutoRunSetup(false); update it to also reset the marketplace
state by calling setShowMarketplaceSheet(false) so closing the AutoRunSetupSheet
(including via keyboard) won't leave the marketplace sheet/backdrop open —
locate handleAutoRunCloseSetup in App.tsx and add the call to
setShowMarketplaceSheet(false) alongside the existing
setShowAutoRunSetup(false).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: abf16d2b-a265-494b-ab1a-a5cff40f898e

📥 Commits

Reviewing files that changed from the base of the PR and between f36de1f and 1fb9842.

📒 Files selected for processing (9)
  • src/__tests__/main/web-server/handlers/messageHandlers.test.ts
  • src/__tests__/main/web-server/web-server-factory.test.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/managers/CallbackRegistry.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/web/mobile/App.tsx
  • src/web/mobile/AutoRunSetupSheet.tsx
✅ Files skipped from review due to trivial changes (3)
  • src/tests/main/web-server/web-server-factory.test.ts
  • src/main/web-server/web-server-factory.ts
  • src/main/web-server/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/web/mobile/AutoRunSetupSheet.tsx
  • src/main/web-server/managers/CallbackRegistry.ts
  • src/main/web-server/WebServer.ts
  • src/tests/main/web-server/handlers/messageHandlers.test.ts

@chr1syy

chr1syy commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Two UX findings from soaking #947 into 0.16.0-RC-build

Finding 1 — Import folder prefill contradicts the server validator (functional bug)

The mobile import flow prefills the Import to folder input with the marketplace manifest's full identifier (e.g. development/readme-accuracy). When the user taps Import Playbook, the server-side handler in src/main/web-server/handlers/messageHandlers.ts rejects it with:

targetFolderName must be a single folder name without separators

So the import does not work out of the box — every user clicking Import on a category-namespaced playbook hits this and has to manually edit the input down to a single segment. Manually editing it down to e.g. readme-accuracy succeeds and the playbook appears in the AutoRun list, so the underlying import path is healthy; only the default value is wrong.

Positive side-effect: the path-traversal validator that catches .. / / is working correctly. Step 5 of the test plan ("craft a malicious WS message with targetFolderName: '../foo'") is implicitly verified by this very interaction — the same validator that's rejecting the slash would reject ...

Suggested fix (smallest surface): client-side sanitize the prefill to a single segment before populating the input. Two reasonable options:

// Last-segment-only (simplest)
const defaultFolder = manifestId.split('/').pop() ?? manifestId;

// Or replace separators
const defaultFolder = manifestId.replace(/[/\\]/g, '-');

Pair with a small "will install at: <autoRunFolderPath>/<resolvedName>" preview line under the input so the user sees the resolved path before tapping Import. This also makes the validator essentially unreachable from the UI in normal use, leaving it as a defensive backstop only.

The alternative — relaxing the server validator to accept nested paths bounded to autoRunFolderPath — is bigger, riskier, and goes against the simplicity of the current single-segment guarantee. I'd skip that.

Finding 2 — Bottom-nav Run and Playbooks open the same screen

In the mobile bottom-nav / left drawer, tapping Run and tapping Playbooks both open the AutoRun setup sheet. The two affordances are functionally identical right now.

The naming implies a separation:

  • Run → AutoRun setup (pick a doc, configure, launch). What both currently do.
  • Playbooks → the Playbook Exchange browser, opened directly without going through the AutoRun setup sheet first.

Right now you have to: tap Playbooks → AutoRun setup opens → tap Browse Playbook Exchange → 90vh sheet slides up. That's three taps to get to the Exchange when the user already said "Playbooks."

Suggested fix: route the Playbooks bottom-nav entry directly into the Marketplace sheet (the same one currently mounted under AutoRun setup → Browse). Keep the Browse entry inside AutoRun setup so users in mid-launch flow can still discover it.

Other findings from the same soak

  • ✅ Marketplace sheet renders, manifest loads, playbooks + categories visible.
  • ✅ Search and category filtering work.
  • ✅ Detail view (README + document) renders via MobileMarkdownRenderer.
  • ✅ Once the folder name is single-segment, import succeeds and refreshes the AutoRun document list (per AR-PARITY-01).
  • ✅ Path-traversal validator on the server is functional (caught by Finding 1 incidentally).

Both findings are UX, not security/correctness — leaving these unlabeled. Upgrade to blocking if Finding 1 is considered a ship-stopper for the 0.16.17-RC release.

@chr1syy chr1syy added the blocking Can't move forward due to some hurdle. label May 4, 2026
@chr1syy

chr1syy commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Upgrading Finding 1 (import folder prefill contradicts the validator) to blocking — confirmed with chr1syy. "Import doesn't work without manually editing the prefill" is broken-out-of-the-box behavior, not polish. Finding 2 (Run/Playbooks bottom-nav redundancy) remains a non-blocking UX redesign suggestion.

The mobile Playbook Exchange prefilled the import folder with a
`category/title` slug, which the server's `assertSafeTargetFolderName`
guard rejects (no separators allowed). Every user tapping Import on a
category-namespaced playbook hit this and had to manually edit the
input down to a single segment.

Switch the prefill to a title-only slug. Category is already shown via
the tile chip and detail-view eyebrow, so re-encoding it into the
folder name added no information.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai re-review please
@coderabbitai re-review please

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@chr1syy, kicking off a full re-review now. I'll take into account all the changes from the two upstream merges and the feedback rounds, including the blocking Finding 1 (import folder prefill sanitization) that is still open.

[full_review]

Comment on lines +90 to +92
// Import state
const [isImporting, setIsImporting] = useState(false);
const [importError, setImportError] = useState<string | null>(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 importError is used as a dual-purpose state: it is set both when a README/document preview fetch fails (inside handleSelectPlaybook and handleSelectDocument) and when the actual import fails. Because this error is displayed in the import footer next to the "Import Playbook" button, a README fetch failure immediately surfaces there (e.g. "Failed to load README") with no visual cue that the error relates to the preview panel rather than an import operation. A user who taps a playbook, sees the preview fail, and then reads the error in the import footer will reasonably believe an import was attempted and failed. Consider separating the two concerns with a distinct previewError state displayed in the preview area.

Suggested change
// Import state
const [isImporting, setIsImporting] = useState(false);
const [importError, setImportError] = useState<string | null>(null);
// Import state
const [isImporting, setIsImporting] = useState(false);
const [importError, setImportError] = useState<string | null>(null);
// Preview-fetch errors (README / document load) — shown in the preview area,
// not in the import footer, so users aren't misled into thinking an import failed.
const [previewError, setPreviewError] = useState<string | null>(null);

The marketplace sheet's `importError` was overloaded — `handleSelectPlaybook`
and `handleSelectDocument` set it on README/document fetch failures, but it
rendered in the import footer next to the Import Playbook button. A user
who tapped a playbook, saw the preview fail, and read the error in the
footer would reasonably believe an import had been attempted and failed.

Split into a dedicated `previewError` state that renders inline in the
preview area, leaving `importError` exclusively for actual import failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Addressed greptile's P1 inline finding on MarketplaceSheet.tsx:92. Pushed 60f0ce67.

Fix — split preview vs import error state

importError was dual-purpose: handleSelectPlaybook and handleSelectDocument set it on README/document fetch failures, but it rendered in the import footer next to the Import Playbook button. A user who tapped a playbook, saw the preview fail, and read the error in the footer would reasonably believe an import had been attempted and failed.

Introduced a dedicated previewError state:

  • handleSelectPlaybook and handleSelectDocument now write to previewError only.
  • handleImport continues to write to importError only.
  • previewError renders as an inline alert inside the preview pane (above where the markdown would render), with role="alert" and the same theme-error coloring used elsewhere.
  • Reset paths (Escape key handler, back button, new-playbook selection, new-document selection) clear both flags so a stale preview error never lingers across selections.

Verification

  • npm run lint clean (TypeScript, all configs).
  • npm run lint:eslint clean.
  • prettier --check clean.
  • Scoped suite passes: mobile App (95).

Other open items from the latest review pass:

  • coderabbitai's re-review request was acknowledged but no actionable comments have arrived yet.
  • The previous round's nitpicks ('Marketplace not configured' wording, handleAutoRunCloseSetup resetting showMarketplaceSheet) are non-blocking — happy to fold them in if either bot upgrades them.

@chr1syy

chr1syy commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Folder prefill fix verified — unblocking

Re-tested 2026-05-07 against the soak build (e4585946 fix(web): single-segment slug for mobile marketplace import folder + 60f0ce67 fix(web): split preview vs import error state in marketplace sheet).

What's fixed

  • Import folder prefill is now single-segment. A category-namespaced playbook (id like category/name) prefills as name only. Tapping Import Playbook without editing the input now succeeds; the imported playbook appears in the AutoRun document list afterward. The validator-vs-prefill contradiction from yesterday is resolved.

Removing the blocking label.

Non-blocking follow-up (mobile/web parity gap)

Desktop has an "Add Docs" / Docs Overview affordance for discovering and adding AutoRun documents. The mobile/web equivalent is missing — users on mobile have to navigate directly to the Playbook Exchange or to already-imported docs, with no equivalent overview surface. The Playbook Exchange (this PR) is one entry point; the Docs Overview is the broader contextualizing surface around it.

Added as Gap 3 to the running mobile/web parity follow-ups gist (alongside PR #946's launch-affordance gaps): https://gist.github.com/chr1syy/67630166ff5217a97d7b1fad91201f3b

Suggested direction in the gist: mirror the desktop component on the webui, reuse the WS-side IPC via the same shared-service pattern this PR established (marketplace-service.ts), and have the Playbook Exchange become one item inside the broader overview rather than the only entry point.

Other items still untested in this pass (deferred from yesterday's plan)

  • Step 6 — desktop MarketplaceModal regression sanity (proves the IPC handler refactor didn't break the desktop path).
  • Step 7 — SSH-remote import lands on the remote AutoRun folder.
  • Finding 2 from yesterday — Run and Playbooks bottom-nav still open the same screen. Was filed as non-blocking and not addressed in this round; decide whether to ship as-is or push a follow-up.

@chr1syy chr1syy added ready to merge This PR is ready to merge and removed blocking Can't move forward due to some hurdle. labels May 7, 2026
Conflict resolution
- src/main/ipc/handlers/marketplace.ts: kept this PR's thin-wrapper
  refactor (logic in src/main/services/marketplace-service.ts) over
  upstream's inline import body. Ported upstream's marketplace
  compatibility check (isCompatible against minMaestroVersion) into
  the service layer instead, so both the IPC and WebSocket import
  paths get the defense-in-depth gate without duplicating the inline
  body.

Verification
- npm run lint clean (TypeScript, all configs).
- prettier and eslint clean on touched files.
- Scoped suites pass: marketplace IPC, message handlers,
  web-server-factory, mobile App, marketplace-compatibility (344 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Merged `upstream/rc` again. Pushed `7bea26d3`.

Conflict resolution

Single conflicted file: `src/main/ipc/handlers/marketplace.ts`. Upstream introduced marketplace compatibility gating (`isCompatible` against `minMaestroVersion`) inside the inline IPC import body, while this PR had refactored that body into a thin wrapper around `src/main/services/marketplace-service.ts`.

Resolved by:

  • Keeping the thin-wrapper IPC handler (no behavior loss — service still implements the full import flow, including the partial-import doc filtering and ENOENT-only swallowing from the previous review round).
  • Moving the compatibility check into the service layer (`importMarketplacePlaybook`), right after the `marketplacePlaybook` lookup. Both the IPC handler and the WebSocket `marketplace_import_playbook` callback now share the same defense-in-depth gate against incompatible `minMaestroVersion` — so a future CLI / deep-link / programmatic / mobile WS bypass attempt all hit the same throw-with-clear-message path.

The IPC handler imports pulled in by upstream (`MarketplaceFetchError`, `MarketplaceManifest`, etc.) were dropped; the service file already owns those.

Verification

  • `npm run lint` clean (TypeScript, all configs).
  • `npm run lint:eslint` clean.
  • `prettier --check` clean on touched files.
  • Scoped suites pass: marketplace IPC, message handlers, web-server-factory, mobile App, marketplace-compatibility — 344 tests total.

PR is back to `MERGEABLE` / `CLEAN`.

Resolves seven CodeRabbit findings on PR RunMaestro#947's Playbook Exchange code.
All edits keep the existing typed-result protocol and recoverable-UX
contracts; the changes are about making real production failures visible
and refusing to persist a half-imported playbook.

Service layer (`src/main/services/marketplace-service.ts`):

- Import path now falls back to a stale official cache when
  `fetchManifest()` fails, mirroring the browse path. Without this, a
  visible playbook served from the same stale cache could disappear at
  import time and the user would see "Playbook not found".
- Refuse to persist a playbook when every document failed to write.
  The per-doc loop is intentionally tolerant so one bad file doesn't
  block the rest, but the previous code still returned success with
  `documents: []` — closing the marketplace sheet and leaving the user
  with an unusable imported entry. Now throws a `MarketplaceImportError`
  before the playbook file is touched, and only when the playbook
  actually had documents (asset-only playbooks still import).

WebSocket handlers (`src/main/web-server/handlers/messageHandlers.ts`):

- `isUntrustedLocalPath` now rejects `.` / `..` segments and any
  backslash anywhere in the path, not just absolute / `~` / Windows
  drive prefixes. Defense-in-depth at the entry point so future
  downstream changes can't re-open a relative-traversal bypass.
- All four marketplace catch blocks (`get_manifest`, `get_document`,
  `get_readme`, `import_playbook`) now route through a new
  `reportMarketplaceHandlerError` helper that calls `captureException`
  before sending the typed failure. Mirrors the existing
  `reportHandlerError` for non-marketplace handlers; preserves the
  client-visible typed result so mobile clients waiting on the
  `marketplace_*_result` type still get their response.

Web server factory (`src/main/web-server/web-server-factory.ts`):

- Marketplace import catch now reports the original exception to Sentry
  with `{ operation, sessionId, playbookId, targetFolderName }` context
  before returning `{ success: false }`. Keeps the existing
  recoverable-UX path while making the real cause visible in production
  error tracking.

Mobile MarketplaceSheet (`src/web/mobile/MarketplaceSheet.tsx`):

- Manifest / README / document / import catch blocks now `console.error`
  the underlying exception so the original cause is visible in browser
  devtools. Web bundle has no Sentry, so this matches the existing
  pattern in `AutoRunDocumentViewer` rather than introducing a new
  Sentry surface.
- Returning to the README view from a document preview now clears
  `previewError` and re-fetches the README via a new `loadReadmeFor`
  helper. Previously the sheet could stay stuck on a stale "No README
  available" / fetch error after the user tapped a doc and went back.

Tests:

- `marketplace.test.ts`: new "should fail the import when all documents
  fail to fetch" verifies the empty-import guard fires and no playbook
  is persisted. New "should fall back to expired cache when network
  fetch fails during import" exercises the import-path stale-cache
  fallback.
- `messageHandlers.test.ts`: new parameterized suite validates that
  `isUntrustedLocalPath` rejects `..`, `.`, embedded dot segments, and
  embedded backslashes via the typed `marketplace_get_document_result`.

All scoped marketplace + web-server-handler tests pass (198/198).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chr1syy added a commit to chr1syy/Maestro that referenced this pull request May 9, 2026
Closes Gap 3 from the PR RunMaestro#946/RunMaestro#947 mobile/web AutoRun parity follow-up.
Desktop has an "Add Docs" affordance that surfaces both Create-doc and
Marketplace as co-equal entry points. On mobile/web, the empty state of
AutoRunInline only offered "+ Create document" — users with a fresh
AutoRun folder had no in-UI path to discover existing playbooks and had
to know to open the launch sheet just to find the marketplace entry.

- Add onOpenMarketplace?: () => void prop to AutoRunInline. Render a
  "Browse Playbook Exchange" CTA next to "+ Create document" in the
  empty state when the prop is provided. Both buttons share the same
  vertical stack so the discoverability matches desktop's overview.
- Plumb the prop through AutoRunPanel (full-screen overlay),
  AutoRunTabContent / RightDrawer (inline tab), and RightPanel.
- Wire handleOpenMarketplaceSheet through from App.tsx for both
  AutoRunPanel and RightPanel render sites; reuses the same
  MarketplaceSheet and WS infrastructure introduced by PR RunMaestro#947, so no
  new IPC/server surface.

The CTA is opt-in (prop is optional) so existing callers and tests keep
working without change. Existing scoped tests pass; new
AutoRunInline.test.tsx covers the empty-state CTA visibility,
hidden-when-unset, and onClick behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all seven CodeRabbit findings in ba1017b5 (pushed to feat/web-playbook-exchange, so it appears on both this PR and #947). Summary:

Service layer (marketplace-service.ts)

  • Import path now falls back to a stale cache when fetchManifest() fails, mirroring the browse path so a visible playbook can't disappear at import time.
  • Refuse to persist a playbook when every document failed to write (only when the playbook actually had documents — asset-only imports still work). Throws MarketplaceImportError before the playbook file is touched.

WebSocket handlers (messageHandlers.ts)

  • isUntrustedLocalPath now also rejects . / .. segments and any embedded backslash, on top of the existing absolute / ~ / Windows-drive-prefix checks.
  • All four marketplace catch blocks (get_manifest, get_document, get_readme, import_playbook) now route through a new reportMarketplaceHandlerError helper that calls captureException before sending the typed failure. Mirrors the existing reportHandlerError pattern while preserving the typed-result protocol mobile clients wait on.

Web server factory (web-server-factory.ts)

  • Marketplace-import catch now reports the original exception to Sentry with { operation, sessionId, playbookId, targetFolderName } context before returning { success: false }.

Mobile sheet (MarketplaceSheet.tsx)

  • Manifest / README / document / import catch blocks console.error the underlying exception so the original cause is visible in browser devtools (web bundle has no Sentry — same pattern used in AutoRunDocumentViewer).
  • Returning to the README view from a doc preview now clears previewError and re-fetches the README via a new loadReadmeFor helper. Previously the sheet could stay stuck on a stale "No README available" / fetch error after the user tapped a doc and went back.

Tests (marketplace.test.ts, messageHandlers.test.ts)

  • New: empty-import guard fires and no playbook is persisted.
  • New: stale-cache fallback during import.
  • New: parameterized isUntrustedLocalPath traversal/backslash rejection via the typed result.

All 205 tests in the scoped suites pass; lint + prettier clean.

chr1syy added a commit to chr1syy/Maestro that referenced this pull request May 10, 2026
…e/web

# Conflicts:
#	src/main/web-server/web-server-factory.ts
@pedramamini

Copy link
Copy Markdown
Collaborator

Reviewed and ready to land in spirit, but two things need to happen before merge:

1. Rebase on current rc. This branch is now in conflict with rc after #946 / #974 / #976 / others landed — the web-server scaffolding (web-server-factory.ts, messageHandlers.ts, CallbackRegistry.ts, WebServer.ts, AutoRunSetupSheet.tsx, etc.) overlaps with what #946 added. Please rebase onto origin/rc and resolve.

2. Fix the SSH silent-downgrade in src/main/web-server/web-server-factory.ts. The desktop IPC (src/main/ipc/handlers/marketplace.ts:137-139) explicitly throws SSH remote not found or disabled when a session opted into SSH but the remote can't be resolved — exactly the loud-fail pattern called out in CLAUDE.md ("when the user enabled SSH but the configured remote can't be resolved, fail loudly").

The new WS callback path (resolveSessionSshConfig → marketplace import) returns undefined in three failure modes (missing remoteId, remote not in store, enabled: false) and then proceeds to import locally. A user with an SSH-enabled session whose remote is misconfigured will silently land the playbook on the desktop's local filesystem instead of the remote host. Tests for this exact regression were added on the desktop side (should fail loudly when SSH remote ID does not resolve) but the equivalent guard is absent on the WS path.

Also: resolveSessionSshConfig reads session.sshRemoteId || session.sessionSshRemoteConfig?.remoteId but never inspects sessionSshRemoteConfig.enabled. AgentSshRemoteConfig (src/shared/types.ts:626) treats enabled as the source of truth — a session with enabled: false and a populated remoteId would be wrongly treated as remote.

Fix: distinguish "no SSH configured" from "SSH configured but unresolvable" in the WS callback. Return { success: false, error: 'SSH remote not found or disabled' } for the latter. Gate the resolution on sessionSshRemoteConfig?.enabled === true before pulling remoteId. Add a WS-side test mirroring the desktop unresolvable-remote regression test.

Once those two are in I'll merge it.

@pedramamini

Copy link
Copy Markdown
Collaborator

@chr1syy ping — see comment above for the rebase + SSH fix needed before this can land. Same applies to #973 which is stacked on top.

chr1syy added a commit to chr1syy/Maestro that referenced this pull request May 10, 2026
Closes Gap 3 from the PR RunMaestro#946/RunMaestro#947 mobile/web AutoRun parity follow-up.
Desktop has an "Add Docs" affordance that surfaces both Create-doc and
Marketplace as co-equal entry points. On mobile/web, the empty state of
AutoRunInline only offered "+ Create document" — users with a fresh
AutoRun folder had no in-UI path to discover existing playbooks and had
to know to open the launch sheet just to find the marketplace entry.

- Add onOpenMarketplace?: () => void prop to AutoRunInline. Render a
  "Browse Playbook Exchange" CTA next to "+ Create document" in the
  empty state when the prop is provided. Both buttons share the same
  vertical stack so the discoverability matches desktop's overview.
- Plumb the prop through AutoRunPanel (full-screen overlay),
  AutoRunTabContent / RightDrawer (inline tab), and RightPanel.
- Wire handleOpenMarketplaceSheet through from App.tsx for both
  AutoRunPanel and RightPanel render sites; reuses the same
  MarketplaceSheet and WS infrastructure introduced by PR RunMaestro#947, so no
  new IPC/server surface.

The CTA is opt-in (prop is optional) so existing callers and tests keep
working without change. Existing scoped tests pass; new
AutoRunInline.test.tsx covers the empty-state CTA visibility,
hidden-when-unset, and onClick behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The WS marketplace_import_playbook callback's resolveSessionSshConfig
returned undefined for three distinct conditions:
1. No SSH configured on the session.
2. SSH configured (sessionSshRemoteConfig.enabled / legacy sshRemoteId)
   but remoteId is missing or null.
3. SSH configured but the matching sshRemotes entry is missing or
   disabled.

Cases 2 and 3 silently downgraded to a local import — landing the
playbook on the desktop's local filesystem when the user explicitly
opted into SSH. The desktop IPC handler already rejects this with
'SSH remote not found or disabled' (matching CLAUDE.md's loud-fail
SSH-spawn pattern); the WS path now mirrors that behavior.

Also: gate on sessionSshRemoteConfig.enabled === true before pulling
remoteId. AgentSshRemoteConfig treats `enabled` as the source of
truth — a session with `enabled: false` and a populated remoteId must
not be treated as remote.

Resolver now throws on unresolvable; the import callback catches the
throw before the captureException try-block so user-misconfiguration
errors return a typed { success: false, error } without polluting
Sentry.

Tests: four new web-server-factory specs covering all three
unresolvable conditions plus the enabled:false-with-remoteId case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chr1syy

chr1syy commented May 10, 2026

Copy link
Copy Markdown
Contributor Author

@pedramamini both asks addressed in `92576d97`.

1. Rebase / merge state

Branch is now in sync with `upstream/rc` (round 4 merge in `8eb5b558`). `gh pr view` reports `mergeable: MERGEABLE`, `mergeStateStatus: CLEAN`. No outstanding conflicts.

2. SSH silent-downgrade in `web-server-factory.ts`

Reworked `resolveSessionSshConfig` to mirror the desktop IPC handler's loud-fail behavior:

  • Source of truth: `sessionSshRemoteConfig.enabled === true` is now the gate. A session with `enabled: false` and a populated `remoteId` is treated as no-SSH (passes through to local import). The legacy top-level `sshRemoteId` field still implies enabled when present, matching how the rest of the codebase reads it (`sessionHelpers.ts:384`).
  • Three unresolvable conditions now throw `'SSH remote not found or disabled'`:
    1. `enabled: true` but `remoteId` is null/missing.
    2. `remoteId` doesn't appear in `sshRemotes`.
    3. Matching `sshRemotes` entry has `enabled: false`.
  • Pre-resolution in the import callback: SSH is resolved before the import try-block. Throws are caught and returned as `{ success: false, error }` without routing through `captureException` — user misconfiguration isn't an exceptional bug.

Tests

Added `importMarketplacePlaybookCallback behavior` block (4 specs) in `src/tests/main/web-server/web-server-factory.test.ts`:

  • `enabled: true` + remoteId not in store → loud fail.
  • `enabled: true` + matching entry has `enabled: false` → loud fail.
  • `enabled: true` + `remoteId: null` → loud fail.
  • `enabled: false` + populated remoteId → `importMarketplacePlaybook` called with `sshConfig: undefined` (correctly treated as local).

Mocked `marketplace-service.importMarketplacePlaybook` and added `captureException` / `electron.app`+`BrowserWindow` to the existing electron mock so the import callback is testable end-to-end.

Verification

  • `npm run lint` clean (TypeScript, all configs).
  • `npm run lint:eslint` clean.
  • `prettier --check` clean.
  • Scoped suites pass: web-server-factory (48 + 4 new), message handlers, marketplace IPC, mobile App — 347 tests total.

@pedramamini
pedramamini merged commit 9b9c4d9 into RunMaestro:rc May 10, 2026
3 checks passed
pedramamini pushed a commit that referenced this pull request May 10, 2026
Closes Gap 3 from the PR #946/#947 mobile/web AutoRun parity follow-up.
Desktop has an "Add Docs" affordance that surfaces both Create-doc and
Marketplace as co-equal entry points. On mobile/web, the empty state of
AutoRunInline only offered "+ Create document" — users with a fresh
AutoRun folder had no in-UI path to discover existing playbooks and had
to know to open the launch sheet just to find the marketplace entry.

- Add onOpenMarketplace?: () => void prop to AutoRunInline. Render a
  "Browse Playbook Exchange" CTA next to "+ Create document" in the
  empty state when the prop is provided. Both buttons share the same
  vertical stack so the discoverability matches desktop's overview.
- Plumb the prop through AutoRunPanel (full-screen overlay),
  AutoRunTabContent / RightDrawer (inline tab), and RightPanel.
- Wire handleOpenMarketplaceSheet through from App.tsx for both
  AutoRunPanel and RightPanel render sites; reuses the same
  MarketplaceSheet and WS infrastructure introduced by PR #947, so no
  new IPC/server surface.

The CTA is opt-in (prop is optional) so existing callers and tests keep
working without change. Existing scoped tests pass; new
AutoRunInline.test.tsx covers the empty-state CTA visibility,
hidden-when-unset, and onClick behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready to merge This PR is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants