devops-bot-sdk 1.2.0__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/PKG-INFO +1 -1
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/pyproject.toml +1 -1
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/client.py +291 -17
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/config.py +3 -0
- devops_bot_sdk-1.4.0/sdk/ipc/handlers.py +816 -0
- devops_bot_sdk-1.4.0/sdk/local_exec.py +175 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/models/requests.py +33 -5
- devops_bot_sdk-1.4.0/sdk/models/responses.py +165 -0
- devops_bot_sdk-1.4.0/sdk/test_pipeline.py +391 -0
- devops_bot_sdk-1.2.0/sdk/ipc/handlers.py +0 -243
- devops_bot_sdk-1.2.0/sdk/models/responses.py +0 -67
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/README.md +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/sdk/test.py +0 -0
- {devops_bot_sdk-1.2.0 → devops_bot_sdk-1.4.0}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devops-bot-sdk"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.0"
|
|
8
8
|
description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "LicenseRef-Proprietary"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -30,7 +30,7 @@ Rules:
|
|
|
30
30
|
- All data egress through submit_webhook only
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
__version__ = "1.
|
|
33
|
+
__version__ = "1.4.0"
|
|
34
34
|
__author__ = "AgentOS"
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
@@ -29,15 +29,18 @@ from sdk.models.requests import (
|
|
|
29
29
|
from sdk.models.responses import (
|
|
30
30
|
BootstrapData, ChatHistory, OAuthStarted, PipelineResult, PipelineStatus,
|
|
31
31
|
ThreadSummary, WebhookAck,
|
|
32
|
+
OrchestrateAutoStatusResponse, OrchestrateAutoStopResponse, OrchestrateAutoResumeResponse,
|
|
33
|
+
OrchestrateAutoStopAllResponse, OrchestrateAutoResumeAllResponse,
|
|
32
34
|
)
|
|
33
35
|
from sdk.sse import _check_status, stream_with_reconnect
|
|
34
36
|
|
|
35
37
|
logger = logging.getLogger(__name__)
|
|
36
38
|
|
|
37
|
-
SDK_VERSION = "1.
|
|
39
|
+
SDK_VERSION = "1.4.0"
|
|
38
40
|
_POLL_INTERVAL = 3.0
|
|
39
41
|
_POLL_TIMEOUT = 600.0
|
|
40
|
-
_ORCHESTRATE_TIMEOUT
|
|
42
|
+
_ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
|
|
43
|
+
_AUTO_ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — same budget for multi-task auto runs
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
class BackendClient:
|
|
@@ -248,38 +251,273 @@ class BackendClient:
|
|
|
248
251
|
_check_status(resp.status_code, self._base_url)
|
|
249
252
|
return resp.json()
|
|
250
253
|
|
|
251
|
-
async def orchestrate(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
`
|
|
254
|
+
async def orchestrate(
|
|
255
|
+
self,
|
|
256
|
+
req: OrchestrateRequest,
|
|
257
|
+
on_status_update=None,
|
|
258
|
+
) -> PipelineResult:
|
|
259
|
+
"""POST /api/v1/orchestrate (background) → poll status → PipelineResult.
|
|
260
|
+
|
|
261
|
+
Runs the full System-B pipeline (engine selection + ML_TASK_FLOW lifecycle).
|
|
262
|
+
A real dev run takes minutes, so this uses the backend's **background**
|
|
263
|
+
mode: it POSTs `background:true` (202 + session_id), then polls
|
|
264
|
+
/orchestrate/status/{session_id} until the run is terminal — avoiding
|
|
265
|
+
HTTP timeouts on the long call.
|
|
266
|
+
|
|
267
|
+
Minimal request: OrchestrateRequest(jira_task_id="..."). The backend
|
|
268
|
+
hydrates user_id from the co_ token, role_slug from the profile,
|
|
269
|
+
user_input from the task's summary + description, and project_path via
|
|
270
|
+
its AI workspace resolver — so none of them need to be sent.
|
|
271
|
+
|
|
272
|
+
`on_status_update`: optional async callable(status: str, state: dict).
|
|
273
|
+
Called whenever the polled status changes — useful for emitting
|
|
274
|
+
intermediate `awaiting_approval` events to the IPC layer without
|
|
275
|
+
stopping the poll loop.
|
|
261
276
|
"""
|
|
262
277
|
body = req.model_dump(exclude_none=True)
|
|
278
|
+
body["background"] = True
|
|
279
|
+
# Best-effort local fill — the backend derives user_id from the co_
|
|
280
|
+
# token anyway, so a token in an unexpected shape is not an error.
|
|
263
281
|
if not body.get("user_id"):
|
|
264
282
|
derived = self._user_id_from_token()
|
|
265
|
-
if
|
|
266
|
-
|
|
267
|
-
body["user_id"] = derived
|
|
283
|
+
if derived:
|
|
284
|
+
body["user_id"] = derived
|
|
268
285
|
|
|
286
|
+
# 1. Kick off the background run (202).
|
|
269
287
|
try:
|
|
270
|
-
async with httpx.AsyncClient(timeout=
|
|
288
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
271
289
|
resp = await client.post(
|
|
272
290
|
self._url("/api/v1/orchestrate"),
|
|
273
291
|
json=body,
|
|
274
292
|
headers=self._headers,
|
|
275
293
|
)
|
|
276
294
|
_check_status(resp.status_code, self._url("/api/v1/orchestrate"))
|
|
277
|
-
|
|
295
|
+
session_id = resp.json()["session_id"]
|
|
296
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
297
|
+
raise
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
raise BackendUnreachable(self._base_url, str(exc)) from exc
|
|
300
|
+
|
|
301
|
+
# 2. Poll until terminal (or the overall deadline).
|
|
302
|
+
# "awaiting_approval" is NOT terminal — we keep polling so the client
|
|
303
|
+
# receives the final result after the approver replies on WhatsApp.
|
|
304
|
+
terminal = {"completed", "failed"}
|
|
305
|
+
last_reported_status: str | None = None
|
|
306
|
+
last_reported_step: str | None = None
|
|
307
|
+
deadline = asyncio.get_event_loop().time() + _ORCHESTRATE_TIMEOUT
|
|
308
|
+
while asyncio.get_event_loop().time() < deadline:
|
|
309
|
+
await asyncio.sleep(_POLL_INTERVAL)
|
|
310
|
+
try:
|
|
311
|
+
state = await self._orchestrate_status(session_id)
|
|
312
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
313
|
+
raise
|
|
314
|
+
except Exception:
|
|
315
|
+
continue # transient — keep polling
|
|
316
|
+
|
|
317
|
+
current_status = str(state.get("status", ""))
|
|
318
|
+
current_step = state.get("current_step")
|
|
319
|
+
|
|
320
|
+
# Notify caller on any status or step change — lets the IPC layer
|
|
321
|
+
# forward real-time progress events to the desktop without stopping
|
|
322
|
+
# the poll loop.
|
|
323
|
+
if on_status_update and (
|
|
324
|
+
current_status != last_reported_status
|
|
325
|
+
or current_step != last_reported_step
|
|
326
|
+
):
|
|
327
|
+
last_reported_status = current_status
|
|
328
|
+
last_reported_step = current_step
|
|
329
|
+
try:
|
|
330
|
+
await on_status_update(current_status, state)
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
if current_status in terminal:
|
|
335
|
+
return PipelineResult(**state)
|
|
336
|
+
return PipelineResult(session_id=session_id, status="failed",
|
|
337
|
+
error="Pipeline poll timed out.")
|
|
338
|
+
|
|
339
|
+
async def _orchestrate_status(self, session_id: str) -> dict:
|
|
340
|
+
"""GET /api/v1/orchestrate/status/{session_id}"""
|
|
341
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
342
|
+
resp = await client.get(
|
|
343
|
+
self._url(f"/api/v1/orchestrate/status/{session_id}"),
|
|
344
|
+
headers=self._headers,
|
|
345
|
+
)
|
|
346
|
+
_check_status(resp.status_code, self._base_url)
|
|
347
|
+
return resp.json()
|
|
348
|
+
|
|
349
|
+
# ── orchestrate-auto ──────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
async def orchestrate_auto(
|
|
352
|
+
self,
|
|
353
|
+
on_status_update=None,
|
|
354
|
+
) -> OrchestrateAutoStatusResponse:
|
|
355
|
+
"""POST /api/v1/orchestrate-auto → poll status → OrchestrateAutoStatusResponse.
|
|
356
|
+
|
|
357
|
+
Auto-discovers all "To Do" Jira tasks assigned to the current user and
|
|
358
|
+
runs the full System-B pipeline for each sequentially in the background.
|
|
359
|
+
Returns 202 immediately; this method polls GET /api/v1/orchestrate-auto/status
|
|
360
|
+
until the overall run reaches a terminal state (completed / failed / no_tasks).
|
|
361
|
+
|
|
362
|
+
on_status_update: optional async callable(auto_status: str, state: dict).
|
|
363
|
+
Called whenever auto_status or any per-task status changes.
|
|
364
|
+
"""
|
|
365
|
+
try:
|
|
366
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
367
|
+
resp = await client.post(
|
|
368
|
+
self._url("/api/v1/orchestrate-auto"),
|
|
369
|
+
headers=self._headers,
|
|
370
|
+
)
|
|
371
|
+
_check_status(resp.status_code, self._url("/api/v1/orchestrate-auto"))
|
|
372
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
373
|
+
raise
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
raise BackendUnreachable(self._base_url, str(exc)) from exc
|
|
376
|
+
|
|
377
|
+
return await self._poll_auto_terminal(on_status_update)
|
|
378
|
+
|
|
379
|
+
async def _poll_auto_terminal(
|
|
380
|
+
self,
|
|
381
|
+
on_status_update=None,
|
|
382
|
+
) -> OrchestrateAutoStatusResponse:
|
|
383
|
+
"""Poll GET /orchestrate-auto/status until the run reaches a terminal state.
|
|
384
|
+
|
|
385
|
+
`paused` counts as terminal: a whole-run stop (this client's or another's)
|
|
386
|
+
ends the wait — the run continues only after an explicit resume.
|
|
387
|
+
"""
|
|
388
|
+
terminal = {"completed", "failed", "no_tasks", "paused"}
|
|
389
|
+
last_auto_status: str | None = None
|
|
390
|
+
last_task_statuses: dict[str, str] = {}
|
|
391
|
+
deadline = asyncio.get_event_loop().time() + _AUTO_ORCHESTRATE_TIMEOUT
|
|
392
|
+
|
|
393
|
+
while asyncio.get_event_loop().time() < deadline:
|
|
394
|
+
await asyncio.sleep(_POLL_INTERVAL)
|
|
395
|
+
try:
|
|
396
|
+
state = await self._orchestrate_auto_status()
|
|
397
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
398
|
+
raise
|
|
399
|
+
except Exception:
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
auto_status = str(state.get("auto_status", ""))
|
|
403
|
+
tasks = state.get("tasks", [])
|
|
404
|
+
current_task_statuses = {
|
|
405
|
+
t.get("session_id", ""): t.get("status", "") for t in tasks
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if on_status_update and (
|
|
409
|
+
auto_status != last_auto_status
|
|
410
|
+
or current_task_statuses != last_task_statuses
|
|
411
|
+
):
|
|
412
|
+
last_auto_status = auto_status
|
|
413
|
+
last_task_statuses = current_task_statuses
|
|
414
|
+
try:
|
|
415
|
+
await on_status_update(auto_status, state)
|
|
416
|
+
except Exception:
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
if auto_status in terminal:
|
|
420
|
+
return OrchestrateAutoStatusResponse(**state)
|
|
421
|
+
|
|
422
|
+
return OrchestrateAutoStatusResponse(auto_status="failed")
|
|
423
|
+
|
|
424
|
+
async def orchestrate_auto_stop(self, session_id: str) -> OrchestrateAutoStopResponse:
|
|
425
|
+
"""POST /api/v1/orchestrate-auto/stop/{session_id} — pause a running pipeline.
|
|
426
|
+
|
|
427
|
+
Cancels the asyncio background task on the server and saves a resume
|
|
428
|
+
snapshot to Redis. The session can be continued via orchestrate_auto_resume().
|
|
429
|
+
"""
|
|
430
|
+
url = self._url(f"/api/v1/orchestrate-auto/stop/{session_id}")
|
|
431
|
+
try:
|
|
432
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
433
|
+
resp = await client.post(url, headers=self._headers)
|
|
434
|
+
_check_status(resp.status_code, url)
|
|
435
|
+
return OrchestrateAutoStopResponse(**resp.json())
|
|
278
436
|
except (BackendAuthFailed, BackendVersionTooOld):
|
|
279
437
|
raise
|
|
280
438
|
except Exception as exc:
|
|
281
439
|
raise BackendUnreachable(self._base_url, str(exc)) from exc
|
|
282
440
|
|
|
441
|
+
async def orchestrate_auto_resume(
|
|
442
|
+
self,
|
|
443
|
+
session_id: str,
|
|
444
|
+
on_status_update=None,
|
|
445
|
+
) -> OrchestrateAutoStatusResponse:
|
|
446
|
+
"""POST /api/v1/orchestrate-auto/resume/{session_id} → poll status → OrchestrateAutoStatusResponse.
|
|
447
|
+
|
|
448
|
+
Resumes a paused pipeline session from its last LangGraph checkpoint —
|
|
449
|
+
no previously-completed steps are re-executed. Returns 202 immediately;
|
|
450
|
+
this method polls GET /api/v1/orchestrate-auto/status until the run is terminal.
|
|
451
|
+
|
|
452
|
+
on_status_update: optional async callable(auto_status: str, state: dict).
|
|
453
|
+
"""
|
|
454
|
+
url = self._url(f"/api/v1/orchestrate-auto/resume/{session_id}")
|
|
455
|
+
try:
|
|
456
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
457
|
+
resp = await client.post(url, headers=self._headers)
|
|
458
|
+
_check_status(resp.status_code, url)
|
|
459
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
460
|
+
raise
|
|
461
|
+
except Exception as exc:
|
|
462
|
+
raise BackendUnreachable(self._base_url, str(exc)) from exc
|
|
463
|
+
|
|
464
|
+
return await self._poll_auto_terminal(on_status_update)
|
|
465
|
+
|
|
466
|
+
async def orchestrate_auto_stop_all(self) -> OrchestrateAutoStopAllResponse:
|
|
467
|
+
"""POST /api/v1/orchestrate-auto/stop — pause the WHOLE auto run.
|
|
468
|
+
|
|
469
|
+
The in-flight session is cancelled and checkpointed (paused), every
|
|
470
|
+
session still queued stays queued, and auto_status becomes `paused`.
|
|
471
|
+
Continue the run with orchestrate_auto_resume_all(). For pausing a
|
|
472
|
+
single session only, use orchestrate_auto_stop(session_id).
|
|
473
|
+
"""
|
|
474
|
+
url = self._url("/api/v1/orchestrate-auto/stop")
|
|
475
|
+
try:
|
|
476
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
477
|
+
resp = await client.post(url, headers=self._headers)
|
|
478
|
+
_check_status(resp.status_code, url)
|
|
479
|
+
return OrchestrateAutoStopAllResponse(**resp.json())
|
|
480
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
481
|
+
raise
|
|
482
|
+
except Exception as exc:
|
|
483
|
+
raise BackendUnreachable(self._base_url, str(exc)) from exc
|
|
484
|
+
|
|
485
|
+
async def orchestrate_auto_resume_all(
|
|
486
|
+
self,
|
|
487
|
+
on_status_update=None,
|
|
488
|
+
) -> OrchestrateAutoStatusResponse:
|
|
489
|
+
"""POST /api/v1/orchestrate-auto/resume → poll status → OrchestrateAutoStatusResponse.
|
|
490
|
+
|
|
491
|
+
Resumes the WHOLE auto run paused via orchestrate_auto_stop_all(): the
|
|
492
|
+
paused session continues from its LangGraph checkpoint (no steps
|
|
493
|
+
re-executed), then the remaining queued sessions run in their original
|
|
494
|
+
order; already-completed sessions are skipped. Returns 202 immediately;
|
|
495
|
+
this method polls GET /api/v1/orchestrate-auto/status until terminal.
|
|
496
|
+
|
|
497
|
+
on_status_update: optional async callable(auto_status: str, state: dict).
|
|
498
|
+
"""
|
|
499
|
+
url = self._url("/api/v1/orchestrate-auto/resume")
|
|
500
|
+
try:
|
|
501
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
502
|
+
resp = await client.post(url, headers=self._headers)
|
|
503
|
+
_check_status(resp.status_code, url)
|
|
504
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
505
|
+
raise
|
|
506
|
+
except Exception as exc:
|
|
507
|
+
raise BackendUnreachable(self._base_url, str(exc)) from exc
|
|
508
|
+
|
|
509
|
+
return await self._poll_auto_terminal(on_status_update)
|
|
510
|
+
|
|
511
|
+
async def _orchestrate_auto_status(self) -> dict:
|
|
512
|
+
"""GET /api/v1/orchestrate-auto/status"""
|
|
513
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
514
|
+
resp = await client.get(
|
|
515
|
+
self._url("/api/v1/orchestrate-auto/status"),
|
|
516
|
+
headers=self._headers,
|
|
517
|
+
)
|
|
518
|
+
_check_status(resp.status_code, self._base_url)
|
|
519
|
+
return resp.json()
|
|
520
|
+
|
|
283
521
|
async def approve_task(self, task_id: str, decision: str, note: str = "") -> dict:
|
|
284
522
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
285
523
|
resp = await client.post(
|
|
@@ -290,6 +528,42 @@ class BackendClient:
|
|
|
290
528
|
_check_status(resp.status_code, self._base_url)
|
|
291
529
|
return resp.json()
|
|
292
530
|
|
|
531
|
+
async def approval_reply(
|
|
532
|
+
self,
|
|
533
|
+
task_id: str,
|
|
534
|
+
decision: str = "APPROVED",
|
|
535
|
+
*,
|
|
536
|
+
local_execution_result: dict | None = None,
|
|
537
|
+
approver_role: str = "TEAM_LEAD",
|
|
538
|
+
approver_name: str = "Desktop",
|
|
539
|
+
raw_reply: str = "",
|
|
540
|
+
) -> dict:
|
|
541
|
+
"""POST /api/v1/webhooks/approval-reply — resume a System-B run after approval.
|
|
542
|
+
|
|
543
|
+
Auth uses the same co_ token (accepted by the endpoint's task-token path).
|
|
544
|
+
For the local-execution hybrid, pass ``local_execution_result`` so the
|
|
545
|
+
server injects the desktop's locally-produced result instead of
|
|
546
|
+
dispatching agents on the server.
|
|
547
|
+
"""
|
|
548
|
+
body: dict = {
|
|
549
|
+
"taskId": task_id,
|
|
550
|
+
"approvalId": "desktop-local",
|
|
551
|
+
"decision": decision,
|
|
552
|
+
"approverRole": approver_role,
|
|
553
|
+
"approverName": approver_name,
|
|
554
|
+
"rawReply": raw_reply or f"{decision} (desktop local execution)",
|
|
555
|
+
}
|
|
556
|
+
if local_execution_result is not None:
|
|
557
|
+
body["localExecutionResult"] = local_execution_result
|
|
558
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
559
|
+
resp = await client.post(
|
|
560
|
+
self._url("/api/v1/webhooks/approval-reply"),
|
|
561
|
+
json=body,
|
|
562
|
+
headers=self._headers,
|
|
563
|
+
)
|
|
564
|
+
_check_status(resp.status_code, self._base_url)
|
|
565
|
+
return resp.json()
|
|
566
|
+
|
|
293
567
|
# ── Tasks ──────────────────────────────────────────────────────────
|
|
294
568
|
|
|
295
569
|
async def list_tasks(
|
|
@@ -115,6 +115,9 @@ def get_token() -> str:
|
|
|
115
115
|
|
|
116
116
|
def configure_cli(rotate: bool = False) -> None:
|
|
117
117
|
"""Interactive `agentos configure` — paste your apiTokenHash once."""
|
|
118
|
+
if not rotate:
|
|
119
|
+
rotate = "--rotate" in sys.argv
|
|
120
|
+
|
|
118
121
|
print("AgentOS SDK — Configuration")
|
|
119
122
|
print("=" * 40)
|
|
120
123
|
|