runspec-console 0.44.2__tar.gz → 0.45.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {runspec_console-0.44.2 → runspec_console-0.45.0}/CHANGELOG.md +27 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/PKG-INFO +1 -1
- {runspec_console-0.44.2 → runspec_console-0.45.0}/pyproject.toml +1 -1
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/bridge.py +191 -2
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/sources/chat.py +4 -1
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_chat_bridge.py +81 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/.gitignore +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/IMPL_NOTES.md +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/README.md +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/SECURITY.md +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/dev_build.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/examples/console-adapter-plugin/.github/prompts/create-console-plugin.prompt.md +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/examples/console-adapter-plugin/AGENTS.md +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/examples/console-adapter-plugin/README.md +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/examples/console-adapter-plugin/console_adapter_plugin/__init__.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/examples/console-adapter-plugin/console_adapter_plugin/adapter.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/examples/console-adapter-plugin/pyproject.toml +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/examples/console-adapter-plugin/tests/test_adapter_conformance.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/__init__.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/adapters/__init__.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/adapters/anthropic.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/adapters/base.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/adapters/bedrock.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/adapters/langserve.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/adapters/openai.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/adapters/testing.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/app.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/catalog.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/config.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/discovery.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/events.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/executor.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/filters_store.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/fs_browse.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/fs_transfer.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/hosts.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/notify.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/runspec.toml +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/sources/__init__.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/sources/chat_transport.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/sources/notifications.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/sources/outlook.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/ssh_pool.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/tools/__init__.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/tools/generate_ssh_key.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/trigger_runner.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/triggers.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/runspec_console/version.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/__init__.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_adapter_testing.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_analytics.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_anthropic_adapter.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_autonomy_gate.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_bridge_adapter.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_catalog.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_chat_history.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_chat_source.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_chat_transport.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_command_tools.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_config.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_copy_ssh_id.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_devtools.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_discovery_globals.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_dispatch_queue.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_draft_send.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_events.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_executor_ssh.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_filters_store.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_fs_browse.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_fs_transfer.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_key_pubkey.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_langserve_adapter.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_list_dir_unsaved.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_logs_management.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_notifications.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_notify.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_outlook.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_path_normalize.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_plugin_adapters.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_positional_args.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_prompt_caching.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_refresh_cycle.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_relay_filter.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_remote_command_injection.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_rotate_ssh_key.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_save_config_pool.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_secret_args.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_shutdown_abort.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_ssh_logging.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_ssh_pool.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_terminal.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_trigger_config.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_trigger_runner.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_triggers.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_usage.py +0 -0
- {runspec_console-0.44.2 → runspec_console-0.45.0}/tests/test_version.py +0 -0
|
@@ -8,6 +8,33 @@ Version numbers follow [Semantic Versioning](https://semver.org/).
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [0.45.0] — 2026-06-12
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **Room connection tester (Rooms tab → "Test").** Diagnoses a room/queue's
|
|
15
|
+
connection on demand over the *real* paramiko path the supervisor uses (HTTP
|
|
16
|
+
proxy, key, ssh_config, then the tunnel or exec relay), in an isolated attempt
|
|
17
|
+
that ignores the pool's connect-backoff and doesn't disturb the live
|
|
18
|
+
connection. It reports the actual blocker with a localised stage —
|
|
19
|
+
**ssh** (reach/auth the host, incl. the proxy), **channel** (tunnel/exec
|
|
20
|
+
relay), or **room** (the relay reached the host but the room server isn't
|
|
21
|
+
answering on the port) — plus an actionable hint, in a dialog. No more reading
|
|
22
|
+
tracebacks or fumbling with a local `ssh` command (which exercises OpenSSH
|
|
23
|
+
ControlMaster/config the console never uses).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **Blocked SSH port-forwarding now logs one actionable line, not a repeating
|
|
27
|
+
traceback.** A host with `AllowTcpForwarding no` refuses the direct-tcpip room
|
|
28
|
+
tunnel (`Administratively prohibited`) — a permanent condition the reconnect
|
|
29
|
+
loop would otherwise re-report as a full traceback every backoff. The
|
|
30
|
+
supervisor now detects it and points at the fix (`room_via = "exec"`); the same
|
|
31
|
+
classification powers the connection tester.
|
|
32
|
+
- **Agent-mode chat source logs its queue.** `listening on queue 'helpdesk/<q>'`
|
|
33
|
+
instead of the empty `listening in room ''` (an agent subscribes to a namespace,
|
|
34
|
+
not a room).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
11
38
|
## [0.44.2] — 2026-06-12
|
|
12
39
|
|
|
13
40
|
### Changed
|
|
@@ -150,6 +150,71 @@ class _BridgeTriggerOps:
|
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
|
|
153
|
+
def _chat_connect_hint(exc: BaseException, *, via: str) -> str | None:
|
|
154
|
+
"""A concise, actionable message for a known-*permanent* chat-connect failure,
|
|
155
|
+
or ``None`` to fall back to a full traceback.
|
|
156
|
+
|
|
157
|
+
``Administratively prohibited`` on a ``direct-tcpip`` channel means the host
|
|
158
|
+
refuses SSH port-forwarding (``AllowTcpForwarding no``) — retrying the tunnel
|
|
159
|
+
will never succeed, so a 20-line traceback every backoff is just noise. The
|
|
160
|
+
fix is the exec relay, so point the operator straight at it."""
|
|
161
|
+
if via != "exec" and "Administratively prohibited" in str(exc):
|
|
162
|
+
return (
|
|
163
|
+
"SSH port-forwarding is blocked on this host (Administratively "
|
|
164
|
+
'prohibited). Set room_via = "exec" on the host entry to reach the '
|
|
165
|
+
"room over an exec channel (console-room connect) instead of a tunnel."
|
|
166
|
+
)
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _classify_room_connection(exc: BaseException | None, *, via: str) -> dict[str, Any]:
|
|
171
|
+
"""Turn a room-connect attempt's outcome into a structured, human-readable
|
|
172
|
+
result for the UI's connection tester: ``{ok, stage, message, hint}``.
|
|
173
|
+
|
|
174
|
+
``stage`` localises the failure — ``ssh`` (reaching/authing the host, incl.
|
|
175
|
+
the HTTP proxy), ``channel`` (opening the tunnel / exec relay), ``room`` (the
|
|
176
|
+
relay reached the host but not the room server), or ``ok``. ``hint`` is an
|
|
177
|
+
actionable next step when we recognise the failure."""
|
|
178
|
+
if exc is None:
|
|
179
|
+
return {"ok": True, "stage": "ok", "message": "", "hint": None}
|
|
180
|
+
text = str(exc) or type(exc).__name__
|
|
181
|
+
low = text.lower()
|
|
182
|
+
if "in connect-backoff" in low:
|
|
183
|
+
return {
|
|
184
|
+
"ok": False,
|
|
185
|
+
"stage": "ssh",
|
|
186
|
+
"message": f"Host is in connect-backoff after recent failures ({text}). The always-on reconnect loop is already retrying — try again in a moment.",
|
|
187
|
+
"hint": None,
|
|
188
|
+
}
|
|
189
|
+
if isinstance(exc, TimeoutError) or "timed out" in low or "timeout" in low:
|
|
190
|
+
return {
|
|
191
|
+
"ok": False,
|
|
192
|
+
"stage": "ssh",
|
|
193
|
+
"message": "Timed out reaching the host — the SSH connection (through the HTTP proxy, if configured) didn't complete in time.",
|
|
194
|
+
"hint": "If the proxy works for discovery/transfers, this is usually transient at startup (many connections open at once) — retry. If it persists, check the [ssh] proxy host:port is reachable.",
|
|
195
|
+
}
|
|
196
|
+
if "administratively prohibited" in low:
|
|
197
|
+
return {
|
|
198
|
+
"ok": False,
|
|
199
|
+
"stage": "channel",
|
|
200
|
+
"message": "The host refused the tunnel — SSH port-forwarding is disabled (AllowTcpForwarding no).",
|
|
201
|
+
"hint": _chat_connect_hint(exc, via=via),
|
|
202
|
+
}
|
|
203
|
+
if "authentication" in low or "not a valid" in low or "no authentication" in low:
|
|
204
|
+
return {
|
|
205
|
+
"ok": False,
|
|
206
|
+
"stage": "ssh",
|
|
207
|
+
"message": f"SSH authentication failed ({text}). Check this host's key / identityFile.",
|
|
208
|
+
"hint": None,
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
"ok": False,
|
|
212
|
+
"stage": "ssh",
|
|
213
|
+
"message": text,
|
|
214
|
+
"hint": _chat_connect_hint(exc, via=via),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
153
218
|
class _RotaPresenceWatcher:
|
|
154
219
|
"""Pushes console presence to the chat room as the working-hours rota flips.
|
|
155
220
|
|
|
@@ -5054,8 +5119,15 @@ class Bridge:
|
|
|
5054
5119
|
)
|
|
5055
5120
|
try:
|
|
5056
5121
|
source.start(self._on_chat_event)
|
|
5057
|
-
except Exception:
|
|
5058
|
-
|
|
5122
|
+
except Exception as exc:
|
|
5123
|
+
# A blocked-forwarding failure is permanent — log one actionable
|
|
5124
|
+
# line, not a fresh traceback every backoff. Genuinely unexpected
|
|
5125
|
+
# errors still get the full traceback.
|
|
5126
|
+
hint = _chat_connect_hint(exc, via=str(spec.get("via") or "tunnel"))
|
|
5127
|
+
if hint:
|
|
5128
|
+
logger.error("chat: room %r — %s", room, hint)
|
|
5129
|
+
else:
|
|
5130
|
+
logger.exception("chat: connect to room %r failed", room)
|
|
5059
5131
|
if self._chat_stop.wait(backoff):
|
|
5060
5132
|
break
|
|
5061
5133
|
backoff = min(backoff * 2, self._CHAT_BACKOFF_MAX)
|
|
@@ -5291,6 +5363,123 @@ class Bridge:
|
|
|
5291
5363
|
rooms.append(entry)
|
|
5292
5364
|
return {"rooms": rooms}
|
|
5293
5365
|
|
|
5366
|
+
def test_room_connection(self, host: str) -> dict[str, Any]:
|
|
5367
|
+
"""Diagnose a room/queue's connection the way the supervisor reaches it —
|
|
5368
|
+
an isolated, fresh attempt (no pool backoff, no effect on the live
|
|
5369
|
+
connection) over the *same* paramiko path (HTTP proxy, key, ssh_config),
|
|
5370
|
+
so the UI can show the real outcome instead of a buried traceback.
|
|
5371
|
+
|
|
5372
|
+
Steps, matching the transport: connect+auth to the host (the ``ssh``
|
|
5373
|
+
stage — where the proxy timeout surfaces), open the tunnel / exec relay
|
|
5374
|
+
(``channel`` — where blocked forwarding surfaces), and for exec confirm
|
|
5375
|
+
the relay didn't immediately exit (``room`` — the room server is actually
|
|
5376
|
+
listening). Returns the ``_classify_room_connection`` shape plus the host
|
|
5377
|
+
/ room / queue it tested."""
|
|
5378
|
+
from .executor import _make_ssh_client
|
|
5379
|
+
|
|
5380
|
+
specs = [
|
|
5381
|
+
s
|
|
5382
|
+
for s in self._room_specs()
|
|
5383
|
+
if s.get("host") == host or s.get("ssh_target") == host
|
|
5384
|
+
]
|
|
5385
|
+
if not specs:
|
|
5386
|
+
return {
|
|
5387
|
+
"ok": False,
|
|
5388
|
+
"stage": "config",
|
|
5389
|
+
"host": host,
|
|
5390
|
+
"message": f"No room or queue is configured on host {host!r}.",
|
|
5391
|
+
"hint": "Add room/room_port (group chat) or room_queue/room_port (help desk) to this host.",
|
|
5392
|
+
}
|
|
5393
|
+
spec = specs[0]
|
|
5394
|
+
cfg = self.get_config().get("ssh")
|
|
5395
|
+
cfg = cfg if isinstance(cfg, dict) else None
|
|
5396
|
+
via = str(spec.get("via") or "tunnel")
|
|
5397
|
+
meta = {
|
|
5398
|
+
"host": host,
|
|
5399
|
+
"room": spec["room"],
|
|
5400
|
+
"queue": str(spec.get("queue") or ""),
|
|
5401
|
+
}
|
|
5402
|
+
|
|
5403
|
+
client = None
|
|
5404
|
+
chan = None
|
|
5405
|
+
try:
|
|
5406
|
+
try:
|
|
5407
|
+
client = _make_ssh_client(
|
|
5408
|
+
str(spec["ssh_target"]), spec.get("identity_file") or None, cfg
|
|
5409
|
+
)
|
|
5410
|
+
except Exception as exc: # noqa: BLE001 — report, don't raise
|
|
5411
|
+
return {**_classify_room_connection(exc, via=via), **meta}
|
|
5412
|
+
|
|
5413
|
+
transport = client.get_transport()
|
|
5414
|
+
if transport is None:
|
|
5415
|
+
return {
|
|
5416
|
+
**_classify_room_connection(OSError("transport closed"), via=via),
|
|
5417
|
+
**meta,
|
|
5418
|
+
}
|
|
5419
|
+
try:
|
|
5420
|
+
if via == "exec":
|
|
5421
|
+
chan = transport.open_session()
|
|
5422
|
+
chan.exec_command(str(spec["exec_command"]))
|
|
5423
|
+
else:
|
|
5424
|
+
chan = transport.open_channel(
|
|
5425
|
+
"direct-tcpip",
|
|
5426
|
+
(str(spec.get("dest_host") or "127.0.0.1"), int(spec["port"])),
|
|
5427
|
+
("127.0.0.1", 0),
|
|
5428
|
+
)
|
|
5429
|
+
except Exception as exc: # noqa: BLE001
|
|
5430
|
+
return {**_classify_room_connection(exc, via=via), **meta}
|
|
5431
|
+
|
|
5432
|
+
# Exec: the relay (console-room connect) connects to localhost:port on
|
|
5433
|
+
# the host. If the room server is down it exits straight away — catch
|
|
5434
|
+
# that so "host reachable but room down" isn't reported as success.
|
|
5435
|
+
if via == "exec":
|
|
5436
|
+
time.sleep(1.5)
|
|
5437
|
+
if chan.exit_status_ready():
|
|
5438
|
+
err = ""
|
|
5439
|
+
try:
|
|
5440
|
+
if chan.recv_stderr_ready():
|
|
5441
|
+
err = (
|
|
5442
|
+
chan.recv_stderr(2048)
|
|
5443
|
+
.decode("utf-8", "replace")
|
|
5444
|
+
.strip()
|
|
5445
|
+
)
|
|
5446
|
+
except Exception:
|
|
5447
|
+
pass
|
|
5448
|
+
return {
|
|
5449
|
+
**meta,
|
|
5450
|
+
"ok": False,
|
|
5451
|
+
"stage": "room",
|
|
5452
|
+
"message": (
|
|
5453
|
+
f"Reached the host, but the relay exited — the room "
|
|
5454
|
+
f"server isn't answering on port {spec['port']} "
|
|
5455
|
+
f"(is console-room running there?)."
|
|
5456
|
+
+ (f" [{err}]" if err else "")
|
|
5457
|
+
),
|
|
5458
|
+
"hint": "Start the room server on the host (console-room) and confirm its --ndjson-port matches this host's room_port.",
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
return {
|
|
5462
|
+
**meta,
|
|
5463
|
+
"ok": True,
|
|
5464
|
+
"stage": "ok",
|
|
5465
|
+
"message": (
|
|
5466
|
+
f"Connected to the room on {host} (port {spec['port']}, via {via})."
|
|
5467
|
+
+ (f" Queue {spec['queue']}." if spec.get("queue") else "")
|
|
5468
|
+
),
|
|
5469
|
+
"hint": None,
|
|
5470
|
+
}
|
|
5471
|
+
finally:
|
|
5472
|
+
try:
|
|
5473
|
+
if chan is not None:
|
|
5474
|
+
chan.close()
|
|
5475
|
+
except Exception:
|
|
5476
|
+
pass
|
|
5477
|
+
try:
|
|
5478
|
+
if client is not None:
|
|
5479
|
+
client.close()
|
|
5480
|
+
except Exception:
|
|
5481
|
+
pass
|
|
5482
|
+
|
|
5294
5483
|
def _abort_turns(self, ids: set[str] | None) -> None:
|
|
5295
5484
|
"""Unblock in-flight turns: signal each cancel flag and wake each pending
|
|
5296
5485
|
autonomy-gate confirm (as cancelled). ``ids=None`` aborts everything
|
|
@@ -335,7 +335,10 @@ class ChatSource:
|
|
|
335
335
|
transport = self._transport
|
|
336
336
|
if transport is None:
|
|
337
337
|
return
|
|
338
|
-
|
|
338
|
+
if self.is_agent:
|
|
339
|
+
logger.info("chat source: listening on queue %r", self._namespace)
|
|
340
|
+
else:
|
|
341
|
+
logger.info("chat source: listening in room %r", self._room)
|
|
339
342
|
while not self._stop.is_set():
|
|
340
343
|
try:
|
|
341
344
|
obj = transport.recv()
|
|
@@ -639,3 +639,84 @@ def test_propose_reply_rejects_empty_text():
|
|
|
639
639
|
_propose_stub()._gated_propose_reply({"text": " "}, "card1", "ops")
|
|
640
640
|
)
|
|
641
641
|
assert out.startswith("✗") and "empty" in out
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# ── connect-error hint (blocked port-forwarding → exec) ────────────────────────
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def test_chat_connect_hint_for_blocked_forwarding():
|
|
648
|
+
from runspec_console.bridge import _chat_connect_hint
|
|
649
|
+
|
|
650
|
+
exc = Exception("ChannelException(1, 'Administratively prohibited')")
|
|
651
|
+
hint = _chat_connect_hint(exc, via="tunnel")
|
|
652
|
+
assert hint is not None and 'room_via = "exec"' in hint
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def test_chat_connect_hint_none_in_exec_mode():
|
|
656
|
+
from runspec_console.bridge import _chat_connect_hint
|
|
657
|
+
|
|
658
|
+
# Already on exec → the prohibited-forwarding hint doesn't apply.
|
|
659
|
+
exc = Exception("ChannelException(1, 'Administratively prohibited')")
|
|
660
|
+
assert _chat_connect_hint(exc, via="exec") is None
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def test_chat_connect_hint_none_for_other_errors():
|
|
664
|
+
from runspec_console.bridge import _chat_connect_hint
|
|
665
|
+
|
|
666
|
+
assert _chat_connect_hint(TimeoutError("timed out"), via="tunnel") is None
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# ── room connection tester (classifier) ────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def test_classify_room_connection_success():
|
|
673
|
+
from runspec_console.bridge import _classify_room_connection
|
|
674
|
+
|
|
675
|
+
r = _classify_room_connection(None, via="exec")
|
|
676
|
+
assert r["ok"] and r["stage"] == "ok"
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def test_classify_room_connection_proxy_timeout():
|
|
680
|
+
from runspec_console.bridge import _classify_room_connection
|
|
681
|
+
|
|
682
|
+
r = _classify_room_connection(TimeoutError("timed out"), via="exec")
|
|
683
|
+
assert not r["ok"] and r["stage"] == "ssh"
|
|
684
|
+
assert "proxy" in r["hint"].lower()
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def test_classify_room_connection_blocked_forwarding():
|
|
688
|
+
from runspec_console.bridge import _classify_room_connection
|
|
689
|
+
|
|
690
|
+
exc = Exception("ChannelException(1, 'Administratively prohibited')")
|
|
691
|
+
r = _classify_room_connection(exc, via="tunnel")
|
|
692
|
+
assert not r["ok"] and r["stage"] == "channel"
|
|
693
|
+
assert 'room_via = "exec"' in r["hint"]
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def test_classify_room_connection_backoff():
|
|
697
|
+
from runspec_console.bridge import _classify_room_connection
|
|
698
|
+
|
|
699
|
+
from runspec_console.ssh_pool import BackoffError
|
|
700
|
+
|
|
701
|
+
r = _classify_room_connection(
|
|
702
|
+
BackoffError("host in connect-backoff for 4s"), via="exec"
|
|
703
|
+
)
|
|
704
|
+
assert not r["ok"] and r["stage"] == "ssh" and "backoff" in r["message"].lower()
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def test_classify_room_connection_auth():
|
|
708
|
+
from runspec_console.bridge import _classify_room_connection
|
|
709
|
+
|
|
710
|
+
r = _classify_room_connection(Exception("Authentication failed."), via="exec")
|
|
711
|
+
assert not r["ok"] and r["stage"] == "ssh" and "identityFile" in r["message"]
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def test_test_room_connection_unconfigured_host():
|
|
715
|
+
from runspec_console.bridge import Bridge
|
|
716
|
+
|
|
717
|
+
s = types.SimpleNamespace()
|
|
718
|
+
s._room_specs = lambda: []
|
|
719
|
+
s.get_config = lambda: {}
|
|
720
|
+
s.test_room_connection = types.MethodType(Bridge.test_room_connection, s)
|
|
721
|
+
r = s.test_room_connection("nope")
|
|
722
|
+
assert not r["ok"] and r["stage"] == "config"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{runspec_console-0.44.2 → runspec_console-0.45.0}/examples/console-adapter-plugin/pyproject.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|