Skip to content

feat(app): per-container CF SSH tunnel for VS Code panel#434

Open
skulidropek wants to merge 20 commits into
ProverCoderAI:mainfrom
skulidropek:issue-428
Open

feat(app): per-container CF SSH tunnel for VS Code panel#434
skulidropek wants to merge 20 commits into
ProverCoderAI:mainfrom
skulidropek:issue-428

Conversation

@skulidropek

Copy link
Copy Markdown
Member

Summary

  • New ssh-project-tunnels.ts service starts a dedicated Cloudflare quick tunnel per container keyed by projectKey (separate from share-link tunnels)
  • New POST /projects/by-key/:key/ssh-tunnel API route that starts the tunnel and returns the CF hostname
  • VS Code panel now shows only the CF SSH command — no localhost config — with loading/ready/failed states and a Retry button

User flow

  1. Open a container terminal → click VS Code
  2. Panel shows "Starting Cloudflare tunnel…" briefly
  3. Panel shows:
    • One-time wildcard ~/.ssh/config block (copy button)
    • ssh -o "ProxyCommand=cloudflared access ssh --hostname %h" dev@XXXX.trycloudflare.com (copy button)
    • "Open in VS Code (CF tunnel)" link

Proof of fix

  • Cause: VS Code panel showed localhost SSH config useless for external access
  • Solution: Dedicated per-container cloudflared SSH tunnel started on demand; panel shows only the CF hostname
  • Proof: Playwright test confirmed panel displays the CF SSH command (sorts-anyway-mortgage-gpl.trycloudflare.com) after clicking VS Code on dg-docker-git-issue-428

Closes #428

🤖 Generated with Claude Code

skulidropek and others added 6 commits June 19, 2026 22:15
Implements time-limited share links that give SSH and web terminal
access to containers without exposing the full dashboard.

**Backend:**
- `project-share-links.ts`: global token store at projectsRoot/share-links.json
  with 7-day TTL; tokens are 16 random hex chars (randomBytes(8).toString("hex"))
- `buildShareLinkSshAccess` in ssh-access.ts: generates SSH config snippet
  using host-mapped sshPort and clientHost from request ?host= param
- 4 new HTTP routes: GET /share-links/:token (public, auth via token),
  POST|GET|DELETE /projects/by-key/:projectKey/share-links
- OpenAPI endpoints registered in SharingGroup

**Frontend:**
- `resolveWebAppRoute` now detects 16-hex ?t= tokens as ShareLink route
  (UUID terminal IDs with dashes never match /^[0-9a-f]{16}$/)
- `app-share-link.tsx`: standalone page showing SSH config snippets,
  VS Code Remote-SSH URI buttons, and a web terminal that starts via
  createProjectTerminalSession
- `api-share-links.ts`: typed API client using requestJson + @effect/schema
- `panel-share.tsx`: ContainerShareLinksSection renders per-project
  share link management (generate / revoke) when a project is selected

## Proof of fix

- Cause: no mechanism to share container access without full dashboard URL
- Solution: 16-hex token in /ssh/:projectKey?t=:token identifies the project
  and authorizes SSH config + terminal session creation
- Verification: TypeScript passes (rtk tsc --noEmit) on all three packages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace openUri(window.location.href) with <a href={vscodeUri}> so the
browser reliably triggers the vscode:// protocol handler without
navigating away from the page.

Replace ssh alias-based URI (?ssh=ALIAS) with direct connection format
(?hostName=user@host:port) so VS Code Remote SSH connects immediately
without requiring the user to add an entry to ~/.ssh/config first.

