qbraid-cli 0.15.1__tar.gz → 0.15.3__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.
Files changed (69) hide show
  1. {qbraid_cli-0.15.1/qbraid_cli.egg-info → qbraid_cli-0.15.3}/PKG-INFO +4 -4
  2. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/pyproject.toml +4 -4
  3. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/agents/app.py +278 -32
  4. qbraid_cli-0.15.3/qbraid_cli/agents/permission_response.py +49 -0
  5. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/skills/app.py +49 -5
  6. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3/qbraid_cli.egg-info}/PKG-INFO +4 -4
  7. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli.egg-info/SOURCES.txt +1 -0
  8. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli.egg-info/requires.txt +3 -3
  9. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/LICENSE +0 -0
  10. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/README.md +0 -0
  11. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/__init__.py +0 -0
  12. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/_build_info.py +0 -0
  13. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/_version.py +0 -0
  14. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/account/__init__.py +0 -0
  15. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/account/app.py +0 -0
  16. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/admin/__init__.py +0 -0
  17. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/admin/app.py +0 -0
  18. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/admin/headers.py +0 -0
  19. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/admin/validation.py +0 -0
  20. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/agents/__init__.py +0 -0
  21. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/ai/__init__.py +0 -0
  22. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/ai/app.py +0 -0
  23. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/ai/token.py +0 -0
  24. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/compute/__init__.py +0 -0
  25. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/compute/app.py +0 -0
  26. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/compute/instances.py +0 -0
  27. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/compute/profiles.py +0 -0
  28. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/compute/server.py +0 -0
  29. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/compute/ssh.py +0 -0
  30. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/configure/__init__.py +0 -0
  31. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/configure/actions.py +0 -0
  32. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/configure/app.py +0 -0
  33. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/devices/__init__.py +0 -0
  34. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/devices/app.py +0 -0
  35. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/devices/validation.py +0 -0
  36. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/envs/__init__.py +0 -0
  37. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/envs/activate.py +0 -0
  38. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/envs/app.py +0 -0
  39. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/envs/create.py +0 -0
  40. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/envs/data_handling.py +0 -0
  41. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/envs/groups.py +0 -0
  42. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/envs/publish.py +0 -0
  43. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/envs/registry.py +0 -0
  44. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/envs/share.py +0 -0
  45. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/exceptions.py +0 -0
  46. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/handlers.py +0 -0
  47. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/jobs/__init__.py +0 -0
  48. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/jobs/app.py +0 -0
  49. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/jobs/groups.py +0 -0
  50. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/kernels/__init__.py +0 -0
  51. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/kernels/app.py +0 -0
  52. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/lab/__init__.py +0 -0
  53. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/lab/app.py +0 -0
  54. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/main.py +0 -0
  55. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/pip/__init__.py +0 -0
  56. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/pip/app.py +0 -0
  57. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/pip/hooks.py +0 -0
  58. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/projects/__init__.py +0 -0
  59. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/projects/app.py +0 -0
  60. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/py.typed +0 -0
  61. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/skills/__init__.py +0 -0
  62. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/troubleshoot/__init__.py +0 -0
  63. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli/troubleshoot/app.py +0 -0
  64. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli.egg-info/dependency_links.txt +0 -0
  65. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli.egg-info/entry_points.txt +0 -0
  66. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/qbraid_cli.egg-info/top_level.txt +0 -0
  67. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/setup.cfg +0 -0
  68. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/tests/test_cli_e2e.py +0 -0
  69. {qbraid_cli-0.15.1 → qbraid_cli-0.15.3}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.15.1
3
+ Version: 0.15.3
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.3.3
32
32
  Requires-Dist: questionary>=2.0.0
33
- Requires-Dist: qbraid-core>=0.5.0
33
+ Requires-Dist: qbraid-core>=0.6.5
34
34
  Provides-Extra: compute
35
- Requires-Dist: qbraid-core[compute]>=0.5.0; extra == "compute"
35
+ Requires-Dist: qbraid-core[compute]>=0.6.5; extra == "compute"
36
36
  Provides-Extra: envs
37
- Requires-Dist: qbraid-core[environments]>=0.5.0; extra == "envs"
37
+ Requires-Dist: qbraid-core[environments]>=0.6.5; extra == "envs"
38
38
  Provides-Extra: dev
39
39
  Requires-Dist: isort; extra == "dev"
40
40
  Requires-Dist: black; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qbraid-cli"
7
- version = "0.15.1"
7
+ version = "0.15.3"
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.3.3",
34
34
  "questionary>=2.0.0",
35
- "qbraid-core>=0.5.0",
35
+ "qbraid-core>=0.6.5",
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.5.0"]
47
- envs = ["qbraid-core[environments]>=0.5.0"]
46
+ compute = ["qbraid-core[compute]>=0.6.5"]
47
+ envs = ["qbraid-core[environments]>=0.6.5"]
48
48
  dev = ["isort", "black", "pytest", "pytest-cov", "pytest-asyncio"]
