Skip to content

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

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

feat(app): per-container CF SSH tunnel for VS Code panel#434
skulidropek wants to merge 19 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
    • Добавлены ссылки для совместного доступа с настраиваемым временем истечения
    • Реализован SSH-доступ через Cloudflare туннели для VS Code Remote SSH
    • Добавлена аутентификация по паролю SSH для общих проектов
    • Создан интерфейс управления ссылками совместного доступа (создание, отзыв)
    • Добавлена панель доступа VS Code с конфигурацией SSH и опциями подключения

Walkthrough

Добавлена система share-ссылок для VS Code Remote SSH и браузерного доступа к контейнерам: новые сервисы хранения токенов, управления SSH-паролем и Cloudflare-туннелями на бэкенде; HTTP-эндпоинты создания/удаления/резолва ссылок; клиентский API; публичная страница по токену; панель управления ссылками и кнопка 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 и функция buildShareLinkSshAccess для построения SSH-конфигурации и 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 chpasswd), 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(): резолв по токену, создание (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
WebAppRoute расширен вариантом ShareLink с projectKey и shareToken; resolveWebAppRoute принимает search и извлекает токен t из query-строки пути /ssh/:projectKey?t=... (валидация: 16 hex-символов); App передаёт location.search и рендерит AppShareLink в новой ветке.
Публичная страница share-link
packages/app/src/web/app-share-link.tsx
Компонент AppShareLink со состояниями Loading/Error/Info/Connecting/Terminal/Closed; блоки SshConfigBlock, CfTunnelSetupBlock, SshPasswordBlock и InfoHeader для отображения конфига, Cloudflare-инструкций и VS Code URI; connectTerminalSession создаёт терминальную сессию через API; TerminalView подключает TerminalPanel и управляет жизненным циклом сессии.
Панель VS Code SSH в терминале
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 (loading/failed/ready); VsCodeAccessPanel отображает SSH-команду и VS Code URI; при открытии панели запускается startProjectSshTunnel через useEffect; проп onOpenVsCode протянут от TerminalPanelProps через TerminalPanelLayout до кнопки "VS Code" в TerminalHeader.
Управление ссылками в 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 и условно рендерит секцию.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(100, 149, 237, 0.5)
    Note over Browser,makeRouter: Создание share-link
  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: /ssh/:projectKey?t=<token>
  AppShareLink->>loadShareLink: GET /share-links/:token?host=...
  loadShareLink-->>AppShareLink: ShareLinkInfo
  AppShareLink-->>Browser: Экран Info (SSH-конфиг, VS Code URI, кнопка подключения)
  Browser->>AppShareLink: нажать "connect terminal"
  AppShareLink->>makeRouter: createProjectTerminalSession
  makeRouter-->>AppShareLink: сессия создана
  AppShareLink-->>Browser: Терминальный экран
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> маршрутов через параметр селектора.

Important

Pre-merge checks failed

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

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Requirements Alignment ❌ Error Diff contradicts visible spec and review feedback: (1) review comments at lines 229-239 (async/await violation) and 169-180 (Effect.match pattern) in panel-share.tsx remain unaddressed; (2) promise... Address review feedback in panel-share.tsx, add unit tests for new services, and clarify scope by either documenting share-links feature or isolating it to separate PR.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed Заголовок точно отражает основное изменение: добавление per-container Cloudflare SSH туннелей для VS Code панели.
Description check ✅ Passed PR описание содержит Summary с основными изменениями и User flow, User proof of fix. Хотя отсутствуют некоторые разделы шаблона, основная информация о функциональности представлена достаточно полно.
Linked Issues check ✅ Passed PR полностью соответствует требованиям #428: реализована синхронизация с VS Code через per-container SSH туннели, позволяющие открывать контейнеры через внешние инструменты.
Out of Scope Changes check ✅ Passed Все изменения в PR направлены на реализацию per-container CF SSH туннелей для VS Code панели и связанной функциональности управления доступом, что полностью соответствует заявленным целям.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Security Regression ✅ Passed No high-confidence security regressions found. SSH credentials properly generated with crypto.randomBytes, passed via env vars not string interpolation, temp directories use randomUUID, docker exec...
✨ 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 on lines +83 to +85
const copyText = (text: string): void => {
void navigator.clipboard.writeText(text).catch(() => {})
}

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 | ⚡ Quick win

Дублирование кода: copyText и WILDCARD_SSH_CONFIG.

Функция copyText и константа WILDCARD_SSH_CONFIG дублируются в app-ready-terminal-pane.tsx (строки 170-175). Рекомендуется вынести в общий модуль утилит.

♻️ Предлагаемое решение

Создать общий модуль, например packages/app/src/web/ssh-utils.ts:

// ssh-utils.ts
export const WILDCARD_SSH_CONFIG = `Host *.trycloudflare.com
  ProxyCommand cloudflared access ssh --hostname %h
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null`

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

Also applies to: 113-116

🤖 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.tsx` around lines 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.

Comment on lines +30 to +36
const safeDecodeSegment = (value: string): string | null => {
try {
return decodeURIComponent(value)
} catch {
return 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.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Нарушение гайдлайна: try/catch в продуктовом коде.

Статический анализатор флагает try/catch. По гайдлайнам следует использовать Effect.try.

Однако здесь это boundary-декодирование синхронного decodeURIComponent в чистой CORE-функции роутинга. Поскольку функция должна оставаться синхронной и чистой (PURITY: CORE), а decodeURIComponent бросает исключение только при невалидном input, можно либо:

  1. Оставить как есть с обоснованием (граничный случай декодирования URL)
  2. Вынести в отдельный "аксиоматический" модуль boundary-декодирования
🧰 Tools
🪛 GitHub Check: Lint Effect-TS

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

🤖 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 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.

Sources: Coding guidelines, Linters/SAST tools

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
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 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 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

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