12 KiB
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 и исходный
ProcessACPChannel.swift: ACP поверхssh -T; изучены документация и код, приложение не запускалось и не является плагином Balda. - acp-tunnel, revision
6e0543cd5692a0c2a27f5a7b11fb1ad536315461: просмотрены исходники преобразования cwd и MCP policy; транспорт WebSocket, бинарник не запускался. - Утверждение, что такой прослойки нигде нет и её обязательно писать заново, проверкой опровергнуто.
Сохранённый отчёт
{
"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-проверки
=== 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
=== 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