feat(app): per-container CF SSH tunnel for VS Code panel#434
feat(app): per-container CF SSH tunnel for VS Code panel#434skulidropek wants to merge 19 commits into
Conversation
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>
📝 WalkthroughSummary by CodeRabbitЗаметки о выпуске
WalkthroughДобавлена система share-ссылок для VS Code Remote SSH и браузерного доступа к контейнерам: новые сервисы хранения токенов, управления SSH-паролем и Cloudflare-туннелями на бэкенде; HTTP-эндпоинты создания/удаления/резолва ссылок; клиентский API; публичная страница по токену; панель управления ссылками и кнопка VS Code в терминале. ChangesShare Links & VS Code SSH Access
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: Терминальный экран
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error)
✅ Passed checks (6 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (19)
packages/api/src/api/contracts.tspackages/api/src/api/openapi.tspackages/api/src/api/schema.tspackages/api/src/http.tspackages/api/src/services/project-share-links.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/services/ssh-project-tunnels.tspackages/api/src/services/ssh-share-link-tunnels.tspackages/app/src/web/api-share-links.tspackages/app/src/web/app-ready-main-panels.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/app/src/web/app-share-link.tsxpackages/app/src/web/app-terminal-session-core.tspackages/app/src/web/app.tsxpackages/app/src/web/panel-share.tsxpackages/lib/src/usecases/ssh-access.tspackages/terminal/src/web/panel-terminal-header.tsxpackages/terminal/src/web/panel-terminal-types.tspackages/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.tspackages/api/src/services/project-share-links.tspackages/app/src/web/app-ready-main-panels.tsxpackages/lib/src/usecases/ssh-access.tspackages/terminal/src/web/panel-terminal-header.tsxpackages/api/src/api/contracts.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/api/schema.tspackages/api/src/api/openapi.tspackages/app/src/web/api-share-links.tspackages/app/src/web/app-terminal-session-core.tspackages/app/src/web/app-share-link.tsxpackages/app/src/web/panel-share.tsxpackages/terminal/src/web/panel-terminal.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-project-tunnels.tspackages/api/src/services/ssh-share-link-tunnels.tspackages/api/src/http.tspackages/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.tspackages/api/src/services/project-share-links.tspackages/app/src/web/app-ready-main-panels.tsxpackages/lib/src/usecases/ssh-access.tspackages/terminal/src/web/panel-terminal-header.tsxpackages/api/src/api/contracts.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/api/schema.tspackages/api/src/api/openapi.tspackages/app/src/web/api-share-links.tspackages/app/src/web/app-terminal-session-core.tspackages/app/src/web/app-share-link.tsxpackages/app/src/web/panel-share.tsxpackages/terminal/src/web/panel-terminal.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-project-tunnels.tspackages/api/src/services/ssh-share-link-tunnels.tspackages/api/src/http.tspackages/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.tspackages/api/src/services/project-share-links.tspackages/app/src/web/app-ready-main-panels.tsxpackages/lib/src/usecases/ssh-access.tspackages/terminal/src/web/panel-terminal-header.tsxpackages/api/src/api/contracts.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/api/schema.tspackages/api/src/api/openapi.tspackages/app/src/web/api-share-links.tspackages/app/src/web/app-terminal-session-core.tspackages/app/src/web/app-share-link.tsxpackages/app/src/web/panel-share.tsxpackages/terminal/src/web/panel-terminal.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-project-tunnels.tspackages/api/src/services/ssh-share-link-tunnels.tspackages/api/src/http.tspackages/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 useanytype annotation in TypeScript; useunknownonly at SHELL boundaries for decoding, never exportunknownoutside boundary modules
Never useastype assertions in normal code; only permitasin a single 'axiomatic' module (brands, constructors, constants) after which types flow safely without casts
Always use exhaustive pattern matching for union types through.exhaustive()orMatch.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.tspackages/api/src/services/project-share-links.tspackages/app/src/web/app-ready-main-panels.tsxpackages/lib/src/usecases/ssh-access.tspackages/terminal/src/web/panel-terminal-header.tsxpackages/api/src/api/contracts.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/api/schema.tspackages/api/src/api/openapi.tspackages/app/src/web/api-share-links.tspackages/app/src/web/app-terminal-session-core.tspackages/app/src/web/app-share-link.tsxpackages/app/src/web/panel-share.tsxpackages/terminal/src/web/panel-terminal.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-project-tunnels.tspackages/api/src/services/ssh-share-link-tunnels.tspackages/api/src/http.tspackages/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.tspackages/api/src/services/project-share-links.tspackages/app/src/web/app-ready-main-panels.tsxpackages/lib/src/usecases/ssh-access.tspackages/api/src/api/contracts.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/api/schema.tspackages/api/src/api/openapi.tspackages/app/src/web/api-share-links.tspackages/app/src/web/app-terminal-session-core.tspackages/app/src/web/app-share-link.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-project-tunnels.tspackages/api/src/services/ssh-share-link-tunnels.tspackages/api/src/http.tspackages/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.tspackages/api/src/services/project-share-links.tspackages/app/src/web/app-ready-main-panels.tsxpackages/lib/src/usecases/ssh-access.tspackages/terminal/src/web/panel-terminal-header.tsxpackages/api/src/api/contracts.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/api/schema.tspackages/api/src/api/openapi.tspackages/app/src/web/api-share-links.tspackages/app/src/web/app-terminal-session-core.tspackages/app/src/web/app-share-link.tsxpackages/app/src/web/panel-share.tsxpackages/terminal/src/web/panel-terminal.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-project-tunnels.tspackages/api/src/services/ssh-share-link-tunnels.tspackages/api/src/http.tspackages/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.tspackages/api/src/services/project-share-links.tspackages/app/src/web/app-ready-main-panels.tsxpackages/lib/src/usecases/ssh-access.tspackages/terminal/src/web/panel-terminal-header.tsxpackages/api/src/api/contracts.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/api/schema.tspackages/api/src/api/openapi.tspackages/app/src/web/api-share-links.tspackages/app/src/web/app-terminal-session-core.tspackages/app/src/web/app-share-link.tsxpackages/app/src/web/panel-share.tsxpackages/terminal/src/web/panel-terminal.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-project-tunnels.tspackages/api/src/services/ssh-share-link-tunnels.tspackages/api/src/http.tspackages/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.tspackages/api/src/services/project-share-links.tspackages/app/src/web/app-ready-main-panels.tsxpackages/lib/src/usecases/ssh-access.tspackages/terminal/src/web/panel-terminal-header.tsxpackages/api/src/api/contracts.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/api/schema.tspackages/api/src/api/openapi.tspackages/app/src/web/api-share-links.tspackages/app/src/web/app-terminal-session-core.tspackages/app/src/web/app-share-link.tsxpackages/app/src/web/panel-share.tsxpackages/terminal/src/web/panel-terminal.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-project-tunnels.tspackages/api/src/services/ssh-share-link-tunnels.tspackages/api/src/http.tspackages/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.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/services/ssh-project-tunnels.tspackages/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!
| 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 | ||
| }) |
There was a problem hiding this comment.
Потенциальная гонка при одновременном создании 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.
| 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 }) | ||
| } |
There was a problem hiding this comment.
🧹 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.
| 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]) |
There was a problem hiding this comment.
🧹 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.
| const copyText = (text: string): void => { | ||
| void navigator.clipboard.writeText(text).catch(() => {}) | ||
| } |
There was a problem hiding this comment.
🧹 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.
| const safeDecodeSegment = (value: string): string | null => { | ||
| try { | ||
| return decodeURIComponent(value) | ||
| } catch { | ||
| return null | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Нарушение гайдлайна: try/catch в продуктовом коде.
Статический анализатор флагает try/catch. По гайдлайнам следует использовать Effect.try.
Однако здесь это boundary-декодирование синхронного decodeURIComponent в чистой CORE-функции роутинга. Поскольку функция должна оставаться синхронной и чистой (PURITY: CORE), а decodeURIComponent бросает исключение только при невалидном input, можно либо:
- Оставить как есть с обоснованием (граничный случай декодирования URL)
- Вынести в отдельный "аксиоматический" модуль 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
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>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@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
📒 Files selected for processing (3)
packages/api/src/http.tspackages/app/src/web/api-share-links.tspackages/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.tspackages/api/src/http.tspackages/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.tspackages/api/src/http.tspackages/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.tspackages/api/src/http.tspackages/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 useanytype annotation in TypeScript; useunknownonly at SHELL boundaries for decoding, never exportunknownoutside boundary modules
Never useastype assertions in normal code; only permitasin a single 'axiomatic' module (brands, constructors, constants) after which types flow safely without casts
Always use exhaustive pattern matching for union types through.exhaustive()orMatch.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.tspackages/api/src/http.tspackages/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.tspackages/api/src/http.tspackages/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.tspackages/api/src/http.tspackages/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.tspackages/api/src/http.tspackages/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.tspackages/api/src/http.tspackages/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
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>
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
packages/api/src/http.tspackages/api/src/services/ssh-project-tunnels.tspackages/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.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/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.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/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.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/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 useanytype annotation in TypeScript; useunknownonly at SHELL boundaries for decoding, never exportunknownoutside boundary modules
Never useastype assertions in normal code; only permitasin a single 'axiomatic' module (brands, constructors, constants) after which types flow safely without casts
Always use exhaustive pattern matching for union types through.exhaustive()orMatch.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.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/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.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/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.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/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.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/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.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/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
| export const startSshProjectTunnel = ( | ||
| projectKey: string, | ||
| sshPort: number, | ||
| containerName: string | ||
| ): Effect.Effect<{ hostname: string | null; sshPassword: string }, ApiInternalError> => |
There was a problem hiding this comment.
🧹 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
- 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>
Agent Plan UpdateBranch: 1. Planning DecisionSource: claude - Captured: 2026-06-19T21:48:33Z Questions:
Answer: 2. PlanSource: claude - Captured: 2026-06-19T22:15:30Z Plan: URL-Based Container Access Sharing (Issue #428)ContextПользователь хочет генерировать share-ссылки для контейнеров вида: Открыв ссылку в браузере, получаешь web-терминал к контейнеру + SSH-конфиг для VS Code Remote SSH. Ключевые находки из кодовой базы:
URL-формат и разграничение токенов
Функция детекции: Шаги реализации1. Новый backend-сервис
|
| Файл | Действие |
|---|---|
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 |
Добавить секцию |
Верификация
curl -X POST http://localhost:3334/projects/by-key/<key>/share-links→{ok, link, url}с 16-char hex token- Открыть URL в браузере → рендерится
AppShareLink(не дашборд) - Компонент показывает SSH-конфиг и терминал
- "Open in VS Code" кнопка генерирует
vscode://ms-vscode-remote.remote-ssh/...URI curl /share-links/invalidtoken→ 404curl -X DELETE .../share-links/<token>→ последующий GET → 404?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 | nullUses 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 → loadingimmediately - 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:
- Loading: "Starting Cloudflare tunnel…"
- Ready:
- One-time wildcard setup block (reuse
WILDCARD_SSH_CONFIGconstant fromapp-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
- One-time wildcard setup block (reuse
- 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
cd packages/api && bun run build→ no TS errorscd packages/app && bun run build:web→ no TS errorsdocker cp packages/app/dist-web/. dg-docker-git-issue-428:/home/dev/app/packages/app/dist-web/- Open panel → open any running container terminal → click "VS Code"
- Panel briefly shows "Starting Cloudflare tunnel…"
- Then shows:
- Wildcard SSH config block (one-time setup)
ssh -o "ProxyCommand=..." dev@XXXX.trycloudflare.comwith copy button- "Open in VS Code (CF tunnel)" link
- No localhost config anywhere
- Copy SSH command → run on local machine with
cloudflaredinstalled → 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"`
: nullThe 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
bun run build:web— no TS errors- Deploy to host dist-web
- 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) : nullThe 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
bun run build:web— no TS errors- Deploy to host dist-web
- Open panel → verify SSH config no longer has RemoteCommand/RequestTTY
- 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
Панель показывает два режима:
- Cloudflare tunnel — для доступа из интернета (уже есть)
- Direct SSH — для локальной сети / без интернета, по IP:port
Пользователь на одном LAN может подключиться по прямому SSH без cloudflared.
Verification
bun run build:web— no TS errors- Deploy to host dist-web
- Open panel → VS Code section показывает оба блока
- Direct SSH command:
ssh -p PORT dev@192.168.0.206— подключается к контейнеру - 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
Панель показывает два режима:
- Cloudflare tunnel — для доступа из интернета (уже есть)
- Direct SSH — для локальной сети / без интернета, по IP:port
Пользователь на одном LAN может подключиться по прямому SSH без cloudflared.
Verification
bun run build:web— no TS errors- Deploy to host dist-web
- Open panel → VS Code section показывает оба блока
- Direct SSH command:
ssh -p PORT dev@192.168.0.206— подключается к контейнеру - 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.
Нужно добавить:
- SSH пароль — тот же
devuser password что и для CF tunnel - Автооткрытие папки —
directCommandдолжен открывать/home/dev/appсразу
Why no new API needed
При открытии панели CF туннель стартует автоматически (useEffect → startProjectSshTunnel).
Это генерирует пароль и ставит его на контейнер. 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
bun run build:web— no TS errors- Deploy
- Открыть панель → Direct SSH section показывает пароль (после старта туннеля)
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.tsxCheck / 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
cd packages/app && bun run lint— no ESLint errorscd packages/api && bun run build— no TS errorscd packages/app && bun run build:web— no TS errors- Deploy + open panel → verify tunnel starts, password shown
- Push → CI Lint + Lint Effect-TS should pass
AI Session BackupCommit: 9132fdc
|
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
packages/api/src/api/contracts.tspackages/api/src/services/ssh-password-setup.tspackages/api/src/services/ssh-project-tunnels.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/app/src/web/app-share-link.tsxpackages/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.tspackages/app/src/web/panel-share.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-password-setup.tspackages/app/src/web/app-share-link.tsxpackages/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.tspackages/app/src/web/panel-share.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-password-setup.tspackages/app/src/web/app-share-link.tsxpackages/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.tspackages/app/src/web/panel-share.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-password-setup.tspackages/app/src/web/app-share-link.tsxpackages/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 useanytype annotation in TypeScript; useunknownonly at SHELL boundaries for decoding, never exportunknownoutside boundary modules
Never useastype assertions in normal code; only permitasin a single 'axiomatic' module (brands, constructors, constants) after which types flow safely without casts
Always use exhaustive pattern matching for union types through.exhaustive()orMatch.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.tspackages/app/src/web/panel-share.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-password-setup.tspackages/app/src/web/app-share-link.tsxpackages/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.tspackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-password-setup.tspackages/app/src/web/app-share-link.tsxpackages/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.tspackages/app/src/web/panel-share.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-password-setup.tspackages/app/src/web/app-share-link.tsxpackages/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.tspackages/app/src/web/panel-share.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-password-setup.tspackages/app/src/web/app-share-link.tsxpackages/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.tspackages/app/src/web/panel-share.tsxpackages/app/src/web/app-ready-terminal-pane.tsxpackages/api/src/services/ssh-password-setup.tspackages/app/src/web/app-share-link.tsxpackages/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.tspackages/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. Это решает проблему, отмеченную в предыдущем ревью.
| 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 }) | ||
| }) | ||
| ) |
There was a problem hiding this comment.
Разделите результат мутации и последующий 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.
| onClick={async () => { | ||
| const url = state.newUrl | ||
| if (url !== null) { | ||
| try { | ||
| await navigator.clipboard.writeText(url) | ||
| } catch { | ||
| // ignore clipboard errors | ||
| } | ||
| } | ||
| }} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "panel-share.tsx" -type f 2>/dev/null | head -5Repository: 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
fiRepository: ProverCoderAI/docker-git
Length of output: 109
🏁 Script executed:
# Also check app structure
ls -la packages/app/src/web/ 2>/dev/null | head -20Repository: ProverCoderAI/docker-git
Length of output: 1401
🏁 Script executed:
sed -n '220,245p' packages/app/src/web/panel-share.tsxRepository: 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: 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 -40Repository: 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/ -cRepository: 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
Summary
ssh-project-tunnels.tsservice starts a dedicated Cloudflare quick tunnel per container keyed byprojectKey(separate from share-link tunnels)POST /projects/by-key/:key/ssh-tunnelAPI route that starts the tunnel and returns the CF hostnameUser flow
~/.ssh/configblock (copy button)ssh -o "ProxyCommand=cloudflared access ssh --hostname %h" dev@XXXX.trycloudflare.com(copy button)Proof of fix
localhostSSH config useless for external accesssorts-anyway-mortgage-gpl.trycloudflare.com) after clicking VS Code ondg-docker-git-issue-428Closes #428
🤖 Generated with Claude Code