VS Code docs: "You can enter a user@host or user@host:port connection
string if you don't want to use an SSH config file entry."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ssh-share-link-tunnels.ts: spawns a dedicated cloudflared SSH tunnel
  per share link token (ssh://gatewayIp:sshPort), stores hostname in-memory
- Replace panel HTTP tunnel usage in share link routes with per-token SSH tunnel:
  POST /share-links starts the tunnel and returns cfSshConfigSnippet with the
  CF hostname; DELETE /share-links stops and cleans up the tunnel
- Rename cfPublicHostname → sshCfHostname in buildShareLinkSshAccess to make
  clear this is the SSH-specific tunnel, not the panel HTTP tunnel
- Strip web port suffix from clientHost so HostName in SSH config is a bare
  hostname without port

∀ token: started(token, sshPort) → ∃ cfHostname: cfSshConfigSnippet(token, cfHostname)
The panel HTTP tunnel cannot proxy raw SSH; this gives VS Code Remote SSH
a working path for users outside the LAN.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cess

- Generate a random 12-char SSH password on share link creation
- Enable PasswordAuthentication yes in sshd_config.d/dev.conf inside the
  target container and unlock the dev user via docker exec chpasswd
- Disable password auth and re-lock dev user when the last share link for a
  container is revoked
- Store password in share-links.json (backwards-compatible: old links decode
  sshPassword as null via Schema.optionalWith default)
- Show SSH password block on the share link page with:
  - password + copy button
  - direct SSH command (ssh user@host -p port)
  - cloudflared proxy command (when CF SSH tunnel is active)
  - "No SSH key needed" hint

∀ link: created(link) → passwordSet(link.containerName) ∧ passwordAuth(link.containerName)
∀ container: noLinks(container) → passwordAuthDisabled(container)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace per-host CF SSH config snippet with a wildcard one-time setup block:
  Host *.trycloudflare.com
    ProxyCommand cloudflared access ssh --hostname %h

Once added to ~/.ssh/config, any share link CF tunnel works automatically —
`ssh dev@<hostname>.trycloudflare.com` and VS Code (CF tunnel) need no
per-container config update.

Also simplifies the SSH password block: shows password + direct LAN command
only (CF command is now implicit via the wildcard).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…AI#428)

When clicking the VS Code button on a terminal, automatically starts a
dedicated Cloudflare quick tunnel for that container's SSH port and shows
only the CF SSH command — no localhost config.

- New service: ssh-project-tunnels.ts — per-projectKey cloudflared
  tunnel with idempotent start, keyed separately from share-link tunnels
- New route: POST /projects/by-key/:key/ssh-tunnel — starts tunnel,
  returns { hostname } (blocks up to 15s for hostname resolution)
- VsCodeAccessPanel shows: wildcard ~/.ssh/config setup, CF SSH command
  with copy, and "Open in VS Code (CF tunnel)" link
- Loading / ready / failed states with Retry button on failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Заметки о выпуске

  • New Features
    • Добавлены ссылки для совместного доступа с настраиваемым временем истечения (создание, список, отзыв по токену)
    • Реализован просмотр share link по URL с поддержкой передачи SSH-сессии в терминале
    • Добавлен SSH-доступ для VS Code Remote SSH: Cloudflare туннели и локальный вариант, с SSH-конфигом и командами
    • Добавлена панель управления доступом VS Code (копирование конфигураций/команд, refresh/retry)
    • Добавлена функция выдачи SSH-пароля для получателей share link

Walkthrough

Добавлена система share-ссылок для VS Code Remote SSH и браузерного доступа к контейнерам: новые сервисы хранения токенов, управления SSH-паролем и Cloudflare-туннелями на бэкенде; HTTP-эндпоинты создания/удаления/резолва ссылок; клиентский API; публичная страница по токену с отображением SSH-конфигурации и VS Code URI; панель управления ссылками и кнопка VS Code в терминале с состояниями туннеля.

Changes

Share Links & VS Code SSH Access

Layer / File(s) Summary
Контракты, схемы и OpenAPI
packages/api/src/api/contracts.ts, packages/api/src/api/schema.ts, packages/api/src/api/openapi.ts, packages/lib/src/usecases/ssh-access.ts
Добавлены типы ShareLinkInfo, CreateShareLinkRequest, CreateShareLinkResponse; Schema-константы для валидации; четыре OpenAPI-маршрута с параметром ShareLinkTokenParam; типы ShareLinkSshAccess, ShareLinkSshAccessInput и функция buildShareLinkSshAccess для построения SSH-конфигурации (прямой и через Cloudflare) с VS Code URI.
Сервис хранения share-ссылок
packages/api/src/services/project-share-links.ts
Новый модуль: запись криптографически стойких токенов с TTL в глобальный share-links.json, функции createShareLink, resolveShareLink (с проверкой истечения), deleteShareLink, listShareLinks через Effect и FileSystem.
SSH-пароль контейнера
packages/api/src/services/ssh-password-setup.ts
Генерация криптостойкого 12-символьного пароля без неоднозначных символов, enableContainerPasswordAuth (включает парольный SSH в контейнере, задаёт пароль пользователю dev через docker exec), disableContainerPasswordAuth (best-effort отключение auth и блокирование учётки).
Cloudflare-туннели для share-link и project
packages/api/src/services/ssh-share-link-tunnels.ts, packages/api/src/services/ssh-project-tunnels.ts
Два per-entity модуля управления туннелями: семафор для исключения операций, карта записей туннелей, запуск cloudflared tunnel с парсингом CF hostname из stdout/stderr, polling waitForHostname с таймаутом, SIGTERM/SIGKILL-эскалация при остановке, best-effort cleanup homeDir.
HTTP-роутер: эндпоинты share-links и SSH-туннеля
packages/api/src/http.ts
Пять новых маршрутов в makeRouter(): резолв по токену с вычислением clientHost, создание (201) с включением пароля и запуском share-link туннеля, список, удаление с остановкой туннеля и условным отключением пароля, запуск SSH-туннеля для проекта. Парсер тела запроса и валидация через Schema.
Клиентский API
packages/app/src/web/api-share-links.ts
Schema-схемы для моделей и ответов, экспортируемый тип ShareLinkInfo, функции loadShareLink, createProjectShareLink, listProjectShareLinks, deleteProjectShareLink, startProjectSshTunnel через requestJson и Effect.
Маршрутизация и страница share-link
packages/app/src/web/app-terminal-session-core.ts, packages/app/src/web/app.tsx, packages/app/src/web/app-share-link.tsx, packages/app/src/web/app-share-link-sections.tsx, packages/app/src/web/app-share-link-utils.ts
WebAppRoute расширен вариантом ShareLink с projectKey и shareToken; resolveWebAppRoute принимает search и извлекает токен t из query-строки пути /ssh/:projectKey?t=... (валидация: 16 hex-символов); App передаёт location.search и рендерит AppShareLink. Компонент AppShareLink со состояниями Loading/Error/Info/Connecting/Terminal/Closed; блоки SshConfigBlock, CfTunnelSetupBlock, SshPasswordBlock и InfoHeader для отображения конфига, Cloudflare-инструкций и VS Code URI; connectTerminalSession создаёт терминальную сессию; TerminalView подключает TerminalPanel. Утилиты-стили и функция copyText для буфера обмена.
Панель VS Code SSH и состояние туннеля
packages/app/src/web/panel-vscode-access.tsx, packages/app/src/web/panel-vscode-access-panel.tsx
Новые типы VsCodeAccessInfo и CfTunnelState (idle/loading/ready/failed); функция buildVsCodeAccessInfo для извлечения SSH-параметров проекта; startTunnel инициирует запуск SSH-туннеля через startProjectSshTunnel; хуки useTunnelAutoStart (запуск при открытии панели) и useTunnelPolling (периодическое обновление). Компонент VsCodeAccessPanel отображает SSH-конфиги (CF и direct), команды подключения, VS Code URI и кнопки копирования/открытия.
Интеграция с терминальной панелью
packages/app/src/web/app-ready-terminal-pane.tsx, packages/terminal/src/web/panel-terminal-types.ts, packages/terminal/src/web/panel-terminal.tsx, packages/terminal/src/web/panel-terminal-header.tsx
TerminalPane расширена состояниями vsCodePanelOpen и cfState для управления панелью VS Code; при открытии панели запускается startProjectSshTunnel через useEffect; подключены useTunnelAutoStart и useTunnelPolling. Проп onOpenVsCode протянут от TerminalPanelProps через TerminalPanelLayout до кнопки "VS Code" в TerminalHeader. Типы перенесены в panel-terminal-types.ts.
Управление share-ссылками в SharePanel
packages/app/src/web/panel-share.tsx, packages/app/src/web/app-ready-main-panels.tsx
ContainerShareLinksSection реализует refresh/generate/revoke через Effect.runPromise с состояниями Loading/Error/Loaded; отображает новую ссылку с кнопками copy/open и список активных ссылок с revoke. SharePanel получает проп selectedProjectKey и условно рендерит секцию. ShareScreen передаёт selectedProjectKey из selectedProjectSummary?.projectKey.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(100, 149, 237, 0.5)
    Note over Browser,makeRouter: Создание share-link и запуск SSH-туннеля
  end
  participant Browser
  participant makeRouter
  participant createShareLink
  participant enableContainerPasswordAuth
  participant startSshShareLinkTunnel
  participant buildShareLinkSshAccess

  Browser->>makeRouter: POST /projects/by-key/:projectKey/share-links
  makeRouter->>createShareLink: токен + TTL → запись в share-links.json
  createShareLink-->>makeRouter: ShareLink
  makeRouter->>enableContainerPasswordAuth: docker exec → PasswordAuthentication yes + chpasswd
  enableContainerPasswordAuth-->>makeRouter: void
  makeRouter->>startSshShareLinkTunnel: spawn cloudflared tunnel
  startSshShareLinkTunnel-->>makeRouter: CF hostname | null
  makeRouter->>buildShareLinkSshAccess: sshConfig + vscodeUri
  buildShareLinkSshAccess-->>makeRouter: ShareLinkSshAccess
  makeRouter-->>Browser: 201 { link, url }

  rect rgba(60, 179, 113, 0.5)
    Note over Browser,AppShareLink: Открытие share-ссылки посетителем
  end
  participant AppShareLink
  participant loadShareLink

  Browser->>AppShareLink: GET /ssh/:projectKey?t=<token>
  AppShareLink->>loadShareLink: GET /share-links/:token?host=...
  loadShareLink-->>AppShareLink: ShareLinkInfo + SSH-access
  AppShareLink-->>Browser: Info screen (SSH-конфиг, VS Code URI, кнопка подключения)
  Browser->>AppShareLink: нажать "connect terminal"
  AppShareLink->>makeRouter: createProjectTerminalSession
  makeRouter-->>AppShareLink: сессия создана
  AppShareLink-->>Browser: Terminal screen
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • ProverCoderAI/docker-git#309: Маршрутизация share-link строит URL /ssh/<projectKey>?t=<token> и добавляет парсинг параметра t в resolveWebAppRoute, что совпадает с работой PR #309 по стабилизации /ssh/<projectKey> маршрутов через параметр селектора.
  • ProverCoderAI/docker-git#427: PR #427 добавляет основной OpenAPI-механизм и контракты в packages/api (включая openapi.ts и http.ts), а текущий PR расширяет эту OpenAPI-поверхность, добавляя схемы и эндпоинты share-links.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Requirements Alignment ❌ Error PR fails requirements check: vitest downgrade to ^3.2.0 breaks peer dependency with @effect/vitest@^0.29.0 (requires 4.1.0+), causing test suite failure; zero test coverage for 4 new service files... Restore vitest to ^4.1.9, add test suites for new services, fix shell injection vulnerability by quoting targetDir, enable StrictHostKeyChecking accept-new, and handle Promise rejections in copyText with Effect pattern.
Description check ⚠️ Warning Описание PR не соответствует требуемому шаблону: отсутствуют разделы 'Source TZ / Issues', 'Requirements Alignment', 'Verification' и 'Implemented/Out of scope'. Дополнить описание согласно шаблону: добавить 'Source TZ / Issues', разделы 'Requirements Alignment' (Implemented/Out of scope), и 'Verification'.
Security Regression ⚠️ Warning PR introduces two high-confidence security regressions: (1) shell command injection via unescaped targetDir in SSH command (panel-vscode-access-panel.tsx:114), and (2) disabled SSH host key checkin... Escape targetDir using shell-safe quoting (e.g., single quotes with proper escaping) and change StrictHostKeyChecking from 'no' to 'accept-new' to prevent MITM while accepting first connection.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Название точно отражает основное изменение: добавление CF SSH туннелей для VS Code панели на уровне контейнера (per-container).
Linked Issues check ✅ Passed PR реализует все требования issue #428: on-demand CF SSH туннели, стабильные credentials, двойной режим доступа (CF и локальный), конфигурация SSH и UI с состояниями.
Out of Scope Changes check ✅ Passed Все изменения связаны с основной целью: добавление per-container CF SSH туннелей и VS Code панели для remote SSH доступа; нет посторонних изменений.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@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: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/src/api/contracts.ts`:
- Around line 813-826: The ShareLinkInfo type definition in contracts.ts is
missing the sshPassword field which is present in the corresponding schema.ts
definition and returned in the HTTP response in http.ts, and expected by the
client in api-share-links.ts. Add the missing readonly field sshPassword with
type string | null to the ShareLinkInfo type to ensure it matches the actual
data structure being returned and consumed by clients.

In `@packages/api/src/http.ts`:
- Around line 1185-1196: The enableContainerPasswordAuth call silently swallows
errors using Effect.orElse, but then createShareLink is called unconditionally
with the sshPassword parameter, creating a data integrity issue where the share
link contains a password that cannot actually be used. Modify the code to
capture the result of enableContainerPasswordAuth and conditionally pass either
the sshPassword or null to createShareLink based on whether password
authentication was successfully enabled. Alternatively, you could add a
passwordAuthEnabled flag to the response object to inform the client about the
actual state of password authentication on the container.

In `@packages/api/src/services/project-share-links.ts`:
- Around line 101-117: There is a race condition in the createShareLink function
where concurrent requests can both read the same file state before either
writes, causing data loss. Create a semaphore instance (using
Effect.unsafeMakeSemaphore(1)) at the module level to serialize all
read-modify-write operations on the share links file. Wrap the file read and
write operations in the createShareLink function with the semaphore to ensure
only one operation executes at a time. Apply the same semaphore protection to
the deleteShareLink function to prevent the same race condition from occurring
there.

In `@packages/api/src/services/ssh-password-setup.ts`:
- Around line 56-64: The password is being directly interpolated into the shell
command in the script array without proper escaping. In the line that contains
`echo 'dev:${password}' | chpasswd`, the password variable needs to be properly
escaped to prevent command injection if it contains special characters. Replace
the single-quoted password string with a properly escaped version using double
quotes and appropriate escape sequences, or use a safer method that passes the
password without shell interpolation.

In `@packages/api/src/services/ssh-project-tunnels.ts`:
- Around line 23-165: Extract the shared code from ssh-project-tunnels.ts into a
new ssh-tunnel-core.ts module. Move the common SshTunnelRecord type definition
and all shared utility functions (processEnv, readDefaultGatewayIp,
defaultLocalhostHost, appendLog, consumeChunk, cleanupRecord, waitForChildClose,
stopRecord, attachHandlers, and waitForHostname) into the new core module and
export them. Then import these utilities back into ssh-project-tunnels.ts,
keeping only the projectTunnelMap, projectTunnelLock, and startWaitAttempts
constants along with the module-specific exported functions that use these
shared utilities. Apply the same refactoring to ssh-share-link-tunnels.ts to
eliminate the ~95% code duplication between the two files.
- Around line 167-177: The waitForHostname function does not check for premature
process termination before continuing to wait for the hostname. Add a check for
process termination status (similar to what was implemented in
ssh-share-link-tunnels.ts) to the initial condition in waitForHostname that
currently checks if record.hostname is not null, record.stopping is true, or
remainingAttempts is less than or equal to 0. This check should prevent the
function from continuing to poll if the process has been unexpectedly
terminated, avoiding unnecessary iterations until timeout.

In `@packages/api/src/services/ssh-share-link-tunnels.ts`:
- Around line 167-177: The waitForHostname function does not check if the
cloudflared process has already terminated prematurely, causing it to wait the
full 15 seconds even when the hostname will never appear due to process failure.
Add a check for record.processClosed to the existing condition that checks if
the function should return early, so that if the process has closed, the
function returns immediately instead of continuing to retry.

In `@packages/app/src/web/api-share-links.ts`:
- Around line 35-84: Add comprehensive TSDoc comments above each of the five
exported functions (loadShareLink, createProjectShareLink,
listProjectShareLinks, deleteProjectShareLink, and startProjectSshTunnel) that
include the required annotations: `@pure` or `@effect` (depending on side effects),
`@invariant`, `@precondition`, `@postcondition`, `@complexity`, `@throws`, and functional
comment markers (CHANGE/WHY/REF/SOURCE/FORMAT THEOREM). Each comment block
should describe the function's purpose, document the Effect type indicating
whether it's pure or has effects, specify input/output constraints, complexity
considerations, and any error conditions that may be thrown.

In `@packages/app/src/web/app-ready-terminal-pane.tsx`:
- Around line 386-396: The tunnel state is not being reset when the
vsCodePanelOpen panel is closed, causing stale tunnel configuration to persist
when the panel is reopened. In the useEffect hook of the TerminalPane component,
add logic to reset cfState back to its initial idle state by calling
setCfState({ tag: "idle" }) when vsCodePanelOpen becomes false. This ensures
that the next time the panel is opened, the tunnel will attempt to restart from
a clean state rather than using the cached previous state.

In `@packages/app/src/web/app-share-link.tsx`:
- Around line 83-85: The copyText function and WILDCARD_SSH_CONFIG constant are
duplicated across multiple files. Create a new shared utility module in the web
directory, extract both the copyText function and WILDCARD_SSH_CONFIG constant
into this new module and export them, then update all locations where these
duplicates exist to import and use the shared versions instead of maintaining
separate definitions.

In `@packages/app/src/web/app-terminal-session-core.ts`:
- Around line 30-36: The safeDecodeSegment function uses try/catch which is
flagged by the static analyzer, but this is a boundary-decoding function that
must remain synchronous and pure for CORE routing logic. Add a comment above the
safeDecodeSegment function explaining that this try/catch usage is justified
because decodeURIComponent throws only on invalid input and using Effect.try
would break the synchronous/pure requirements of this CORE function.

In `@packages/app/src/web/panel-share.tsx`:
- Around line 146-173: The refresh, generate, and revoke functions have a race
condition where in-flight requests from an old projectKey can update state after
the projectKey has already changed. To fix this, capture the current projectKey
at the time each request is initiated (in the refresh, generate, and revoke
functions) and before calling setState, verify that the projectKey hasn't
changed. Only apply the state update if the captured projectKey still matches
the current projectKey, otherwise skip the setState call. This prevents stale
responses from overwriting state for a different project.
- Around line 189-192: Remove the eslint-disable-next-line comment from the
useEffect hook and refactor the refresh function to be wrapped with useCallback,
passing projectKey as its dependency array to make it stable. Then update the
useEffect dependency array to include the memoized refresh function instead of
just projectKey, ensuring the linter rule is satisfied without suppression.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: a1add25f-f1ff-4da4-bb43-15070b68eb15

📥 Commits

Reviewing files that changed from the base of the PR and between f601d55 and 49179e6.

📒 Files selected for processing (19)
  • packages/api/src/api/contracts.ts
  • packages/api/src/api/openapi.ts
  • packages/api/src/api/schema.ts
  • packages/api/src/http.ts
  • packages/api/src/services/project-share-links.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-ready-main-panels.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/terminal/src/web/panel-terminal-header.tsx
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/terminal/src/web/panel-terminal.tsx
📜 Review details
⏰ Context from checks skipped due to timeout. (8)
  • GitHub Check: E2E (OpenCode)
  • GitHub Check: Test
  • GitHub Check: E2E (Runtime volumes + SSH)
  • GitHub Check: E2E (Browser command)
  • GitHub Check: E2E (Login context)
  • GitHub Check: E2E (Clone cache)
  • GitHub Check: E2E (Clone auto-open SSH)
  • GitHub Check: Final build (windows-latest)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{sh,bash,py,js,ts,jsx,tsx,go,java,rb,php}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce command injection or unsafe shell/process execution with user-controlled input

Files:

  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/api/src/services/project-share-links.ts
  • packages/app/src/web/app-ready-main-panels.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/terminal/src/web/panel-terminal-header.tsx
  • packages/api/src/api/contracts.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/api/schema.ts
  • packages/api/src/api/openapi.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app.tsx
**/*.{py,js,ts,jsx,tsx,go,java,rb,php,sh,bash,c,cpp}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce path traversal or writes outside intended project/container state directories

Files:

  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/api/src/services/project-share-links.ts
  • packages/app/src/web/app-ready-main-panels.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/terminal/src/web/panel-terminal-header.tsx
  • packages/api/src/api/contracts.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/api/schema.ts
  • packages/api/src/api/openapi.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app.tsx
**/*.{js,ts,jsx,tsx,py,java,go,rb,php,sh,bash,yml,yaml,json,env*,toml,cfg,config,dockerfile,dockerignore}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files expose credentials, tokens, private-keys, or PII in source, generated config, logs, or CI output

Files:

  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/api/src/services/project-share-links.ts
  • packages/app/src/web/app-ready-main-panels.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/terminal/src/web/panel-terminal-header.tsx
  • packages/api/src/api/contracts.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/api/schema.ts
  • packages/api/src/api/openapi.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: FUNCTIONAL CORE: Write only pure functions with immutable data and mathematical operations in core modules; no side effects, mutations, or external service calls
IMPERATIVE SHELL: Isolate all side effects (IO, network, database, environment/process) in a thin SHELL layer; CORE never calls SHELL, only SHELL → CORE
Never use any type annotation in TypeScript; use unknown only at SHELL boundaries for decoding, never export unknown outside boundary modules
Never use as type assertions in normal code; only permit as in a single 'axiomatic' module (brands, constructors, constants) after which types flow safely without casts
Always use exhaustive pattern matching for union types through .exhaustive() or Match.exhaustive() from effect-ts; never use switch statements or unhandled type branches
Use Effect<Success, Error, Requirements> monad from effect-ts for all effects; compose through pipe() and Effect.flatMap(); never use async/await, raw Promise chains (then/catch), or Promise.all in product code
Interoperate with Promise/exceptions only in SHELL through Effect.try/Effect.tryPromise with typed error mapping; never leave raw exceptions or untyped errors in the domain
Use Effect.acquireRelease + Effect.scoped for resource management with guaranteed finalization; never manage resources with try/finally or manual cleanup
All external services (database, HTTP, environment) must be accessed through Effect-based interfaces and Layer-based dependency injection; never call external APIs directly
Provide comprehensive TSDoc comments with mathematical notation: @pure, @effect, @invariant, @precondition, @postcondition, @complexity, @throws, and CHANGE/WHY/REF/SOURCE/FORMAT THEOREM functional comment markers
No console.*, process direct calls, or untyped environment access in product code; all such operations must be abstracted through Layer-based services in SHELL
Boundary data from external sources (HTTP, database, environment) must be decoded/valida...

Files:

  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/api/src/services/project-share-links.ts
  • packages/app/src/web/app-ready-main-panels.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/terminal/src/web/panel-terminal-header.tsx
  • packages/api/src/api/contracts.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/api/schema.ts
  • packages/api/src/api/openapi.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app.tsx
**/{browser*,server*,app*,*.ts,*.js}

📄 CodeRabbit inference engine (README.md)

Web version must listen on 0.0.0.0 by default for accessibility across LAN devices

Files:

  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/api/src/services/project-share-links.ts
  • packages/app/src/web/app-ready-main-panels.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/api/src/api/contracts.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/api/schema.ts
  • packages/api/src/api/openapi.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app.tsx
**/{cli*,command*,auto*,*.ts,*.tsx}

📄 CodeRabbit inference engine (README.md)

Implement auto-mode agent selection logic to choose Claude, Codex, Gemini, or Grok randomly from available authorized providers, or allow forced selection with --auto=

Files:

  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/api/src/services/project-share-links.ts
  • packages/app/src/web/app-ready-main-panels.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/terminal/src/web/panel-terminal-header.tsx
  • packages/api/src/api/contracts.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/api/schema.ts
  • packages/api/src/api/openapi.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app.tsx
**/*

⚙️ CodeRabbit configuration file

**/*: Ты строгий ревьюер SPEC DRIVEN DEVELOPMENT.

Перед выводами изучи README.md, другие *.md файлы, linked issues,
PR description, PR comments/discussion и релевантную кодовую базу.

Сверь изменения с исходным ТЗ/спекой и обсуждением. Флагай любой уход
от спеки, недокументированное изменение поведения, отсутствие тестов
для заявленного поведения и security-риск. Если спека не видна,
попроси автора добавить ее в issue или PR description.

Проверь решение с точки зрения формальной верификации: какие инварианты,
предусловия и постусловия можно доказать математически, а где доказуемость
слабая. Оцени решение с точки зрения теории игр: устойчивы ли стимулы,
нет ли выгодного обхода правил, и какое решение было бы сильнее.

Files:

  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/api/src/services/project-share-links.ts
  • packages/app/src/web/app-ready-main-panels.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/terminal/src/web/panel-terminal-header.tsx
  • packages/api/src/api/contracts.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/api/schema.ts
  • packages/api/src/api/openapi.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app.tsx
**

⚙️ CodeRabbit configuration file

**: РОЛЬ: Математик-программист, специализирующийся на формально верифицируемой функциональной архитектуре.

ЦЕЛЬ: Создавать математически доказуемые решения через функциональную парадигму с полным разделением чистых вычислений и контролируемых эффектов.

МОДЕЛЬ РАССУЖДЕНИЯ:

  • Не выдавать “личные мнения”. Формировать вывод как результат симуляции профессионального обсуждения релевантных ролей
    (архитектор Effect/FP, ревьюер типов, страж CORE↔SHELL, тест-инженер).
  • Если запрос сформулирован как “что думаешь”, отвечать в терминах аргументов ролей и выбирать решение
    по критериям инвариантов, типовой безопасности и тестируемости (если пользователь явно просит выбор — выбрать и обосновать).

ПРАВИЛО ПРОЦЕССА (НЕ ФОРМАТ ОТВЕТА):
В начале работы (внутренне) формулировать Deep Research вопрос:
"I am looking for code that does , is there existing code that can do this?"
Далее:

  • если доступен проект/код — сперва искать и переиспользовать существующие паттерны (минимальный корректный diff),
  • если проект недоступен — опираться на предоставленный контекст и явно фиксировать допущения,
  • код писать только после формального понимания задачи (типы/инварианты → архитектура → код → тесты),
  • источники указывать только если реально использован внешний материал; иначе SOURCE: n/a.

ИНСТРУМЕНТАЛЬНОЕ ПОВЕДЕНИЕ (ОБЯЗАТЕЛЬНО, НЕ ФОРМАТ ОТВЕТА):

  • Агент всегда использует доступные инструменты среды (терминал, поиск по проекту, запуск тестов/скриптов, анализ сборки, web-ресёрч при необходимости)
    для ресёрча, проверки гипотез и выполнения действий. Приоритет: проверяемость, воспроизводимость, минимальный риск.
  • Агент не предлагает “гайд” как замену действия. Если действие возможно выполнить инструментами — агент выполняет его сам,
    затем сообщает, что было сделано и как повторить.
  • Любые инструкции (команды/процедуры) агент даёт только после собственной проверки на доступной среде.
    Если проверить невозможно — явно фиксирует ограничение и перечисляе...

Files:

  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/api/src/services/project-share-links.ts
  • packages/app/src/web/app-ready-main-panels.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/terminal/src/web/panel-terminal-header.tsx
  • packages/api/src/api/contracts.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/api/schema.ts
  • packages/api/src/api/openapi.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app.tsx
🧠 Learnings (1)
📚 Learning: 2026-05-18T07:43:38.131Z
Learnt from: skulidropek
Repo: ProverCoderAI/docker-git PR: 313
File: packages/api/src/services/panel-cloudflare-tunnel.ts:123-135
Timestamp: 2026-05-18T07:43:38.131Z
Learning: In this repo’s ProverCoderAI/docker-git architecture, it is intentional for service modules under packages/api/src/services/ to call Node.js APIs directly (e.g., fs, child_process, fetch). Do not treat direct Node API usage in these service modules as a dependency-injection (DI) violation. This codebase provides the NodeContext at the program boundary (entry point) rather than injecting Context.Tag/Layer services into individual service modules; reviewers should only flag DI/context issues if the expected boundary setup is missing.

Applied to files:

  • packages/api/src/services/project-share-links.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/api/src/services/ssh-share-link-tunnels.ts
🪛 GitHub Check: Lint Effect-TS
packages/app/src/web/app-terminal-session-core.ts

[failure] 31-31:
Avoid try/catch in product code. Use Effect.try / Effect.catch*

🔇 Additional comments (23)
packages/app/src/web/api-share-links.ts (1)

75-84: Контракт между клиентом и сервером согласован; дополнительный envelope отсутствует.

Сервер возвращает ровно { hostname } (без обёртки ok), что совпадает со схемой клиента SshTunnelResponseSchema. Декодирование пройдёт успешно; панель VS Code не перейдёт в ошибку при успешном запуске туннеля.

			> Likely an incorrect or invalid review comment.
packages/app/src/web/app-ready-main-panels.tsx (1)

64-64: LGTM!

packages/api/src/api/contracts.ts (1)

828-836: LGTM!

packages/api/src/api/schema.ts (1)

395-428: LGTM!

packages/api/src/api/openapi.ts (1)

33-36: LGTM!

Also applies to: 742-743, 752-759

packages/lib/src/usecases/ssh-access.ts (1)

186-263: LGTM!

packages/api/src/services/project-share-links.ts (1)

119-150: LGTM!

packages/api/src/services/ssh-password-setup.ts (1)

25-28: LGTM!

Also applies to: 67-89

packages/api/src/services/ssh-share-link-tunnels.ts (1)

194-272: LGTM!

packages/api/src/services/ssh-project-tunnels.ts (1)

194-272: LGTM!

packages/app/src/web/app-terminal-session-core.ts (1)

4-6: LGTM!

Also applies to: 20-28, 38-47

packages/app/src/web/app.tsx (1)

18-18: LGTM!

Also applies to: 223-237

packages/app/src/web/app-share-link.tsx (1)

1-82: LGTM!

Also applies to: 86-112, 117-441

packages/app/src/web/app-ready-terminal-pane.tsx (1)

2-5: LGTM!

Also applies to: 154-175, 177-201, 203-280, 318-363, 365-384, 397-420

packages/terminal/src/web/panel-terminal-types.ts (1)

6-22: LGTM!

packages/terminal/src/web/panel-terminal.tsx (1)

53-76: LGTM!

Also applies to: 257-282

packages/terminal/src/web/panel-terminal-header.tsx (1)

19-37: LGTM!

Also applies to: 164-184

packages/api/src/http.ts (6)

41-41: LGTM!

Also applies to: 134-151


578-584: LGTM!


1134-1174: LGTM!


1225-1238: LGTM!


1240-1257: LGTM!


1259-1270: LGTM!

Comment thread packages/api/src/api/contracts.ts
Comment thread packages/api/src/http.ts
Comment on lines +101 to +117
export const createShareLink = (
projectsRoot: string,
projectDir: string,
projectKey: string,
sshPassword: string | null,
ttlMs?: number
): Effect.Effect<ShareLink, ApiInternalError, FileSystem.FileSystem> =>
Effect.gen(function*(_) {
const token = randomBytes(8).toString("hex")
const now = new Date().toISOString()
const expiresAt = new Date(Date.now() + (ttlMs ?? defaultTtlMs)).toISOString()
const link: ShareLink = { token, projectKey, projectDir, createdAt: now, expiresAt, sshPassword }
const file = yield* _(readShareLinksFile(projectsRoot))
const activeLinks = file.links.filter((l) => !isExpired(l))
yield* _(writeShareLinksFile(projectsRoot, { schemaVersion: 1, links: [...activeLinks, link] }))
return link
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Потенциальная гонка при одновременном создании share-ссылок.

Если два запроса на создание ссылки выполняются параллельно, оба прочитают одно состояние файла, затем оба запишут — одна ссылка потеряется. В модулях туннелей используется семафор (Effect.unsafeMakeSemaphore(1)) для сериализации операций.

Рекомендую добавить аналогичный семафор для операций записи в share-links.json.

🔒 Предлагаемое исправление
+const shareLinksLock = Effect.unsafeMakeSemaphore(1)
+
 export const createShareLink = (
   projectsRoot: string,
   projectDir: string,
   projectKey: string,
   sshPassword: string | null,
   ttlMs?: number
 ): Effect.Effect<ShareLink, ApiInternalError, FileSystem.FileSystem> =>
   Effect.gen(function*(_) {
     const token = randomBytes(8).toString("hex")
     const now = new Date().toISOString()
     const expiresAt = new Date(Date.now() + (ttlMs ?? defaultTtlMs)).toISOString()
     const link: ShareLink = { token, projectKey, projectDir, createdAt: now, expiresAt, sshPassword }
     const file = yield* _(readShareLinksFile(projectsRoot))
     const activeLinks = file.links.filter((l) => !isExpired(l))
     yield* _(writeShareLinksFile(projectsRoot, { schemaVersion: 1, links: [...activeLinks, link] }))
     return link
-  })
+  }).pipe(shareLinksLock.withPermits(1))

Аналогично для deleteShareLink.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/services/project-share-links.ts` around lines 101 - 117,
There is a race condition in the createShareLink function where concurrent
requests can both read the same file state before either writes, causing data
loss. Create a semaphore instance (using Effect.unsafeMakeSemaphore(1)) at the
module level to serialize all read-modify-write operations on the share links
file. Wrap the file read and write operations in the createShareLink function
with the semaphore to ensure only one operation executes at a time. Apply the
same semaphore protection to the deleteShareLink function to prevent the same
race condition from occurring there.

Comment thread packages/api/src/services/ssh-password-setup.ts Outdated
Comment on lines +23 to +165
type SshTunnelRecord = {
readonly homeDir: string
process: ChildProcess | null
processClosed: boolean
hostname: string | null
stopping: boolean
stopFiber: Fiber.RuntimeFiber<void> | null
stdoutRemainder: string
stderrRemainder: string
}

const projectTunnelMap = new Map<string, SshTunnelRecord>()
const projectTunnelLock = Effect.unsafeMakeSemaphore(1)
const startWaitAttempts = 60

const sshTunnelHomeDir = (id: string): string => join("/tmp", "docker-git-project-tunnels", id)

const processEnv = (homeDir: string): Readonly<Record<string, string | undefined>> => ({
HOME: homeDir,
NO_COLOR: "1",
PATH: process.env["PATH"],
SSL_CERT_DIR: process.env["SSL_CERT_DIR"],
SSL_CERT_FILE: process.env["SSL_CERT_FILE"]
})

const readDefaultGatewayIp = (): Effect.Effect<string | null> =>
Effect.try(() => parseLinuxDefaultGatewayIp(readFileSync("/proc/net/route", "utf8"))).pipe(
Effect.orElse(() => Effect.succeed(null))
)

const defaultLocalhostHost = (): Effect.Effect<string> => {
const configured = process.env["DOCKER_GIT_PANEL_TUNNEL_LOCALHOST_HOST"]?.trim()
if (configured !== undefined && configured.length > 0) {
return Effect.succeed(configured)
}
return existsSync("/.dockerenv")
? readDefaultGatewayIp().pipe(Effect.map((ip) => ip ?? "172.17.0.1"))
: Effect.succeed("127.0.0.1")
}

const appendLog = (record: SshTunnelRecord, text: string): void => {
if (record.hostname !== null) return
const url = parseTryCloudflareUrl(text)
if (url === null) return
try {
record.hostname = new URL(url).hostname
} catch {
// ignore malformed URL
}
}

const consumeChunk = (
record: SshTunnelRecord,
stream: "stderr" | "stdout",
chunk: Buffer
): void => {
const incoming = chunk.toString("utf8").replaceAll("\r", "\n")
const withRemainder = (stream === "stdout" ? record.stdoutRemainder : record.stderrRemainder) + incoming
const lines = withRemainder.split("\n")
const tail = lines.pop() ?? ""
for (const line of lines) {
appendLog(record, line)
}
if (stream === "stdout") {
record.stdoutRemainder = tail
} else {
record.stderrRemainder = tail
}
}

const cleanupRecord = (record: SshTunnelRecord): void => {
try {
rmSync(record.homeDir, { force: true, recursive: true })
} catch {
// best effort
}
}

const waitForChildClose = (
record: SshTunnelRecord,
child: ChildProcess
): Effect.Effect<void> => {
if (record.processClosed) {
return Effect.void
}
return Effect.async((resume) => {
const alreadyExited = child.exitCode !== null || child.signalCode !== null
let completed = false
let killTimer: ReturnType<typeof setTimeout> | null = null
const complete = (): void => {
if (completed) return
completed = true
child.off("close", complete)
child.off("error", complete)
if (killTimer !== null) clearTimeout(killTimer)
resume(Effect.void)
}
child.once("close", complete)
child.once("error", complete)
if (!alreadyExited && !child.killed) {
try {
child.kill("SIGTERM")
} catch {
complete()
return
}
}
if (!alreadyExited) {
killTimer = setTimeout(() => {
try {
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL")
} catch {
complete()
}
}, 2_000)
killTimer.unref()
}
})
}

const stopRecord = (record: SshTunnelRecord): Effect.Effect<void> => {
if (record.stopFiber !== null) {
return Fiber.join(record.stopFiber).pipe(Effect.asVoid)
}
const child = record.process
record.stopping = true
const fiber = Effect.runFork(
(child === null ? Effect.void : waitForChildClose(record, child)).pipe(
Effect.tap(() => Effect.sync(() => { cleanupRecord(record) }))
)
)
record.stopFiber = fiber
return Fiber.join(fiber).pipe(Effect.asVoid)
}

const attachHandlers = (record: SshTunnelRecord, child: ChildProcess): void => {
record.process = child
record.processClosed = false
child.stdout?.on("data", (chunk: Buffer) => { consumeChunk(record, "stdout", chunk) })
child.stderr?.on("data", (chunk: Buffer) => { consumeChunk(record, "stderr", chunk) })
child.on("close", () => { record.processClosed = true })
child.on("error", () => { record.processClosed = true })
}

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 | 🔵 Trivial | ⚖️ Poor tradeoff

Значительное дублирование кода с ssh-share-link-tunnels.ts.

Файлы ssh-share-link-tunnels.ts и ssh-project-tunnels.ts идентичны на ~95%: типы (SshTunnelRecord), вспомогательные функции (processEnv, readDefaultGatewayIp, defaultLocalhostHost, appendLog, consumeChunk, cleanupRecord, waitForChildClose, stopRecord, attachHandlers, waitForHostname).

Рекомендуется вынести общую логику в модуль ssh-tunnel-core.ts, оставив в каждом файле только специфичный Map и экспортируемые функции.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/services/ssh-project-tunnels.ts` around lines 23 - 165,
Extract the shared code from ssh-project-tunnels.ts into a new
ssh-tunnel-core.ts module. Move the common SshTunnelRecord type definition and
all shared utility functions (processEnv, readDefaultGatewayIp,
defaultLocalhostHost, appendLog, consumeChunk, cleanupRecord, waitForChildClose,
stopRecord, attachHandlers, and waitForHostname) into the new core module and
export them. Then import these utilities back into ssh-project-tunnels.ts,
keeping only the projectTunnelMap, projectTunnelLock, and startWaitAttempts
constants along with the module-specific exported functions that use these
shared utilities. Apply the same refactoring to ssh-share-link-tunnels.ts to
eliminate the ~95% code duplication between the two files.

Comment on lines +386 to +396
export const TerminalPane = (props: TerminalPaneProps): JSX.Element => {
const [vsCodePanelOpen, setVsCodePanelOpen] = useState(false)
const [cfState, setCfState] = useState<CfTunnelState>({ tag: "idle" })
const runtime = resolveTerminalPaneRuntime(props)
const bodyContent = terminalBodyContent(props, runtime)

useEffect(() => {
if (!vsCodePanelOpen || runtime.browserProjectKey === undefined) return
if (cfState.tag === "idle") {
startTunnel(runtime.browserProjectKey, setCfState)
}
}, [vsCodePanelOpen, runtime.browserProjectKey, cfState.tag])

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 | 🔵 Trivial | 💤 Low value

Состояние туннеля не сбрасывается при закрытии панели.

Когда vsCodePanelOpen становится false, а затем снова true, туннель не перезапускается, потому что cfState остаётся в предыдущем состоянии (например, ready или failed). Это может быть ожидаемым поведением (кэширование), но если туннель был остановлен на сервере, UI покажет устаревший hostname.

Рассмотрите сброс cfState при закрытии панели или добавьте TTL/refresh логику.

♻️ Вариант: сброс состояния при закрытии
 const onOpenVsCode = vsCodeInfo !== null ? () => { setVsCodePanelOpen(true) } : undefined
 const vsCodeBodyContent = vsCodePanelOpen && vsCodeInfo !== null
   ? (
     <VsCodeAccessPanel
       cfState={cfState}
       info={vsCodeInfo}
-      onClose={() => { setVsCodePanelOpen(false) }}
+      onClose={() => {
+        setVsCodePanelOpen(false)
+        setCfState({ tag: "idle" })
+      }}
       onRetry={() => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/app-ready-terminal-pane.tsx` around lines 386 - 396, The
tunnel state is not being reset when the vsCodePanelOpen panel is closed,
causing stale tunnel configuration to persist when the panel is reopened. In the
useEffect hook of the TerminalPane component, add logic to reset cfState back to
its initial idle state by calling setCfState({ tag: "idle" }) when
vsCodePanelOpen becomes false. This ensures that the next time the panel is
opened, the tunnel will attempt to restart from a clean state rather than using
the cached previous state.

Comment thread packages/app/src/web/app-share-link.tsx Outdated
Comment thread packages/app/src/web/app-terminal-session-core.ts Outdated
Comment thread packages/app/src/web/panel-share.tsx Outdated
Comment thread packages/app/src/web/panel-share.tsx Outdated
skulidropek and others added 2 commits June 20, 2026 11:08
When clicking VS Code, the panel now:
- enables password auth on the container (via chpasswd)
- returns sshPassword from POST /ssh-tunnel
- displays the password with a copy button

Fixes: user was prompted for password without knowing it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds -t and "cd DIR && exec \$SHELL" so the SSH session starts
directly in the project folder instead of the default home.

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

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/src/http.ts`:
- Around line 1264-1275: The enableContainerPasswordAuth call silently fails
using Effect.orElse(() => Effect.void), but sshPassword is still returned in the
jsonResponse, causing users to receive a password that won't work since password
authentication may not be enabled on the container. Fix this by capturing the
success or failure state of enableContainerPasswordAuth - either conditionally
set sshPassword to null if the authentication enablement fails, or add a
passwordAuthEnabled boolean flag to the jsonResponse object to indicate whether
password authentication was successfully enabled. Ensure the response accurately
reflects whether the SSH password auth is actually functional on the container.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: a2ad41be-99a5-43d6-8fba-f2fe39f1d207

📥 Commits

Reviewing files that changed from the base of the PR and between 49179e6 and 85ddedb.

📒 Files selected for processing (3)
  • packages/api/src/http.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
📜 Review details
⏰ Context from checks skipped due to timeout. (8)
  • GitHub Check: Test
  • GitHub Check: E2E (OpenCode)
  • GitHub Check: E2E (Clone auto-open SSH)
  • GitHub Check: E2E (Login context)
  • GitHub Check: E2E (Clone cache)
  • GitHub Check: E2E (Runtime volumes + SSH)
  • GitHub Check: E2E (Browser command)
  • GitHub Check: Final build (windows-latest)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{sh,bash,py,js,ts,jsx,tsx,go,java,rb,php}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce command injection or unsafe shell/process execution with user-controlled input

Files:

  • packages/app/src/web/api-share-links.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
**/*.{py,js,ts,jsx,tsx,go,java,rb,php,sh,bash,c,cpp}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce path traversal or writes outside intended project/container state directories

Files:

  • packages/app/src/web/api-share-links.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
**/*.{js,ts,jsx,tsx,py,java,go,rb,php,sh,bash,yml,yaml,json,env*,toml,cfg,config,dockerfile,dockerignore}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files expose credentials, tokens, private-keys, or PII in source, generated config, logs, or CI output

Files:

  • packages/app/src/web/api-share-links.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: FUNCTIONAL CORE: Write only pure functions with immutable data and mathematical operations in core modules; no side effects, mutations, or external service calls
IMPERATIVE SHELL: Isolate all side effects (IO, network, database, environment/process) in a thin SHELL layer; CORE never calls SHELL, only SHELL → CORE
Never use any type annotation in TypeScript; use unknown only at SHELL boundaries for decoding, never export unknown outside boundary modules
Never use as type assertions in normal code; only permit as in a single 'axiomatic' module (brands, constructors, constants) after which types flow safely without casts
Always use exhaustive pattern matching for union types through .exhaustive() or Match.exhaustive() from effect-ts; never use switch statements or unhandled type branches
Use Effect<Success, Error, Requirements> monad from effect-ts for all effects; compose through pipe() and Effect.flatMap(); never use async/await, raw Promise chains (then/catch), or Promise.all in product code
Interoperate with Promise/exceptions only in SHELL through Effect.try/Effect.tryPromise with typed error mapping; never leave raw exceptions or untyped errors in the domain
Use Effect.acquireRelease + Effect.scoped for resource management with guaranteed finalization; never manage resources with try/finally or manual cleanup
All external services (database, HTTP, environment) must be accessed through Effect-based interfaces and Layer-based dependency injection; never call external APIs directly
Provide comprehensive TSDoc comments with mathematical notation: @pure, @effect, @invariant, @precondition, @postcondition, @complexity, @throws, and CHANGE/WHY/REF/SOURCE/FORMAT THEOREM functional comment markers
No console.*, process direct calls, or untyped environment access in product code; all such operations must be abstracted through Layer-based services in SHELL
Boundary data from external sources (HTTP, database, environment) must be decoded/valida...

Files:

  • packages/app/src/web/api-share-links.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
**/{browser*,server*,app*,*.ts,*.js}

📄 CodeRabbit inference engine (README.md)

Web version must listen on 0.0.0.0 by default for accessibility across LAN devices

Files:

  • packages/app/src/web/api-share-links.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
**/{cli*,command*,auto*,*.ts,*.tsx}

📄 CodeRabbit inference engine (README.md)

Implement auto-mode agent selection logic to choose Claude, Codex, Gemini, or Grok randomly from available authorized providers, or allow forced selection with --auto=

Files:

  • packages/app/src/web/api-share-links.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
**/*

⚙️ CodeRabbit configuration file

**/*: Ты строгий ревьюер SPEC DRIVEN DEVELOPMENT.

Перед выводами изучи README.md, другие *.md файлы, linked issues,
PR description, PR comments/discussion и релевантную кодовую базу.

Сверь изменения с исходным ТЗ/спекой и обсуждением. Флагай любой уход
от спеки, недокументированное изменение поведения, отсутствие тестов
для заявленного поведения и security-риск. Если спека не видна,
попроси автора добавить ее в issue или PR description.

Проверь решение с точки зрения формальной верификации: какие инварианты,
предусловия и постусловия можно доказать математически, а где доказуемость
слабая. Оцени решение с точки зрения теории игр: устойчивы ли стимулы,
нет ли выгодного обхода правил, и какое решение было бы сильнее.

Files:

  • packages/app/src/web/api-share-links.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
**

⚙️ CodeRabbit configuration file

**: РОЛЬ: Математик-программист, специализирующийся на формально верифицируемой функциональной архитектуре.

ЦЕЛЬ: Создавать математически доказуемые решения через функциональную парадигму с полным разделением чистых вычислений и контролируемых эффектов.

МОДЕЛЬ РАССУЖДЕНИЯ:

  • Не выдавать “личные мнения”. Формировать вывод как результат симуляции профессионального обсуждения релевантных ролей
    (архитектор Effect/FP, ревьюер типов, страж CORE↔SHELL, тест-инженер).
  • Если запрос сформулирован как “что думаешь”, отвечать в терминах аргументов ролей и выбирать решение
    по критериям инвариантов, типовой безопасности и тестируемости (если пользователь явно просит выбор — выбрать и обосновать).

ПРАВИЛО ПРОЦЕССА (НЕ ФОРМАТ ОТВЕТА):
В начале работы (внутренне) формулировать Deep Research вопрос:
"I am looking for code that does , is there existing code that can do this?"
Далее:

  • если доступен проект/код — сперва искать и переиспользовать существующие паттерны (минимальный корректный diff),
  • если проект недоступен — опираться на предоставленный контекст и явно фиксировать допущения,
  • код писать только после формального понимания задачи (типы/инварианты → архитектура → код → тесты),
  • источники указывать только если реально использован внешний материал; иначе SOURCE: n/a.

ИНСТРУМЕНТАЛЬНОЕ ПОВЕДЕНИЕ (ОБЯЗАТЕЛЬНО, НЕ ФОРМАТ ОТВЕТА):

  • Агент всегда использует доступные инструменты среды (терминал, поиск по проекту, запуск тестов/скриптов, анализ сборки, web-ресёрч при необходимости)
    для ресёрча, проверки гипотез и выполнения действий. Приоритет: проверяемость, воспроизводимость, минимальный риск.
  • Агент не предлагает “гайд” как замену действия. Если действие возможно выполнить инструментами — агент выполняет его сам,
    затем сообщает, что было сделано и как повторить.
  • Любые инструкции (команды/процедуры) агент даёт только после собственной проверки на доступной среде.
    Если проверить невозможно — явно фиксирует ограничение и перечисляе...

Files:

  • packages/app/src/web/api-share-links.ts
  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
🔇 Additional comments (2)
packages/app/src/web/api-share-links.ts (1)

75-87: LGTM!

packages/app/src/web/app-ready-terminal-pane.tsx (1)

164-168: LGTM!

Also applies to: 216-218, 266-269, 378-382

Comment thread packages/api/src/http.ts Outdated
skulidropek and others added 9 commits June 20, 2026 12:07
Panel now shows a host entry with the actual hostname so it can be
copy-pasted directly into ~/.ssh/config without editing. SSH command
is simplified (no inline -o ProxyCommand since config handles it).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds RemoteCommand and RequestTTY yes to the host SSH config entry
so plain `ssh dev@HOST` opens a shell directly in the project folder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- API: don't return hostname of dead cloudflared process (check processClosed)
- Frontend: poll every 30s when panel is open, auto-restart if tunnel died
- Frontend: add ↻ refresh button in ready state for manual restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: every POST /ssh-tunnel call regenerated a new password and
set it on the container. Polling every 30s was invalidating the password
shown in the panel while the old hostname stayed cached in the frontend.

Fix: password is generated once per fresh tunnel start, stored in the
SshTunnelRecord, and returned consistently while the tunnel is alive.
A new password is only generated when the old tunnel actually dies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show self-contained command that works without ~/.ssh/config setup:
ssh -o "ProxyCommand=cloudflared access ssh --hostname %h" -t dev@HOST "cd /path && exec $SHELL"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ig RemoteCommand

Windows SSH errors "Cannot execute command-line and remote command" when
both a RemoteCommand in ~/.ssh/config and a command argument are present.
Inline command shows only the ProxyCommand option; the config block handles
RemoteCommand for directory switching.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e SSH

RemoteCommand intercepts VS Code's own installer and server-start commands,
causing "Cannot execute command-line and remote command" and failing to
parse the remote port. VS Code needs a clean shell on connect to run
vscode-server. The folder is already handled via the VS Code URI (folder=).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sername

Without User directive VS Code Remote SSH falls back to the local OS username.
Added User ${sshUser} so the config specifies dev@ explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows IP:port SSH config without cloudflared — one port per container.
Host IP taken from window.location.hostname, port from project.sshPort.

- SSH config: Host IP-ssh / HostName IP / Port N / User dev
- Command: ssh -p PORT dev@IP
- VS Code URI using config alias (no cloudflared needed)

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

@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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/src/services/ssh-project-tunnels.ts`:
- Around line 196-200: Add comprehensive TSDoc documentation to the
startSshProjectTunnel function that describes the updated function signature and
behavior. The documentation should include all required annotations: `@pure` to
indicate if the function has side effects, `@effect` to describe the Effect type,
`@invariant` to document any invariants maintained, `@precondition` to specify
requirements for the inputs (projectKey, sshPort, containerName), `@postcondition`
to document the guarantees on the output object with hostname and sshPassword
properties, and `@complexity` to indicate the computational complexity of the
operation. Ensure the JSDoc block clearly documents the newly added
containerName parameter and the structured return type.
- Around line 211-212: The error handling in the password authentication setup
is silently suppressing failures when enableContainerPasswordAuth fails, which
allows the function to return a non-working sshPassword to the client anyway.
Remove the `.pipe(Effect.orElse(() => Effect.void))` error suppression from the
enableContainerPasswordAuth call to allow errors to propagate and prevent
returning an invalid password, or alternatively implement proper error handling
that sets sshPassword to an empty string when enableContainerPasswordAuth fails
so the client receives an indication that password authentication is not
available rather than receiving a non-functional password.

In `@packages/app/src/web/app-ready-terminal-pane.tsx`:
- Around line 289-308: The "Direct SSH (local network)" section displays
connection instructions and configuration but is missing the SSH password
display, forcing users to scroll up to the Cloudflare section to find the
password. Add a password display block in this Direct SSH section (after the
"Add to ~/.ssh/config" code block and before the "Connect via SSH" section) that
shows the SSH password value with appropriate styling and labels, similar to how
password information is presented in other sections of the component.
- Around line 429-447: The useEffect polling logic in the
app-ready-terminal-pane component has a race condition where setCfState calls
from outdated Effect.runPromise executions can overwrite newer state after the
panel is toggled. Introduce an AbortController that gets created when the effect
runs and passed to startProjectSshTunnel, then call abort() in the cleanup
function (the return statement) to cancel in-flight requests when
vsCodePanelOpen changes or the effect unmounts. Alternatively, use a mounted
flag variable that gets set to false in the cleanup function and check this flag
before any setCfState calls to prevent state updates after the effect has been
cleaned up.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: c5cc4342-9c52-4cc0-85ea-bd5bbb86f94b

📥 Commits

Reviewing files that changed from the base of the PR and between 85ddedb and 658a11e.

📒 Files selected for processing (3)
  • packages/api/src/http.ts
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{sh,bash,py,js,ts,jsx,tsx,go,java,rb,php}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce command injection or unsafe shell/process execution with user-controlled input

Files:

  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/*.{py,js,ts,jsx,tsx,go,java,rb,php,sh,bash,c,cpp}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce path traversal or writes outside intended project/container state directories

Files:

  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/*.{js,ts,jsx,tsx,py,java,go,rb,php,sh,bash,yml,yaml,json,env*,toml,cfg,config,dockerfile,dockerignore}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files expose credentials, tokens, private-keys, or PII in source, generated config, logs, or CI output

Files:

  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: FUNCTIONAL CORE: Write only pure functions with immutable data and mathematical operations in core modules; no side effects, mutations, or external service calls
IMPERATIVE SHELL: Isolate all side effects (IO, network, database, environment/process) in a thin SHELL layer; CORE never calls SHELL, only SHELL → CORE
Never use any type annotation in TypeScript; use unknown only at SHELL boundaries for decoding, never export unknown outside boundary modules
Never use as type assertions in normal code; only permit as in a single 'axiomatic' module (brands, constructors, constants) after which types flow safely without casts
Always use exhaustive pattern matching for union types through .exhaustive() or Match.exhaustive() from effect-ts; never use switch statements or unhandled type branches
Use Effect<Success, Error, Requirements> monad from effect-ts for all effects; compose through pipe() and Effect.flatMap(); never use async/await, raw Promise chains (then/catch), or Promise.all in product code
Interoperate with Promise/exceptions only in SHELL through Effect.try/Effect.tryPromise with typed error mapping; never leave raw exceptions or untyped errors in the domain
Use Effect.acquireRelease + Effect.scoped for resource management with guaranteed finalization; never manage resources with try/finally or manual cleanup
All external services (database, HTTP, environment) must be accessed through Effect-based interfaces and Layer-based dependency injection; never call external APIs directly
Provide comprehensive TSDoc comments with mathematical notation: @pure, @effect, @invariant, @precondition, @postcondition, @complexity, @throws, and CHANGE/WHY/REF/SOURCE/FORMAT THEOREM functional comment markers
No console.*, process direct calls, or untyped environment access in product code; all such operations must be abstracted through Layer-based services in SHELL
Boundary data from external sources (HTTP, database, environment) must be decoded/valida...

Files:

  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/{browser*,server*,app*,*.ts,*.js}

📄 CodeRabbit inference engine (README.md)

Web version must listen on 0.0.0.0 by default for accessibility across LAN devices

Files:

  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/{cli*,command*,auto*,*.ts,*.tsx}

📄 CodeRabbit inference engine (README.md)

Implement auto-mode agent selection logic to choose Claude, Codex, Gemini, or Grok randomly from available authorized providers, or allow forced selection with --auto=

Files:

  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/*

⚙️ CodeRabbit configuration file

**/*: Ты строгий ревьюер SPEC DRIVEN DEVELOPMENT.

Перед выводами изучи README.md, другие *.md файлы, linked issues,
PR description, PR comments/discussion и релевантную кодовую базу.

Сверь изменения с исходным ТЗ/спекой и обсуждением. Флагай любой уход
от спеки, недокументированное изменение поведения, отсутствие тестов
для заявленного поведения и security-риск. Если спека не видна,
попроси автора добавить ее в issue или PR description.

Проверь решение с точки зрения формальной верификации: какие инварианты,
предусловия и постусловия можно доказать математически, а где доказуемость
слабая. Оцени решение с точки зрения теории игр: устойчивы ли стимулы,
нет ли выгодного обхода правил, и какое решение было бы сильнее.

Files:

  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**

⚙️ CodeRabbit configuration file

**: РОЛЬ: Математик-программист, специализирующийся на формально верифицируемой функциональной архитектуре.

ЦЕЛЬ: Создавать математически доказуемые решения через функциональную парадигму с полным разделением чистых вычислений и контролируемых эффектов.

МОДЕЛЬ РАССУЖДЕНИЯ:

  • Не выдавать “личные мнения”. Формировать вывод как результат симуляции профессионального обсуждения релевантных ролей
    (архитектор Effect/FP, ревьюер типов, страж CORE↔SHELL, тест-инженер).
  • Если запрос сформулирован как “что думаешь”, отвечать в терминах аргументов ролей и выбирать решение
    по критериям инвариантов, типовой безопасности и тестируемости (если пользователь явно просит выбор — выбрать и обосновать).

ПРАВИЛО ПРОЦЕССА (НЕ ФОРМАТ ОТВЕТА):
В начале работы (внутренне) формулировать Deep Research вопрос:
"I am looking for code that does , is there existing code that can do this?"
Далее:

  • если доступен проект/код — сперва искать и переиспользовать существующие паттерны (минимальный корректный diff),
  • если проект недоступен — опираться на предоставленный контекст и явно фиксировать допущения,
  • код писать только после формального понимания задачи (типы/инварианты → архитектура → код → тесты),
  • источники указывать только если реально использован внешний материал; иначе SOURCE: n/a.

ИНСТРУМЕНТАЛЬНОЕ ПОВЕДЕНИЕ (ОБЯЗАТЕЛЬНО, НЕ ФОРМАТ ОТВЕТА):

  • Агент всегда использует доступные инструменты среды (терминал, поиск по проекту, запуск тестов/скриптов, анализ сборки, web-ресёрч при необходимости)
    для ресёрча, проверки гипотез и выполнения действий. Приоритет: проверяемость, воспроизводимость, минимальный риск.
  • Агент не предлагает “гайд” как замену действия. Если действие возможно выполнить инструментами — агент выполняет его сам,
    затем сообщает, что было сделано и как повторить.
  • Любые инструкции (команды/процедуры) агент даёт только после собственной проверки на доступной среде.
    Если проверить невозможно — явно фиксирует ограничение и перечисляе...

Files:

  • packages/api/src/http.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
🧠 Learnings (1)
📚 Learning: 2026-05-18T07:43:38.131Z
Learnt from: skulidropek
Repo: ProverCoderAI/docker-git PR: 313
File: packages/api/src/services/panel-cloudflare-tunnel.ts:123-135
Timestamp: 2026-05-18T07:43:38.131Z
Learning: In this repo’s ProverCoderAI/docker-git architecture, it is intentional for service modules under packages/api/src/services/ to call Node.js APIs directly (e.g., fs, child_process, fetch). Do not treat direct Node API usage in these service modules as a dependency-injection (DI) violation. This codebase provides the NodeContext at the program boundary (entry point) rather than injecting Context.Tag/Layer services into individual service modules; reviewers should only flag DI/context issues if the expected boundary setup is missing.

Applied to files:

  • packages/api/src/services/ssh-project-tunnels.ts
🔇 Additional comments (3)
packages/api/src/http.ts (1)

1264-1270: LGTM!

packages/app/src/web/app-ready-terminal-pane.tsx (2)

154-163: LGTM!

Also applies to: 171-175, 219-230


417-428: LGTM!

Also applies to: 449-476

Comment on lines +196 to +200
export const startSshProjectTunnel = (
projectKey: string,
sshPort: number,
containerName: string
): Effect.Effect<{ hostname: string | null; sshPassword: string }, ApiInternalError> =>

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 | 🔵 Trivial | 💤 Low value

Отсутствует функциональная документация для изменённой сигнатуры.

Функция startSshProjectTunnel изменила контракт: добавлен параметр containerName, возвращаемый тип расширен до { hostname, sshPassword }. Согласно coding guidelines, требуется TSDoc с @pure, @effect, @invariant, @precondition, @postcondition, @complexity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/services/ssh-project-tunnels.ts` around lines 196 - 200, Add
comprehensive TSDoc documentation to the startSshProjectTunnel function that
describes the updated function signature and behavior. The documentation should
include all required annotations: `@pure` to indicate if the function has side
effects, `@effect` to describe the Effect type, `@invariant` to document any
invariants maintained, `@precondition` to specify requirements for the inputs
(projectKey, sshPort, containerName), `@postcondition` to document the guarantees
on the output object with hostname and sshPassword properties, and `@complexity`
to indicate the computational complexity of the operation. Ensure the JSDoc
block clearly documents the newly added containerName parameter and the
structured return type.

Source: Coding guidelines

Comment thread packages/api/src/services/ssh-project-tunnels.ts Outdated
Comment thread packages/app/src/web/app-ready-terminal-pane.tsx Outdated
Comment thread packages/app/src/web/app-ready-terminal-pane.tsx Outdated
skulidropek and others added 2 commits June 20, 2026 13:17
- directCommand includes -t "cd /home/dev/app && exec $SHELL"
- SSH password shown in Direct SSH section (shared with CF tunnel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- contracts.ts: add sshPassword: string | null to ShareLinkInfo type
- ssh-project-tunnels.ts: exit waitForHostname early when processClosed; propagate enableContainerPasswordAuth failure
- ssh-password-setup.ts: pass password via SSHPW env var instead of shell interpolation to prevent injection
- app-ready-terminal-pane.tsx: add cancelled flag to polling useEffect to prevent stale setState
- panel-share.tsx: replace eslint-disable with useCallback + requestId guard for stale request prevention
- app-share-link.tsx: use RegExp.exec() instead of String.match(); extract nested template literal; async copyText

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@skulidropek

Copy link
Copy Markdown
Member Author

Agent Plan Update

Branch: issue-428 at 9132fdc.

1. Planning Decision

Source: claude - Captured: 2026-06-19T21:48:33Z

Questions:

  • Обновить план и приступить к реализации?

Answer:
да давай. ПУсть принимает ссылку на любой IP который стоит у пользователя в URL. И через него проксирует в VS Code

2. Plan

Source: claude - Captured: 2026-06-19T22:15:30Z

Plan: URL-Based Container Access Sharing (Issue #428)

Context

Пользователь хочет генерировать share-ссылки для контейнеров вида:
http://192.168.0.206:4174/ssh/1f75cb338acf?t=253d7ca9abcdef01

Открыв ссылку в браузере, получаешь web-терминал к контейнеру + SSH-конфиг для VS Code Remote SSH.

Ключевые находки из кодовой базы:

  • /ssh/ роутинг уже существует в app-ready-ssh-link-core.ts, но без токен-авторизации — ?t= сейчас трактуется как terminal ID (UUID-формат). Share-токены (16 hex chars, no dashes) легко отличимы от UUID по формату.
  • buildEditorSshAccess() в packages/lib/src/usecases/ssh-access.ts уже генерирует SSH-конфиг сниппет — переиспользуем.
  • WebAppRoute сейчас имеет только { tag: "Dashboard" } — надо добавить ShareLink вариант.
  • resolveWebAppRoute в app-terminal-session-core.ts всегда возвращает Dashboard — расширяем.
  • app.tsx:229 — единственное Match.when — добавляем второй arm.
  • panel-share.tsx — уже есть Share-панель (Cloudflare tunnel) — добавим секцию project share links.
  • Аутентификации на API-роутах нет — токен обеспечивает "security through obscurity" (как везде).

URL-формат и разграничение токенов

Формат ?t= Интерпретация
16 hex chars, no dashes (253d7ca9abcdef01) Share-токен → ShareLink route
UUID (550e8400-e29b-41d4-...) или иное Terminal ID → существующее поведение

Функция детекции: /^[0-9a-f]{16}$/u.test(t)


Шаги реализации

1. Новый backend-сервис packages/api/src/services/project-share-links.ts

Хранит токены в памяти (Map) + персистирует в <projectDir>/.orch/state/share-links.json.

type ShareLink = {
  readonly token=[REDACTED]       // 16 hex chars (randomBytes(8).toString("hex"))
  readonly projectKey: string
  readonly projectDir: string
  readonly hostIp: string      // из Host header при создании (для SSH-конфига)
  readonly createdAt: string   // ISO
  readonly expiresAt: string   // ISO (default TTL: 7 дней)
}

// Функции:
createShareLink(projectDir, projectKey, hostIp, ttlMs?)
   Effect<ShareLink, ApiInternalError, FileSystem>

resolveShareLink(token)
   Effect<ShareLink | null, never, FileSystem>

deleteShareLink(projectDir, token)
   Effect<void, ApiNotFoundError | ApiInternalError, FileSystem>

listShareLinks(projectDir)
   Effect<ReadonlyArray<ShareLink>, never, FileSystem>

Паттерн хранения: следовать terminal-sessions.ts (fs.existsreadFileString → parse → write).
Токен: randomBytes(8).toString("hex") из node:crypto.


2. Контракты и схемы

packages/api/src/api/contracts.ts — добавить:

type ShareLinkInfo = {
  token, projectKey, projectDir, displayName,
  sshAlias, sshConfigSnippet, workspacePath,
  hostIp, createdAt, expiresAt
}
type CreateShareLinkRequest = { ttlMs?: number }
type CreateShareLinkResponse = { ok: true; link: ShareLinkInfo; url: string }

packages/api/src/api/schema.ts — добавить CreateShareLinkRequestSchema, ShareLinkInfoSchema.


3. HTTP-роуты в packages/api/src/http.ts

Добавить 4 роута:

POST   /projects/by-key/:projectKey/share-links
  → читает Host-header для hostIp
  → createShareLink()
  → buildEditorSshAccess(item, sshKeyPath, hostIp) для configSnippet
  → возвращает { ok, link, url }

GET    /projects/by-key/:projectKey/share-links
  → listShareLinks(projectDir)
  → возвращает { links }

DELETE /projects/by-key/:projectKey/share-links/:token
  → deleteShareLink()
  → возвращает { ok }

GET    /share-links/:token                           ← публичный, токен = авторизация
  → resolveShareLink(token) → 404 если null/expired
  → getProject → buildEditorSshAccess
  → возвращает ShareLinkInfo

Реиспользовать buildEditorSshAccess из packages/lib/src/usecases/ssh-access.ts.
url конструируется из Host-header аналогично resolvePortPublicHost.


4. OpenAPI

packages/openapi/src/openapi-paths.ts — добавить 3 пути и их операции.
packages/api/src/api/openapi.ts — зарегистрировать в buildDockerGitOpenApi().


5. Frontend: новый роут в app-terminal-session-core.ts

export type WebAppRoute =
  | { readonly tag: "Dashboard" }
  | { readonly tag: "ShareLink"; readonly projectKey: string; readonly shareToken=[REDACTED] }

const isShareToken=[REDACTED] string): boolean => /^[0-9a-f]{16}$/u.test(value)

export const resolveWebAppRoute = (pathname: string): WebAppRoute => {
  if (pathname.startsWith("/ssh/")) {
    const projectKey = decodeURIComponent(pathname.slice("/ssh/".length).split("/")[0] ?? "").trim()
    const t = new URLSearchParams(location.search).get("t") ?? ""
    if (projectKey.length > 0 && isShareToken(t)) {
      return { tag: "ShareLink", projectKey, shareToken=[REDACTED] }
    }
  }
  return { tag: "Dashboard" }
}

6. Frontend: новая страница packages/app/src/web/app-share-link.tsx

Standalone компонент (без полного dashboard). Состояние:

type ShareLinkState =
  | { _tag: "Loading" }
  | { _tag: "TokenError"; message: string }
  | { _tag: "Connecting"; info: ShareLinkInfo }
  | { _tag: "Ready"; info: ShareLinkInfo; sessionId: string }
  | { _tag: "Error"; message: string }

Поток:

  1. Mount → GET /api/share-links/:token?host=<window.location.hostname> (public endpoint)
  2. Успех → показать SSH-конфиг + кнопки VS Code
  3. POST /api/projects/by-key/:projectKey/terminal-sessions/start → создать сессию
  4. WebSocket к /api/projects/by-key/:projectKey/terminal-sessions/:sessionId/ws → терминал

Динамический IP из URL браузера:
Фронт передаёт window.location.hostname и window.location.port в запрос как ?host=:

const apiUrl = `/api/share-links/${token}?host=${encodeURIComponent(window.location.hostname)}`

Бэкенд использует этот host вместо Host-header для генерации SSH-конфига.

SSH-конфиг: два варианта

Бэкенд возвращает ShareLinkInfo с двумя снипетами:

type ShareLinkInfo = {
  // ... остальные поля
  sshConfigSnippet: string          // Host через clientHost (LAN/прямой доступ)
  cfSshConfigSnippet: string | null // Host через CF tunnel (если туннель активен)
  sshAlias: string
  workspacePath: string
}

Генерация на бэкенде в роуте GET /share-links/:token:

// Прямой вариант: clientHost = req query ?host= (или Host header fallback)
const directAccess = buildEditorSshAccess(
  template,
  sshKeyPath,
  clientHost   // host из query param / Host header
)

// CF-вариант: если cloudflare tunnel активен и есть publicUrl
const cfSnippet = cfPublicUrl
  ? buildCfSshConfigSnippet(template, cfPublicUrl, sshKeyPath)
  : null

buildCfSshConfigSnippet (новая чистая функция в ssh-access.ts):

// Генерирует SSH-конфиг с ProxyCommand cloudflared
const buildCfSshConfigSnippet = (config, cfHostname, sshKeyPath) => [
  `Host ${alias}-cf`,
  `  HostName ${cfHostname}`,
  `  User ${config.sshUser}`,
  `  Port 22`,
  `  ProxyCommand cloudflared access ssh --hostname %h`,
  sshKeyPath ? `  IdentityFile ${sshKeyPath}` : null,
  sshKeyPath ? `  IdentitiesOnly yes` : null,
].filter(Boolean).join("\n")

VS Code URI кнопки (генерируются на фронте):

// Прямой
`vscode://ms-vscode-remote.remote-ssh/open?ssh=${sshAlias}&folder=${workspacePath}`
// Через CF tunnel
`vscode://ms-vscode-remote.remote-ssh/open?ssh=${sshAlias}-cf&folder=${workspacePath}`

Share-страница показывает оба варианта если CF активен, иначе только прямой.


7. packages/app/src/web/app.tsx

Добавить Match.when arm:

Match.when({ tag: "ShareLink" }, ({ projectKey, shareToken }) => (
  <AppShareLink projectKey={projectKey} shareToken=[REDACTED] viewport={viewport} />
)),

8. packages/app/src/web/panel-share.tsx

Добавить секцию "Container share links" (ниже Cloudflare tunnel):

  • Кнопка "Generate link" → POST /api/projects/by-key/:projectKey/share-links
  • Показать URL с кнопкой copy
  • Список активных токенов с датой истечения и кнопкой Revoke

Затрагиваемые файлы

Файл Действие
packages/api/src/services/project-share-links.ts Создать
packages/api/src/api/contracts.ts Добавить типы
packages/api/src/api/schema.ts Добавить схемы
packages/api/src/http.ts Добавить 4 роута
packages/api/src/api/openapi.ts Зарегистрировать
packages/openapi/src/openapi-paths.ts Добавить пути
packages/app/src/web/app-terminal-session-core.ts Расширить WebAppRoute
packages/app/src/web/app-share-link.tsx Создать
packages/app/src/web/app.tsx Добавить Match.when
packages/app/src/web/panel-share.tsx Добавить секцию

Верификация

  1. curl -X POST http://localhost:3334/projects/by-key/<key>/share-links{ok, link, url} с 16-char hex token
  2. Открыть URL в браузере → рендерится AppShareLink (не дашборд)
  3. Компонент показывает SSH-конфиг и терминал
  4. "Open in VS Code" кнопка генерирует vscode://ms-vscode-remote.remote-ssh/... URI
  5. curl /share-links/invalidtoken → 404
  6. curl -X DELETE .../share-links/<token> → последующий GET → 404
  7. ?t= как UUID-формат → старое поведение (Dashboard), не ShareLink

3. Plan

Source: claude - Captured: 2026-06-20T09:20:48Z

Plan: Add Claude Button to Terminal Session Header

Context

User was on the panel terminal screen (with buttons "Open browser", "Skiller", "Apply", "Task manager", "New terminal", "Images on", "Detach", "Kill") and asked to add a button that opens claude.ai. I incorrectly added the button to the share link page instead. This plan corrects that.

Changes

1. Add Claude button to terminal header

File: packages/terminal/src/web/panel-terminal-header.tsx

In TerminalHeaderActions, add a TerminalActionButton that opens https://claude.ai/new in a new tab. Place it before "Detach" (after project action buttons and images toggle).

The button needs no prop — the URL is hardcoded, so the handler is inline:

<TerminalActionButton
  compactTypingMode={props.compactHeaderMode}
  onClick={() => { window.open("https://claude.ai/new", "_blank", "noopener noreferrer") }}
>
  Claude
</TerminalActionButton>

2. Revert Claude link from share link page

File: packages/app/src/web/app-share-link.tsx

Remove the claudeLinkStyle const and the <a href="https://claude.ai/new" ...>Claude</a> anchor that was added to InfoHeader. Keep the info removal fix in TerminalView (it was a valid unused-variable fix).

3. Rebuild and redeploy

After both changes:

cd packages/app && bun run build:web
docker cp packages/app/dist-web/. dg-docker-git-issue-428:/home/dev/app/packages/app/dist-web/

Verification

Open https://holidays-albums-harry-butler.trycloudflare.com → navigate to any running container terminal → confirm "Claude" button appears in the header row next to "Images on", "Detach", "Kill" → click it → claude.ai/new opens in new tab.

4. Plan

Source: claude - Captured: 2026-06-20T10:08:59Z

Plan: Per-Container CF Tunnel for VS Code Panel

Context

The VS Code panel currently shows localhost SSH config, which is useless for users accessing the panel from outside the host machine via a Cloudflare tunnel. localhost refers to the server, not the user's machine.

Goal: when the user clicks "VS Code" on any container's terminal header, automatically start a dedicated Cloudflare SSH tunnel for that container and show only the CF tunnel command — no localhost config.

Target command format:

ssh -o "ProxyCommand=cloudflared access ssh --hostname %h" dev@HOSTNAME.trycloudflare.com

Changes

1. New service: packages/api/src/services/ssh-project-tunnels.ts

Copy structure from packages/api/src/services/ssh-share-link-tunnels.ts but key by projectKey instead of share-link token.

Exports:

startSshProjectTunnel(projectKey: string, sshPort: number): Effect<string | null, ApiInternalError>
stopSshProjectTunnel(projectKey: string): Effect<void>
getSshProjectTunnelHostname(projectKey: string): string | null

Uses a separate projectTunnelMap + projectTunnelLock (no conflict with share-link maps).
Tmp dir: /tmp/docker-git-project-tunnels/{uuid} (different from /tmp/docker-git-ssh-tunnels/).
startSshProjectTunnel is idempotent — returns existing hostname if tunnel already running.

2. New API route in packages/api/src/http.ts

Add near existing share-link routes (~line 1175):

POST /projects/by-key/:projectKey/ssh-tunnel
  • Validates projectKey, gets project.sshPort
  • Calls startSshProjectTunnel(projectKey, project.sshPort)
  • Blocks up to ~15 s for hostname (same as share-link tunnel)
  • Returns { hostname: string | null }

3. New API call in packages/app/src/web/api.ts

export const startProjectSshTunnel = (
  projectKey: string
): Effect.Effect<{ readonly hostname: string | null }, string>

Uses requestJson("POST", /projects/by-key/${projectKey}/ssh-tunnel, Schema.Struct({ hostname: Schema.NullOr(Schema.String) })).

4. Rework VS Code panel in packages/app/src/web/app-ready-terminal-pane.tsx

VsCodeAccessInfo type — CF-only, remove localhost fields:

type VsCodeAccessInfo = {
  readonly sshUser: string
  readonly targetDir: string
  readonly cfSshCommand: string | null
  readonly cfVscodeUri: string | null
}

TerminalPane — add loading state:

const [cfState, setCfState] = useState<
  | { tag: "idle" }
  | { tag: "loading" }
  | { tag: "ready"; hostname: string }
  | { tag: "failed" }
>({ tag: "idle" })

When vsCodePanelOpen becomes true and browserProjectKey !== undefined:

  • Set cfState → loading immediately
  • Call startProjectSshTunnel(browserProjectKey)
  • On success with hostname: → ready
  • On success with null / on failure: → failed

CF command built from ready state:

ssh -o "ProxyCommand=cloudflared access ssh --hostname %h" dev@HOSTNAME

CF VS Code URI:

vscode://ms-vscode-remote.remote-ssh/open?hostName=sshUser@HOSTNAME&folder=targetDir

VsCodeAccessPanel — show only CF content:

  1. Loading: "Starting Cloudflare tunnel…"
  2. Ready:
    • One-time wildcard setup block (reuse WILDCARD_SSH_CONFIG constant from app-share-link.tsx):
      Host *.trycloudflare.com
        ProxyCommand cloudflared access ssh --hostname %h
        StrictHostKeyChecking no
        UserKnownHostsFile /dev/null
      
    • CF SSH command + copy button
    • "Open in VS Code (CF tunnel)" link
  3. Failed: "Tunnel failed to start" + "Retry" button (calls start again)

Remove all localhost SSH config, ssh alias, and direct VS Code URI from the panel.


Files Modified

File Change
packages/api/src/services/ssh-project-tunnels.ts New — per-project CF SSH tunnel service
packages/api/src/http.ts Add POST /projects/by-key/:key/ssh-tunnel route
packages/app/src/web/api.ts Add startProjectSshTunnel function
packages/app/src/web/app-ready-terminal-pane.tsx CF-only VS Code panel, loading/ready/failed states

Verification

  1. cd packages/api && bun run build → no TS errors
  2. cd packages/app && bun run build:web → no TS errors
  3. docker cp packages/app/dist-web/. dg-docker-git-issue-428:/home/dev/app/packages/app/dist-web/
  4. Open panel → open any running container terminal → click "VS Code"
  5. Panel briefly shows "Starting Cloudflare tunnel…"
  6. Then shows:
    • Wildcard SSH config block (one-time setup)
    • ssh -o "ProxyCommand=..." dev@XXXX.trycloudflare.com with copy button
    • "Open in VS Code (CF tunnel)" link
    • No localhost config anywhere
  7. Copy SSH command → run on local machine with cloudflared installed → connects to container

5. Plan

Source: claude - Captured: 2026-06-20T09:17:00.963Z

Plan: Restore inline ProxyCommand in SSH command

Context

The "Connect via SSH" command was simplified to ssh dev@HOST after the SSH config
block was added. But this requires the user to first paste the config into ~/.ssh/config.
The user wants the command to be self-contained with inline -o ProxyCommand so it
works immediately without any config setup.

Change

File: packages/app/src/web/app-ready-terminal-pane.tsx

One line — restore the full inline command in cfSshCommand:

// Before:
const cfSshCommand = cfState.tag === "ready"
  ? `ssh ${info.sshUser}@${cfState.hostname}`
  : null

// After:
const cfSshCommand = cfState.tag === "ready"
  ? `ssh -o "ProxyCommand=cloudflared access ssh --hostname %h" -t ${info.sshUser}@${cfState.hostname} "cd ${info.targetDir} && exec \\$SHELL"`
  : null

The SSH config block (Add to ~/.ssh/config) stays unchanged — it remains useful for
VS Code Remote SSH and repeated connections. The "Connect via SSH" command just becomes
self-contained again.

Verification

  1. bun run build:web — no TS errors
  2. Deploy to host dist-web
  3. Open panel → click VS Code → verify "Connect via SSH" shows:
    ssh -o "ProxyCommand=cloudflared access ssh --hostname %h" -t dev@HOST "cd /home/dev/app && exec $SHELL"

6. Plan

Source: claude - Captured: 2026-06-20T09:17:00.963Z

Plan: Remove RemoteCommand from SSH config — breaks VS Code Remote SSH

Context

VS Code Remote SSH fails with "Cannot execute command-line and remote command" and
"Failed to parse remote port from server output". Root cause: RemoteCommand in
~/.ssh/config intercepts VS Code's own installer/server-start commands. VS Code
needs to run arbitrary shell commands on connect; RemoteCommand hijacks that.

Plain SSH works fine because the terminal just runs the RemoteCommand as intended.
VS Code Remote SSH does not — it needs a clean shell to run vscode-server.

The folder target is already handled by the VS Code URI:
vscode://ms-vscode-remote.remote-ssh/open?hostName=...&folder=/home/dev/app
So RemoteCommand is redundant for VS Code use.

Change

File: packages/app/src/web/app-ready-terminal-pane.tsx

hostSshConfig at line 170-171 — remove RemoteCommand and RequestTTY yes:

// Before:
const hostSshConfig = (hostname: string, targetDir: string): string =>
  `Host ${hostname}\n  ProxyCommand cloudflared access ssh --hostname %h\n  StrictHostKeyChecking no\n  UserKnownHostsFile /dev/null\n  RemoteCommand cd ${targetDir} && exec $SHELL\n  RequestTTY yes`

// After:
const hostSshConfig = (hostname: string): string =>
  `Host ${hostname}\n  ProxyCommand cloudflared access ssh --hostname %h\n  StrictHostKeyChecking no\n  UserKnownHostsFile /dev/null`

Also update the call site at line ~216:

// Before:
const cfSshConfig = cfState.tag === "ready" ? hostSshConfig(cfState.hostname, info.targetDir) : null

// After:
const cfSshConfig = cfState.tag === "ready" ? hostSshConfig(cfState.hostname) : null

The VS Code URI (lines 220-222) already has folder=targetDir — no change needed there.

Result

SSH config in panel becomes:

Host fur-sheer-barn-spirit.trycloudflare.com
  ProxyCommand cloudflared access ssh --hostname %h
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null

VS Code Remote SSH connects successfully; folder opens via URI folder=/home/dev/app.

Verification

  1. bun run build:web — no TS errors
  2. Deploy to host dist-web
  3. Open panel → verify SSH config no longer has RemoteCommand/RequestTTY
  4. Click "open in VS Code (CF tunnel)" — VS Code connects and opens /home/dev/app

7. Plan

Source: claude - Captured: 2026-06-20T09:17:00.963Z

Plan: Direct SSH (local network) section in VS Code panel

Context

Cloudflare tunnels require internet + DNS. When пользователь на том же LAN или нет
доступа в интернет — нужна прямая SSH связь по IP:port. Сертификаты не нужны —
SSH не использует TLS. project.sshPort уже есть в API.

Host IP берётся из window.location.hostname — браузер уже подключён к этому хосту,
значит SSH тоже доступен по тому же адресу.

Changes

1. packages/app/src/web/app-ready-terminal-pane.tsx

Add sshPort to VsCodeAccessInfo (lines 154-157):

type VsCodeAccessInfo = {
  readonly sshUser: string
  readonly targetDir: string
  readonly sshPort: number        // add
}

Update buildVsCodeAccessInfo:

const buildVsCodeAccessInfo = (project: TerminalPaneProps["project"]): VsCodeAccessInfo | null => {
  if (project === null) return null
  return { sshUser: project.sshUser, targetDir: project.targetDir, sshPort: project.sshPort }
}

Add direct SSH config function (near hostSshConfig):

const directSshConfig = (host: string, sshPort: number, sshUser: string): string =>
  `Host ${host}-ssh\n  HostName ${host}\n  Port ${sshPort}\n  User ${sshUser}\n  StrictHostKeyChecking no\n  UserKnownHostsFile /dev/null`

Note: SSH не поддерживает user@ip:port — порт только через -p. Правильная команда:

ssh -p PORT dev@IP

In VsCodeAccessPanel component, compute from window.location.hostname:

const directHost = window.location.hostname
const directConfig = directSshConfig(directHost, info.sshPort, info.sshUser)
const directCommand = `ssh -p ${info.sshPort} ${info.sshUser}@${directHost}`
const directVscodeUri = `vscode://ms-vscode-remote.remote-ssh/open?hostName=${encodeURIComponent(`${directHost}-ssh`)}&folder=${encodeURIComponent(info.targetDir)}`

Note: VS Code URI uses the config alias (${directHost}-ssh) — пользователь должен добавить config в ~/.ssh/config.

Add "Direct SSH (local network)" section in panel UI — after existing CF tunnel section:

  • Заголовок: "Direct SSH (local network)"
  • SSH config block с copy кнопкой
  • SSH command с copy кнопкой
  • "open in VS Code (direct)" ссылка

Result

Панель показывает два режима:

  1. Cloudflare tunnel — для доступа из интернета (уже есть)
  2. Direct SSH — для локальной сети / без интернета, по IP:port

Пользователь на одном LAN может подключиться по прямому SSH без cloudflared.

Verification

  1. bun run build:web — no TS errors
  2. Deploy to host dist-web
  3. Open panel → VS Code section показывает оба блока
  4. Direct SSH command: ssh -p PORT dev@192.168.0.206 — подключается к контейнеру
  5. Direct VS Code URI работает если config добавлен в ~/.ssh/config

8. Plan

Source: claude - Captured: 2026-06-20T13:00:18.960Z

Plan: Direct SSH (local network) section in VS Code panel

Context

Cloudflare tunnels require internet + DNS. When пользователь на том же LAN или нет
доступа в интернет — нужна прямая SSH связь по IP:port. Сертификаты не нужны —
SSH не использует TLS. project.sshPort уже есть в API.

Host IP берётся из window.location.hostname — браузер уже подключён к этому хосту,
значит SSH тоже доступен по тому же адресу.

Changes

1. packages/app/src/web/app-ready-terminal-pane.tsx

Add sshPort to VsCodeAccessInfo (lines 154-157):

type VsCodeAccessInfo = {
  readonly sshUser: string
  readonly targetDir: string
  readonly sshPort: number        // add
}

Update buildVsCodeAccessInfo:

const buildVsCodeAccessInfo = (project: TerminalPaneProps["project"]): VsCodeAccessInfo | null => {
  if (project === null) return null
  return { sshUser: project.sshUser, targetDir: project.targetDir, sshPort: project.sshPort }
}

Add direct SSH config function (near hostSshConfig):

const directSshConfig = (host: string, sshPort: number, sshUser: string, targetDir: string): string =>
  `Host ${host}-ssh\n  HostName ${host}\n  Port ${sshPort}\n  User ${sshUser}\n  StrictHostKeyChecking no\n  UserKnownHostsFile /dev/null`

In VsCodeAccessPanel component, compute from window.location.hostname:

const directHost = window.location.hostname
const directConfig = directSshConfig(directHost, info.sshPort, info.sshUser, info.targetDir)
const directCommand = `ssh -p ${info.sshPort} ${info.sshUser}@${directHost}`
const directVscodeUri = `vscode://ms-vscode-remote.remote-ssh/open?hostName=${encodeURIComponent(`${directHost}-ssh`)}&folder=${encodeURIComponent(info.targetDir)}`

Note: VS Code URI uses the config alias (${directHost}-ssh) — пользователь должен добавить config в ~/.ssh/config.

Add "Direct SSH (local network)" section in panel UI — after existing CF tunnel section:

  • Заголовок: "Direct SSH (local network)"
  • SSH config block с copy кнопкой
  • SSH command с copy кнопкой
  • "open in VS Code (direct)" ссылка

Result

Панель показывает два режима:

  1. Cloudflare tunnel — для доступа из интернета (уже есть)
  2. Direct SSH — для локальной сети / без интернета, по IP:port

Пользователь на одном LAN может подключиться по прямому SSH без cloudflared.

Verification

  1. bun run build:web — no TS errors
  2. Deploy to host dist-web
  3. Open panel → VS Code section показывает оба блока
  4. Direct SSH command: ssh -p PORT dev@192.168.0.206 — подключается к контейнеру
  5. Direct VS Code URI работает если config добавлен в ~/.ssh/config

9. Plan

Source: claude - Captured: 2026-06-20T09:17:00.963Z

Plan: Add password + targetDir to Direct SSH section

Context

Direct SSH section в панели уже показывает config/command/VS Code URI.
Нужно добавить:

  1. SSH пароль — тот же dev user password что и для CF tunnel
  2. Автооткрытие папкиdirectCommand должен открывать /home/dev/app сразу

Why no new API needed

При открытии панели CF туннель стартует автоматически (useEffectstartProjectSshTunnel).
Это генерирует пароль и ставит его на контейнер. cfState.sshPassword будет доступен
как только туннель запустится — его можно показать и в Direct SSH секции.

Changes — только UI, файл packages/app/src/web/app-ready-terminal-pane.tsx

1. directCommand — добавить -t "cd targetDir && exec $SHELL":

// Before:
const directCommand = `ssh -p ${info.sshPort} ${info.sshUser}@${directHost}`

// After:
const directCommand = `ssh -p ${info.sshPort} -t ${info.sshUser}@${directHost} "cd ${info.targetDir} && exec \\$SHELL"`

-t command работает в PowerShell (проблема была только с VS Code Remote SSH + RemoteCommand).

2. В Direct SSH UI секции — добавить SSH password (когда cfState.tag === "ready"):

{cfState.tag === "ready" && (
  <>
    <div style={{ color: "#8be9fd", fontSize: "0.9em", fontWeight: "bold", marginTop: "10px" }}>SSH password</div>
    <code style={vsCodePanelCodeStyle}>{cfState.sshPassword}</code>
    <button onClick={() => { copyText(cfState.sshPassword) }} style={vsCodePanelCopyBtnStyle} type="button">copy</button>
  </>
)}

Verification

  1. bun run build:web — no TS errors
  2. Deploy
  3. Открыть панель → Direct SSH section показывает пароль (после старта туннеля)
  4. ssh -p PORT -t dev@192.168.0.206 "cd /home/dev/app && exec $SHELL" → входит в папку app

10. Plan

Source: claude - Captured: 2026-06-20T09:17:00.963Z

Plan: Fix all CodeRabbit issues in PR #434

Context

PR #434 has 3 failing CI checks and several CodeRabbit issues:

  • Check / Lint — ESLint errors in app-share-link.tsx (prefer-regexp-exec)
  • Check / Lint Effect-TS — caused by eslint-disable in panel-share.tsx
  • Check / Test — needs investigation
  • CodeRabbit ❌ Error: silent failure in enableContainerPasswordAuth, polling race condition

Fixes

1. packages/api/src/api/contracts.ts — ShareLinkInfo missing sshPassword

Add field at line ~825:

export type ShareLinkInfo = {
  // ...existing fields...
  readonly sshPassword=[REDACTED] | null  // ADD THIS
  readonly createdAt: string
  readonly expiresAt: string
}

2. packages/api/src/services/ssh-project-tunnels.ts

a) waitForHostname — add processClosed early exit (line 174):

