mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-01 13:25:18 +08:00
Expose redacted network diagnostics for migration installation failures
Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
fefe4d8552
commit
079e945b19
@@ -130,3 +130,10 @@ uv run python tests/manual/mcp_smoke.py # MCP server e2e smoke
|
||||
- `langbot-deploy` — Docker/compose deployment + config.
|
||||
- `langbot-mcp-ops` — operating the LangBot MCP server.
|
||||
- [`langbot-cli`](https://github.com/langbot-app/langbot-cli) — `lbctl`, a standalone Service API client for managing running Workspaces.
|
||||
|
||||
### Migration installation diagnostics
|
||||
|
||||
Migration remains an HTTP-only operation. Poll its system task handle and inspect
|
||||
`runtime.result.installations[].diagnostic`: `kind` is an allowlisted network
|
||||
category and `http_status` is present for HTTP failures. Never expose raw exception
|
||||
messages, request URLs, headers or response bodies in this diagnostic object.
|
||||
|
||||
@@ -98,6 +98,23 @@ class MigrationInstallContext(TaskContext):
|
||||
self.progress[key] = min(value, 100) if key == 'progress_percent' else value
|
||||
self.progress['updated_at'] = time.time()
|
||||
|
||||
def record_network_failure(self, exc: httpx.HTTPError):
|
||||
# Deliberate allowlist: never serialize exception messages, URLs,
|
||||
# headers, proxy credentials or response bodies into task metadata.
|
||||
kinds = (
|
||||
(httpx.ConnectTimeout, 'connect_timeout'),
|
||||
(httpx.ReadTimeout, 'read_timeout'),
|
||||
(httpx.WriteTimeout, 'write_timeout'),
|
||||
(httpx.PoolTimeout, 'pool_timeout'),
|
||||
(httpx.ConnectError, 'connect_error'),
|
||||
(httpx.RemoteProtocolError, 'incomplete_response'),
|
||||
(httpx.HTTPStatusError, 'http_status'),
|
||||
)
|
||||
diagnostic = {'kind': next((name for cls, name in kinds if isinstance(exc, cls)), 'network_error')}
|
||||
if isinstance(exc, httpx.HTTPStatusError):
|
||||
diagnostic['http_status'] = exc.response.status_code
|
||||
self.progress['diagnostic'] = diagnostic
|
||||
|
||||
def finish(self, code=None):
|
||||
self.publish()
|
||||
self.progress.update(status='failed' if code else 'completed', code=code, finished_at=time.time())
|
||||
@@ -562,15 +579,18 @@ class PipelineMigrationService:
|
||||
install_code = 'plugin_runtime_unavailable'
|
||||
except MarketplacePluginVersionNotFoundError:
|
||||
install_code = 'plugin_version_unavailable'
|
||||
except httpx.TimeoutException:
|
||||
except httpx.TimeoutException as exc:
|
||||
install_context.record_network_failure(exc)
|
||||
install_code = 'plugin_download_timeout'
|
||||
except httpx.HTTPStatusError as exc:
|
||||
install_context.record_network_failure(exc)
|
||||
install_code = (
|
||||
'plugin_version_unavailable'
|
||||
if exc.response.status_code == 404
|
||||
else 'plugin_marketplace_unavailable'
|
||||
)
|
||||
except httpx.RequestError:
|
||||
except httpx.RequestError as exc:
|
||||
install_context.record_network_failure(exc)
|
||||
install_code = 'plugin_download_failed'
|
||||
except PluginInstallationFailedError as exc:
|
||||
install_code = {
|
||||
|
||||
@@ -12,7 +12,9 @@ Scope (first version): core read operations plus the most common writes for
|
||||
bots, pipelines, LLM/embedding models, knowledge bases, MCP servers, skills,
|
||||
and read-only system info. This intentionally does NOT expose every one of the
|
||||
~25 HTTP route groups — that keeps the agent surface small, safe, and
|
||||
maintainable. Extend deliberately.
|
||||
maintainable. Extend deliberately. Pipeline migration remains HTTP-only; its
|
||||
installation task metadata includes credential-free diagnostic.kind and optional
|
||||
diagnostic.http_status fields, never raw network exception text.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -951,3 +951,31 @@ async def test_missing_marketplace_release_has_specific_migration_result(env):
|
||||
assert all(r['code'] == 'plugin_version_unavailable' for r in metadata['results'])
|
||||
assert metadata['installations'][0]['code'] == 'plugin_version_unavailable'
|
||||
assert 'private-token' not in str(metadata)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('kind', ['connect', 'read_timeout', 'truncated', 'http'])
|
||||
def test_network_diagnostic_is_allowlisted_and_credential_free(kind):
|
||||
import httpx
|
||||
from langbot.pkg.api.http.service.pipeline_migration import MigrationInstallContext
|
||||
|
||||
request = httpx.Request('GET', 'https://secret-user:[email protected]/package?token=private-token')
|
||||
errors = {
|
||||
'connect': httpx.ConnectError('private-token', request=request),
|
||||
'read_timeout': httpx.ReadTimeout('private-token', request=request),
|
||||
'truncated': httpx.RemoteProtocolError('private-token', request=request),
|
||||
'http': httpx.HTTPStatusError(
|
||||
'private-token', request=request, response=httpx.Response(503, request=request, text='private-token')
|
||||
),
|
||||
}
|
||||
ctx = MigrationInstallContext({'author': 'team', 'name': 'Runner', 'version': '1'})
|
||||
ctx.record_network_failure(errors[kind])
|
||||
expected = {
|
||||
'connect': 'connect_error',
|
||||
'read_timeout': 'read_timeout',
|
||||
'truncated': 'incomplete_response',
|
||||
'http': 'http_status',
|
||||
}
|
||||
assert ctx.progress['diagnostic']['kind'] == expected[kind]
|
||||
assert ctx.progress['diagnostic'].get('http_status') == (503 if kind == 'http' else None)
|
||||
assert 'private-token' not in str(ctx.progress)
|
||||
assert 'secret-' not in str(ctx.progress)
|
||||
|
||||
@@ -166,6 +166,16 @@ export default function MigrationInstallProgress({
|
||||
<p className="break-all text-muted-foreground">
|
||||
{item.author}/{item.name}
|
||||
</p>
|
||||
{item.diagnostic && (
|
||||
<p className="text-destructive">
|
||||
{t('pipelineMigration.networkDiagnostic', {
|
||||
kind: item.diagnostic.kind,
|
||||
})}
|
||||
{item.diagnostic.http_status !== undefined
|
||||
? ` · HTTP ${item.diagnostic.http_status}`
|
||||
: ''}
|
||||
</p>
|
||||
)}
|
||||
{item.steps.map((step, index) => (
|
||||
<div key={index} className="flex justify-between gap-2">
|
||||
<span
|
||||
|
||||
@@ -66,6 +66,7 @@ export interface MigrationInstallation {
|
||||
status: 'installing' | 'completed' | 'failed';
|
||||
stage: string;
|
||||
code: string | null;
|
||||
diagnostic?: { kind: string; http_status?: number };
|
||||
progress_percent?: number;
|
||||
download_current?: number;
|
||||
download_total?: number;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
networkDiagnostic: 'Network diagnostic: {{kind}}',
|
||||
complete: 'Done',
|
||||
checkingPlugin: 'Check plugin',
|
||||
preparingPlugin: 'Prepare installation',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
networkDiagnostic: 'Diagnóstico de red: {{kind}}',
|
||||
complete: 'Finalizar',
|
||||
checkingPlugin: 'Comprobar plugin',
|
||||
preparingPlugin: 'Preparar instalación',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
networkDiagnostic: 'ネットワーク診断: {{kind}}',
|
||||
complete: '完了',
|
||||
checkingPlugin: 'プラグインを確認',
|
||||
preparingPlugin: 'インストールを準備',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
networkDiagnostic: 'Диагностика сети: {{kind}}',
|
||||
complete: 'Готово',
|
||||
checkingPlugin: 'Проверка плагина',
|
||||
preparingPlugin: 'Подготовка установки',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
networkDiagnostic: 'การวินิจฉัยเครือข่าย: {{kind}}',
|
||||
complete: 'เสร็จสิ้น',
|
||||
checkingPlugin: 'ตรวจสอบปลั๊กอิน',
|
||||
preparingPlugin: 'เตรียมติดตั้ง',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
networkDiagnostic: 'Chẩn đoán mạng: {{kind}}',
|
||||
complete: 'Hoàn tất',
|
||||
checkingPlugin: 'Kiểm tra plugin',
|
||||
preparingPlugin: 'Chuẩn bị cài đặt',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
networkDiagnostic: '网络诊断:{{kind}}',
|
||||
complete: '完成',
|
||||
checkingPlugin: '检查插件',
|
||||
preparingPlugin: '准备安装',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
networkDiagnostic: '網路診斷:{{kind}}',
|
||||
complete: '完成',
|
||||
checkingPlugin: '檢查外掛',
|
||||
preparingPlugin: '準備安裝',
|
||||
|
||||
Reference in New Issue
Block a user