# Balda → SSH → ACP: проверка 2026-10-18 ## Статус этой заметки — 2026-10-19 Ниже сохранён датированный технический прогон 2026-10-18, включая его тогдашние открытые проверки. Текущая эксплуатационная сводка: [[personal/projects/balda/setup]], планы: [[personal/plans/balda-remote-agent]] и [[personal/plans/workspace-gateway]]. С тех пор приняты production SSH/SRT к Mac, SMB доступ/отзыв, shared Beads fixture, native approval bridge и generic SSH Manager 0.3.2 с импортом существующего доверенного доступа Mac и проверкой установленного ключа из клиента; текущая приёмка: [[personal/projects/balda/ssh-client-route-2026-10-19]]. Полный модельный Prism lifecycle и реальные project DB остаются отдельной приёмкой. ## Исторический протокол проверки Проверено на исходниках Balda `a908ee1a5ff2`, runtime `v2.6.3`, ACP adapter `v2.6.1`. Актуальный план: [[personal/plans/balda-remote-agent]]. ## Вывод Штатный `generic_acp.cmd` Balda запускает обычный OpenSSH-клиент и работает с ACP-агентом по SSH без отдельной реализации ACP-транспорта. ## Что именно запускалось - Контрольный SSH-сервер на `golang.org/x/crypto/ssh` в отдельном временном контейнере; клиент — настоящий `/usr/bin/ssh -T` с проверкой закреплённого ключа хоста. - Штатная фабрика провайдеров Balda/runtime, настоящий ADK runner и штатный обработчик разрешений Balda. - Контрольный ACP-агент для разрешения, отказа, отмены и вызова настоящего HTTP MCP-инструмента. - Реальный установленный OpenCode: `initialize`, `session/new`, разрыв SSH, новое подключение и `session/resume`. - Штатные тесты Balda для подтверждения в Mattermost, отклонения чужого ответа и отмены по timeout; сообщений в реальный Mattermost не отправляли. Контейнер работал от `node`, с read-only rootfs, `cap-drop=ALL`, без privileged и host mounts. Каталог `/Users/admin/Developer` и production `/state/state.db` внутри отсутствовали. Тестовые контейнеры и производный образ удалены; исходные Balda и Eagle продолжили работу. ## Выявленные условия - Высокоуровневый ACP adapter Balda проверяет `cwd` локально; путь, существующий только на удалённом хосте, отклоняется до создания ACP-сессии. - Для прямого SSH нужны согласованные абсолютные пути либо отдельное сопоставление cwd; один `cd` в удалённой команде не меняет `session/new.cwd`. - MCP-адреса должны быть достижимы из удалённой среды и проходить проверку HTTP hostname; Balda автоматически не создаёт SSH port forwarding. - В тесте вызов MCP прошёл по явно настроенному достижимому адресу; проверка DNS rebinding разрешала только тестовые hostname. - `permissions.mode: ask` обрабатывает `session/request_permission`, поступивший от агента; режим самого агента также должен запрашивать разрешения. - Файловая изоляция обеспечивается правами ОС и sandbox среды; выбор cwd её не заменяет. - На момент проверки рабочая конфигурация Balda сохраняла `allow_all`; тест не менял её. ## Границы доказательства Модели не вызывались; полный Prism lifecycle и реальные удалённые Claude/Codex не запускались. Проверка протокола не является приёмкой production SSH-хоста, полной политики sandbox каждого агента или доступа к исходной БД Beads. Для реального OpenCode проверены создание и возобновление сессии, а подтверждения и вызов MCP проверены контрольным ACP-агентом. Поддержка продолжения сессии зависит от capability и сохранённого состояния конкретного агента. ## Готовые проекты - [Scarf: ACP Subprocess](https://github.com/awizemann/scarf/wiki/ACP-Subprocess) и исходный `ProcessACPChannel.swift`: ACP поверх `ssh -T`; изучены документация и код, приложение не запускалось и не является плагином Balda. - [acp-tunnel](https://github.com/benthecarman/acp-tunnel), revision `6e0543cd5692a0c2a27f5a7b11fb1ad536315461`: просмотрены исходники преобразования cwd и MCP policy; транспорт WebSocket, бинарник не запускался. - Утверждение, что такой прослойки нигде нет и её обязательно писать заново, проверкой опровергнуто. ## Сохранённый отчёт ```json { "source": { "balda": "a908ee1a5ff2", "runtime": "v2.6.3", "acpagent": "v2.6.1", "remote_image": "balda-prism:20261018" }, "verified": { "native_generic_acp_with_real_ssh": true, "native_allow_and_deny_callback_roundtrip": true, "ask_without_context_denied": true, "native_mattermost_permission_unit_tests": true, "wrong_requester_rejected_unit_test": true, "permission_timeout_cancels_unit_test": true, "actual_remote_http_mcp_tool_call": true, "local_only_cwd_validation_reproduced": true, "prompt_cancellation_bounded_ms": 253, "fixture_session_resume_after_ssh_reconnect": true, "real_opencode_initialize_and_new_session_over_ssh": true, "real_opencode_resume_after_ssh_reconnect": true, "pinned_host_key_mismatch_rejected": true, "test_container_host_repositories_and_production_state_unavailable": true }, "requirements": [ "SSH credentials and pinned host key", "Prepared remote ACP agent and native authentication", "Remote repository present; matching cwd paths or cwd mapping adapter", "MCP endpoint reachable from remote environment with correct hostname policy", "OS filesystem isolation independently of ACP permissions", "Native ask policy for interactive review; production config currently allow_all" ], "limits": { "model_inference_performed": false, "production_config_changed": false, "production_sessions_reset": false, "external_chat_messages_sent": false, "real_claude_or_codex_remote_runs_tested": false, "full_prism_lifecycle_tested": false, "acp_tunnel_binary_run": false, "scarf_application_run": false }, "existing_projects": { "acp_tunnel": { "revision": "6e0543cd5692a0c2a27f5a7b11fb1ad536315461", "assessment": "Source reviewed: WebSocket transport, cwd mapping and MCP policy; no SSH transport; not required for tested direct SSH path" }, "scarf": "Primary source reviewed: ssh -T carries ACP and permission requests; separate application, not a Balda plugin" }, "cleanup": "Both temporary containers and their derived image removed; baseline image preserved", "evidence": [ "compat.log", "opencode-live.log", "compat_test.go", "cmd/fixture/main.go" ], "evidence_sha256": { "compat.log": "5952082d73d418b41b0d92661305926ed5f5a902a250f735562a417ddc7ab2b1", "opencode-live.log": "4a9fcaa1fe7619dbb371bffad6327c7364a6f5dfd3bc34c973aa8323eb05df5d", "compat_test.go": "d3eb2ccc55c4b5ce7b479a3f7d02689219b6c9a32880f76efb9a79665fa0ded5", "cmd/fixture/main.go": "90188cb209f97b5b11e015f3c76decc7219398dd66abc037ea013dea9aaec80b" } } ``` ## Протокол SSH-проверки ```text === RUN TestNativeGenericACPThroughSSH === RUN TestNativeGenericACPThroughSSH/approve === RUN TestNativeGenericACPThroughSSH/deny 2026/10/18 16:21:01 INFO connection closed component=runtime.agentfactory.acp subcomponent=acpagent.client cause="peer connection closed" 2026/10/18 16:21:01 INFO connection closed component=runtime.agentfactory.acp subcomponent=acpagent.client cause="peer connection closed" === RUN TestNativeGenericACPThroughSSH/ask_missing_context_fails_closed 2026/10/18 16:21:01 INFO connection closed component=runtime.agentfactory.acp subcomponent=acpagent.client cause="peer connection closed" --- PASS: TestNativeGenericACPThroughSSH (0.16s) --- PASS: TestNativeGenericACPThroughSSH/approve (0.05s) --- PASS: TestNativeGenericACPThroughSSH/deny (0.05s) --- PASS: TestNativeGenericACPThroughSSH/ask_missing_context_fails_closed (0.06s) === RUN TestMCPRealToolThroughSSH 2026/10/18 16:21:01 INFO connection closed component=runtime.agentfactory.acp subcomponent=acpagent.client cause="peer connection closed" --- PASS: TestMCPRealToolThroughSSH (0.06s) === RUN TestNativeRemoteOnlyCWDRejectedLocally compat_test.go:124: observed local validation: stat acp session cwd "/tmp/acp-remote-only-20261018": stat /tmp/acp-remote-only-20261018: no such file or directory 2026/10/18 16:21:01 INFO connection closed component=runtime.agentfactory.acp subcomponent=acpagent.client cause="peer connection closed" --- PASS: TestNativeRemoteOnlyCWDRejectedLocally (0.03s) === RUN TestCancellationThroughSSH compat_test.go:135: native cancellation completed in 252.165125ms: context deadline exceeded 2026/10/18 16:21:02 INFO connection closed component=runtime.agentfactory.acp subcomponent=acpagent.client cause="peer connection closed" --- PASS: TestCancellationThroughSSH (0.28s) === RUN TestResumeAfterSSHReconnect --- PASS: TestResumeAfterSSHReconnect (0.09s) === RUN TestRealOpenCodeACPInitializeAndSessionOverSSH compat_test.go:194: real OpenCode ACP initialization and session/new succeeded without inference --- PASS: TestRealOpenCodeACPInitializeAndSessionOverSSH (0.81s) === RUN TestPinnedHostKeyRejectsMismatch --- PASS: TestPinnedHostKeyRejectsMismatch (0.01s) === RUN TestConfigurationRecord compat_test.go:226: {"acpagent":"v2.6.1","balda":"a908ee1a","model_inference":false,"production_changes":false,"runtime":"v2.6.3"} --- PASS: TestConfigurationRecord (0.00s) PASS ``` ## Реальный OpenCode после обязательного initialize и с повторным SSH ```text === RUN TestRealOpenCodeACPInitializeAndSessionOverSSH compat_test.go:189: real OpenCode capabilities: {"auth":{},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"sessionCapabilities":{"close":{},"fork":{},"list":{},"resume":{}}} compat_test.go:198: real OpenCode ACP initialization and session/new succeeded without inference compat_test.go:213: real OpenCode session/resume after SSH reconnect succeeded without inference --- PASS: TestRealOpenCodeACPInitializeAndSessionOverSSH (1.56s) PASS ```