if (record.hostname !== null || record.stopping || record.processClosed || remainingAttempts <= 0) {

b) enableContainerPasswordAuth — propagate failure (line 212):

// Before:
yield* _(enableContainerPasswordAuth(containerName, sshPassword).pipe(Effect.orElse(() => Effect.void)))
// After: remove orElse — let failure propagate to outer handler in http.ts
yield* _(enableContainerPasswordAuth(containerName, sshPassword))

3. packages/api/src/services/ssh-password-setup.ts — safe password via env var

Replace echo 'dev:${password}' with env-var injection (security warning):

// dockerExec accepts env now
const dockerExecWithEnv = (
  containerName: string,
  env: Record<string, string>,
  script: string
): Effect.Effect<string, ApiInternalError>

Script: printf 'dev:%s' "$SSHPW" | chpasswd, passed with -e SSHPW=${password} to docker exec.

4. packages/app/src/web/app-ready-terminal-pane.tsx — polling cancelled flag

Add cancelled flag to cleanup in the 30s polling useEffect:

let cancelled = false
const id = setInterval(() => {
  void Effect.runPromise(
    startProjectSshTunnel(projectKey).pipe(
      Effect.match({
        onFailure: () => { if (!cancelled) setCfState({ tag: "failed" }) },
        onSuccess: ({ hostname, sshPassword }) => {
          if (cancelled) return
          if (hostname === null) { setCfState({ tag: "failed" }); return }
          setCfState({ tag: "ready", hostname, sshPassword })
        }
      })
    )
  )
}, 30_000)
return () => { cancelled = true; clearInterval(id) }