49
49
 
50
50
  [project.scripts]
@@ -4,6 +4,7 @@
4
4
  """Agents CLI - Monitor and manage AI coding agent sessions."""
5
5
 
6
6
  import inspect
7
+ import json
7
8
  import os
8
9
  import re
9
10
  import signal
@@ -207,7 +208,6 @@ def agents_register():
207
208
 
208
209
  Reads session data from stdin JSON (used by hooks).
209
210
  """
210
- import json
211
211
  import sys
212
212
 
213
213
  try:
@@ -237,7 +237,6 @@ def agents_deregister():
237
237
 
238
238
  Reads session data from stdin JSON (used by hooks).
239
239
  """
240
- import json
241
240
  import sys
242
241
 
243
242
  try:
@@ -381,10 +380,9 @@ def agents_list(
381
380
 
382
381
  # JSON output mode
383
382
  if json_output:
384
- import json as _json
385
383
 
386
384
  output = [s.model_dump(mode="json", by_alias=True, exclude_none=True) for s in sessions]
387
- print(_json.dumps(output, indent=2))
385
+ print(json.dumps(output, indent=2))
388
386
  return
389
387
 
390
388
  # Sort
@@ -1109,6 +1107,57 @@ class ApprovalMode(str, Enum):
1109
1107
  AUTO = "auto"
1110
1108
 
1111
1109
 
1110
+ class ExecutionMode(str, Enum):
1111
+ """Codex command isolation, independent of approval prompts."""
1112
+
1113
+ NATIVE = "native"
1114
+ WORKSPACE = "workspace"
1115
+ CONTAINER = "container"
1116
+
1117
+
1118
+ def _execution_kwargs(method: Callable[..., Any], mode: Optional[str]) -> Dict[str, Any]:
1119
+ """Never silently discard an explicitly selected execution policy."""
1120
+ if mode is None:
1121
+ return {}
1122
+ try:
1123
+ params = inspect.signature(method).parameters
1124
+ supported = "execution_mode" in params or any(
1125
+ param.kind is inspect.Parameter.VAR_KEYWORD for param in params.values()
1126
+ )
1127
+ except (TypeError, ValueError):
1128
+ supported = False
1129
+ if not supported:
1130
+ handle_error(
1131
+ error_type="UnsupportedOptionError",
1132
+ message="--execution-mode requires a newer qbraid-core. Upgrade qbraid-core first.",
1133
+ include_traceback=False,
1134
+ )
1135
+ return {"execution_mode": mode}
1136
+
1137
+
1138
+ def _request_id_kwargs(method: Callable[..., Any], request_id: Optional[str]) -> Dict[str, Any]:
1139
+ """Require explicit Core support before promising remote replay protection."""
1140
+ if request_id is None:
1141
+ return {}
1142
+ try:
1143
+ params = inspect.signature(method).parameters
1144
+ supported = "request_id" in params or any(
1145
+ param.kind is inspect.Parameter.VAR_KEYWORD for param in params.values()
1146
+ )
1147
+ except (TypeError, ValueError):
1148
+ supported = False
1149
+ if not supported:
1150
+ handle_error(
1151
+ error_type="UnsupportedOptionError",
1152
+ message=(
1153
+ "--request-id requires a newer qbraid-core. "
1154
+ "Upgrade Core on this machine and the destination before launching."
1155
+ ),
1156
+ include_traceback=False,
1157
+ )
1158
+ return {"request_id": request_id}
1159
+
1160
+
1112
1161
  def _approval_kwargs(method: Callable[..., Any], approval: Optional[str]) -> Dict[str, Any]:
1113
1162
  """Forward ``approval`` only to a qbraid-core new enough to accept it.
1114
1163
 
@@ -1147,6 +1196,115 @@ def _approval_kwargs(method: Callable[..., Any], approval: Optional[str]) -> Dic
1147
1196
  return {}
1148
1197
 
1149
1198
 
1199
+ @agents_app.command("readiness")
1200
+ def agents_readiness(
1201
+ tool: str = typer.Option(..., "--tool", help="Provider to inspect: claude, codex, or codeq."),
1202
+ cwd: Optional[str] = typer.Option(None, "--cwd", help="Working directory on this machine."),
1203
+ approval: Optional[ApprovalMode] = typer.Option(None, "--approval"),
1204
+ execution_mode: Optional[ExecutionMode] = typer.Option(None, "--execution-mode"),
1205
+ json_output: bool = typer.Option(False, "--json", help="Print structured checks and recovery actions."),
1206
+ verify_auth: bool = typer.Option(
1207
+ False,
1208
+ "--verify-auth",
1209
+ help="Explicitly check provider authentication; does not sign in or transfer credentials.",
1210
+ ),
1211
+ ):
1212
+ """Check this destination before launch.
1213
+
1214
+ Known blockers exit with status 1.
1215
+ """
1216
+ try:
1217
+ from qbraid_core.services.agents.readiness import agent_readiness
1218
+ except ImportError:
1219
+ typer.echo("Destination readiness requires a newer qbraid-core.", err=True)
1220
+ raise typer.Exit(2) from None
1221
+ report = agent_readiness(
1222
+ tool,
1223
+ cwd=cwd,
1224
+ approval=approval.value if approval else None,
1225
+ execution_mode=execution_mode.value if execution_mode else None,
1226
+ verify_auth=verify_auth,
1227
+ )
1228
+ if json_output:
1229
+ print(json.dumps(report))
1230
+ else:
1231
+ for check in report["checks"]:
1232
+ console.print(f"{check['id']}: {check['state']} — {check['message']}", markup=False)
1233
+ for action in report.get("auth", {}).get("actions", []):
1234
+ if action.get("command"):
1235
+ console.print(str(action["command"]), markup=False)
1236
+ console.print("Unknown checks are not proof of readiness. Use --json for recovery actions.")
1237
+ if not report["ready"]:
1238
+ raise typer.Exit(1)
1239
+
1240
+
1241
+ @agents_app.command("machine")
1242
+ def agents_machine(
1243
+ serve: bool = typer.Option(
1244
+ False, "--serve", help="Serve JSON lines over authenticated SSH stdin/stdout."
1245
+ ),
1246
+ once: bool = typer.Option(
1247
+ False, "--once", help="Read one JSON request from stdin and return one response."
1248
+ ),
1249
+ ):
1250
+ """Serve the fleet machine protocol.
1251
+
1252
+ Protocol v1 writes only JSON frames to stdout.
1253
+ """
1254
+ if serve == once:
1255
+ raise typer.BadParameter("Choose exactly one of --serve or --once.")
1256
+ try:
1257
+ from qbraid_core.services.agents.machine_protocol import serve_machine
1258
+ except ImportError:
1259
+ typer.echo("Machine protocol requires a newer qbraid-core.", err=True)
1260
+ raise typer.Exit(2) from None
1261
+ serve_machine(once=once)
1262
+
1263
+
1264
+ @agents_app.command("settings")
1265
+ def agents_settings(
1266
+ execution_mode: Optional[ExecutionMode] = typer.Option(
1267
+ None, "--execution-mode", help="Save the Codex execution default for this machine."
1268
+ ),
1269
+ reset: bool = typer.Option(False, "--reset", help="Remove this machine's execution preference."),
1270
+ json_output: bool = typer.Option(
1271
+ False, "--json", help="Print structured settings and sandbox readiness."
1272
+ ),
1273
+ ):
1274
+ """Inspect or configure agent execution.
1275
+
1276
+ Defaults are shared by the CLI and dashboard on this machine.
1277
+ """
1278
+ try:
1279
+ from qbraid_core.services.agents.execution import execution_settings, set_execution_default
1280
+
1281
+ if reset and execution_mode is not None:
1282
+ raise ValueError("Choose --reset or --execution-mode, not both.")
1283
+ if reset:
1284
+ settings = set_execution_default(None)
1285
+ elif execution_mode is not None:
1286
+ settings = set_execution_default(execution_mode.value)
1287
+ else:
1288
+ settings = execution_settings()
1289
+ if json_output:
1290
+ print(json.dumps(settings))
1291
+ else:
1292
+ console.print(
1293
+ "Codex execution default: " + (settings["execution_mode"] or "not set (legacy defaults)")
1294
+ )
1295
+ console.print("Scope: this machine; applies to new CLI and dashboard launches.")
1296
+ console.print(settings["workspace"]["detail"])
1297
+ console.print("Override one launch with --execution-mode native|workspace|container.")
1298
+ except ImportError:
1299
+ handle_error(
1300
+ error_type="UnsupportedOptionError",
1301
+ message="Agent execution settings require a newer qbraid-core.",
1302
+ include_traceback=False,
1303
+ )
1304
+ except Exception as error:
1305
+ handle_error(error_type=type(error).__name__, message=str(error), include_traceback=False)
1306
+
1307
+
1150
1308
  @agents_app.command("launch")
1151
1309
  def agents_launch(
1152
1310
  tool: Optional[str] = typer.Option(
@@ -1167,6 +1325,18 @@ def agents_launch(
1167
1325
  compute: Optional[str] = typer.Option(
1168
1326
  None, "--compute", "-c", help="SSH alias for remote compute server"
1169
1327
  ),
1328
+ request_id: Optional[str] = typer.Option(
1329
+ None, "--request-id", help="Remote launch UUID retained for reconciling an interrupted request."
1330
+ ),
1331
+ execution_mode: Optional[ExecutionMode] = typer.Option(
1332
+ None,
1333
+ "--execution-mode",
1334
+ help=(
1335
+ "Codex only: native uses Codex defaults; workspace adds its workspace sandbox; "
1336
+ "container uses current user access without an extra Codex sandbox (no sudo). "
1337
+ "Omitted preserves existing behavior. Independent of --approval."
1338
+ ),
1339
+ ),
1170
1340
  approval: Optional[ApprovalMode] = typer.Option(
1171
1341
  None,
1172
1342
  "--approval",
@@ -1177,11 +1347,11 @@ def agents_launch(
1177
1347
 
1178
1348
  Starts the agent in a managed tmux session.
1179
1349
 
1180
- The agent runs detached, so nothing is watching its confirmation prompts.
1181
- Pass --approval auto for an agent you intend to leave running: it starts
1182
- the tool in its own non-interactive mode instead of parking on a prompt
1183
- until you come back. Approval mode is remembered, so `qbraid agents
1184
- resume` restores it.
1350
+ Detached agents may wait for approval until you open their terminal or dashboard.
1351
+ Approval and execution mode are independent and remembered for resume.
1352
+ --approval auto disables approval prompts; --execution-mode container
1353
+ explicitly uses the current user's access without an extra Codex sandbox.
1354
+ Choose it only where you trust the surrounding machine/container isolation.
1185
1355
  """
1186
1356
  try:
1187
1357
  from qbraid_core.services.agents import AgentLauncher, AgentLauncherError
@@ -1197,6 +1367,12 @@ def agents_launch(
1197
1367
  tag = tag or result.get("tags")
1198
1368
  repo = repo or result.get("repo")
1199
1369
 
1370
+ if request_id is not None:
1371
+ import uuid
1372
+
1373
+ if not compute or str(uuid.UUID(request_id)) != request_id:
1374
+ raise AgentLauncherError("--request-id requires --compute and a canonical UUID.")
1375
+
1200
1376
  launcher = AgentLauncher()
1201
1377
 
1202
1378
  # Remote launch via SSH
@@ -1210,7 +1386,9 @@ def agents_launch(
1210
1386
  instructions=instructions,
1211
1387
  agent_type=agent_type,
1212
1388
  tags=tag,
1389
+ **_request_id_kwargs(launcher.remote_launch, request_id),
1213
1390
  **_approval_kwargs(launcher.remote_launch, approval.value if approval else None),
1391
+ **_execution_kwargs(launcher.remote_launch, execution_mode.value if execution_mode else None),
1214
1392
  )
1215
1393
  else:
1216
1394
 
@@ -1228,6 +1406,7 @@ def agents_launch(
1228
1406
  team_id=team,
1229
1407
  tags=tag,
1230
1408
  **_approval_kwargs(launcher.launch, approval.value if approval else None),
1409
+ **_execution_kwargs(launcher.launch, execution_mode.value if execution_mode else None),
1231
1410
  progress_callback=_clone_progress if repo else None,
1232
1411
  )
1233
1412
 
@@ -1287,7 +1466,9 @@ def _interactive_resume(console_: Console) -> str:
1287
1466
  import questionary
1288
1467
 
1289
1468
  registry = _get_registry()
1290
- all_sessions = registry.load_registry().sessions
1469
+ # Use the display enumeration so retained transcript aliases do not appear
1470
+ # as duplicate choices. Stopped sessions remain available for resuming.
1471
+ all_sessions = registry.search_sessions(include_stopped=True)
1291
1472
 
1292
1473
  # Show sessions that could be resumed (have a session_id, prefer recent)
1293
1474
  resumable = sorted(
@@ -1345,6 +1526,9 @@ def agents_resume(
1345
1526
  None, help="Session ID, prefix, or name. Omit for interactive picker."
1346
1527
  ),
1347
1528
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Agent name for the resumed session"),
1529
+ execution_mode: Optional[ExecutionMode] = typer.Option(
1530
+ None, "--execution-mode", help="Override the recorded Codex execution mode for this resume."
1531
+ ),
1348
1532
  ):
1349
1533
  """Resume a previous agent session.
1350
1534
 
@@ -1361,7 +1545,11 @@ def agents_resume(
1361
1545
 
1362
1546
  # Try resume — handle active session and ambiguous matches
1363
1547
  try:
1364
- session = launcher.resume(session_id=session_id, name=name)
1548
+ session = launcher.resume(
1549
+ session_id=session_id,
1550
+ name=name,
1551
+ **_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
1552
+ )
1365
1553
  except AgentLauncherError as e:
1366
1554
  err_msg = str(e)
1367
1555
 
@@ -1407,6 +1595,14 @@ def agents_resume(
1407
1595
  # A fork of a detached agent must not quietly
1408
1596
  # become one that waits for a human.
1409
1597
  **_approval_kwargs(launcher.launch, getattr(original, "approval", None)),
1598
+ **_execution_kwargs(
1599
+ launcher.launch,
1600
+ (
1601
+ execution_mode.value
1602
+ if execution_mode
1603
+ else getattr(original, "execution_mode", None)
1604
+ ),
1605
+ ),
1410
1606
  )
1411
1607
  else:
1412
1608
  console.print("[red]Could not find original session to fork.[/red]")
@@ -1425,7 +1621,7 @@ def agents_resume(
1425
1621
  import questionary
1426
1622
 
1427
1623
  registry = _get_registry()
1428
- all_sessions = registry.load_registry().sessions
1624
+ all_sessions = registry.search_sessions(include_stopped=True)
1429
1625
  matches = [
1430
1626
  s
1431
1627
  for s in all_sessions
@@ -1438,6 +1634,13 @@ def agents_resume(
1438
1634
  ]
1439
1635
  matches.sort(key=lambda s: s.last_activity or s.started_at or 0, reverse=True)
1440
1636
 
1637
+ if not matches:
1638
+ console.print(
1639
+ "[yellow]No visible sessions match this prefix. "
1640
+ "Use a full session ID to resume a retained record.[/yellow]"
1641
+ )
1642
+ raise typer.Exit(code=1)
1643
+
1441
1644
  choices = []
1442
1645
  for s in matches:
1443
1646
  sid = s.session_id[:8]
@@ -1459,7 +1662,11 @@ def agents_resume(
1459
1662
  if selected is None:
1460
1663
  raise typer.Exit()
1461
1664
 
1462
- session = launcher.resume(session_id=selected, name=name)
1665
+ session = launcher.resume(
1666
+ session_id=selected,
1667
+ name=name,
1668
+ **_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
1669
+ )
1463
1670
 
1464
1671
  status_val = session.status.value if session.status else "idle"
1465
1672
  style = _status_style(status_val)
@@ -1718,9 +1925,8 @@ def agents_watch(
1718
1925
  else:
1719
1926
  # Parse and render
1720
1927
  try:
1721
- import json as _json
1722
1928
 
1723
- event = _json.loads(line)
1929
+ event = json.loads(line)
1724
1930
  entry_type = event.get("type", "unknown")
1725
1931
  content = event.get("content", "")
1726
1932
  ts = event.get("timestamp")
@@ -1780,6 +1986,11 @@ def agents_watch(
1780
1986
  can_prompt = interactive and not stream
1781
1987
  can_notify = stream
1782
1988
 
1989
+ if can_prompt or can_notify:
1990
+ from .permission_response import require_bound_permission_support
1991
+
1992
+ require_bound_permission_support()
1993
+
1783
1994
  # Resolve the session ID used for permission files
1784
1995
  perm_session_id = session.session_id
1785
1996
 
@@ -1840,9 +2051,17 @@ def agents_watch(
1840
2051
  # (stdin can't be read from multiple threads safely).
1841
2052
  import threading
1842
2053
 
1843
- from qbraid_core.services.agents.permissions import get_pending_request, respond_to_request
2054
+ from qbraid_core.services.agents.permissions import get_pending_request
2055
+
2056
+ from .permission_response import (
2057
+ UnsupportedPermissionApproval,
2058
+ request_identity,
2059
+ respond_to_displayed_request,
2060
+ supports_bound_approval,
2061
+ )
1844
2062
 
1845
2063
  perm_stop_event = threading.Event()
2064
+ perm_retry = threading.Event()
1846
2065
  perm_pending = threading.Event() # Set when a permission request is detected
1847
2066
  perm_data: dict = {} # Stores the current request
1848
2067
 
@@ -1850,8 +2069,12 @@ def agents_watch(
1850
2069
 
1851
2070
  def _permission_detector():
1852
2071
  """Background thread that detects permission requests."""
2072
+ last_request_id = None
1853
2073
  while not perm_stop_event.is_set():
1854
2074
  if not perm_pending.is_set():
2075
+ if perm_retry.is_set():
2076
+ last_request_id = None
2077
+ perm_retry.clear()
1855
2078
  request = None
1856
2079
 
1857
2080
  if oc_port:
@@ -1877,15 +2100,21 @@ def agents_watch(
1877
2100
  # Claude/Codex: check file-based permission request
1878
2101
  request = get_pending_request(perm_session_id)
1879
2102
 
1880
- if request:
2103
+ if not isinstance(request, dict):
2104
+ request = None
2105
+ identity = request_identity(perm_session_id, request) if request else None
2106
+ if request and identity != last_request_id:
2107
+ last_request_id = identity
2108
+ perm_data.clear()
1881
2109
  perm_data.update(request)
1882
2110
  perm_pending.set()
1883
2111
 
1884
2112
  if can_notify:
1885
- import json as _json
1886
2113
 
1887
2114
  notification = {
1888
2115
  "type": "permission_required",
2116
+ "request_id": identity,
2117
+ "protocol_version": request.get("protocol_version", 1),
1889
2118
  "session_id": perm_session_id,
1890
2119
  "tool_name": request.get("tool_name", "?"),
1891
2120
  "summary": request.get("summary", ""),
@@ -1894,7 +2123,11 @@ def agents_watch(
1894
2123
  if request.get("opencode_request_id"):
1895
2124
  notification["opencode_request_id"] = request["opencode_request_id"]
1896
2125
  notification["opencode_port"] = oc_port
1897
- print(_json.dumps(notification), flush=True)
2126
+ print(json.dumps(notification), flush=True)
2127
+
2128
+ if not can_prompt:
2129
+ perm_data.clear()
2130
+ perm_pending.clear()
1898
2131
 
1899
2132
  # Wait for main loop to clear the flag
1900
2133
  while perm_pending.is_set() and not perm_stop_event.is_set():
@@ -1937,7 +2170,13 @@ def agents_watch(
1937
2170
  console.print()
1938
2171
  console.print(f"[bold yellow]Permission required:[/bold yellow] [bold]{tool_name}[/bold]")
1939
2172
  console.print(f"[dim]{summary}[/dim]")
1940
- console.print(" [bold]1.[/bold] Allow")
2173
+ if supports_bound_approval(perm_data):
2174
+ console.print(" [bold]1.[/bold] Allow")
2175
+ else:
2176
+ console.print(
2177
+ "[dim]Automatic Allow unavailable for this legacy gate; "
2178
+ "restart the agent or use its terminal.[/dim]"
2179
+ )
1941
2180
  console.print(" [bold]2.[/bold] Deny")
1942
2181
  console.print(" [bold]3.[/bold] Pass to agent (show Claude's prompt)")
1943
2182
 
@@ -1953,8 +2192,8 @@ def agents_watch(
1953
2192
  oc_reply_map = {"allow": "once", "deny": "reject", "ask": "once"}
1954
2193
  reply_to_permission(oc_req_port, oc_req_id, oc_reply_map.get(decision, "once"))
1955
2194
  else:
1956
- # Claude: reply via file
1957
- respond_to_request(perm_session_id, decision, reason)
2195
+ # Bind the reply to the request actually shown to the user.
2196
+ respond_to_displayed_request(perm_session_id, perm_data, decision, reason)
1958
2197
 
1959
2198
  try:
1960
2199
  choice = console.input("[dim]> [/dim]").strip()
@@ -1971,16 +2210,21 @@ def agents_watch(
1971
2210
  _send_response("ask")
1972
2211
  console.print("[dim]Invalid — passed to agent's terminal.[/dim]")
1973
2212
  except (KeyboardInterrupt, EOFError):
1974
- _send_response("ask")
2213
+ try:
2214
+ _send_response("ask")
2215
+ except ValueError as exc:
2216
+ perm_retry.set()
2217
+ console.print(f"[yellow]{exc}[/yellow]")
2218
+ except UnsupportedPermissionApproval as exc:
2219
+ console.print(f"[yellow]{exc}[/yellow]")
2220
+ except ValueError as exc:
2221
+ perm_retry.set()
2222
+ console.print(f"[yellow]{exc}[/yellow]")
1975
2223
 
1976
- perm_pending.clear()
1977
2224
  perm_data.clear()
1978
- # Delete the request file to prevent re-detection (Claude only)
1979
- if not oc_req_id:
1980
- req_file = (
1981
- Path(os.path.expanduser("~/.qbraid/permissions")) / f"{perm_session_id}.request.json"
1982
- )
1983
- req_file.unlink(missing_ok=True)
2225
+ perm_pending.clear()
2226
+ # Only the tool-side gate owns request-file cleanup. Removing it
2227
+ # here can erase the next request after the tool consumes our reply.
1984
2228
  continue
1985
2229
 
1986
2230
  # Drain available transcript entries
@@ -2010,7 +2254,6 @@ def agents_watch(
2010
2254
  tool_info = f" ({entry.tool_name})"
2011
2255
 
2012
2256
  if stream:
2013
- import json as _json
2014
2257
 
2015
2258
  event = {
2016
2259
  "type": entry_type,
@@ -2021,7 +2264,7 @@ def agents_watch(
2021
2264
  "cost_usd": entry.cost_usd,
2022
2265
  "token_count": entry.token_count,
2023
2266
  }
2024
- print(_json.dumps({k: v for k, v in event.items() if v is not None}), flush=True)
2267
+ print(json.dumps({k: v for k, v in event.items() if v is not None}), flush=True)
2025
2268
  else:
2026
2269
  line = f"{prefix}{type_label}{tool_info}: [{style}]{content}[/{style}]"
2027
2270
  console.print(line, highlight=False)
@@ -2030,6 +2273,9 @@ def agents_watch(
2030
2273
  console.print("\n[dim]Stopped watching.[/dim]")
2031
2274
  except (typer.Exit, SystemExit):
2032
2275
  raise
2276
+ except ValueError as e:
2277
+ handle_error(message=str(e), include_traceback=False)
2278
+ raise typer.Exit(code=1)
2033
2279
  except Exception as e:
2034
2280
  handle_error(message=str(e))
2035
2281
  raise typer.Exit(code=1)
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2026, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """Bind watch-mode decisions to the permission request displayed to the user."""
5
+
6
+ from qbraid_core.services.agents import permissions
7
+
8
+
9
+ class UnsupportedPermissionApproval(ValueError):
10
+ """The displayed gate cannot safely accept an automatic approval."""
11
+
12
+
13
+ def supports_bound_approval(request: dict) -> bool:
14
+ """Only explicit v2-or-newer gates can bind automatic approvals."""
15
+ try:
16
+ return int(request.get("protocol_version", 1)) >= 2
17
+ except (TypeError, ValueError, OverflowError):
18
+ return False
19
+
20
+
21
+ def require_bound_permission_support() -> None:
22
+ """Refuse permission handling on core versions that ignore request identity."""
23
+ if not getattr(permissions, "SUPPORTS_BOUND_PERMISSION_RESPONSES", False):
24
+ raise ValueError(
25
+ "Permission handling requires a qbraid-core release with bound-response support; "
26
+ "upgrade core first, or watch read-only with --no-interactive."
27
+ )
28
+
29
+
30
+ def request_identity(session_id: str, request: dict) -> str:
31
+ """Support nonce requests and the timestamp identity of older integrations."""
32
+ return (
33
+ request.get("request_id")
34
+ or request.get("opencode_request_id")
35
+ or (f"{session_id}:{request['timestamp']}" if request.get("timestamp") is not None else session_id)
36
+ )
37
+
38
+
39
+ def respond_to_displayed_request(session_id: str, request: dict, decision: str, reason: str = "") -> None:
40
+ """Reject an expired prompt without reporting success or deleting gate state."""
41
+ require_bound_permission_support()
42
+ if decision == "allow" and not supports_bound_approval(request):
43
+ raise UnsupportedPermissionApproval(
44
+ "This legacy gate cannot bind approvals. Restart the agent or use its terminal."
45
+ )
46
+ if not permissions.respond_to_permission(
47
+ session_id, request_identity(session_id, request), decision, reason
48
+ ):
49
+ raise ValueError("Permission request expired or was already answered")
@@ -271,7 +271,7 @@ def skills_install(
271
271
 
272
272
  try:
273
273
  console.print(f"[dim]Installing skill '{slug}'...[/dim]")
274
- results = run_progress_task(install_skill)
274
+ results = run_progress_task(install_skill, include_error_traceback=False)
275
275
 
276
276
  if legacy_core and force:
277
277
  console.print(
@@ -329,6 +329,11 @@ def skills_uninstall(
329
329
  "-t",
330
330
  help="Target AI coding tool(s) to uninstall from",
331
331
  ),
332
+ path: Optional[str] = typer.Option(
333
+ None,
334
+ "--path",
335
+ help="Uninstall from this directory instead of the tool directories",
336
+ ),
332
337
  ) -> None:
333
338
  """Uninstall a skill from AI coding tools.
334
339
 
@@ -337,9 +342,15 @@ def skills_uninstall(
337
342
  """
338
343
 
339
344
  def uninstall_skill():
345
+ from pathlib import Path as _Path
346
+
340
347
  from qbraid_core.services.skills import SkillsClient
341
348
 
342
349
  client = SkillsClient()
350
+ if path:
351
+ # `install --path` had no counterpart, so such an install could
352
+ # only be removed with `rm -rf`.
353
+ return client.uninstall_skill(slug, install_path=_Path(path).expanduser())
343
354
  return client.uninstall_skill(slug, target=target.value)
344
355
 
345
356
  console = Console()
@@ -359,6 +370,8 @@ def skills_uninstall(
359
370
  else:
360
371
  console.print(f"[yellow]Skill '{slug}' was not installed in any location.[/yellow]")
361
372
 
373
+ except typer.Exit:
374
+ raise
362
375
  except Exception as err: # pylint: disable=broad-exception-caught
363
376
  handle_error(message=f"Failed to uninstall skill: {err}")
364
377
 
@@ -371,23 +384,39 @@ def skills_installed(
371
384
  "-t",
372
385
  help="Show skills for specific tool only",
373
386
  ),
387
+ path: Optional[str] = typer.Option(
388
+ None,
389
+ "--path",
390
+ help="List skills installed in this directory",
391
+ ),
374
392
  ) -> None:
375
393
  """List locally installed skills.
376
394
 
377
395
  Shows each skill's name, registry slug and version per tool.
378
396
  """
397
+ # `all` is in the enum for `install`/`uninstall`, where it means every
398
+ # tool. There is no directory called "all", so passing it here reached
399
+ # get_skills_install_path and raised.
400
+ one_target = target if target and target is not SkillTargetOption.ALL else None
379
401
 
380
402
  def list_installed():
403
+ from pathlib import Path as _Path
404
+
381
405
  from qbraid_core.services.skills import SkillsClient
382
406
 
383
407
  client = SkillsClient()
408
+ install_path = _Path(path).expanduser() if path else None
384
409
  if hasattr(client, "list_all_installed_skills_detailed"):
385
- if target:
386
- return {target.value: client.list_installed_skills_detailed(target=target.value)}
410
+ if install_path is not None:
411
+ return {"custom": client.list_installed_skills_detailed(install_path=install_path)}
412
+ if one_target:
413
+ return {one_target.value: client.list_installed_skills_detailed(target=one_target.value)}
387
414
  return client.list_all_installed_skills_detailed()
388
415
  # older qbraid-core: directory names only
389
- if target:
390
- listed = {target.value: client.list_installed_skills(target=target.value)}
416
+ if install_path is not None:
417
+ listed = {"custom": client.list_installed_skills(install_path=install_path)}
418
+ elif one_target:
419
+ listed = {one_target.value: client.list_installed_skills(target=one_target.value)}
391
420
  else:
392
421
  listed = client.list_all_installed_skills()
393
422
  return {
@@ -424,6 +453,10 @@ def skills_installed(
424
453
 
425
454
  console.print("")
426
455
 
456
+ except typer.Exit:
457
+ # handle_error already reported and chose the exit code; catching it
458
+ # again below printed a second, empty "Error:" block.
459
+ raise
427
460
  except Exception as err: # pylint: disable=broad-exception-caught
428
461
  handle_error(message=f"Failed to list installed skills: {err}")
429
462
 
@@ -431,13 +464,22 @@ def skills_installed(
431
464
  @skills_app.command(name="check")
432
465
  def skills_check(
433
466
  slug: str = typer.Argument(..., help="The skill slug to check"),
467
+ path: Optional[str] = typer.Option(
468
+ None,
469
+ "--path",
470
+ help="Check this directory instead of the tool directories",
471
+ ),
434
472
  ) -> None:
435
473
  """Check if a skill is installed and where."""
436
474
 
437
475
  def check_skill():
476
+ from pathlib import Path as _Path
477
+
438
478
  from qbraid_core.services.skills import SkillsClient
439
479
 
440
480
  client = SkillsClient()
481
+ if path:
482
+ return {"custom": client.is_skill_installed(slug, install_path=_Path(path).expanduser())}
441
483
  return client.is_skill_installed_anywhere(slug)
442
484
 
443
485
  console = Console()
@@ -458,6 +500,8 @@ def skills_check(
458
500
  console.print(f"\n[yellow]Skill '{slug}' is not installed anywhere.[/yellow]")
459
501
  console.print(f"\n[dim]Install it with: qbraid skills install {slug}[/dim]")
460
502
 
503
+ except typer.Exit:
504
+ raise
461
505
  except Exception as err: # pylint: disable=broad-exception-caught
462
506
  handle_error(message=f"Failed to check skill: {err}")
463
507
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.15.1
3
+ Version: 0.15.3
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.3.3
32
32
  Requires-Dist: questionary>=2.0.0
33
- Requires-Dist: qbraid-core>=0.5.0
33
+ Requires-Dist: qbraid-core>=0.6.5
34
34
  Provides-Extra: compute
35
- Requires-Dist: qbraid-core[compute]>=0.5.0; extra == "compute"
35
+ Requires-Dist: qbraid-core[compute]>=0.6.5; extra == "compute"
36
36
  Provides-Extra: envs
37
- Requires-Dist: qbraid-core[environments]>=0.5.0; extra == "envs"
37
+ Requires-Dist: qbraid-core[environments]>=0.6.5; extra == "envs"
38
38
  Provides-Extra: dev
39
39
  Requires-Dist: isort; extra == "dev"
40
40
  Requires-Dist: black; extra == "dev"
@@ -22,6 +22,7 @@ qbraid_cli/admin/headers.py
22
22
  qbraid_cli/admin/validation.py
23
23
  qbraid_cli/agents/__init__.py
24
24
  qbraid_cli/agents/app.py
25
+ qbraid_cli/agents/permission_response.py
25
26
  qbraid_cli/ai/__init__.py
26
27
  qbraid_cli/ai/app.py
27
28
  qbraid_cli/ai/token.py
@@ -2,10 +2,10 @@ typer>=0.12.1
2
2
  rich>=10.11.0
3
3
  click<=8.3.3
4
4
  questionary>=2.0.0
5
- qbraid-core>=0.5.0
5
+ qbraid-core>=0.6.5
6
6
 
7
7
  [compute]
8
- qbraid-core[compute]>=0.5.0
8
+ qbraid-core[compute]>=0.6.5
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.5.0
18
+ qbraid-core[environments]>=0.6.5
File without changes
File without changes
File without changes