qbraid-cli 0.16.6__tar.gz → 0.16.8__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.
- {qbraid_cli-0.16.6/qbraid_cli.egg-info → qbraid_cli-0.16.8}/PKG-INFO +6 -6
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/README.md +2 -2
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/pyproject.toml +4 -4
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/agents/app.py +403 -102
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/comms/app.py +32 -3
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8/qbraid_cli.egg-info}/PKG-INFO +6 -6
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/requires.txt +3 -3
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/LICENSE +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/_build_info.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/_version.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/account/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/account/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/admin/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/admin/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/admin/headers.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/admin/validation.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/agents/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/agents/permission_response.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/ai/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/ai/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/ai/token.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/comms/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/instances.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/profiles.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/server.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/ssh.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/configure/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/configure/actions.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/configure/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/devices/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/devices/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/devices/validation.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/activate.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/create.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/data_handling.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/groups.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/publish.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/registry.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/share.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/exceptions.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/handlers.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/jobs/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/jobs/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/jobs/groups.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/kernels/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/kernels/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/lab/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/lab/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/main.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/pip/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/pip/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/pip/hooks.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/projects/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/projects/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/py.typed +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/skills/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/skills/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/troubleshoot/__init__.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/troubleshoot/app.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/SOURCES.txt +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/dependency_links.txt +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/entry_points.txt +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/top_level.txt +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/setup.cfg +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/tests/test_cli_e2e.py +0 -0
- {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.8
|
|
4
4
|
Summary: Command Line Interface for interacting with all parts of the qBraid platform.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -30,11 +30,11 @@ Requires-Dist: typer>=0.12.1
|
|
|
30
30
|
Requires-Dist: rich>=10.11.0
|
|
31
31
|
Requires-Dist: click<=8.5.0
|
|
32
32
|
Requires-Dist: questionary>=2.0.0
|
|
33
|
-
Requires-Dist: qbraid-core>=0.6.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.6.19
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.19; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]>=0.6.19; extra == "envs"
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: isort; extra == "dev"
|
|
40
40
|
Requires-Dist: black; extra == "dev"
|
|
@@ -172,5 +172,5 @@ qbraid comms email send --subject "Progress" --file report.txt --json
|
|
|
172
172
|
qbraid comms slack read --json
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
See [communication commands](docs/
|
|
176
|
-
|
|
175
|
+
See [communication commands](https://docs.qbraid.com/v2/cli/api-reference/qbraid_comms) for Slack connection, stdin input,
|
|
176
|
+
message limits, and pagination.
|
|
@@ -127,5 +127,5 @@ qbraid comms email send --subject "Progress" --file report.txt --json
|
|
|
127
127
|
qbraid comms slack read --json
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
See [communication commands](docs/
|
|
131
|
-
|
|
130
|
+
See [communication commands](https://docs.qbraid.com/v2/cli/api-reference/qbraid_comms) for Slack connection, stdin input,
|
|
131
|
+
message limits, and pagination.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qbraid-cli"
|
|
7
|
-
version = "0.16.
|
|
7
|
+
version = "0.16.8"
|
|
8
8
|
description = "Command Line Interface for interacting with all parts of the qBraid platform."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "qBraid Development Team", email = "contact@qbraid.com" }]
|
|
@@ -32,7 +32,7 @@ dependencies = [
|
|
|
32
32
|
"rich>=10.11.0",
|
|
33
33
|
"click<=8.5.0",
|
|
34
34
|
"questionary>=2.0.0",
|
|
35
|
-
"qbraid-core>=0.6.
|
|
35
|
+
"qbraid-core>=0.6.19",
|
|
36
36
|
]
|
|
37
37
|
requires-python = ">= 3.10"
|
|
38
38
|
|
|
@@ -43,8 +43,8 @@ Documentation = "https://docs.qbraid.com/v2/cli/api-reference/qbraid"
|
|
|
43
43
|
Discord = "https://discord.gg/KugF6Cnncm"
|
|
44
44
|
|
|
45
45
|
[project.optional-dependencies]
|
|
46
|
-
compute = ["qbraid-core[compute]>=0.6.
|
|
47
|
-
envs = ["qbraid-core[environments]>=0.6.
|
|
46
|
+
compute = ["qbraid-core[compute]>=0.6.19"]
|
|
47
|
+
envs = ["qbraid-core[environments]>=0.6.19"]
|
|
48
48
|
dev = ["isort", "black", "pytest", "pytest-cov", "pytest-asyncio"]
|
|
49
49
|
|
|
50
50
|
[project.scripts]
|
|
@@ -17,6 +17,7 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
17
17
|
import click
|
|
18
18
|
import typer
|
|
19
19
|
from rich.console import Console
|
|
20
|
+
from rich.markup import escape
|
|
20
21
|
from rich.panel import Panel
|
|
21
22
|
from rich.table import Table
|
|
22
23
|
|
|
@@ -114,7 +115,6 @@ def _remote_failure(stdout: str, stderr: str) -> str:
|
|
|
114
115
|
"Session not found" into "Warning: Permanently added ... to the list of
|
|
115
116
|
known hosts", which names the one thing that went right.
|
|
116
117
|
"""
|
|
117
|
-
from rich.markup import escape
|
|
118
118
|
|
|
119
119
|
def useful(text: str) -> list[str]:
|
|
120
120
|
# The remote's Rich error panel arrives as box-drawing borders around
|
|
@@ -204,12 +204,91 @@ def _since_to_timestamp(since_str: str) -> float:
|
|
|
204
204
|
return time.time() - seconds
|
|
205
205
|
|
|
206
206
|
|
|
207
|
+
def _plain(value) -> str:
|
|
208
|
+
"""A user- or agent-supplied value, escaped so Rich prints it literally."""
|
|
209
|
+
return escape(str(value))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _core_method(registry, name: str):
|
|
213
|
+
"""A registry method the installed qbraid-core actually defines, else None.
|
|
214
|
+
|
|
215
|
+
Checked on the class so older Core releases (and test doubles) fall back
|
|
216
|
+
to the previous behavior instead of calling a missing method.
|
|
217
|
+
"""
|
|
218
|
+
if not callable(getattr(type(registry), name, None)):
|
|
219
|
+
return None
|
|
220
|
+
return getattr(registry, name)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _calling_session(registry) -> Optional[str]:
|
|
224
|
+
"""The live agent session this command runs inside, if any.
|
|
225
|
+
|
|
226
|
+
Newer qbraid-core finds it by process ancestry, so unsetting
|
|
227
|
+
QBRAID_SESSION_ID does not make an agent look like the person, and a
|
|
228
|
+
person typing in a pane that kept the variable is not mistaken for the
|
|
229
|
+
agent that exited. Older Core falls back to the variable, unless it names
|
|
230
|
+
a session the registry records as stopped or errored.
|
|
231
|
+
"""
|
|
232
|
+
find = _core_method(registry, "calling_session")
|
|
233
|
+
if find is not None:
|
|
234
|
+
try:
|
|
235
|
+
return find()
|
|
236
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
237
|
+
if _is_registry_error(err):
|
|
238
|
+
# Core could not tell which agent this is (for example, the
|
|
239
|
+
# caller matches more than one live session). Refuse rather
|
|
240
|
+
# than fail open to "the person".
|
|
241
|
+
console.print(
|
|
242
|
+
f"[red]Cannot tell which agent session this command runs in: {_plain(err)}[/red]"
|
|
243
|
+
)
|
|
244
|
+
raise typer.Exit(code=1) from err
|
|
245
|
+
# any other failure: fall back to the variable, as older Core does
|
|
246
|
+
env_sid = os.environ.get("QBRAID_SESSION_ID") or None
|
|
247
|
+
if env_sid and _session_status(registry, env_sid) in ("stopped", "error"):
|
|
248
|
+
return None
|
|
249
|
+
return env_sid
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _is_registry_error(err: Exception) -> bool:
|
|
253
|
+
"""Whether ``err`` is qbraid-core's AgentRegistryError (or a subclass)."""
|
|
254
|
+
try:
|
|
255
|
+
# pylint: disable-next=import-outside-toplevel
|
|
256
|
+
from qbraid_core.services.agents.exceptions import AgentRegistryError
|
|
257
|
+
except ImportError:
|
|
258
|
+
return False
|
|
259
|
+
return isinstance(err, AgentRegistryError)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _session_status(registry, session_id: str) -> Optional[str]:
|
|
263
|
+
"""The registry status of a session ("stopped", "working", ...), or None
|
|
264
|
+
when it is not registered or the registry cannot be read."""
|
|
265
|
+
try:
|
|
266
|
+
session = registry.get_session(session_id)
|
|
267
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
268
|
+
return None
|
|
269
|
+
status = getattr(session, "status", None)
|
|
270
|
+
value = getattr(status, "value", status)
|
|
271
|
+
return value if isinstance(value, str) else None
|
|
272
|
+
|
|
273
|
+
|
|
207
274
|
def _find_session_by_prefix(registry, session_id: str):
|
|
208
275
|
"""Find a session by ID, prefix, or name from the registry.
|
|
209
276
|
|
|
210
277
|
Searches session_id (exact/prefix), then tmux_window and terminal_name.
|
|
211
278
|
Returns the matched AgentSession or None.
|
|
212
279
|
"""
|
|
280
|
+
resolve = _core_method(registry, "resolve_session")
|
|
281
|
+
if resolve is not None:
|
|
282
|
+
# Newer Core: exact id, unique prefix, then a name that prefers the live
|
|
283
|
+
# session over stopped namesakes; ambiguity is an error, not a guess.
|
|
284
|
+
try:
|
|
285
|
+
return resolve(session_id)
|
|
286
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
287
|
+
if type(err).__name__ != "AgentSessionAmbiguousError":
|
|
288
|
+
raise
|
|
289
|
+
console.print(f"[red]{_plain(err)}[/red]")
|
|
290
|
+
raise typer.Exit(code=1) from err
|
|
291
|
+
|
|
213
292
|
all_sessions = registry.load_registry().sessions
|
|
214
293
|
|
|
215
294
|
# Try session_id exact or prefix match first
|
|
@@ -766,9 +845,9 @@ def agents_status(
|
|
|
766
845
|
|
|
767
846
|
# Team info
|
|
768
847
|
if s.team_id:
|
|
769
|
-
lines.append(f"[bold]Team ID:[/bold] {s.team_id}")
|
|
848
|
+
lines.append(f"[bold]Team ID:[/bold] {_plain(s.team_id)}")
|
|
770
849
|
if s.role:
|
|
771
|
-
lines.append(f"[bold]Role:[/bold] {s.role}")
|
|
850
|
+
lines.append(f"[bold]Role:[/bold] {_plain(s.role)}")
|
|
772
851
|
|
|
773
852
|
# Transcript path
|
|
774
853
|
if s.transcript_path:
|
|
@@ -807,8 +886,24 @@ def agents_update(
|
|
|
807
886
|
status: Optional[str] = typer.Option(
|
|
808
887
|
None, "--status", help="Set session status (working, waiting, idle, error, stopped)"
|
|
809
888
|
),
|
|
889
|
+
agent_type: Optional[str] = typer.Option(
|
|
890
|
+
None,
|
|
891
|
+
"--type",
|
|
892
|
+
help="Set the agent type (leader, worker, observer, reviewer); '' clears it.",
|
|
893
|
+
),
|
|
894
|
+
team: Optional[str] = typer.Option(None, "--team", help="Join a team by id; '' leaves it."),
|
|
895
|
+
parent: Optional[str] = typer.Option(
|
|
896
|
+
None, "--parent", help="Set the parent session (id, prefix or name); '' clears it."
|
|
897
|
+
),
|
|
810
898
|
):
|
|
811
|
-
"""Update metadata fields on an agent session.
|
|
899
|
+
"""Update metadata fields on an agent session.
|
|
900
|
+
|
|
901
|
+
--type, --team and --parent let agents started from a terminal join a
|
|
902
|
+
team after launch, so they can message each other. From your own shell
|
|
903
|
+
any change is allowed. Inside an agent, it may only place teamless
|
|
904
|
+
sessions or its teammates into a team it leads, never as or out of
|
|
905
|
+
leader. These are coordination guardrails, not a security boundary.
|
|
906
|
+
"""
|
|
812
907
|
try:
|
|
813
908
|
from qbraid_core.services.agents import (
|
|
814
909
|
AgentSessionNotFoundError,
|
|
@@ -820,7 +915,7 @@ def agents_update(
|
|
|
820
915
|
# Support prefix and name matching
|
|
821
916
|
match = _find_session_by_prefix(registry, session_id)
|
|
822
917
|
if not match:
|
|
823
|
-
console.print(f"[red]Session not found: {session_id}[/red]")
|
|
918
|
+
console.print(f"[red]Session not found: {_plain(session_id)}[/red]")
|
|
824
919
|
console.print("[dim]Use 'qbraid agents list' to see active sessions.[/dim]")
|
|
825
920
|
raise typer.Exit(code=1)
|
|
826
921
|
matched_id = match.session_id
|
|
@@ -842,7 +937,9 @@ def agents_update(
|
|
|
842
937
|
valid_statuses = {"working", "waiting", "idle", "error", "stopped"}
|
|
843
938
|
if status not in valid_statuses:
|
|
844
939
|
valid_opts = ", ".join(sorted(valid_statuses))
|
|
845
|
-
console.print(
|
|
940
|
+
console.print(
|
|
941
|
+
f"[red]Invalid status: {_plain(repr(status))}. Must be one of: {valid_opts}[/red]"
|
|
942
|
+
)
|
|
846
943
|
raise typer.Exit(code=1)
|
|
847
944
|
updates["status"] = status
|
|
848
945
|
|
|
@@ -854,31 +951,70 @@ def agents_update(
|
|
|
854
951
|
merged.append(t)
|
|
855
952
|
updates["tags"] = merged
|
|
856
953
|
|
|
857
|
-
|
|
954
|
+
team_change = agent_type is not None or team is not None or parent is not None
|
|
955
|
+
if not updates and not team_change:
|
|
858
956
|
console.print(
|
|
859
|
-
"[yellow]No updates specified. Use --name, --model, --cwd, --status,
|
|
957
|
+
"[yellow]No updates specified. Use --name, --model, --cwd, --status, --tag, "
|
|
958
|
+
"--type, --team or --parent.[/yellow]"
|
|
860
959
|
)
|
|
861
960
|
raise typer.Exit(code=0)
|
|
862
961
|
|
|
863
|
-
|
|
962
|
+
if team_change:
|
|
963
|
+
assign = _core_method(registry, "assign_team")
|
|
964
|
+
if assign is None:
|
|
965
|
+
console.print(
|
|
966
|
+
"[red]This qbraid-core cannot change a session's team or role.[/red] "
|
|
967
|
+
"Upgrade qbraid-core, or launch the agent with --type/--team/--parent."
|
|
968
|
+
)
|
|
969
|
+
raise typer.Exit(code=1)
|
|
970
|
+
parent_id = parent
|
|
971
|
+
if parent:
|
|
972
|
+
parent_match = _find_session_by_prefix(registry, parent)
|
|
973
|
+
if not parent_match:
|
|
974
|
+
console.print(f"[red]Parent session not found: {_plain(parent)}[/red]")
|
|
975
|
+
raise typer.Exit(code=1)
|
|
976
|
+
parent_id = parent_match.session_id
|
|
977
|
+
try:
|
|
978
|
+
assign(
|
|
979
|
+
matched_id,
|
|
980
|
+
team_id=team,
|
|
981
|
+
agent_type=agent_type,
|
|
982
|
+
parent_session_id=parent_id,
|
|
983
|
+
actor_session_id=_calling_session(registry),
|
|
984
|
+
)
|
|
985
|
+
except AgentSessionNotFoundError:
|
|
986
|
+
raise
|
|
987
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
988
|
+
console.print(f"[red]{_plain(err)}[/red]")
|
|
989
|
+
raise typer.Exit(code=1) from err
|
|
990
|
+
|
|
991
|
+
updated = (
|
|
992
|
+
registry.update_session(matched_id, **updates) if updates else registry.get_session(matched_id)
|
|
993
|
+
)
|
|
864
994
|
|
|
865
995
|
# Display updated session info
|
|
866
996
|
status_val = updated.status.value if updated.status else "idle"
|
|
867
997
|
style = _status_style(status_val)
|
|
868
998
|
|
|
999
|
+
# Every value here can come from a user or an agent: escape it so a
|
|
1000
|
+
# name or team such as "[/]" prints literally instead of breaking Rich.
|
|
869
1001
|
lines = []
|
|
870
|
-
lines.append(f"[bold]Session ID:[/bold] {updated.session_id}")
|
|
1002
|
+
lines.append(f"[bold]Session ID:[/bold] {_plain(updated.session_id)}")
|
|
871
1003
|
lines.append(f"[bold]Tool:[/bold] {updated.tool.value if updated.tool else '-'}")
|
|
872
|
-
lines.append(f"[bold]Status:[/bold] [{style}]{status_val}[/{style}]")
|
|
873
|
-
lines.append(f"[bold]Model:[/bold] {updated.model_name or '-'}")
|
|
874
|
-
lines.append(f"[bold]Name:[/bold] {updated.terminal_name or '-'}")
|
|
875
|
-
lines.append(f"[bold]CWD:[/bold] {updated.cwd or '-'}")
|
|
1004
|
+
lines.append(f"[bold]Status:[/bold] [{style}]{_plain(status_val)}[/{style}]")
|
|
1005
|
+
lines.append(f"[bold]Model:[/bold] {_plain(updated.model_name or '-')}")
|
|
1006
|
+
lines.append(f"[bold]Name:[/bold] {_plain(updated.terminal_name or '-')}")
|
|
1007
|
+
lines.append(f"[bold]CWD:[/bold] {_plain(updated.cwd or '-')}")
|
|
876
1008
|
if updated.tags:
|
|
877
|
-
lines.append(f"[bold]Tags:[/bold] {', '.join(updated.tags)}")
|
|
1009
|
+
lines.append(f"[bold]Tags:[/bold] {_plain(', '.join(updated.tags))}")
|
|
1010
|
+
if updated.agent_type or updated.team_id or updated.parent_session_id:
|
|
1011
|
+
lines.append(f"[bold]Type:[/bold] {_plain(updated.agent_type or 'worker (default)')}")
|
|
1012
|
+
lines.append(f"[bold]Team:[/bold] {_plain(updated.team_id or '-')}")
|
|
1013
|
+
lines.append(f"[bold]Parent:[/bold] {_plain(updated.parent_session_id or '-')}")
|
|
878
1014
|
|
|
879
1015
|
panel = Panel(
|
|
880
1016
|
"\n".join(lines),
|
|
881
|
-
title=f"Updated Session [{updated.session_id[:8]}]",
|
|
1017
|
+
title=_plain(f"Updated Session [{updated.session_id[:8]}]"),
|
|
882
1018
|
border_style="green",
|
|
883
1019
|
)
|
|
884
1020
|
console.print(panel)
|
|
@@ -886,7 +1022,7 @@ def agents_update(
|
|
|
886
1022
|
except (typer.Exit, SystemExit):
|
|
887
1023
|
raise
|
|
888
1024
|
except AgentSessionNotFoundError:
|
|
889
|
-
console.print(f"[red]Session not found: {session_id}[/red]")
|
|
1025
|
+
console.print(f"[red]Session not found: {_plain(session_id)}[/red]")
|
|
890
1026
|
console.print("[dim]Use 'qbraid agents list' to see active sessions.[/dim]")
|
|
891
1027
|
raise typer.Exit(code=1)
|
|
892
1028
|
except Exception as e:
|
|
@@ -1278,6 +1414,27 @@ def _request_id_kwargs(method: Callable[..., Any], request_id: Optional[str]) ->
|
|
|
1278
1414
|
return {"request_id": request_id}
|
|
1279
1415
|
|
|
1280
1416
|
|
|
1417
|
+
def _fork_execution(original: Any, approval: Optional[Any], execution_mode: Optional[Any]):
|
|
1418
|
+
"""Execution mode for a fork of ``original``.
|
|
1419
|
+
|
|
1420
|
+
An explicit --execution-mode wins. Otherwise the original's mode is reused
|
|
1421
|
+
-- unless it was YOLO's forced container and the fork asks for a safer
|
|
1422
|
+
approval, or the fork asks for auto-review, which Core pins to the
|
|
1423
|
+
workspace sandbox. Inheriting there carried danger-full-access into a
|
|
1424
|
+
session labelled manual or auto-review (core#378 review F2).
|
|
1425
|
+
"""
|
|
1426
|
+
if execution_mode:
|
|
1427
|
+
return execution_mode.value
|
|
1428
|
+
requested = approval.value if approval else None
|
|
1429
|
+
recorded = getattr(original, "approval", None)
|
|
1430
|
+
effective = requested or recorded
|
|
1431
|
+
if effective in ("auto-review", "yolo") or (recorded == "yolo" and requested not in (None, "yolo")):
|
|
1432
|
+
# auto-review is pinned to workspace and yolo to container by core;
|
|
1433
|
+
# passing the original's mode would make core refuse the fork.
|
|
1434
|
+
return None
|
|
1435
|
+
return getattr(original, "execution_mode", None)
|
|
1436
|
+
|
|
1437
|
+
|
|
1281
1438
|
def _approval_kwargs(method: Callable[..., Any], approval: Optional[str]) -> Dict[str, Any]:
|
|
1282
1439
|
"""Forward ``approval`` only to a qbraid-core new enough to accept it.
|
|
1283
1440
|
|
|
@@ -1425,6 +1582,43 @@ def agents_settings(
|
|
|
1425
1582
|
handle_error(error_type=type(error).__name__, message=str(error), include_traceback=False)
|
|
1426
1583
|
|
|
1427
1584
|
|
|
1585
|
+
def _hook_trust_kwargs(method, value):
|
|
1586
|
+
"""Fail before launch when an explicit choice cannot be honored."""
|
|
1587
|
+
if value is None:
|
|
1588
|
+
return {}
|
|
1589
|
+
|
|
1590
|
+
from qbraid_core.services.agents import AgentLauncherError
|
|
1591
|
+
|
|
1592
|
+
params = inspect.signature(method).parameters
|
|
1593
|
+
if "trust_hooks" not in params and not any(
|
|
1594
|
+
p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values()
|
|
1595
|
+
):
|
|
1596
|
+
raise AgentLauncherError("Hook trust requires a newer qbraid-core on this machine.")
|
|
1597
|
+
return {"trust_hooks": value}
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
def _startup_pending(session):
|
|
1601
|
+
return session.tool.value in ("codex", "claude") and not getattr(session, "startup_confirmed_at", None)
|
|
1602
|
+
|
|
1603
|
+
|
|
1604
|
+
def _startup_notice(session, compute=None):
|
|
1605
|
+
import shlex
|
|
1606
|
+
|
|
1607
|
+
suffix = f" --compute {shlex.quote(compute)}" if compute else ""
|
|
1608
|
+
if _startup_pending(session):
|
|
1609
|
+
console.print(
|
|
1610
|
+
"Session created; startup is not yet confirmed. It may be waiting for sign-in, "
|
|
1611
|
+
"folder trust, or hook review. The initial task waits until setup is complete.",
|
|
1612
|
+
markup=False,
|
|
1613
|
+
)
|
|
1614
|
+
if session.tool.value == "codex":
|
|
1615
|
+
console.print(
|
|
1616
|
+
"Approve hooks in the agent's terminal (/hooks); Codex remembers unchanged ones.",
|
|
1617
|
+
markup=False,
|
|
1618
|
+
)
|
|
1619
|
+
console.print(f" qbraid agents attach {shlex.quote(session.session_id)}{suffix}", markup=False)
|
|
1620
|
+
|
|
1621
|
+
|
|
1428
1622
|
@agents_app.command("launch")
|
|
1429
1623
|
def agents_launch(
|
|
1430
1624
|
backend: TerminalBackend = typer.Option(
|
|
@@ -1470,6 +1664,15 @@ def agents_launch(
|
|
|
1470
1664
|
"decides. yolo: no permission checks. ask/auto: legacy modes, unchanged."
|
|
1471
1665
|
),
|
|
1472
1666
|
),
|
|
1667
|
+
trust_hooks: bool = typer.Option(
|
|
1668
|
+
False,
|
|
1669
|
+
"--trust-hooks",
|
|
1670
|
+
help=(
|
|
1671
|
+
"Codex only, DANGEROUS: run ALL enabled hooks (yours, the project's and "
|
|
1672
|
+
"plugins') without Codex's hook review, for this launch only. Not retained on "
|
|
1673
|
+
"resume. Refused with --repo. Independent of --approval and --yolo."
|
|
1674
|
+
),
|
|
1675
|
+
),
|
|
1473
1676
|
yolo: bool = typer.Option(
|
|
1474
1677
|
False, "--yolo", help="Claude: skip permissions. Codex: bypass approvals and its sandbox."
|
|
1475
1678
|
),
|
|
@@ -1498,6 +1701,14 @@ def agents_launch(
|
|
|
1498
1701
|
tag = tag or result.get("tags")
|
|
1499
1702
|
repo = repo or result.get("repo")
|
|
1500
1703
|
|
|
1704
|
+
if trust_hooks and repo:
|
|
1705
|
+
# Codex would run the clone's own .codex hooks unreviewed once its folder is
|
|
1706
|
+
# trusted. Core refuses this too; refuse here before cloning anything.
|
|
1707
|
+
raise AgentLauncherError(
|
|
1708
|
+
"--trust-hooks cannot be combined with --repo: the repository's project hooks "
|
|
1709
|
+
"would run without review. Clone and vet it, then launch with --cwd."
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1501
1712
|
if yolo:
|
|
1502
1713
|
if tool not in ("claude", "claude-auto", "codex"):
|
|
1503
1714
|
raise AgentLauncherError("--yolo is supported only for Claude and Codex.")
|
|
@@ -1535,6 +1746,7 @@ def agents_launch(
|
|
|
1535
1746
|
tags=tag,
|
|
1536
1747
|
**_request_id_kwargs(launcher.remote_launch, request_id),
|
|
1537
1748
|
**_approval_kwargs(launcher.remote_launch, approval.value if approval else None),
|
|
1749
|
+
**_hook_trust_kwargs(launcher.remote_launch, True if trust_hooks else None),
|
|
1538
1750
|
**_execution_kwargs(launcher.remote_launch, execution_mode.value if execution_mode else None),
|
|
1539
1751
|
)
|
|
1540
1752
|
else:
|
|
@@ -1553,11 +1765,16 @@ def agents_launch(
|
|
|
1553
1765
|
team_id=team,
|
|
1554
1766
|
tags=tag,
|
|
1555
1767
|
**_approval_kwargs(launcher.launch, approval.value if approval else None),
|
|
1768
|
+
**_hook_trust_kwargs(launcher.launch, True if trust_hooks else None),
|
|
1556
1769
|
**_execution_kwargs(launcher.launch, execution_mode.value if execution_mode else None),
|
|
1557
1770
|
progress_callback=_clone_progress if repo else None,
|
|
1558
1771
|
)
|
|
1559
1772
|
|
|
1560
|
-
status_val =
|
|
1773
|
+
status_val = (
|
|
1774
|
+
"starting / check terminal"
|
|
1775
|
+
if _startup_pending(session)
|
|
1776
|
+
else (session.status.value if session.status else "idle")
|
|
1777
|
+
)
|
|
1561
1778
|
style = _status_style(status_val)
|
|
1562
1779
|
|
|
1563
1780
|
lines = []
|
|
@@ -1583,7 +1800,7 @@ def agents_launch(
|
|
|
1583
1800
|
console.print(panel)
|
|
1584
1801
|
console.print()
|
|
1585
1802
|
console.print("[dim]Open from the Agent Dashboard or attach with:[/dim]")
|
|
1586
|
-
|
|
1803
|
+
_startup_notice(session, compute)
|
|
1587
1804
|
|
|
1588
1805
|
except AgentLauncherError as e:
|
|
1589
1806
|
console.print(
|
|
@@ -1686,6 +1903,14 @@ def agents_resume(
|
|
|
1686
1903
|
approval: Optional[ApprovalMode] = typer.Option(
|
|
1687
1904
|
None, "--approval", help="Override recorded permissions for this resume. Omit to preserve them."
|
|
1688
1905
|
),
|
|
1906
|
+
trust_hooks: bool = typer.Option(
|
|
1907
|
+
False,
|
|
1908
|
+
"--trust-hooks",
|
|
1909
|
+
help=(
|
|
1910
|
+
"Codex only, DANGEROUS: run ALL enabled hooks without Codex's hook review, "
|
|
1911
|
+
"for this resume only. Hooks are reviewed again on every run that omits it."
|
|
1912
|
+
),
|
|
1913
|
+
),
|
|
1689
1914
|
compute: Optional[str] = typer.Option(None, "--compute", "-c", help="SSH alias for remote compute"),
|
|
1690
1915
|
):
|
|
1691
1916
|
"""Resume a previous agent session.
|
|
@@ -1716,6 +1941,8 @@ def agents_resume(
|
|
|
1716
1941
|
flags += f" --execution-mode {shlex.quote(execution_mode.value)}"
|
|
1717
1942
|
if approval:
|
|
1718
1943
|
flags += f" --approval {shlex.quote(approval.value)}"
|
|
1944
|
+
if trust_hooks:
|
|
1945
|
+
flags += " --trust-hooks"
|
|
1719
1946
|
# markup=False -- see agents_status.
|
|
1720
1947
|
console.print(
|
|
1721
1948
|
_ssh_cmd(compute, f"qbraid agents resume {shlex.quote(session_id)}{flags}"),
|
|
@@ -1735,6 +1962,7 @@ def agents_resume(
|
|
|
1735
1962
|
session_id=session_id,
|
|
1736
1963
|
name=name,
|
|
1737
1964
|
**(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
|
|
1965
|
+
**_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
|
|
1738
1966
|
**_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
|
|
1739
1967
|
)
|
|
1740
1968
|
except AgentLauncherError as e:
|
|
@@ -1779,6 +2007,12 @@ def agents_resume(
|
|
|
1779
2007
|
cwd=original.cwd,
|
|
1780
2008
|
name=fork_name,
|
|
1781
2009
|
instructions=None,
|
|
2010
|
+
**_hook_trust_kwargs(
|
|
2011
|
+
launcher.launch,
|
|
2012
|
+
# Never inherited from the original: only an explicit
|
|
2013
|
+
# --trust-hooks on this command bypasses hook review.
|
|
2014
|
+
True if trust_hooks else None,
|
|
2015
|
+
),
|
|
1782
2016
|
# A fork of a detached agent must not quietly
|
|
1783
2017
|
# become one that waits for a human.
|
|
1784
2018
|
**_approval_kwargs(
|
|
@@ -1787,11 +2021,7 @@ def agents_resume(
|
|
|
1787
2021
|
),
|
|
1788
2022
|
**_execution_kwargs(
|
|
1789
2023
|
launcher.launch,
|
|
1790
|
-
(
|
|
1791
|
-
execution_mode.value
|
|
1792
|
-
if execution_mode
|
|
1793
|
-
else getattr(original, "execution_mode", None)
|
|
1794
|
-
),
|
|
2024
|
+
_fork_execution(original, approval, execution_mode),
|
|
1795
2025
|
),
|
|
1796
2026
|
)
|
|
1797
2027
|
else:
|
|
@@ -1806,60 +2036,68 @@ def agents_resume(
|
|
|
1806
2036
|
|
|
1807
2037
|
elif "Multiple sessions match" not in err_msg:
|
|
1808
2038
|
raise
|
|
2039
|
+
else:
|
|
2040
|
+
# Only an ambiguous prefix reaches the picker. A completed fork
|
|
2041
|
+
# used to fall through to it, find no matches and exit 1 after
|
|
2042
|
+
# the fork had already launched.
|
|
2043
|
+
# Show filtered picker with only matching sessions
|
|
2044
|
+
import questionary
|
|
2045
|
+
|
|
2046
|
+
registry = _get_registry()
|
|
2047
|
+
all_sessions = registry.search_sessions(include_stopped=True)
|
|
2048
|
+
matches = [
|
|
2049
|
+
s
|
|
2050
|
+
for s in all_sessions
|
|
2051
|
+
if (
|
|
2052
|
+
s.session_id == session_id
|
|
2053
|
+
or s.session_id.startswith(session_id)
|
|
2054
|
+
or (s.tmux_window and s.tmux_window == session_id)
|
|
2055
|
+
or (s.terminal_name and s.terminal_name == session_id)
|
|
2056
|
+
)
|
|
2057
|
+
]
|
|
2058
|
+
matches.sort(key=lambda s: s.last_activity or s.started_at or 0, reverse=True)
|
|
1809
2059
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
s.session_id
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2060
|
+
if not matches:
|
|
2061
|
+
console.print(
|
|
2062
|
+
"[yellow]No visible sessions match this prefix. "
|
|
2063
|
+
"Use a full session ID to resume a retained record.[/yellow]"
|
|
2064
|
+
)
|
|
2065
|
+
raise typer.Exit(code=1)
|
|
2066
|
+
|
|
2067
|
+
choices = []
|
|
2068
|
+
for s in matches:
|
|
2069
|
+
sid = s.session_id[:8]
|
|
2070
|
+
tool_val = s.tool.value if s.tool else "?"
|
|
2071
|
+
status_val = s.status.value if s.status else "?"
|
|
2072
|
+
cwd_display = s.cwd or ""
|
|
2073
|
+
if len(cwd_display) > 30:
|
|
2074
|
+
cwd_display = "..." + cwd_display[-27:]
|
|
2075
|
+
age_str = _time_ago(s.last_activity or s.started_at)
|
|
2076
|
+
label = f"{sid} {tool_val:<8} {status_val:<8} {cwd_display:<30} {age_str}"
|
|
2077
|
+
choices.append(questionary.Choice(title=label, value=s.session_id))
|
|
2078
|
+
|
|
2079
|
+
console.print(f"[yellow]Multiple sessions match '{session_id}':[/yellow]")
|
|
2080
|
+
selected = questionary.select(
|
|
2081
|
+
"Select a session to resume:",
|
|
2082
|
+
choices=choices,
|
|
2083
|
+
instruction="(use arrow keys)",
|
|
2084
|
+
).ask()
|
|
2085
|
+
if selected is None:
|
|
2086
|
+
raise typer.Exit()
|
|
1826
2087
|
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2088
|
+
session = launcher.resume(
|
|
2089
|
+
session_id=selected,
|
|
2090
|
+
name=name,
|
|
2091
|
+
**(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
|
|
2092
|
+
**_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
|
|
2093
|
+
**_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
|
|
1831
2094
|
)
|
|
1832
|
-
raise typer.Exit(code=1)
|
|
1833
|
-
|
|
1834
|
-
choices = []
|
|
1835
|
-
for s in matches:
|
|
1836
|
-
sid = s.session_id[:8]
|
|
1837
|
-
tool_val = s.tool.value if s.tool else "?"
|
|
1838
|
-
status_val = s.status.value if s.status else "?"
|
|
1839
|
-
cwd_display = s.cwd or ""
|
|
1840
|
-
if len(cwd_display) > 30:
|
|
1841
|
-
cwd_display = "..." + cwd_display[-27:]
|
|
1842
|
-
age_str = _time_ago(s.last_activity or s.started_at)
|
|
1843
|
-
label = f"{sid} {tool_val:<8} {status_val:<8} {cwd_display:<30} {age_str}"
|
|
1844
|
-
choices.append(questionary.Choice(title=label, value=s.session_id))
|
|
1845
2095
|
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
).ask()
|
|
1852
|
-
if selected is None:
|
|
1853
|
-
raise typer.Exit()
|
|
1854
|
-
|
|
1855
|
-
session = launcher.resume(
|
|
1856
|
-
session_id=selected,
|
|
1857
|
-
name=name,
|
|
1858
|
-
**(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
|
|
1859
|
-
**_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
|
|
1860
|
-
)
|
|
1861
|
-
|
|
1862
|
-
status_val = session.status.value if session.status else "idle"
|
|
2096
|
+
status_val = (
|
|
2097
|
+
"starting / check terminal"
|
|
2098
|
+
if _startup_pending(session)
|
|
2099
|
+
else (session.status.value if session.status else "idle")
|
|
2100
|
+
)
|
|
1863
2101
|
style = _status_style(status_val)
|
|
1864
2102
|
|
|
1865
2103
|
lines = []
|
|
@@ -1880,7 +2118,7 @@ def agents_resume(
|
|
|
1880
2118
|
console.print(panel)
|
|
1881
2119
|
console.print()
|
|
1882
2120
|
console.print("[dim]Attach with:[/dim]")
|
|
1883
|
-
|
|
2121
|
+
_startup_notice(session)
|
|
1884
2122
|
|
|
1885
2123
|
except AgentLauncherError as e:
|
|
1886
2124
|
console.print(Panel(str(e), title="Resume Error", border_style="red"))
|
|
@@ -2845,17 +3083,44 @@ def hooks_uninstall():
|
|
|
2845
3083
|
# =============================================================================
|
|
2846
3084
|
|
|
2847
3085
|
|
|
2848
|
-
def _resolve_from_session(explicit: Optional[str]) -> str:
|
|
3086
|
+
def _resolve_from_session(explicit: Optional[str], registry=None) -> str:
|
|
2849
3087
|
"""Determine the sender session id.
|
|
2850
3088
|
|
|
2851
|
-
Priority: explicit --from >
|
|
2852
|
-
|
|
3089
|
+
Priority: explicit --from > the agent session this command runs inside
|
|
3090
|
+
(process ancestry, else QBRAID_SESSION_ID) > fail with a helpful error.
|
|
3091
|
+
An explicit sender may be an id prefix or a name, like a recipient, but
|
|
3092
|
+
inside an agent it must be that agent.
|
|
2853
3093
|
"""
|
|
3094
|
+
caller = _calling_session(registry) if registry is not None else None
|
|
2854
3095
|
if explicit:
|
|
2855
|
-
|
|
3096
|
+
sender = explicit
|
|
3097
|
+
if registry is not None:
|
|
3098
|
+
match = _find_session_by_prefix(registry, explicit)
|
|
3099
|
+
if match is not None:
|
|
3100
|
+
sender = match.session_id
|
|
3101
|
+
if caller and sender != caller:
|
|
3102
|
+
# A coordination guardrail, not authentication: an agent speaks
|
|
3103
|
+
# as itself so recipients and permission rules see the real sender.
|
|
3104
|
+
console.print(
|
|
3105
|
+
f"[red]This command runs inside agent session {_plain(caller[:8])}; it cannot send "
|
|
3106
|
+
f"as {_plain(explicit)}.[/red] Omit --from to send as yourself."
|
|
3107
|
+
)
|
|
3108
|
+
raise typer.Exit(code=1)
|
|
3109
|
+
return sender
|
|
3110
|
+
if caller:
|
|
3111
|
+
return caller
|
|
2856
3112
|
env_sid = os.environ.get("QBRAID_SESSION_ID")
|
|
2857
|
-
if env_sid:
|
|
3113
|
+
if registry is None and env_sid:
|
|
2858
3114
|
return env_sid
|
|
3115
|
+
if env_sid:
|
|
3116
|
+
# The variable outlived its agent (a pane kept it after the agent
|
|
3117
|
+
# stopped): whoever types here is the person, not that agent.
|
|
3118
|
+
console.print(
|
|
3119
|
+
f"[red]QBRAID_SESSION_ID names {_plain(env_sid[:8])}, which is not a running "
|
|
3120
|
+
"agent, so this shell does not send as it.[/red] Pass --from <session-id> to "
|
|
3121
|
+
"choose a sender."
|
|
3122
|
+
)
|
|
3123
|
+
raise typer.Exit(code=1)
|
|
2859
3124
|
console.print(
|
|
2860
3125
|
"[red]No sender session id available.[/red] "
|
|
2861
3126
|
"Pass --from <session-id>, or run this inside a qBraid-managed "
|
|
@@ -2911,13 +3176,34 @@ def _resolve_target_session(registry, identifier: str) -> str:
|
|
|
2911
3176
|
match = _find_session_by_prefix(registry, identifier)
|
|
2912
3177
|
if match is None:
|
|
2913
3178
|
console.print(
|
|
2914
|
-
f"[red]No session matched '{identifier}'.[/red] "
|
|
3179
|
+
f"[red]No session matched '{_plain(identifier)}'.[/red] "
|
|
2915
3180
|
"Try 'qbraid agents list' to see available sessions."
|
|
2916
3181
|
)
|
|
2917
3182
|
raise typer.Exit(code=1)
|
|
2918
3183
|
return match.session_id
|
|
2919
3184
|
|
|
2920
3185
|
|
|
3186
|
+
def _note_offline_recipient(registry, session_id: str) -> None:
|
|
3187
|
+
"""Say so when the recipient is not running: an inbox keeps the message,
|
|
3188
|
+
but nothing reads it until the session is resumed."""
|
|
3189
|
+
status = _session_status(registry, session_id)
|
|
3190
|
+
if status not in ("stopped", "error"):
|
|
3191
|
+
return
|
|
3192
|
+
try:
|
|
3193
|
+
from qbraid_core.services.agents import messaging # pylint: disable=import-outside-toplevel
|
|
3194
|
+
|
|
3195
|
+
successor = getattr(messaging, "live_successor", None)
|
|
3196
|
+
if callable(successor) and successor(registry, session_id): # pylint: disable=not-callable
|
|
3197
|
+
return # Core forwards it to the resumed session and says so.
|
|
3198
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
3199
|
+
pass
|
|
3200
|
+
console.print(
|
|
3201
|
+
f"[yellow]Note: {_plain(session_id[:8])} is {status}. The message waits in its inbox and is "
|
|
3202
|
+
"read when the conversation is resumed (in place, or with `qbraid agents resume`, "
|
|
3203
|
+
"which carries unread messages to the new session).[/yellow]"
|
|
3204
|
+
)
|
|
3205
|
+
|
|
3206
|
+
|
|
2921
3207
|
def _resolve_team_targets(registry, team_id: str, exclude_session_id: Optional[str] = None) -> list:
|
|
2922
3208
|
"""Return every session in a team, optionally excluding one (usually the sender).
|
|
2923
3209
|
|
|
@@ -2947,7 +3233,8 @@ def _parse_target_csv(csv: str, registry) -> list[str]:
|
|
|
2947
3233
|
resolved.append(match.session_id)
|
|
2948
3234
|
if unresolved:
|
|
2949
3235
|
console.print(
|
|
2950
|
-
f"[red]No session matched: {', '.join(unresolved)}[/red]. "
|
|
3236
|
+
f"[red]No session matched: {_plain(', '.join(unresolved))}[/red]. "
|
|
3237
|
+
"Broadcast aborted; nothing sent."
|
|
2951
3238
|
)
|
|
2952
3239
|
raise typer.Exit(code=1)
|
|
2953
3240
|
return resolved
|
|
@@ -2985,7 +3272,15 @@ def _deliver_to_many(
|
|
|
2985
3272
|
registry=registry,
|
|
2986
3273
|
validate_permissions=validate_permissions,
|
|
2987
3274
|
)
|
|
2988
|
-
delivered.append((target_sid, msg.id))
|
|
3275
|
+
delivered.append((getattr(msg, "to_session", None) or target_sid, msg.id))
|
|
3276
|
+
# Older Core has no forwarded_from; newer Core types it Optional[str].
|
|
3277
|
+
forwarded = getattr(msg, "forwarded_from", None)
|
|
3278
|
+
if isinstance(forwarded, str) and forwarded:
|
|
3279
|
+
old_sid, new_sid = str(forwarded), str(msg.to_session)
|
|
3280
|
+
console.print(
|
|
3281
|
+
f"[dim]{_plain(old_sid[:8])} was resumed as {_plain(new_sid[:8])}; "
|
|
3282
|
+
"delivered to the running session.[/dim]"
|
|
3283
|
+
)
|
|
2989
3284
|
except AgentMessagingError as err:
|
|
2990
3285
|
failed.append((target_sid, str(err)))
|
|
2991
3286
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
@@ -2993,10 +3288,11 @@ def _deliver_to_many(
|
|
|
2993
3288
|
|
|
2994
3289
|
for sid, mid in delivered:
|
|
2995
3290
|
console.print(
|
|
2996
|
-
f"[green]→[/green] Sent to [bold]{sid}[/bold] "
|
|
3291
|
+
f"[green]→[/green] Sent to [bold]{_plain(sid)}[/bold] "
|
|
3292
|
+
f"[dim](id={_plain(mid)}, type={_plain(message_type)})[/dim]"
|
|
2997
3293
|
)
|
|
2998
3294
|
for sid, reason in failed:
|
|
2999
|
-
console.print(f"[red]✗[/red] [bold]{sid}[/bold]: {reason}")
|
|
3295
|
+
console.print(f"[red]✗[/red] [bold]{_plain(sid)}[/bold]: {_plain(reason)}")
|
|
3000
3296
|
|
|
3001
3297
|
if failed and not delivered:
|
|
3002
3298
|
return 1
|
|
@@ -3005,6 +3301,7 @@ def _deliver_to_many(
|
|
|
3005
3301
|
return 0
|
|
3006
3302
|
|
|
3007
3303
|
|
|
3304
|
+
# Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
|
|
3008
3305
|
@agents_app.command("send-to")
|
|
3009
3306
|
def agents_send_to(
|
|
3010
3307
|
to: str = typer.Argument(
|
|
@@ -3043,7 +3340,7 @@ def agents_send_to(
|
|
|
3043
3340
|
|
|
3044
3341
|
Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
|
|
3045
3342
|
the machine that owns it, so there is no ``--compute``. Cross-host
|
|
3046
|
-
messaging
|
|
3343
|
+
messaging is not supported yet.
|
|
3047
3344
|
|
|
3048
3345
|
|
|
3049
3346
|
Delivery is a JSONL append at ~/.qbraid/inboxes/<to-session-id>.jsonl
|
|
@@ -3059,8 +3356,9 @@ def agents_send_to(
|
|
|
3059
3356
|
raise typer.Exit(code=1) from err
|
|
3060
3357
|
|
|
3061
3358
|
registry = _get_registry()
|
|
3062
|
-
from_sid = _resolve_from_session(from_)
|
|
3359
|
+
from_sid = _resolve_from_session(from_, registry)
|
|
3063
3360
|
to_sid = _resolve_target_session(registry, to)
|
|
3361
|
+
_note_offline_recipient(registry, to_sid)
|
|
3064
3362
|
|
|
3065
3363
|
code = _deliver_to_many(
|
|
3066
3364
|
[to_sid],
|
|
@@ -3074,6 +3372,7 @@ def agents_send_to(
|
|
|
3074
3372
|
raise typer.Exit(code=code)
|
|
3075
3373
|
|
|
3076
3374
|
|
|
3375
|
+
# Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
|
|
3077
3376
|
@agents_app.command("broadcast")
|
|
3078
3377
|
def agents_broadcast(
|
|
3079
3378
|
text: str = typer.Argument(..., help="Message body — delivered to every target."),
|
|
@@ -3115,7 +3414,7 @@ def agents_broadcast(
|
|
|
3115
3414
|
|
|
3116
3415
|
Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
|
|
3117
3416
|
the machine that owns it, so there is no ``--compute``. Cross-host
|
|
3118
|
-
messaging
|
|
3417
|
+
messaging is not supported yet.
|
|
3119
3418
|
|
|
3120
3419
|
Two selectors — pick exactly one:
|
|
3121
3420
|
* ``--team <team_id>`` — every session with that team_id (except sender)
|
|
@@ -3140,7 +3439,7 @@ def agents_broadcast(
|
|
|
3140
3439
|
raise typer.Exit(code=2)
|
|
3141
3440
|
|
|
3142
3441
|
registry = _get_registry()
|
|
3143
|
-
from_sid = _resolve_from_session(from_)
|
|
3442
|
+
from_sid = _resolve_from_session(from_, registry)
|
|
3144
3443
|
|
|
3145
3444
|
if team is not None:
|
|
3146
3445
|
members = _resolve_team_targets(registry, team, exclude_session_id=from_sid)
|
|
@@ -3201,14 +3500,15 @@ def _render_messages_table(messages, session_id: str, full_ids: bool = False) ->
|
|
|
3201
3500
|
from_display = msg.from_session if full_ids else msg.from_session[:12]
|
|
3202
3501
|
table.add_row(
|
|
3203
3502
|
ts,
|
|
3204
|
-
from_display,
|
|
3205
|
-
msg.type.value,
|
|
3503
|
+
_plain(from_display),
|
|
3504
|
+
_plain(msg.type.value),
|
|
3206
3505
|
read_marker,
|
|
3207
|
-
msg.text,
|
|
3506
|
+
_plain(msg.text),
|
|
3208
3507
|
)
|
|
3209
3508
|
console.print(table)
|
|
3210
3509
|
|
|
3211
3510
|
|
|
3511
|
+
# Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
|
|
3212
3512
|
@inbox_app.callback()
|
|
3213
3513
|
def inbox_default( # pylint: disable=too-many-arguments
|
|
3214
3514
|
ctx: typer.Context,
|
|
@@ -3247,7 +3547,7 @@ def inbox_default( # pylint: disable=too-many-arguments
|
|
|
3247
3547
|
|
|
3248
3548
|
Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
|
|
3249
3549
|
the machine that owns it, so there is no ``--compute``. Cross-host
|
|
3250
|
-
messaging
|
|
3550
|
+
messaging is not supported yet.
|
|
3251
3551
|
"""
|
|
3252
3552
|
if ctx.invoked_subcommand is not None:
|
|
3253
3553
|
return
|
|
@@ -3279,8 +3579,8 @@ def inbox_default( # pylint: disable=too-many-arguments
|
|
|
3279
3579
|
from_display = msg.from_session if wide else msg.from_session[:12]
|
|
3280
3580
|
console.print(
|
|
3281
3581
|
f"[dim]{ts}[/dim] "
|
|
3282
|
-
f"[cyan]{from_display}[/cyan] "
|
|
3283
|
-
f"[yellow]({msg.type.value})[/yellow] {msg.text}"
|
|
3582
|
+
f"[cyan]{_plain(from_display)}[/cyan] "
|
|
3583
|
+
f"[yellow]({_plain(msg.type.value)})[/yellow] {_plain(msg.text)}"
|
|
3284
3584
|
)
|
|
3285
3585
|
if mark_read_flag:
|
|
3286
3586
|
mark_read(session_id, message_ids=[msg.id])
|
|
@@ -3343,6 +3643,7 @@ def inbox_mark_read(
|
|
|
3343
3643
|
# =============================================================================
|
|
3344
3644
|
|
|
3345
3645
|
|
|
3646
|
+
# Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
|
|
3346
3647
|
@agents_app.command("team")
|
|
3347
3648
|
def agents_team(
|
|
3348
3649
|
team_id: str = typer.Argument(..., help="Team identifier (matches AgentSession.team_id)."),
|
|
@@ -3357,7 +3658,7 @@ def agents_team(
|
|
|
3357
3658
|
|
|
3358
3659
|
Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
|
|
3359
3660
|
the machine that owns it, so there is no ``--compute``. Cross-host
|
|
3360
|
-
messaging
|
|
3661
|
+
messaging is not supported yet.
|
|
3361
3662
|
|
|
3362
3663
|
Answers the leader-orchestrator question "who is on my team, how many
|
|
3363
3664
|
unread messages do they have, and what was the last thing they heard from
|
|
@@ -3380,7 +3681,7 @@ def agents_team(
|
|
|
3380
3681
|
|
|
3381
3682
|
if not members:
|
|
3382
3683
|
console.print(
|
|
3383
|
-
f"[yellow]No sessions found with team_id '{team_id}'.[/yellow] "
|
|
3684
|
+
f"[yellow]No sessions found with team_id '{_plain(team_id)}'.[/yellow] "
|
|
3384
3685
|
"Check 'qbraid agents list' for known team_ids."
|
|
3385
3686
|
)
|
|
3386
3687
|
raise typer.Exit(code=0)
|
|
@@ -3400,7 +3701,7 @@ def agents_team(
|
|
|
3400
3701
|
table = Table(
|
|
3401
3702
|
show_header=True,
|
|
3402
3703
|
header_style="bold",
|
|
3403
|
-
title=f"Team: {team_id} ({len(members)} member{'s' if len(members) != 1 else ''})",
|
|
3704
|
+
title=f"Team: {_plain(team_id)} ({len(members)} member{'s' if len(members) != 1 else ''})",
|
|
3404
3705
|
)
|
|
3405
3706
|
table.add_column("You", justify="center", no_wrap=True)
|
|
3406
3707
|
table.add_column("Session", style="cyan", no_wrap=wide)
|
|
@@ -3432,12 +3733,12 @@ def agents_team(
|
|
|
3432
3733
|
|
|
3433
3734
|
table.add_row(
|
|
3434
3735
|
"★" if s.session_id == me_sid else "",
|
|
3435
|
-
session_display,
|
|
3436
|
-
s.agent_type or "worker",
|
|
3736
|
+
_plain(session_display),
|
|
3737
|
+
_plain(s.agent_type or "worker"),
|
|
3437
3738
|
f"[{status_style}]{s.status or 'unknown'}[/{status_style}]",
|
|
3438
3739
|
f"[{unread_style}]{unread_count}[/{unread_style}]",
|
|
3439
|
-
last_from,
|
|
3440
|
-
last_text,
|
|
3740
|
+
_plain(last_from),
|
|
3741
|
+
_plain(last_text),
|
|
3441
3742
|
)
|
|
3442
3743
|
|
|
3443
3744
|
console.print(table)
|
|
@@ -76,7 +76,16 @@ def slack_send(
|
|
|
76
76
|
file: Optional[str] = typer.Option(None, "--file", "-f", help="UTF-8 file, or - for stdin."),
|
|
77
77
|
as_json: bool = typer.Option(False, "--json", help="Output the API result as JSON."),
|
|
78
78
|
):
|
|
79
|
-
"""Send a Slack DM to yourself.
|
|
79
|
+
"""Send a Slack DM to yourself.
|
|
80
|
+
|
|
81
|
+
Give the message as an argument or with --file (- reads stdin), not both.
|
|
82
|
+
Messages are limited to 3,000 characters. Invalid input exits 2 and API
|
|
83
|
+
failures exit 1. Sends are never retried, since delivery can succeed even
|
|
84
|
+
if the response is lost.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
qbraid comms slack send "Tests passed" --json
|
|
88
|
+
"""
|
|
80
89
|
text = _message(message, file)
|
|
81
90
|
data = _run("send_slack", text)
|
|
82
91
|
_sent(data, as_json, "Slack update sent.")
|
|
@@ -89,7 +98,16 @@ def email_send(
|
|
|
89
98
|
file: Optional[str] = typer.Option(None, "--file", "-f", help="UTF-8 file, or - for stdin."),
|
|
90
99
|
as_json: bool = typer.Option(False, "--json", help="Output the API result as JSON."),
|
|
91
100
|
):
|
|
92
|
-
"""Email an update to yourself.
|
|
101
|
+
"""Email an update to yourself.
|
|
102
|
+
|
|
103
|
+
Mail goes to your qBraid account address. Give the body as an argument or
|
|
104
|
+
with --file (- reads stdin), not both. Bodies are limited to 20,000
|
|
105
|
+
characters, and the API allows 20 emails per hour. Invalid input exits 2
|
|
106
|
+
and API failures exit 1. Sends are never retried.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
qbraid comms email send --subject "Progress" --file report.txt --json
|
|
110
|
+
"""
|
|
93
111
|
text = _message(message, file)
|
|
94
112
|
data = _run("send_email", subject, text)
|
|
95
113
|
_sent(data, as_json, "Email update sent.")
|
|
@@ -108,7 +126,18 @@ def slack_read(
|
|
|
108
126
|
),
|
|
109
127
|
as_json: bool = typer.Option(False, "--json", help="Output messages and pagination as JSON."),
|
|
110
128
|
):
|
|
111
|
-
"""Read your messages to the Slack bot.
|
|
129
|
+
"""Read your messages to the Slack bot.
|
|
130
|
+
|
|
131
|
+
Returns one page, newest first, of messages you sent in the bot DM; thread
|
|
132
|
+
replies are not included. --limit applies before filtering, so an empty
|
|
133
|
+
page can still have a next cursor. Keep --after fixed while following
|
|
134
|
+
cursors, and keep timestamps as strings, not floats. Reading does not mark
|
|
135
|
+
messages as read. Slack rate limits can be as low as one request per
|
|
136
|
+
minute, so avoid tight polling loops.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
qbraid comms slack read --after 1789940000.000001 --json
|
|
140
|
+
"""
|
|
112
141
|
data = _run("read_slack", after=after, cursor=cursor, limit=limit)
|
|
113
142
|
if as_json:
|
|
114
143
|
typer.echo(json.dumps(data, ensure_ascii=True))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.8
|
|
4
4
|
Summary: Command Line Interface for interacting with all parts of the qBraid platform.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -30,11 +30,11 @@ Requires-Dist: typer>=0.12.1
|
|
|
30
30
|
Requires-Dist: rich>=10.11.0
|
|
31
31
|
Requires-Dist: click<=8.5.0
|
|
32
32
|
Requires-Dist: questionary>=2.0.0
|
|
33
|
-
Requires-Dist: qbraid-core>=0.6.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.6.19
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.19; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]>=0.6.19; extra == "envs"
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: isort; extra == "dev"
|
|
40
40
|
Requires-Dist: black; extra == "dev"
|
|
@@ -172,5 +172,5 @@ qbraid comms email send --subject "Progress" --file report.txt --json
|
|
|
172
172
|
qbraid comms slack read --json
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
See [communication commands](docs/
|
|
176
|
-
|
|
175
|
+
See [communication commands](https://docs.qbraid.com/v2/cli/api-reference/qbraid_comms) for Slack connection, stdin input,
|
|
176
|
+
message limits, and pagination.
|
|
@@ -2,10 +2,10 @@ typer>=0.12.1
|
|
|
2
2
|
rich>=10.11.0
|
|
3
3
|
click<=8.5.0
|
|
4
4
|
questionary>=2.0.0
|
|
5
|
-
qbraid-core>=0.6.
|
|
5
|
+
qbraid-core>=0.6.19
|
|
6
6
|
|
|
7
7
|
[compute]
|
|
8
|
-
qbraid-core[compute]>=0.6.
|
|
8
|
+
qbraid-core[compute]>=0.6.19
|
|
9
9
|
|
|
10
10
|
[dev]
|
|
11
11
|
isort
|
|
@@ -15,4 +15,4 @@ pytest-cov
|
|
|
15
15
|
pytest-asyncio
|
|
16
16
|
|
|
17
17
|
[envs]
|
|
18
|
-
qbraid-core[environments]>=0.6.
|
|
18
|
+
qbraid-core[environments]>=0.6.19
|
|
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
|