5. packages/app/src/web/panel-share.tsx — remove eslint-disable + requestId guard

Replace refresh() + eslint-disable-next-line with useCallback + requestId:

import { useCallback, useEffect, useRef, useState } from "react"

const requestIdRef = useRef(0)

const refresh = useCallback(() => {
  const id = ++requestIdRef.current
  setState({ _tag: "Loading" })
  void Effect.runPromise(
    listProjectShareLinks(projectKey).pipe(
      Effect.match({
        onFailure: (msg) => { if (requestIdRef.current === id) setState({ _tag: "Error", message: msg }) },
        onSuccess: (links) => {
          if (requestIdRef.current !== id) return
          setState((s) => ({ _tag: "Loaded", links, newUrl: s._tag === "Loaded" ? s.newUrl : null }))
        }
      })
    )
  )
}, [projectKey])

useEffect(() => { refresh() }, [refresh])

Apply same requestId guard to generate and revoke.

6. packages/app/src/web/app-share-link.tsx — RegExp.exec() (lint error)

Replace String.match() with RegExp.exec():

// Before:
const sshHostname = info.sshConfigSnippet.match(/HostName\s+(\S+)/)?.[1] ?? "host"
const sshPort = info.sshConfigSnippet.match(/Port\s+(\d+)/)?.[1] ?? "22"
// After:
const sshHostname = /HostName\s+(\S+)/.exec(info.sshConfigSnippet)?.[1] ?? "host"
const sshPort = /Port\s+(\d+)/.exec(info.sshConfigSnippet)?.[1] ?? "22"

Verification

  1. cd packages/app && bun run lint — no ESLint errors
  2. cd packages/api && bun run build — no TS errors
  3. cd packages/app && bun run build:web — no TS errors
  4. Deploy + open panel → verify tunnel starts, password shown
  5. Push → CI Lint + Lint Effect-TS should pass

@skulidropek

skulidropek commented Jun 20, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 9132fdc
Status: skipped
Message: No session directories found.

git status

On branch issue-428
Your branch is up to date with 'origin/issue-428'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   packages/app/src/web/api-share-links.ts
	modified:   packages/app/src/web/app-terminal-session-core.ts
	modified:   packages/app/src/web/app.tsx

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	packages/app/dist-web-copy/

no changes added to commit (use "git add" and/or "git commit -a")

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/app/src/web/panel-share.tsx`:
- Around line 169-180: The issue is that listProjectShareLinks failure in the
Effect.flatMap causes the entire generate/revoke operation to fail, even though
the actual mutation (create/delete) already succeeded on the server. Decouple
the mutation result from the refresh operation by handling them separately:
first ensure the mutation success is stored in state independently, then handle
the listProjectShareLinks refresh as a separate operation that does not override
the success state if it fails. Consider using Effect.tap or Effect.catchAll to
handle refresh errors gracefully without converting successful mutations into
error states.
- Around line 229-239: The onClick handler uses async/await which violates the
project's rules against async/await in product code. Refactor the onClick
handler to use Effect.tryPromise wrapped with Effect.runPromise instead of
async/await, following the same pattern used in other component methods like
refresh, generate, and revoke. Replace the try-catch block around
navigator.clipboard.writeText with the Effect-based error handling approach to
maintain consistency across the component and comply with project conventions.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 5292bc9e-cda8-45a9-8fe1-eacba0c7a8d4

📥 Commits

Reviewing files that changed from the base of the PR and between 658a11e and 9132fdc.

📒 Files selected for processing (6)
  • packages/api/src/api/contracts.ts
  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/services/ssh-project-tunnels.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/panel-share.tsx
📜 Review details
⏰ Context from checks skipped due to timeout. (8)
  • GitHub Check: Test
  • GitHub Check: E2E (Runtime volumes + SSH)
  • GitHub Check: E2E (Clone auto-open SSH)
  • GitHub Check: E2E (OpenCode)
  • GitHub Check: E2E (Login context)
  • GitHub Check: E2E (Browser command)
  • GitHub Check: E2E (Clone cache)
  • GitHub Check: Final build (windows-latest)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{sh,bash,py,js,ts,jsx,tsx,go,java,rb,php}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce command injection or unsafe shell/process execution with user-controlled input

Files:

  • packages/api/src/api/contracts.ts
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-password-setup.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/*.{py,js,ts,jsx,tsx,go,java,rb,php,sh,bash,c,cpp}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce path traversal or writes outside intended project/container state directories

Files:

  • packages/api/src/api/contracts.ts
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-password-setup.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/*.{js,ts,jsx,tsx,py,java,go,rb,php,sh,bash,yml,yaml,json,env*,toml,cfg,config,dockerfile,dockerignore}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files expose credentials, tokens, private-keys, or PII in source, generated config, logs, or CI output

Files:

  • packages/api/src/api/contracts.ts
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-password-setup.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: FUNCTIONAL CORE: Write only pure functions with immutable data and mathematical operations in core modules; no side effects, mutations, or external service calls
IMPERATIVE SHELL: Isolate all side effects (IO, network, database, environment/process) in a thin SHELL layer; CORE never calls SHELL, only SHELL → CORE
Never use any type annotation in TypeScript; use unknown only at SHELL boundaries for decoding, never export unknown outside boundary modules
Never use as type assertions in normal code; only permit as in a single 'axiomatic' module (brands, constructors, constants) after which types flow safely without casts
Always use exhaustive pattern matching for union types through .exhaustive() or Match.exhaustive() from effect-ts; never use switch statements or unhandled type branches
Use Effect<Success, Error, Requirements> monad from effect-ts for all effects; compose through pipe() and Effect.flatMap(); never use async/await, raw Promise chains (then/catch), or Promise.all in product code
Interoperate with Promise/exceptions only in SHELL through Effect.try/Effect.tryPromise with typed error mapping; never leave raw exceptions or untyped errors in the domain
Use Effect.acquireRelease + Effect.scoped for resource management with guaranteed finalization; never manage resources with try/finally or manual cleanup
All external services (database, HTTP, environment) must be accessed through Effect-based interfaces and Layer-based dependency injection; never call external APIs directly
Provide comprehensive TSDoc comments with mathematical notation: @pure, @effect, @invariant, @precondition, @postcondition, @complexity, @throws, and CHANGE/WHY/REF/SOURCE/FORMAT THEOREM functional comment markers
No console.*, process direct calls, or untyped environment access in product code; all such operations must be abstracted through Layer-based services in SHELL
Boundary data from external sources (HTTP, database, environment) must be decoded/valida...

Files:

  • packages/api/src/api/contracts.ts
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-password-setup.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/{browser*,server*,app*,*.ts,*.js}

📄 CodeRabbit inference engine (README.md)

Web version must listen on 0.0.0.0 by default for accessibility across LAN devices

Files:

  • packages/api/src/api/contracts.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-password-setup.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/{cli*,command*,auto*,*.ts,*.tsx}

📄 CodeRabbit inference engine (README.md)

Implement auto-mode agent selection logic to choose Claude, Codex, Gemini, or Grok randomly from available authorized providers, or allow forced selection with --auto=

Files:

  • packages/api/src/api/contracts.ts
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-password-setup.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**/*

⚙️ CodeRabbit configuration file

**/*: Ты строгий ревьюер SPEC DRIVEN DEVELOPMENT.

Перед выводами изучи README.md, другие *.md файлы, linked issues,
PR description, PR comments/discussion и релевантную кодовую базу.

Сверь изменения с исходным ТЗ/спекой и обсуждением. Флагай любой уход
от спеки, недокументированное изменение поведения, отсутствие тестов
для заявленного поведения и security-риск. Если спека не видна,
попроси автора добавить ее в issue или PR description.

Проверь решение с точки зрения формальной верификации: какие инварианты,
предусловия и постусловия можно доказать математически, а где доказуемость
слабая. Оцени решение с точки зрения теории игр: устойчивы ли стимулы,
нет ли выгодного обхода правил, и какое решение было бы сильнее.

Files:

  • packages/api/src/api/contracts.ts
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-password-setup.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
**

⚙️ CodeRabbit configuration file

**: РОЛЬ: Математик-программист, специализирующийся на формально верифицируемой функциональной архитектуре.

ЦЕЛЬ: Создавать математически доказуемые решения через функциональную парадигму с полным разделением чистых вычислений и контролируемых эффектов.

МОДЕЛЬ РАССУЖДЕНИЯ:

  • Не выдавать “личные мнения”. Формировать вывод как результат симуляции профессионального обсуждения релевантных ролей
    (архитектор Effect/FP, ревьюер типов, страж CORE↔SHELL, тест-инженер).
  • Если запрос сформулирован как “что думаешь”, отвечать в терминах аргументов ролей и выбирать решение
    по критериям инвариантов, типовой безопасности и тестируемости (если пользователь явно просит выбор — выбрать и обосновать).

ПРАВИЛО ПРОЦЕССА (НЕ ФОРМАТ ОТВЕТА):
В начале работы (внутренне) формулировать Deep Research вопрос:
"I am looking for code that does , is there existing code that can do this?"
Далее:

  • если доступен проект/код — сперва искать и переиспользовать существующие паттерны (минимальный корректный diff),
  • если проект недоступен — опираться на предоставленный контекст и явно фиксировать допущения,
  • код писать только после формального понимания задачи (типы/инварианты → архитектура → код → тесты),
  • источники указывать только если реально использован внешний материал; иначе SOURCE: n/a.

ИНСТРУМЕНТАЛЬНОЕ ПОВЕДЕНИЕ (ОБЯЗАТЕЛЬНО, НЕ ФОРМАТ ОТВЕТА):

  • Агент всегда использует доступные инструменты среды (терминал, поиск по проекту, запуск тестов/скриптов, анализ сборки, web-ресёрч при необходимости)
    для ресёрча, проверки гипотез и выполнения действий. Приоритет: проверяемость, воспроизводимость, минимальный риск.
  • Агент не предлагает “гайд” как замену действия. Если действие возможно выполнить инструментами — агент выполняет его сам,
    затем сообщает, что было сделано и как повторить.
  • Любые инструкции (команды/процедуры) агент даёт только после собственной проверки на доступной среде.
    Если проверить невозможно — явно фиксирует ограничение и перечисляе...

Files:

  • packages/api/src/api/contracts.ts
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/services/ssh-password-setup.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/api/src/services/ssh-project-tunnels.ts
🧠 Learnings (1)
📚 Learning: 2026-05-18T07:43:38.131Z
Learnt from: skulidropek
Repo: ProverCoderAI/docker-git PR: 313
File: packages/api/src/services/panel-cloudflare-tunnel.ts:123-135
Timestamp: 2026-05-18T07:43:38.131Z
Learning: In this repo’s ProverCoderAI/docker-git architecture, it is intentional for service modules under packages/api/src/services/ to call Node.js APIs directly (e.g., fs, child_process, fetch). Do not treat direct Node API usage in these service modules as a dependency-injection (DI) violation. This codebase provides the NodeContext at the program boundary (entry point) rather than injecting Context.Tag/Layer services into individual service modules; reviewers should only flag DI/context issues if the expected boundary setup is missing.

Applied to files:

  • packages/api/src/services/ssh-password-setup.ts
  • packages/api/src/services/ssh-project-tunnels.ts
🪛 OpenGrep (1.22.0)
packages/app/src/web/app-share-link.tsx

[ERROR] 200-200: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 201-201: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 202-202: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (20)
packages/api/src/api/contracts.ts (1)

813-827: LGTM!

packages/api/src/services/ssh-password-setup.ts (2)

30-42: LGTM!

Also applies to: 54-64


76-88: LGTM!

packages/api/src/services/ssh-project-tunnels.ts (2)

169-179: LGTM!


196-251: LGTM!

packages/app/src/web/app-share-link.tsx (9)

200-202: Ложное срабатывание статического анализатора — это RegExp.exec(), не child_process.exec().

OpenGrep ошибочно интерпретирует вызовы RegExp.prototype.exec() как child_process.exec/execSync. Эти строки безопасно извлекают данные из строки SSH-конфигурации с помощью регулярных выражений — никакого выполнения команд не происходит.

Source: Linters/SAST tools


18-29: LGTM!


88-94: LGTM!


106-122: LGTM!


129-194: LGTM!


248-290: LGTM!


292-345: LGTM!


360-398: LGTM!


493-524: LGTM!

packages/app/src/web/app-ready-terminal-pane.tsx (6)

177-183: Дублирование copyText уже отмечено в предыдущем ревью.

Функция copyText дублируется в app-share-link.tsx. Рекомендуется вынести в общий модуль утилит.


211-265: LGTM!


268-283: LGTM!


352-416: LGTM!


501-522: LGTM!


524-597: Корректная реализация защиты от race condition в polling effect.

Флаг isCancelled в эффекте polling (строки 540, 546, 549) правильно предотвращает обновление состояния после cleanup. Это решает проблему, отмеченную в предыдущем ревью.

Comment thread packages/app/src/web/panel-share.tsx Outdated
Comment on lines +169 to +180
Effect.flatMap(({ url }) =>
listProjectShareLinks(projectKey).pipe(
Effect.map((links) => {
if (requestIdRef.current === id) setState({ _tag: "Loaded", links, newUrl: url })
})
)
),
Effect.catchAll((msg) =>
Effect.sync(() => {
if (requestIdRef.current === id) setState({ _tag: "Error", message: msg })
})
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Разделите результат мутации и последующий refresh, чтобы не терять успешные операции при сбое списка.

На Line 169-180 и Line 188-198 ошибка listProjectShareLinks делает весь сценарий generate/revoke ошибочным, даже если create/delete уже успешно выполнился на сервере. Это даёт ложный Error и стимулирует повторные клики (например, повторную генерацию лишних share-link).

💡 Минимальный вариант правки
-      createProjectShareLink(projectKey).pipe(
-        Effect.flatMap(({ url }) =>
-          listProjectShareLinks(projectKey).pipe(
-            Effect.map((links) => {
-              if (requestIdRef.current === id) setState({ _tag: "Loaded", links, newUrl: url })
-            })
-          )
-        ),
-        Effect.catchAll((msg) =>
-          Effect.sync(() => {
-            if (requestIdRef.current === id) setState({ _tag: "Error", message: msg })
-          })
-        )
-      )
+      createProjectShareLink(projectKey).pipe(
+        Effect.flatMap(({ url }) =>
+          listProjectShareLinks(projectKey).pipe(
+            Effect.match({
+              onFailure: () => {
+                if (requestIdRef.current !== id) return
+                setState((s) =>
+                  s._tag === "Loaded"
+                    ? { ...s, newUrl: url }
+                    : { _tag: "Loaded", links: [], newUrl: url }
+                )
+              },
+              onSuccess: (links) => {
+                if (requestIdRef.current === id) setState({ _tag: "Loaded", links, newUrl: url })
+              }
+            })
+          )
+        ),
+        Effect.catchAll((msg) =>
+          Effect.sync(() => {
+            if (requestIdRef.current === id) setState({ _tag: "Error", message: msg })
+          })
+        )
+      )

Also applies to: 188-198

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/panel-share.tsx` around lines 169 - 180, The issue is
that listProjectShareLinks failure in the Effect.flatMap causes the entire
generate/revoke operation to fail, even though the actual mutation
(create/delete) already succeeded on the server. Decouple the mutation result
from the refresh operation by handling them separately: first ensure the
mutation success is stored in state independently, then handle the
listProjectShareLinks refresh as a separate operation that does not override the
success state if it fails. Consider using Effect.tap or Effect.catchAll to
handle refresh errors gracefully without converting successful mutations into
error states.

Comment thread packages/app/src/web/panel-share.tsx Outdated
Comment on lines +229 to +239
onClick={async () => {
const url = state.newUrl
if (url !== null) {
try {
await navigator.clipboard.writeText(url)
} catch {
// ignore clipboard errors
}
}
}}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "panel-share.tsx" -type f 2>/dev/null | head -5

Repository: ProverCoderAI/docker-git

Length of output: 107


🏁 Script executed:

ls -la packages/app/src/web/panel-share.tsx 2>/dev/null || echo "File not found at that path"

Repository: ProverCoderAI/docker-git

Length of output: 154


🏁 Script executed:

if [ -f packages/app/src/web/panel-share.tsx ]; then
  wc -l packages/app/src/web/panel-share.tsx
else
  echo "Searching for panel-share files..."
  find . -name "*panel-share*" -type f 2>/dev/null
fi

Repository: ProverCoderAI/docker-git

Length of output: 109


🏁 Script executed:

# Also check app structure
ls -la packages/app/src/web/ 2>/dev/null | head -20

Repository: ProverCoderAI/docker-git

Length of output: 1401


🏁 Script executed:

sed -n '220,245p' packages/app/src/web/panel-share.tsx

Repository: ProverCoderAI/docker-git

Length of output: 1092


🏁 Script executed:

# Check the full function/component context
sed -n '200,260p' packages/app/src/web/panel-share.tsx

Repository: ProverCoderAI/docker-git

Length of output: 2465


🏁 Script executed:

# Check imports and overall architecture of this file
head -50 packages/app/src/web/panel-share.tsx

Repository: ProverCoderAI/docker-git

Length of output: 1451


🏁 Script executed:

# Check if Effect is used elsewhere in this component or APP layer
grep -n "import.*Effect\|from.*effect" packages/app/src/web/panel-share.tsx

Repository: ProverCoderAI/docker-git

Length of output: 179


🏁 Script executed:

# Check broader APP architecture: are Effect patterns used in React components?
rg "Effect\.(tryPromise|runPromise|run)" packages/app/src/web/ -A 2 -B 2 | head -40

Repository: ProverCoderAI/docker-git

Length of output: 2628


🏁 Script executed:

# Check if async/await is used elsewhere in APP web layer
rg "onClick.*async|onClick=.*async" packages/app/src/web/ -c

Repository: ProverCoderAI/docker-git

Length of output: 107


Замените async/await на Effect.tryPromise с Effect.runPromise в обработчике клика.

В обработчик onClick на строках 229–239 используется async/await, что нарушает правило проекта (запрещены в продукт-коде). Проект уже применяет Effect.runPromise для аналогичных операций в других методах компонента (refresh, generate, revoke). Используйте тот же паттерн для согласованности.

Рекомендуемое исправление
-              onClick={async () => {
-                const url = state.newUrl
-                if (url !== null) {
-                  try {
-                    await navigator.clipboard.writeText(url)
-                  } catch {
-                    // ignore clipboard errors
-                  }
-                }
-              }}
+              onClick={() => {
+                const url = state.newUrl
+                if (url === null) return
+                void Effect.runPromise(
+                  Effect.tryPromise({
+                    try: () => navigator.clipboard.writeText(url),
+                    catch: () => new Error("clipboard write failed")
+                  }).pipe(Effect.ignore)
+                )
+              }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/panel-share.tsx` around lines 229 - 239, The onClick
handler uses async/await which violates the project's rules against async/await
in product code. Refactor the onClick handler to use Effect.tryPromise wrapped
with Effect.runPromise instead of async/await, following the same pattern used
in other component methods like refresh, generate, and revoke. Replace the
try-catch block around navigator.clipboard.writeText with the Effect-based error
handling approach to maintain consistency across the component and comply with
project conventions.

Source: Coding guidelines

@skulidropek

Copy link
Copy Markdown
Member Author

Agent Plan Update

Branch: issue-428 at 9132fdc.

1. Plan

Source: claude - Captured: 2026-06-20T19:43:22Z

Plan: Fix CI/CD failures on PR #434

Context

PR #434 has 3 failing CI checks. All three are pre-existing issues (existed on main before our branch), but must be fixed for CI to go green:

  1. Lintpanel-terminal.tsx exceeds 300-line limit (was 322 on main, 324 after our 2-line addition)
  2. Lint Effect-TSapp-terminal-session-core.ts:31 uses try/catch, violating no-try/catch rule
  3. Test@effect/vitest@0.29.0 incompatible with vitest@^4.1.9ctx.onTestFinished is not a function

Fix 1 — panel-terminal.tsx max-lines

Files:

  • packages/terminal/src/web/panel-terminal.tsx (currently 324 lines, max 300)
  • packages/terminal/src/web/panel-terminal-types.ts (receives extracted types)

Move 5 private type aliases from panel-terminal.tsx lines 31–76 into panel-terminal-types.ts:

Add to panel-terminal-types.ts:

// New imports (add alongside existing ones):
import type { MobileTerminalKey } from "./terminal-mobile-controls.js"
import type { TerminalInputController, TerminalStatus } from "./terminal-panel-runtime.js"

// New exported types (append after TerminalPanelProps):
export type RefState<T> = { current: T }

export type TerminalNotificationHandlers = {
  readonly notifyAttachFailure: () => void
  readonly notifyExit: (info: TerminalExitInfo) => void
  readonly notifyMessage: (message: string) => void
}

export type InlineImagePreviewState = {
  readonly inlineImagePreviewsEnabled: boolean
  readonly inlineImagePreviewsEnabledRef: RefState<boolean>
  readonly toggleInlineImagePreviews: () => void
}

export type MobileTerminalControlState = {
  readonly handleMobileKeyPress: (key: MobileTerminalKey) => void
  readonly mobileControlsCollapsed: boolean
  readonly mobileCtrlArmed: boolean
  readonly toggleMobileControls: () => void
  readonly toggleMobileCtrl: () => void
}

export type TerminalPanelLayoutProps =
  & Pick<
    TerminalPanelProps,
    | "bodyContent" | "keyboardOpen" | "mobileMode"
    | "onApplyProject" | "onOpenBrowser" | "onOpenSkiller"
    | "onOpenTaskManager" | "onOpenTerminal" | "onOpenVsCode" | "session"
  >
  & InlineImagePreviewState
  & MobileTerminalControlState
  & {
    readonly compactHeaderMode: boolean
    readonly compactTypingMode: boolean
    readonly handleDetach: () => void
    readonly handleKill: () => void
    readonly hostRef: RefState<HTMLDivElement | null>
    readonly status: TerminalStatus
  }

In panel-terminal.tsx:

  • Remove lines 31–76 (46 lines of type declarations)
  • Replace with one import type line pulling those 5 types + RefState from ./panel-terminal-types.js
  • Remove now-redundant TerminalConnectionState, TerminalInputController individual imports (they move inside types file)
  • Net: −45 lines → ~279 lines

Fix 2 — app-terminal-session-core.ts:31 try/catch

File: packages/app/src/web/app-terminal-session-core.ts

// Add at top:
import { Effect } from "effect"

// Replace lines 30–36 (safeDecodeSegment):
const safeDecodeSegment = (value: string): string | null =>
  Effect.runSync(
    Effect.try(() => decodeURIComponent(value)).pipe(
      Effect.catchAll(() => Effect.succeed(null))
    )
  )

Same string | null interface; no callers change; satisfies no-try/catch lint rule.


Fix 3 — API tests: @effect/vitest version bump

File: packages/api/package.json

@effect/vitest@0.29.0 uses ctx.onTestFinished() which does not exist in vitest 4.x. Fix: update to latest version.

cd /home/dev/app/packages/api
bun add @effect/vitest@latest
bun install
bun test 2>&1 | tail -10

If latest @effect/vitest requires a newer effect, also update:

bun add effect@latest @effect/schema@latest

Verification

  1. bun run lint 2>&1 | grep ERROR — 0 errors from our files
  2. cd packages/api && bun test 2>&1 | tail -5 — tests pass
  3. cd packages/app && bun run build:web — no TS errors
  4. Push → CI Lint + Lint Effect-TS + Test all green

…lations

- Extract shared style constants to app-share-link-utils.ts (eliminates max-lines in app-share-link-sections.tsx)
- Split panel-vscode-access.tsx into hooks file + visual panel-vscode-access-panel.tsx
- Refactor SshPasswordBlock to parseSshSnippet helper (fixes complexity)
- Extract tryResolveShareLink from resolveWebAppRoute (fixes complexity in app-terminal-session-core.ts)
- Group buildShareLinkSshAccess 7 params into ShareLinkSshAccessInput type (fixes max-params in ssh-access.ts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@skulidropek

skulidropek commented Jun 20, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 0847b71
Status: skipped
Message: No session directories found.

git status

On branch issue-428
Your branch is up to date with 'origin/issue-428'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	packages/app/dist-web-copy/

nothing added to commit but untracked files present (use "git add" to track)

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
packages/lib/src/usecases/ssh-access.ts (1)

213-231: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Нарушение иммутабельности: использование push() вместо функционального паттерна.

buildDirectSshLines использует мутабельный lines.push() (строка 229), что противоречит coding guidelines. При этом buildCfSshLines уже реализована правильно через spread-синтаксис.

♻️ Рефакторинг для согласованности с buildCfSshLines
 const buildDirectSshLines = (
   alias: string,
   sshHostname: string,
   sshUser: string,
   sshPort: number,
   sshKeyPath: string | null
-): Array<string> => {
-  const lines = [
-    `Host ${alias}`,
-    `  HostName ${sshHostname}`,
-    `  User ${sshUser}`,
-    `  Port ${sshPort}`,
-    `  LogLevel ERROR`,
-    `  StrictHostKeyChecking no`,
-    `  UserKnownHostsFile /dev/null`
-  ]
-  if (sshKeyPath !== null) lines.push(`  IdentityFile ${sshKeyPath}`, `  IdentitiesOnly yes`)
-  return lines
-}
+): ReadonlyArray<string> => [
+  `Host ${alias}`,
+  `  HostName ${sshHostname}`,
+  `  User ${sshUser}`,
+  `  Port ${sshPort}`,
+  `  LogLevel ERROR`,
+  `  StrictHostKeyChecking no`,
+  `  UserKnownHostsFile /dev/null`,
+  ...(sshKeyPath === null ? [] : [`  IdentityFile ${sshKeyPath}`, `  IdentitiesOnly yes`])
+]

As per coding guidelines: "Use ReadonlyArray and readonly properties throughout; never use mutable array methods (push, splice, etc.)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lib/src/usecases/ssh-access.ts` around lines 213 - 231, The
buildDirectSshLines function violates immutability guidelines by using the
mutable push() method to add elements to the lines array. Replace the push()
call with a functional approach using spread syntax or conditional array
concatenation. Instead of conditionally pushing elements after the array is
created, use a ternary operator or conditional spread to build the complete
array in a single immutable declaration, ensuring consistency with how
buildCfSshLines is implemented.

Source: Coding guidelines

♻️ Duplicate comments (2)
packages/app/src/web/panel-share.tsx (2)

184-205: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Та же проблема: ошибка list после успешного delete вводит в заблуждение.

Аналогично runGenerateLink — если deleteProjectShareLink выполнился успешно, но listProjectShareLinks упал, пользователь увидит Error и может не понять, что ссылка уже отозвана. Примените тот же паттерн разделения результата мутации и refresh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/panel-share.tsx` around lines 184 - 205, The
runRevokeLink function currently chains deleteProjectShareLink and
listProjectShareLinks together, causing an error from listProjectShareLinks to
mislead users into thinking the deletion failed when it actually succeeded.
Refactor runRevokeLink to apply the same pattern used in runGenerateLink by
separating the mutation result from the refresh operation. First update the
state immediately after deleteProjectShareLink succeeds to reflect the
successful deletion, then handle the listProjectShareLinks call separately so
that failures during the refresh do not obscure the fact that the deletion
operation completed successfully.

160-182: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ошибка list после успешного create приводит к ложному состоянию Error.

Если createProjectShareLink успешно выполнился на сервере, но последующий listProjectShareLinks упал (например, сетевая ошибка), UI покажет Error-состояние. Пользователь может повторить клик и создать дубликаты share-ссылок. Разделите результат мутации и refresh — при сбое refresh показывайте частичный успех с newUrl.

Предлагаемое исправление
 const runGenerateLink = (
   projectKey: string,
   id: number,
   idRef: { readonly current: number },
   setState: Dispatch<SetStateAction<ShareLinksState>>
 ): void => {
   void Effect.runPromise(
     createProjectShareLink(projectKey).pipe(
       Effect.flatMap(({ url }) =>
         listProjectShareLinks(projectKey).pipe(
-          Effect.map((links) => {
-            if (idRef.current === id) setState({ _tag: "Loaded", links, newUrl: url })
-          })
+          Effect.match({
+            onFailure: () => {
+              if (idRef.current !== id) return
+              setState((s) =>
+                s._tag === "Loaded"
+                  ? { ...s, newUrl: url }
+                  : { _tag: "Loaded", links: [], newUrl: url }
+              )
+            },
+            onSuccess: (links) => {
+              if (idRef.current === id) setState({ _tag: "Loaded", links, newUrl: url })
+            }
+          })
         )
       ),
       Effect.catchAll((msg) =>
         Effect.sync(() => {
           if (idRef.current === id) setState({ _tag: "Error", message: msg })
         })
       )
     )
   )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/panel-share.tsx` around lines 160 - 182, The issue is
that if createProjectShareLink succeeds but listProjectShareLinks fails, the
entire operation is treated as an error, even though the link was created
successfully on the server. To fix this, separate the mutation and refresh
concerns in the runGenerateLink function: first, after createProjectShareLink
succeeds, immediately set the state with the newUrl and any previously cached
links, then attempt to refresh the list separately, and if that refresh fails,
either silently ignore the error (since the link was already created) or handle
it gracefully without transitioning the UI to the Error state. This prevents
duplicate link creation when users retry after network failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/package.json`:
- Line 57: The vitest downgrade to ^3.2.0 in packages/api/package.json creates a
peer dependency conflict with `@effect/vitest`@0.29.0 which requires vitest@4.1.0,
causing test failures in packages/api/tests/ where files import from
`@effect/vitest`. Additionally, this breaks version consistency with other
monorepo packages using vitest@^4.1.9. Either revert the vitest version back to
^4.1.9 with documentation explaining why the downgrade was necessary if
attempted, or upgrade `@effect/vitest` to a version compatible with vitest@3.2.0
and clearly document the reason for the entire migration in the commit message.

In `@packages/app/src/web/app-share-link-sections.tsx`:
- Around line 177-179: The type assertion `as string` on the `info.sshPassword`
parameter in the onClick handler callback is redundant because the type has
already been narrowed to string by the null-check guard on line 168. Remove the
`as string` assertion and pass `info.sshPassword` directly to the copyText
function call to allow TypeScript to properly track type narrowing and prevent
potential type regressions.

In `@packages/app/src/web/app-share-link-utils.ts`:
- Line 46: The copyText function is unused code that also has an unhandled
Promise rejection since navigator.clipboard.writeText returns a Promise that is
not being handled. Either remove the copyText function entirely if it is not
needed, or if it will be used in the future, refactor it to properly handle the
Promise rejection by wrapping it in Effect.tryPromise similar to how
copyPanelShareTunnelUrl is implemented in actions-share.ts, ensuring that any
clipboard write errors are properly caught and managed within the Effect layer
instead of leaving unhandled rejections.

In `@packages/app/src/web/app-terminal-session-core.ts`:
- Around line 32-37: The safeDecodeSegment function currently uses Effect.try
with catchAll piped afterwards, which loses error type information and violates
the Effect contract. Refactor this function to use the parameter form of
Effect.try that accepts both try and catch options, where the try function
attempts the decodeURIComponent operation and the catch parameter handles any
thrown error by returning null. This approach maintains proper error type
handling and aligns with the existing project pattern used in
packages/app/src/web/api-terminal.ts.

In `@packages/app/src/web/panel-vscode-access-panel.tsx`:
- Around line 36-40: Both the hostSshConfig and directSshConfig functions are
disabling SSH host key verification by setting StrictHostKeyChecking no and
UserKnownHostsFile /dev/null, which creates a security vulnerability. Remove
these two insecure configuration lines from both functions to enable proper host
key verification by default.
- Around line 113-115: The directCommand string in the SSH command construction
is vulnerable to shell injection because info.targetDir is not properly escaped
when inserted into the shell command. To fix this, wrap info.targetDir with
single quotes to prevent special characters like semicolons, double quotes, and
dollar signs from being interpreted as shell syntax. Additionally, properly
escape any single quotes that might appear within the targetDir value itself by
replacing them with '\'' (end single quote, escaped single quote, start single
quote). This ensures that when the command is copy-pasted into a terminal, the
path is treated as a literal string regardless of its contents.

---

Outside diff comments:
In `@packages/lib/src/usecases/ssh-access.ts`:
- Around line 213-231: The buildDirectSshLines function violates immutability
guidelines by using the mutable push() method to add elements to the lines
array. Replace the push() call with a functional approach using spread syntax or
conditional array concatenation. Instead of conditionally pushing elements after
the array is created, use a ternary operator or conditional spread to build the
complete array in a single immutable declaration, ensuring consistency with how
buildCfSshLines is implemented.

---

Duplicate comments:
In `@packages/app/src/web/panel-share.tsx`:
- Around line 184-205: The runRevokeLink function currently chains
deleteProjectShareLink and listProjectShareLinks together, causing an error from
listProjectShareLinks to mislead users into thinking the deletion failed when it
actually succeeded. Refactor runRevokeLink to apply the same pattern used in
runGenerateLink by separating the mutation result from the refresh operation.
First update the state immediately after deleteProjectShareLink succeeds to
reflect the successful deletion, then handle the listProjectShareLinks call
separately so that failures during the refresh do not obscure the fact that the
deletion operation completed successfully.
- Around line 160-182: The issue is that if createProjectShareLink succeeds but
listProjectShareLinks fails, the entire operation is treated as an error, even
though the link was created successfully on the server. To fix this, separate
the mutation and refresh concerns in the runGenerateLink function: first, after
createProjectShareLink succeeds, immediately set the state with the newUrl and
any previously cached links, then attempt to refresh the list separately, and if
that refresh fails, either silently ignore the error (since the link was already
created) or handle it gracefully without transitioning the UI to the Error
state. This prevents duplicate link creation when users retry after network
failures.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 47a14458-d02e-45ca-88dc-7272d56ff1bb

📥 Commits

Reviewing files that changed from the base of the PR and between 9132fdc and 0847b71.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • packages/api/package.json
  • packages/api/src/http.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/app/src/web/app-share-link-sections.tsx
  • packages/app/src/web/app-share-link-utils.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/app.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/panel-vscode-access-panel.tsx
  • packages/app/src/web/panel-vscode-access.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/terminal/src/web/panel-terminal.tsx
📜 Review details
⏰ Context from checks skipped due to timeout. (9)
  • GitHub Check: E2E (Clone cache)
  • GitHub Check: E2E (Browser command)
  • GitHub Check: E2E (OpenCode)
  • GitHub Check: E2E (Clone auto-open SSH)
  • GitHub Check: E2E (Login context)
  • GitHub Check: E2E (Runtime volumes + SSH)
  • GitHub Check: Lint
  • GitHub Check: Test
  • GitHub Check: Final build (windows-latest)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,ts,jsx,tsx,py,java,go,rb,php,sh,bash,yml,yaml,json,env*,toml,cfg,config,dockerfile,dockerignore}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files expose credentials, tokens, private-keys, or PII in source, generated config, logs, or CI output

Files:

  • packages/api/package.json
  • packages/app/src/web/app-share-link-utils.ts
  • packages/app/src/web/app.tsx
  • packages/app/src/web/panel-vscode-access-panel.tsx
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/panel-vscode-access.tsx
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-share-link-sections.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/http.ts
**/{package*.json,requirements*.txt,setup.py,setup.cfg,Pipfile,Pipfile.lock,pyproject.toml,pom.xml,build.gradle,Gemfile,Gemfile.lock,go.mod,go.sum,composer.json,Cargo.toml,Cargo.lock}

📄 CodeRabbit inference engine (Custom checks)

Fail if dependency or package-manager changes materially increase supply-chain risk without justification

Files:

  • packages/api/package.json
**/*

⚙️ CodeRabbit configuration file

**/*: Ты строгий ревьюер SPEC DRIVEN DEVELOPMENT.

Перед выводами изучи README.md, другие *.md файлы, linked issues,
PR description, PR comments/discussion и релевантную кодовую базу.

Сверь изменения с исходным ТЗ/спекой и обсуждением. Флагай любой уход
от спеки, недокументированное изменение поведения, отсутствие тестов
для заявленного поведения и security-риск. Если спека не видна,
попроси автора добавить ее в issue или PR description.

Проверь решение с точки зрения формальной верификации: какие инварианты,
предусловия и постусловия можно доказать математически, а где доказуемость
слабая. Оцени решение с точки зрения теории игр: устойчивы ли стимулы,
нет ли выгодного обхода правил, и какое решение было бы сильнее.

Files:

  • packages/api/package.json
  • packages/app/src/web/app-share-link-utils.ts
  • packages/app/src/web/app.tsx
  • packages/app/src/web/panel-vscode-access-panel.tsx
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/panel-vscode-access.tsx
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-share-link-sections.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/http.ts
**

⚙️ CodeRabbit configuration file

**: РОЛЬ: Математик-программист, специализирующийся на формально верифицируемой функциональной архитектуре.

ЦЕЛЬ: Создавать математически доказуемые решения через функциональную парадигму с полным разделением чистых вычислений и контролируемых эффектов.

МОДЕЛЬ РАССУЖДЕНИЯ:

  • Не выдавать “личные мнения”. Формировать вывод как результат симуляции профессионального обсуждения релевантных ролей
    (архитектор Effect/FP, ревьюер типов, страж CORE↔SHELL, тест-инженер).
  • Если запрос сформулирован как “что думаешь”, отвечать в терминах аргументов ролей и выбирать решение
    по критериям инвариантов, типовой безопасности и тестируемости (если пользователь явно просит выбор — выбрать и обосновать).

ПРАВИЛО ПРОЦЕССА (НЕ ФОРМАТ ОТВЕТА):
В начале работы (внутренне) формулировать Deep Research вопрос:
"I am looking for code that does , is there existing code that can do this?"
Далее:

  • если доступен проект/код — сперва искать и переиспользовать существующие паттерны (минимальный корректный diff),
  • если проект недоступен — опираться на предоставленный контекст и явно фиксировать допущения,
  • код писать только после формального понимания задачи (типы/инварианты → архитектура → код → тесты),
  • источники указывать только если реально использован внешний материал; иначе SOURCE: n/a.

ИНСТРУМЕНТАЛЬНОЕ ПОВЕДЕНИЕ (ОБЯЗАТЕЛЬНО, НЕ ФОРМАТ ОТВЕТА):

  • Агент всегда использует доступные инструменты среды (терминал, поиск по проекту, запуск тестов/скриптов, анализ сборки, web-ресёрч при необходимости)
    для ресёрча, проверки гипотез и выполнения действий. Приоритет: проверяемость, воспроизводимость, минимальный риск.
  • Агент не предлагает “гайд” как замену действия. Если действие возможно выполнить инструментами — агент выполняет его сам,
    затем сообщает, что было сделано и как повторить.
  • Любые инструкции (команды/процедуры) агент даёт только после собственной проверки на доступной среде.
    Если проверить невозможно — явно фиксирует ограничение и перечисляе...

Files:

  • packages/api/package.json
  • packages/app/src/web/app-share-link-utils.ts
  • packages/app/src/web/app.tsx
  • packages/app/src/web/panel-vscode-access-panel.tsx
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/panel-vscode-access.tsx
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-share-link-sections.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/http.ts
**/*.{sh,bash,py,js,ts,jsx,tsx,go,java,rb,php}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce command injection or unsafe shell/process execution with user-controlled input

Files:

  • packages/app/src/web/app-share-link-utils.ts
  • packages/app/src/web/app.tsx
  • packages/app/src/web/panel-vscode-access-panel.tsx
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/panel-vscode-access.tsx
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-share-link-sections.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/http.ts
**/*.{py,js,ts,jsx,tsx,go,java,rb,php,sh,bash,c,cpp}

📄 CodeRabbit inference engine (Custom checks)

Fail if changed files introduce path traversal or writes outside intended project/container state directories

Files:

  • packages/app/src/web/app-share-link-utils.ts
  • packages/app/src/web/app.tsx
  • packages/app/src/web/panel-vscode-access-panel.tsx
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/panel-vscode-access.tsx
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-share-link-sections.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/http.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: FUNCTIONAL CORE: Write only pure functions with immutable data and mathematical operations in core modules; no side effects, mutations, or external service calls
IMPERATIVE SHELL: Isolate all side effects (IO, network, database, environment/process) in a thin SHELL layer; CORE never calls SHELL, only SHELL → CORE
Never use any type annotation in TypeScript; use unknown only at SHELL boundaries for decoding, never export unknown outside boundary modules
Never use as type assertions in normal code; only permit as in a single 'axiomatic' module (brands, constructors, constants) after which types flow safely without casts
Always use exhaustive pattern matching for union types through .exhaustive() or Match.exhaustive() from effect-ts; never use switch statements or unhandled type branches
Use Effect<Success, Error, Requirements> monad from effect-ts for all effects; compose through pipe() and Effect.flatMap(); never use async/await, raw Promise chains (then/catch), or Promise.all in product code
Interoperate with Promise/exceptions only in SHELL through Effect.try/Effect.tryPromise with typed error mapping; never leave raw exceptions or untyped errors in the domain
Use Effect.acquireRelease + Effect.scoped for resource management with guaranteed finalization; never manage resources with try/finally or manual cleanup
All external services (database, HTTP, environment) must be accessed through Effect-based interfaces and Layer-based dependency injection; never call external APIs directly
Provide comprehensive TSDoc comments with mathematical notation: @pure, @effect, @invariant, @precondition, @postcondition, @complexity, @throws, and CHANGE/WHY/REF/SOURCE/FORMAT THEOREM functional comment markers
No console.*, process direct calls, or untyped environment access in product code; all such operations must be abstracted through Layer-based services in SHELL
Boundary data from external sources (HTTP, database, environment) must be decoded/valida...

Files:

  • packages/app/src/web/app-share-link-utils.ts
  • packages/app/src/web/app.tsx
  • packages/app/src/web/panel-vscode-access-panel.tsx
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/panel-vscode-access.tsx
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-share-link-sections.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/http.ts
**/{browser*,server*,app*,*.ts,*.js}

📄 CodeRabbit inference engine (README.md)

Web version must listen on 0.0.0.0 by default for accessibility across LAN devices

Files:

  • packages/app/src/web/app-share-link-utils.ts
  • packages/app/src/web/app.tsx
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-share-link-sections.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/http.ts
**/{cli*,command*,auto*,*.ts,*.tsx}

📄 CodeRabbit inference engine (README.md)

Implement auto-mode agent selection logic to choose Claude, Codex, Gemini, or Grok randomly from available authorized providers, or allow forced selection with --auto=

Files:

  • packages/app/src/web/app-share-link-utils.ts
  • packages/app/src/web/app.tsx
  • packages/app/src/web/panel-vscode-access-panel.tsx
  • packages/terminal/src/web/panel-terminal-types.ts
  • packages/app/src/web/app-share-link.tsx
  • packages/lib/src/usecases/ssh-access.ts
  • packages/app/src/web/app-terminal-session-core.ts
  • packages/app/src/web/panel-vscode-access.tsx
  • packages/app/src/web/api-share-links.ts
  • packages/app/src/web/app-share-link-sections.tsx
  • packages/terminal/src/web/panel-terminal.tsx
  • packages/app/src/web/panel-share.tsx
  • packages/app/src/web/app-ready-terminal-pane.tsx
  • packages/api/src/http.ts
🪛 GitHub Check: Lint Effect-TS
packages/app/src/web/app-terminal-session-core.ts

[failure] 35-35:
Avoid catchAll that discards typed errors; map or propagate explicitly

🪛 OpenGrep (1.22.0)
packages/app/src/web/app-share-link-sections.tsx

[ERROR] 160-160: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 161-161: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 162-162: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (14)
packages/lib/src/usecases/ssh-access.ts (2)

203-211: LGTM!


233-276: LGTM!

packages/api/src/http.ts (2)

1149-1157: LGTM!


1197-1205: LGTM!

packages/app/src/web/api-share-links.ts (1)

45-54: LGTM!

packages/app/src/web/panel-share.tsx (3)

2-2: LGTM!

Also applies to: 14-14


138-158: LGTM!


207-318: LGTM!

packages/app/src/web/app.tsx (1)

225-236: LGTM!

packages/app/src/web/app-share-link.tsx (1)

31-65: LGTM!

Also applies to: 67-114, 116-159, 166-185

packages/app/src/web/panel-vscode-access.tsx (1)

19-88: LGTM!

packages/app/src/web/app-ready-terminal-pane.tsx (1)

248-267: LGTM!

packages/terminal/src/web/panel-terminal-types.ts (1)

44-70: LGTM!

packages/terminal/src/web/panel-terminal.tsx (1)

19-26: LGTM!

Also applies to: 273-282

Comment thread packages/api/package.json
"typescript": "^6.0.3",
"typescript-eslint": "^8.61.1",
"vitest": "^4.1.9"
"vitest": "^3.2.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Vitest и связанные пакеты в package.json файлах =="
fd -a "package.json" | while read -r f; do
  echo "--- $f"
  rg -n '"vitest"|"`@vitest/`' "$f" || true
done

echo
echo "== Упоминания vitest в конфигурациях и тестовых скриптах =="
rg -n --glob '**/*.{json,ts,js,mjs,cjs,yml,yaml}' '\bvitest\b|`@vitest/`' .

echo
echo "== Lockfile entries (если есть) =="
fd -a 'pnpm-lock.yaml|package-lock.json|bun.lockb|bun.lock|yarn.lock' | while read -r lf; do
  echo "--- $lf"
  rg -n 'vitest|`@vitest/`' "$lf" || true
done

Repository: ProverCoderAI/docker-git

Length of output: 50380


Даунгрейд vitest до ^3.2.0 нарушает peer dependency requirement @effect/vitest и создаёт критический риск.

Версия @effect/vitest@0.29.0 (используется всеми пакетами, включая api) требует vitest@4.1.0 как peer dependency. Изменение packages/api/package.json на vitest@^3.2.0 конфликтует с этим требованием и приведёт к сбоям при запуске тестов (все test-файлы в packages/api/tests/ импортируют из @effect/vitest). Кроме того, это нарушает консистентность версий в монорепо, где все остальные пакеты используют vitest@^4.1.9.

Либо вернуть vitest@^4.1.9 с объяснением почему это не удалось, либо обновить @effect/vitest на совместимую версию и задокументировать причину миграции.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/package.json` at line 57, The vitest downgrade to ^3.2.0 in
packages/api/package.json creates a peer dependency conflict with
`@effect/vitest`@0.29.0 which requires vitest@4.1.0, causing test failures in
packages/api/tests/ where files import from `@effect/vitest`. Additionally, this
breaks version consistency with other monorepo packages using vitest@^4.1.9.
Either revert the vitest version back to ^4.1.9 with documentation explaining
why the downgrade was necessary if attempted, or upgrade `@effect/vitest` to a
version compatible with vitest@3.2.0 and clearly document the reason for the
entire migration in the commit message.

Source: Coding guidelines

Comment on lines +177 to +179
onClick={() => {
copyText(info.sshPassword as string)
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Проверка на оставшиеся type assertions в этом файле
rg -n '\s+as\s+' packages/app/src/web/app-share-link-sections.tsx
# Ожидание после фикса: нет совпадений

Repository: ProverCoderAI/docker-git

Length of output: 121


🏁 Script executed:

#!/bin/bash
# Examine context around lines 177-179 to verify type narrowing
cat -n packages/app/src/web/app-share-link-sections.tsx | sed -n '150,200p'

Repository: ProverCoderAI/docker-git

Length of output: 2076


🏁 Script executed:

#!/bin/bash
# Check the type definition of ShareLinkInfo to confirm sshPassword type
rg -A 15 'type ShareLinkInfo|interface ShareLinkInfo' packages/app/src/web/app-share-link-sections.tsx

Repository: ProverCoderAI/docker-git

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Find where ShareLinkInfo type is defined
fd -t f '\.tsx?$' packages/app/src/web | xargs rg -l 'type ShareLinkInfo|interface ShareLinkInfo'

Repository: ProverCoderAI/docker-git

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Check the ShareLinkInfo type definition
cat -n packages/app/src/web/api-share-links.ts | grep -A 20 'type ShareLinkInfo\|interface ShareLinkInfo'

Repository: ProverCoderAI/docker-git

Length of output: 926


🏁 Script executed:

#!/bin/bash
# Find ShareLinkInfoSchema definition
rg -B 5 -A 15 'const ShareLinkInfoSchema' packages/app/src/web/api-share-links.ts

Repository: ProverCoderAI/docker-git

Length of output: 673


Удалите избыточный as string — тип уже сужен через null-check.

На строке 168 выполняется проверка if (info.sshPassword === null) return null, которая сужает тип sshPassword с string | null до string. На строке 175 вы уже используете info.sshPassword напрямую в JSX без assertion. Assertion на строке 178 является избыточным и маскирует потенциальные регрессии типов.

Исправление
-            copyText(info.sshPassword as string)
+            copyText(info.sshPassword)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/app-share-link-sections.tsx` around lines 177 - 179, The
type assertion `as string` on the `info.sshPassword` parameter in the onClick
handler callback is redundant because the type has already been narrowed to
string by the null-check guard on line 168. Remove the `as string` assertion and
pass `info.sshPassword` directly to the copyText function call to allow
TypeScript to properly track type narrowing and prevent potential type
regressions.

Source: Coding guidelines

textAlign: "center"
}

export const copyText = (text: string): void => void navigator.clipboard.writeText(text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Проверка текущего использования writeText в helper
rg -n 'navigator\.clipboard\.writeText' packages/app/src/web/app-share-link-utils.ts -C2
# Ожидание после фикса: вызов сопровождается .catch(...)

Repository: ProverCoderAI/docker-git

Length of output: 169


🏁 Script executed:

# Найти все использования copyText в приложении
rg -n 'copyText' packages/app/src --type ts --type tsx

# Проверить контекст вызова
fd -e ts -e tsx packages/app/src/web/ -x grep -l 'clipboard\|copyText' {}

# Посмотреть архитектуру обработки ошибок в shell-слое
rg -n 'catch.*=>.*\{\}' packages/app/src --type ts --type tsx | head -20

# Проверить наличие тестов для этой функции
fd -e test.ts -e test.tsx -e spec.ts packages/app --exec grep -l 'copyText' {}

Repository: ProverCoderAI/docker-git

Length of output: 868


🏁 Script executed:

#!/bin/bash

# Найти все использования copyText
rg -n 'copyText' packages/app/src -g '*.ts'

# Посмотреть содержимое файла целиком для контекста
cat -n packages/app/src/web/app-share-link-utils.ts

# Проверить наличие архитектурных паттернов обработки эффектов
rg -n 'Effect\.' packages/app/src/web -g '*.ts' | head -20

# Поищем catch-обработку в shell-слое
rg -n '\.catch\(' packages/app/src -g '*.ts' | head -15

Repository: ProverCoderAI/docker-git

Length of output: 3279


🏁 Script executed:

#!/bin/bash

# Поиск всех мест, где вызывается copyText
rg -n 'copyText\(' packages/app/src -g '*.ts'

# Посмотреть как обрабатываются Promise в shell-слое
rg -n 'navigator\.' packages/app/src -g '*.ts'

# Проверить, используется ли Effect для clipboard операций где-то ещё
rg -n 'clipboard' packages/app/src -g '*.ts' -i

# Посмотреть, есть ли паттерны обработки unhandled rejections
rg -n 'unhandledRejection\|Promise\.reject' packages/app/src -g '*.ts'

# Проверить architecture docs
cat packages/app/README.md 2>/dev/null || cat README.md 2>/dev/null | head -100

Repository: ProverCoderAI/docker-git

Length of output: 3696


🏁 Script executed:

#!/bin/bash

# Посмотреть контекст использования в actions-share.ts
sed -n '75,95p' packages/app/src/web/actions-share.ts

# Посмотреть импорты и контекст вокруг copyText
rg -n -B5 -A5 'copyText\(' packages/app/src/web -g '*.ts'

# Проверить, есть ли обработка reject в actions-share.ts
sed -n '80,100p' packages/app/src/web/actions-share.ts

# Посмотреть полный контекст функции в actions-share.ts
cat -n packages/app/src/web/actions-share.ts | head -120

Repository: ProverCoderAI/docker-git

Length of output: 4173


🏁 Script executed:

#!/bin/bash

# Найти все места, где используется copyText из app-share-link-utils
rg -n 'from.*app-share-link-utils|import.*copyText' packages/app/src -g '*.ts'

# Посмотреть, где и как вызывается copyText
rg -n 'copyText' packages/app/src -g '*.ts' -B2 -A2

# Проверить архитектуру обработки эффектов в действиях
cat -n packages/app/src/web/actions-shared.ts | head -50

Repository: ProverCoderAI/docker-git

Length of output: 2483


🏁 Script executed:

#!/bin/bash

# Проверить, импортируется ли copyText где-нибудь
rg -n 'import.*copyText|copyText' packages/ -g '*.ts' --type-list

# Также проверим расширение поиска
fd -e ts . packages/ | xargs grep -l 'copyText' 2>/dev/null

# Проверить структуру проекта
ls -la packages/app/src/web/ | head -20

Repository: ProverCoderAI/docker-git

Length of output: 1854


🏁 Script executed:

#!/bin/bash

# Более тщательный поиск - может быть импорт с другим синтаксисом
rg 'copyText' packages/ -g '*.ts'

# Проверить используется ли она в компонентах React
find packages/app/src -name '*.ts' -exec grep -l 'copyText' {} \;

# Посмотреть весь файл app-share-link-utils.ts
cat packages/app/src/web/app-share-link-utils.ts

# Проверить если copyText используется через динамический импорт
rg 'app-share-link-utils' packages/ -g '*.ts'

Repository: ProverCoderAI/docker-git

Length of output: 1280


🏁 Script executed:

#!/bin/bash

# Проверить точно - импортируется ли copyText в других файлах
grep -r 'copyText' packages/ --include='*.ts' 2>/dev/null | grep -v 'export const copyText'

# Искать любые импорты из app-share-link-utils
grep -r 'from.*app-share-link-utils\|import.*from.*app-share-link-utils' packages/ --include='*.ts'

# Проверить, может быть это внутреннее использование в самом файле
grep -n 'copyText' packages/app/src/web/app-share-link-utils.ts

Repository: ProverCoderAI/docker-git

Length of output: 160


copyText — неиспользуемая функция с потенциальным unhandled rejection.

Функция не импортируется нигде в коде (dead code), однако её сигнатура содержит необработанный Promise reject. При её будущем использовании это вызовет unhandled rejection. Более того, функция нарушает архитектуру SHELL-слоя: должна быть обёрнута в Effect.tryPromise (как показано в copyPanelShareTunnelUrl в actions-share.ts, строки 84–86).

Рекомендуемое исправление:

Либо удалить неиспользуемую функцию, либо переписать с правильной обработкой:

Вариант 1: удалить (если функция действительно не нужна)
-export const copyText = (text: string): void => void navigator.clipboard.writeText(text)
Вариант 2: переписать с Effect (если функция будет использоваться)
-export const copyText = (text: string): void => void navigator.clipboard.writeText(text)
+export const copyText = (text: string): Effect.Effect<void, string> =>
+  Effect.tryPromise({
+    try: () => navigator.clipboard.writeText(text),
+    catch: () => "Failed to copy text"
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/app-share-link-utils.ts` at line 46, The copyText
function is unused code that also has an unhandled Promise rejection since
navigator.clipboard.writeText returns a Promise that is not being handled.
Either remove the copyText function entirely if it is not needed, or if it will
be used in the future, refactor it to properly handle the Promise rejection by
wrapping it in Effect.tryPromise similar to how copyPanelShareTunnelUrl is
implemented in actions-share.ts, ensuring that any clipboard write errors are
properly caught and managed within the Effect layer instead of leaving unhandled
rejections.

Comment on lines +32 to +37
const safeDecodeSegment = (value: string): string | null =>
Effect.runSync(
Effect.try(() => decodeURIComponent(value)).pipe(
Effect.catchAll(() => Effect.succeed(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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Проверка, что проблемный catchAll удален из данного файла
rg -n 'Effect\.catchAll\(\(\)\s*=>\s*Effect\.succeed\(null\)\)' packages/app/src/web/app-terminal-session-core.ts
# Ожидание: нет совпадений

Repository: ProverCoderAI/docker-git

Length of output: 121


🏁 Script executed:

cat -n packages/app/src/web/app-terminal-session-core.ts | head -50

Repository: ProverCoderAI/docker-git

Length of output: 2491


🏁 Script executed:

rg -A 5 -B 5 'safeDecodeSegment' packages/app/src/web/app-terminal-session-core.ts

Repository: ProverCoderAI/docker-git

Length of output: 937


🏁 Script executed:

# Проверить конфиг эффект-линтера и правила для catchAll
fd -t f '\.eslintrc|eslint\.config|effect.*lint' --type f | head -20

Repository: ProverCoderAI/docker-git

Length of output: 234


🏁 Script executed:

# Поиск других похожих паттернов с catchAll в проекте
rg 'Effect\.catchAll' packages/app/src/ -c

Repository: ProverCoderAI/docker-git

Length of output: 249


🏁 Script executed:

cat packages/app/eslint.config.mts | grep -A 20 -B 5 "effect\|Effect\|catchAll"

Repository: ProverCoderAI/docker-git

Length of output: 8778


🏁 Script executed:

# Проверить, есть ли правила против runSync в CORE модулях
rg 'runSync|runPromise' packages/app/src/ -c

Repository: ProverCoderAI/docker-git

Length of output: 829


🏁 Script executed:

# Проверить гайдлайны: содержит ли проект документацию о CORE/SHELL разделении
fd -t f 'ARCHITECTURE\|process\.md\|CORE.*SHELL' packages/app/ --type f

Repository: ProverCoderAI/docker-git

Length of output: 50


🏁 Script executed:

# Проверить, действительно ли Effect.try поддерживает синтаксис из предложения
rg 'Effect\.try\s*\(\{' packages/ -A 3

Repository: ProverCoderAI/docker-git

Length of output: 6759


🏁 Script executed:

# Посмотреть, как правильно используется Effect.try в проекте
rg 'Effect\.try' packages/app/src -A 2 -B 1 | head -40

Repository: ProverCoderAI/docker-git

Length of output: 2904


🏁 Script executed:

# Проверить, есть ли в проекте примеры обработки URIError специфично
rg 'URIError\|decodeURIComponent' packages/ -B 2 -A 2

Repository: ProverCoderAI/docker-git

Length of output: 50


🏁 Script executed:

# Проверить, находится ли app-terminal-session-core.ts действительно в CORE слое
# и есть ли файлы, которые используют safeDecodeSegment
rg 'safeDecodeSegment' packages/ -B 2 -A 2

Repository: ProverCoderAI/docker-git

Length of output: 1107


Используй Effect.try с параметром catch вместо catchAll.

Функция safeDecodeSegment перехватывает все ошибки через catchAll, что стирает информацию о типе ошибки и нарушает контракт Effect. В проекте уже есть правильный паттерн — используй структуру { try, catch } как в packages/app/src/web/api-terminal.ts:

💡 Исправление
 const safeDecodeSegment = (value: string): string | null =>
   Effect.runSync(
-    Effect.try(() => decodeURIComponent(value)).pipe(
-      Effect.catchAll(() => Effect.succeed(null))
-    )
+    Effect.try({
+      try: () => decodeURIComponent(value),
+      catch: () => null
+    })
   )
🧰 Tools
🪛 GitHub Check: Lint Effect-TS

[failure] 35-35:
Avoid catchAll that discards typed errors; map or propagate explicitly

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/app-terminal-session-core.ts` around lines 32 - 37, The
safeDecodeSegment function currently uses Effect.try with catchAll piped
afterwards, which loses error type information and violates the Effect contract.
Refactor this function to use the parameter form of Effect.try that accepts both
try and catch options, where the try function attempts the decodeURIComponent
operation and the catch parameter handles any thrown error by returning null.
This approach maintains proper error type handling and aligns with the existing
project pattern used in packages/app/src/web/api-terminal.ts.

Source: Linters/SAST tools

Comment on lines +36 to +40
const hostSshConfig = (hostname: string, sshUser: string): string =>
`Host ${hostname}\n User ${sshUser}\n ProxyCommand cloudflared access ssh --hostname %h\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null`

const directSshConfig = (host: string, sshPort: number, sshUser: string): string =>
`Host ${host}-ssh\n HostName ${host}\n Port ${sshPort}\n User ${sshUser}\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Не отключайте проверку host key в SSH-конфигах по умолчанию.

На Line 37 и Line 40 вы принудительно отключаете проверку хоста (StrictHostKeyChecking no + UserKnownHostsFile /dev/null). Это открывает путь к MITM и компрометации отображаемого SSH-пароля.

💡 Предлагаемое исправление
 const hostSshConfig = (hostname: string, sshUser: string): string =>
-  `Host ${hostname}\n  User ${sshUser}\n  ProxyCommand cloudflared access ssh --hostname %h\n  StrictHostKeyChecking no\n  UserKnownHostsFile /dev/null`
+  `Host ${hostname}\n  User ${sshUser}\n  ProxyCommand cloudflared access ssh --hostname %h\n  StrictHostKeyChecking accept-new`

 const directSshConfig = (host: string, sshPort: number, sshUser: string): string =>
-  `Host ${host}-ssh\n  HostName ${host}\n  Port ${sshPort}\n  User ${sshUser}\n  StrictHostKeyChecking no\n  UserKnownHostsFile /dev/null`
+  `Host ${host}-ssh\n  HostName ${host}\n  Port ${sshPort}\n  User ${sshUser}\n  StrictHostKeyChecking accept-new`

As per coding guidelines, "Флагай ... любой ... security-риск".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/panel-vscode-access-panel.tsx` around lines 36 - 40,
Both the hostSshConfig and directSshConfig functions are disabling SSH host key
verification by setting StrictHostKeyChecking no and UserKnownHostsFile
/dev/null, which creates a security vulnerability. Remove these two insecure
configuration lines from both functions to enable proper host key verification
by default.

Source: Coding guidelines

Comment on lines +113 to +115
directCommand: String
.raw`ssh -p ${info.sshPort} -t ${info.sshUser}@${directHost} "cd ${info.targetDir} && exec \$SHELL"`,
directVscodeUri: `vscode://ms-vscode-remote.remote-ssh/open?hostName=${

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unsanitized interpolation is present in the current code.
rg -n 'directCommand:.*targetDir' packages/app/src/web/panel-vscode-access-panel.tsx

# Demonstrate how an unescaped path breaks command boundaries.
python - <<'PY'
target_dir = '"; echo INJECTED; #'
cmd = f'ssh -p 22 -t user@host "cd {target_dir} && exec \\$SHELL"'
print(cmd)
PY

Repository: ProverCoderAI/docker-git

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Check if file exists and list its location
git ls-files | grep -i 'panel-vscode-access-panel'

# If found, read the actual content around lines 113-115
fd -t f 'panel-vscode-access-panel' packages/

Repository: ProverCoderAI/docker-git

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Get more info about the file structure
if [ -f "packages/app/src/web/panel-vscode-access-panel.tsx" ]; then
  echo "=== File exists, reading lines 100-130 ==="
  sed -n '100,130p' packages/app/src/web/panel-vscode-access-panel.tsx | cat -n
else
  echo "File not found at expected path, searching..."
  find . -name '*panel-vscode-access-panel*' -type f 2>/dev/null | head -5
fi

Repository: ProverCoderAI/docker-git

Length of output: 1499


🏁 Script executed:

#!/bin/bash
# Check how copyText is implemented and used
sed -n '1,50p' packages/app/src/web/panel-vscode-access-panel.tsx | grep -A 10 "copyText"

# Search for any existing shell escaping utilities in the project
rg -l "shellEscape|shellQuote|quote.*shell" --type ts --type tsx packages/

Repository: ProverCoderAI/docker-git

Length of output: 337


🏁 Script executed:

#!/bin/bash
# Look for how directCommand is used after construction
sed -n '20,80p' packages/app/src/web/panel-vscode-access-panel.tsx | grep -B 5 -A 5 "directCommand"

# Check if there are any tests for this vulnerability
rg -n "targetDir.*ssh|injection.*ssh" --type ts --type tsx packages/ 2>/dev/null | head -10

Repository: ProverCoderAI/docker-git

Length of output: 712


Экранируйте targetDir в SSH-команде — сейчас возможна инъекция при копипасте.

На строке 114 info.targetDir вставляется в shell-команду без безопасного quoting. Когда пользователь копирует команду через кнопку и вставляет её в терминал, специальные символы (;, ", $(), и т.д.) в пути могут привести к выполнению неожиданных команд.

Функция copyText() записывает строку напрямую в буфер обмена без экранирования. Например, если targetDir = '"; echo hacked; #', результирующая команда в терминале выполнит injected код:

ssh -p 22 -t user@host "cd "; echo hacked; # && exec $SHELL"

Используйте защиту одинарными кавычками с правильным экранированием встроенных кавычек:

💡 Предлагаемое исправление
+const shellSingleQuote = (value: string): string => `'${value.replaceAll("'", `'\\''`)}'`
+
 const buildVsCodeAccessValues = (cfState: CfTunnelState, info: VsCodeAccessInfo): VsCodeAccessValues => {
   const directHost = location.hostname
@@
+  const remoteCommand = `cd -- ${shellSingleQuote(info.targetDir)} && exec "$SHELL"`
   return {
@@
-    directCommand: String
-      .raw`ssh -p ${info.sshPort} -t ${info.sshUser}@${directHost} "cd ${info.targetDir} && exec \$SHELL"`,
+    directCommand: `ssh -p ${info.sshPort} -t ${info.sshUser}@${directHost} ${shellSingleQuote(remoteCommand)}`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/web/panel-vscode-access-panel.tsx` around lines 113 - 115,
The directCommand string in the SSH command construction is vulnerable to shell
injection because info.targetDir is not properly escaped when inserted into the
shell command. To fix this, wrap info.targetDir with single quotes to prevent
special characters like semicolons, double quotes, and dollar signs from being
interpreted as shell syntax. Additionally, properly escape any single quotes
that might appear within the targetDir value itself by replacing them with '\''
(end single quote, escaped single quote, start single quote). This ensures that
when the command is copy-pasted into a terminal, the path is treated as a
literal string regardless of its contents.

Source: Coding guidelines

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

Labels

None yet

Projects

None yet

1 participant