tactus 0.34.1__py3-none-any.whl → 0.35.1__py3-none-any.whl
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.
- tactus/__init__.py +1 -1
- tactus/adapters/broker_log.py +17 -14
- tactus/adapters/channels/__init__.py +17 -15
- tactus/adapters/channels/base.py +16 -7
- tactus/adapters/channels/broker.py +43 -13
- tactus/adapters/channels/cli.py +19 -15
- tactus/adapters/channels/host.py +40 -25
- tactus/adapters/channels/ipc.py +82 -31
- tactus/adapters/channels/sse.py +41 -23
- tactus/adapters/cli_hitl.py +19 -19
- tactus/adapters/cli_log.py +4 -4
- tactus/adapters/control_loop.py +138 -99
- tactus/adapters/cost_collector_log.py +9 -9
- tactus/adapters/file_storage.py +56 -52
- tactus/adapters/http_callback_log.py +23 -13
- tactus/adapters/ide_log.py +17 -9
- tactus/adapters/lua_tools.py +4 -5
- tactus/adapters/mcp.py +16 -19
- tactus/adapters/mcp_manager.py +46 -30
- tactus/adapters/memory.py +9 -9
- tactus/adapters/plugins.py +42 -42
- tactus/broker/client.py +75 -78
- tactus/broker/protocol.py +57 -57
- tactus/broker/server.py +252 -197
- tactus/cli/app.py +3 -1
- tactus/cli/control.py +2 -2
- tactus/core/config_manager.py +181 -135
- tactus/core/dependencies/registry.py +66 -48
- tactus/core/dsl_stubs.py +222 -163
- tactus/core/exceptions.py +10 -1
- tactus/core/execution_context.py +152 -112
- tactus/core/lua_sandbox.py +72 -64
- tactus/core/message_history_manager.py +138 -43
- tactus/core/mocking.py +41 -27
- tactus/core/output_validator.py +49 -44
- tactus/core/registry.py +94 -80
- tactus/core/runtime.py +211 -176
- tactus/core/template_resolver.py +16 -16
- tactus/core/yaml_parser.py +55 -45
- tactus/docs/extractor.py +7 -6
- tactus/ide/server.py +119 -78
- tactus/primitives/control.py +10 -6
- tactus/primitives/file.py +48 -46
- tactus/primitives/handles.py +47 -35
- tactus/primitives/host.py +29 -27
- tactus/primitives/human.py +154 -137
- tactus/primitives/json.py +22 -23
- tactus/primitives/log.py +26 -26
- tactus/primitives/message_history.py +285 -31
- tactus/primitives/model.py +15 -9
- tactus/primitives/procedure.py +86 -64
- tactus/primitives/procedure_callable.py +58 -51
- tactus/primitives/retry.py +31 -29
- tactus/primitives/session.py +42 -29
- tactus/primitives/state.py +54 -43
- tactus/primitives/step.py +9 -13
- tactus/primitives/system.py +34 -21
- tactus/primitives/tool.py +44 -31
- tactus/primitives/tool_handle.py +76 -54
- tactus/primitives/toolset.py +25 -22
- tactus/sandbox/config.py +4 -4
- tactus/sandbox/container_runner.py +161 -107
- tactus/sandbox/docker_manager.py +20 -20
- tactus/sandbox/entrypoint.py +16 -14
- tactus/sandbox/protocol.py +15 -15
- tactus/stdlib/classify/llm.py +1 -3
- tactus/stdlib/core/validation.py +0 -3
- tactus/testing/pydantic_eval_runner.py +1 -1
- tactus/utils/asyncio_helpers.py +27 -0
- tactus/utils/cost_calculator.py +7 -7
- tactus/utils/model_pricing.py +11 -12
- tactus/utils/safe_file_library.py +156 -132
- tactus/utils/safe_libraries.py +27 -27
- tactus/validation/error_listener.py +18 -5
- tactus/validation/semantic_visitor.py +392 -333
- tactus/validation/validator.py +89 -49
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/METADATA +15 -3
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/RECORD +81 -80
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/WHEEL +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -105,10 +105,10 @@ class ContainerRunner:
|
|
|
105
105
|
"""
|
|
106
106
|
self.config = config
|
|
107
107
|
|
|
108
|
-
# Parse image name and tag from config.image (e.g., "tactus-sandbox:local")
|
|
109
|
-
|
|
110
|
-
image_name =
|
|
111
|
-
image_tag =
|
|
108
|
+
# Parse image name and tag from config.image (e.g., "tactus-sandbox:local").
|
|
109
|
+
image_name_parts = config.image.split(":")
|
|
110
|
+
image_name = image_name_parts[0] if len(image_name_parts) > 0 else "tactus-sandbox"
|
|
111
|
+
image_tag = image_name_parts[1] if len(image_name_parts) > 1 else "local"
|
|
112
112
|
|
|
113
113
|
self.docker_manager = DockerManager(
|
|
114
114
|
image_name=image_name,
|
|
@@ -138,8 +138,9 @@ class ContainerRunner:
|
|
|
138
138
|
return
|
|
139
139
|
|
|
140
140
|
# Check if auto-rebuild is disabled
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
auto_rebuild_env_value = os.environ.get("TACTUS_AUTO_REBUILD_SANDBOX", "true").lower()
|
|
142
|
+
auto_rebuild_enabled = auto_rebuild_env_value in ("true", "1", "yes")
|
|
143
|
+
if not auto_rebuild_enabled:
|
|
143
144
|
logger.debug("Auto-rebuild disabled via TACTUS_AUTO_REBUILD_SANDBOX")
|
|
144
145
|
return
|
|
145
146
|
|
|
@@ -242,7 +243,7 @@ class ContainerRunner:
|
|
|
242
243
|
else f"tactus-sandbox-{uuid.uuid4().hex[:8]}"
|
|
243
244
|
)
|
|
244
245
|
|
|
245
|
-
|
|
246
|
+
docker_command = [
|
|
246
247
|
"docker",
|
|
247
248
|
"run",
|
|
248
249
|
"--rm", # Remove container after exit
|
|
@@ -251,27 +252,27 @@ class ContainerRunner:
|
|
|
251
252
|
container_name,
|
|
252
253
|
]
|
|
253
254
|
|
|
254
|
-
|
|
255
|
+
docker_command.extend(["--network", self.config.network])
|
|
255
256
|
|
|
256
257
|
# Resource limits
|
|
257
258
|
if self.config.limits.memory:
|
|
258
|
-
|
|
259
|
+
docker_command.extend(["--memory", self.config.limits.memory])
|
|
259
260
|
if self.config.limits.cpus:
|
|
260
|
-
|
|
261
|
+
docker_command.extend(["--cpus", self.config.limits.cpus])
|
|
261
262
|
|
|
262
263
|
# Working directory mount is handled by SandboxConfig.add_default_volumes()
|
|
263
264
|
# which adds ".:/workspace:rw" to config.volumes (unless mount_current_dir=False)
|
|
264
265
|
|
|
265
266
|
# Mount MCP servers if available
|
|
266
267
|
if mcp_servers_path and mcp_servers_path.exists():
|
|
267
|
-
|
|
268
|
+
docker_command.extend(["-v", f"{mcp_servers_path}:/mcp-servers:ro"])
|
|
268
269
|
|
|
269
270
|
# Development mode: mount live Tactus source code
|
|
270
271
|
if self.config.dev_mode:
|
|
271
272
|
tactus_src_dir = self._find_tactus_source_dir()
|
|
272
273
|
if tactus_src_dir:
|
|
273
|
-
logger.info(
|
|
274
|
-
|
|
274
|
+
logger.info("[DEV MODE] Mounting live Tactus source from: %s", tactus_src_dir)
|
|
275
|
+
docker_command.extend(["-v", f"{tactus_src_dir}/tactus:/app/tactus:ro"])
|
|
275
276
|
else:
|
|
276
277
|
logger.warning(
|
|
277
278
|
"[DEV MODE] Could not locate Tactus source directory, using baked-in version"
|
|
@@ -279,31 +280,36 @@ class ContainerRunner:
|
|
|
279
280
|
|
|
280
281
|
# Additional user-configured volumes
|
|
281
282
|
for volume in self.config.volumes:
|
|
282
|
-
|
|
283
|
+
docker_command.extend(
|
|
284
|
+
["-v", self._normalize_volume_spec(volume, base_dir=volume_base_dir)]
|
|
285
|
+
)
|
|
283
286
|
|
|
284
287
|
# User-configured additional env vars
|
|
285
288
|
for key, value in self.config.env.items():
|
|
286
289
|
if key in self._BLOCKED_CONTAINER_ENV_KEYS:
|
|
287
|
-
logger.warning(
|
|
290
|
+
logger.warning(
|
|
291
|
+
"[SANDBOX] Refusing to pass secret env var into container: %s",
|
|
292
|
+
key,
|
|
293
|
+
)
|
|
288
294
|
continue
|
|
289
|
-
|
|
295
|
+
docker_command.extend(["--env", f"{key}={value}"])
|
|
290
296
|
|
|
291
297
|
# Optional per-run callback URL for HTTP event streaming (IDE).
|
|
292
298
|
if callback_url:
|
|
293
|
-
|
|
299
|
+
docker_command.extend(["--env", f"TACTUS_CALLBACK_URL={callback_url}"])
|
|
294
300
|
|
|
295
301
|
# Extra env vars for this run
|
|
296
302
|
if extra_env:
|
|
297
303
|
for key, value in extra_env.items():
|
|
298
|
-
|
|
304
|
+
docker_command.extend(["--env", f"{key}={value}"])
|
|
299
305
|
|
|
300
306
|
# Working directory inside container
|
|
301
|
-
|
|
307
|
+
docker_command.extend(["-w", "/workspace"])
|
|
302
308
|
|
|
303
309
|
# Image name
|
|
304
|
-
|
|
310
|
+
docker_command.append(self.config.image)
|
|
305
311
|
|
|
306
|
-
return
|
|
312
|
+
return docker_command
|
|
307
313
|
|
|
308
314
|
def _normalize_volume_spec(self, volume: str, base_dir: Optional[Path]) -> str:
|
|
309
315
|
"""
|
|
@@ -319,27 +325,31 @@ class ContainerRunner:
|
|
|
319
325
|
- volume_name:/container[:mode] (left unchanged)
|
|
320
326
|
"""
|
|
321
327
|
# Basic split: host:container[:mode]
|
|
322
|
-
|
|
323
|
-
if len(
|
|
328
|
+
volume_parts = volume.split(":")
|
|
329
|
+
if len(volume_parts) < 2:
|
|
324
330
|
return volume
|
|
325
331
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
332
|
+
host_path_raw = volume_parts[0]
|
|
333
|
+
container_path = volume_parts[1]
|
|
334
|
+
mount_mode = volume_parts[2] if len(volume_parts) > 2 else None
|
|
329
335
|
|
|
330
|
-
host_is_path =
|
|
336
|
+
host_is_path = (
|
|
337
|
+
host_path_raw.startswith(("/", "./", "../", "~"))
|
|
338
|
+
or host_path_raw == "."
|
|
339
|
+
or host_path_raw == ".."
|
|
340
|
+
)
|
|
331
341
|
if not host_is_path:
|
|
332
342
|
# Named volume (or other special form) - leave unchanged
|
|
333
343
|
return volume
|
|
334
344
|
|
|
335
|
-
host_path = Path(
|
|
345
|
+
host_path = Path(host_path_raw).expanduser()
|
|
336
346
|
if not host_path.is_absolute():
|
|
337
347
|
host_path = (base_dir or Path.cwd()) / host_path
|
|
338
348
|
host_path = host_path.resolve()
|
|
339
349
|
|
|
340
|
-
if
|
|
341
|
-
return f"{host_path}:{
|
|
342
|
-
return f"{host_path}:{
|
|
350
|
+
if mount_mode:
|
|
351
|
+
return f"{host_path}:{container_path}:{mount_mode}"
|
|
352
|
+
return f"{host_path}:{container_path}"
|
|
343
353
|
|
|
344
354
|
async def run(
|
|
345
355
|
self,
|
|
@@ -374,24 +384,24 @@ class ContainerRunner:
|
|
|
374
384
|
"""
|
|
375
385
|
# Ensure sandbox is up to date (auto-rebuild if code changed)
|
|
376
386
|
# Skip for IDE to avoid blocking UI - IDE has its own rebuild mechanism
|
|
377
|
-
|
|
378
|
-
self._ensure_sandbox_up_to_date(skip_for_ide=
|
|
387
|
+
skip_rebuild_for_ide_execution = (event_handler is not None) or (callback_url is not None)
|
|
388
|
+
self._ensure_sandbox_up_to_date(skip_for_ide=skip_rebuild_for_ide_execution)
|
|
379
389
|
|
|
380
|
-
|
|
381
|
-
|
|
390
|
+
execution_identifier = str(uuid.uuid4())[:8]
|
|
391
|
+
start_timestamp = time.time()
|
|
382
392
|
broker_server = None
|
|
383
393
|
|
|
384
394
|
# Create temporary workspace if not provided
|
|
385
|
-
|
|
395
|
+
temp_workspace_path = None
|
|
386
396
|
if working_dir is None:
|
|
387
|
-
|
|
388
|
-
working_dir = Path(
|
|
397
|
+
temp_workspace_path = tempfile.mkdtemp(prefix="tactus-sandbox-")
|
|
398
|
+
working_dir = Path(temp_workspace_path)
|
|
389
399
|
|
|
390
400
|
# If we have a source file, copy its directory contents
|
|
391
401
|
if source_file_path:
|
|
392
|
-
|
|
393
|
-
if
|
|
394
|
-
for item in
|
|
402
|
+
source_parent_dir = Path(source_file_path).parent
|
|
403
|
+
if source_parent_dir.exists():
|
|
404
|
+
for item in source_parent_dir.iterdir():
|
|
395
405
|
if item.is_file():
|
|
396
406
|
shutil.copy2(item, working_dir / item.name)
|
|
397
407
|
elif item.is_dir() and not item.name.startswith("."):
|
|
@@ -459,7 +469,9 @@ class ContainerRunner:
|
|
|
459
469
|
|
|
460
470
|
scheme = "tls" if broker_transport == "tls" else "tcp"
|
|
461
471
|
broker_env = {
|
|
462
|
-
"TACTUS_BROKER_SOCKET":
|
|
472
|
+
"TACTUS_BROKER_SOCKET": (
|
|
473
|
+
f"{scheme}://{self.config.broker_host}:" f"{broker_server.bound_port}"
|
|
474
|
+
)
|
|
463
475
|
}
|
|
464
476
|
else:
|
|
465
477
|
raise SandboxError(
|
|
@@ -469,19 +481,19 @@ class ContainerRunner:
|
|
|
469
481
|
working_dir=working_dir,
|
|
470
482
|
mcp_servers_path=mcp_path if mcp_path.exists() else None,
|
|
471
483
|
extra_env=broker_env,
|
|
472
|
-
execution_id=
|
|
484
|
+
execution_id=execution_identifier,
|
|
473
485
|
callback_url=callback_url,
|
|
474
486
|
volume_base_dir=volume_base_dir,
|
|
475
487
|
)
|
|
476
488
|
|
|
477
|
-
logger.debug(
|
|
489
|
+
logger.debug("Docker command: %s", " ".join(docker_cmd))
|
|
478
490
|
|
|
479
491
|
# Create execution request
|
|
480
492
|
request = ExecutionRequest(
|
|
481
493
|
source=source,
|
|
482
494
|
working_dir="/workspace",
|
|
483
495
|
params=params or {},
|
|
484
|
-
execution_id=
|
|
496
|
+
execution_id=execution_identifier,
|
|
485
497
|
run_id=run_id,
|
|
486
498
|
source_file_path=source_file_path,
|
|
487
499
|
format=format,
|
|
@@ -527,19 +539,19 @@ class ContainerRunner:
|
|
|
527
539
|
llm_backend_config=llm_backend_config,
|
|
528
540
|
)
|
|
529
541
|
|
|
530
|
-
result.duration_seconds = time.time() -
|
|
542
|
+
result.duration_seconds = time.time() - start_timestamp
|
|
531
543
|
return result
|
|
532
544
|
|
|
533
545
|
except asyncio.TimeoutError:
|
|
534
546
|
return ExecutionResult.timeout(
|
|
535
|
-
duration_seconds=time.time() -
|
|
547
|
+
duration_seconds=time.time() - start_timestamp,
|
|
536
548
|
)
|
|
537
549
|
except Exception as e:
|
|
538
|
-
logger.exception(
|
|
550
|
+
logger.exception("Sandbox execution failed: %s", e)
|
|
539
551
|
return ExecutionResult.failure(
|
|
540
552
|
error=str(e),
|
|
541
553
|
error_type=type(e).__name__,
|
|
542
|
-
duration_seconds=time.time() -
|
|
554
|
+
duration_seconds=time.time() - start_timestamp,
|
|
543
555
|
)
|
|
544
556
|
finally:
|
|
545
557
|
if broker_server is not None:
|
|
@@ -549,11 +561,11 @@ class ContainerRunner:
|
|
|
549
561
|
logger.debug("[BROKER] Failed to close broker server", exc_info=True)
|
|
550
562
|
|
|
551
563
|
# Cleanup temp directory
|
|
552
|
-
if
|
|
564
|
+
if temp_workspace_path:
|
|
553
565
|
try:
|
|
554
|
-
shutil.rmtree(
|
|
555
|
-
except Exception as e:
|
|
556
|
-
logger.warning(
|
|
566
|
+
shutil.rmtree(temp_workspace_path)
|
|
567
|
+
except Exception as e: # pragma: no cover
|
|
568
|
+
logger.warning("Failed to cleanup temp dir: %s", e) # pragma: no cover
|
|
557
569
|
|
|
558
570
|
async def _run_container(
|
|
559
571
|
self,
|
|
@@ -607,80 +619,104 @@ class ContainerRunner:
|
|
|
607
619
|
return
|
|
608
620
|
|
|
609
621
|
async def handle_broker_request(
|
|
610
|
-
writer: asyncio.StreamWriter,
|
|
622
|
+
writer: asyncio.StreamWriter, request_payload: dict[str, Any]
|
|
611
623
|
) -> None:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
624
|
+
request_id = request_payload.get("id") or ""
|
|
625
|
+
request_method = request_payload.get("method")
|
|
626
|
+
request_params = request_payload.get("params") or {}
|
|
615
627
|
|
|
616
|
-
if not isinstance(
|
|
628
|
+
if not isinstance(request_id, str) or not isinstance(request_method, str):
|
|
617
629
|
await send_event(
|
|
618
630
|
writer,
|
|
619
631
|
{
|
|
620
|
-
"id": str(
|
|
632
|
+
"id": str(request_id) if request_id else "",
|
|
621
633
|
"event": "error",
|
|
622
634
|
"error": {"type": "BadRequest", "message": "Missing id/method"},
|
|
623
635
|
},
|
|
624
636
|
)
|
|
625
637
|
return
|
|
626
638
|
|
|
627
|
-
if
|
|
628
|
-
event =
|
|
639
|
+
if request_method == "events.emit":
|
|
640
|
+
event = (
|
|
641
|
+
request_params.get("event") if isinstance(request_params, dict) else None
|
|
642
|
+
)
|
|
629
643
|
if isinstance(event, dict) and event_handler is not None:
|
|
630
644
|
try:
|
|
631
645
|
event_handler(event)
|
|
632
646
|
except Exception:
|
|
633
647
|
logger.debug("[BROKER] event_handler raised", exc_info=True)
|
|
634
|
-
await send_event(
|
|
648
|
+
await send_event(
|
|
649
|
+
writer,
|
|
650
|
+
{"id": request_id, "event": "done", "data": {"ok": True}},
|
|
651
|
+
)
|
|
635
652
|
return
|
|
636
653
|
|
|
637
|
-
if
|
|
638
|
-
request_data =
|
|
654
|
+
if request_method == "control.request":
|
|
655
|
+
request_data = (
|
|
656
|
+
request_params.get("request") if isinstance(request_params, dict) else None
|
|
657
|
+
)
|
|
639
658
|
if control_handler is not None:
|
|
640
659
|
try:
|
|
641
660
|
# Send delivered event
|
|
642
|
-
await send_event(writer, {"id":
|
|
661
|
+
await send_event(writer, {"id": request_id, "event": "delivered"})
|
|
643
662
|
|
|
644
663
|
# Call control handler and await response
|
|
645
664
|
response_data = await control_handler(request_data)
|
|
646
665
|
|
|
647
666
|
# Send response event
|
|
648
667
|
await send_event(
|
|
649
|
-
writer,
|
|
668
|
+
writer,
|
|
669
|
+
{
|
|
670
|
+
"id": request_id,
|
|
671
|
+
"event": "response",
|
|
672
|
+
"data": response_data,
|
|
673
|
+
},
|
|
650
674
|
)
|
|
651
675
|
except asyncio.TimeoutError:
|
|
652
676
|
await send_event(
|
|
653
677
|
writer,
|
|
654
|
-
{
|
|
678
|
+
{
|
|
679
|
+
"id": request_id,
|
|
680
|
+
"event": "timeout",
|
|
681
|
+
"data": {"timed_out": True},
|
|
682
|
+
},
|
|
655
683
|
)
|
|
656
684
|
except Exception as e:
|
|
657
685
|
logger.debug("[BROKER] control.request handler raised", exc_info=True)
|
|
658
686
|
await send_event(
|
|
659
687
|
writer,
|
|
660
|
-
{
|
|
688
|
+
{
|
|
689
|
+
"id": request_id,
|
|
690
|
+
"event": "error",
|
|
691
|
+
"error": {"message": str(e)},
|
|
692
|
+
},
|
|
661
693
|
)
|
|
662
694
|
else:
|
|
663
695
|
await send_event(
|
|
664
696
|
writer,
|
|
665
697
|
{
|
|
666
|
-
"id":
|
|
698
|
+
"id": request_id,
|
|
667
699
|
"event": "error",
|
|
668
700
|
"error": {"message": "No control handler configured"},
|
|
669
701
|
},
|
|
670
702
|
)
|
|
671
703
|
return
|
|
672
704
|
|
|
673
|
-
if
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
705
|
+
if request_method == "tool.call":
|
|
706
|
+
tool_name = (
|
|
707
|
+
request_params.get("name") if isinstance(request_params, dict) else None
|
|
708
|
+
)
|
|
709
|
+
tool_args = (
|
|
710
|
+
request_params.get("args") if isinstance(request_params, dict) else None
|
|
711
|
+
)
|
|
712
|
+
if tool_args is None:
|
|
713
|
+
tool_args = {}
|
|
678
714
|
|
|
679
|
-
if not isinstance(
|
|
715
|
+
if not isinstance(tool_name, str) or not tool_name:
|
|
680
716
|
await send_event(
|
|
681
717
|
writer,
|
|
682
718
|
{
|
|
683
|
-
"id":
|
|
719
|
+
"id": request_id,
|
|
684
720
|
"event": "error",
|
|
685
721
|
"error": {
|
|
686
722
|
"type": "BadRequest",
|
|
@@ -689,11 +725,11 @@ class ContainerRunner:
|
|
|
689
725
|
},
|
|
690
726
|
)
|
|
691
727
|
return
|
|
692
|
-
if not isinstance(
|
|
728
|
+
if not isinstance(tool_args, dict):
|
|
693
729
|
await send_event(
|
|
694
730
|
writer,
|
|
695
731
|
{
|
|
696
|
-
"id":
|
|
732
|
+
"id": request_id,
|
|
697
733
|
"event": "error",
|
|
698
734
|
"error": {
|
|
699
735
|
"type": "BadRequest",
|
|
@@ -704,16 +740,16 @@ class ContainerRunner:
|
|
|
704
740
|
return
|
|
705
741
|
|
|
706
742
|
try:
|
|
707
|
-
result = tool_registry.call(
|
|
743
|
+
result = tool_registry.call(tool_name, tool_args)
|
|
708
744
|
except KeyError:
|
|
709
745
|
await send_event(
|
|
710
746
|
writer,
|
|
711
747
|
{
|
|
712
|
-
"id":
|
|
748
|
+
"id": request_id,
|
|
713
749
|
"event": "error",
|
|
714
750
|
"error": {
|
|
715
751
|
"type": "ToolNotAllowed",
|
|
716
|
-
"message": f"Tool not allowlisted: {
|
|
752
|
+
"message": f"Tool not allowlisted: {tool_name}",
|
|
717
753
|
},
|
|
718
754
|
},
|
|
719
755
|
)
|
|
@@ -723,7 +759,7 @@ class ContainerRunner:
|
|
|
723
759
|
await send_event(
|
|
724
760
|
writer,
|
|
725
761
|
{
|
|
726
|
-
"id":
|
|
762
|
+
"id": request_id,
|
|
727
763
|
"event": "error",
|
|
728
764
|
"error": {"type": type(e).__name__, "message": str(e)},
|
|
729
765
|
},
|
|
@@ -731,32 +767,33 @@ class ContainerRunner:
|
|
|
731
767
|
return
|
|
732
768
|
|
|
733
769
|
await send_event(
|
|
734
|
-
writer,
|
|
770
|
+
writer,
|
|
771
|
+
{"id": request_id, "event": "done", "data": {"result": result}},
|
|
735
772
|
)
|
|
736
773
|
return
|
|
737
774
|
|
|
738
|
-
if
|
|
775
|
+
if request_method != "llm.chat":
|
|
739
776
|
await send_event(
|
|
740
777
|
writer,
|
|
741
778
|
{
|
|
742
|
-
"id":
|
|
779
|
+
"id": request_id,
|
|
743
780
|
"event": "error",
|
|
744
781
|
"error": {
|
|
745
782
|
"type": "MethodNotFound",
|
|
746
|
-
"message": f"Unknown method: {
|
|
783
|
+
"message": f"Unknown method: {request_method}",
|
|
747
784
|
},
|
|
748
785
|
},
|
|
749
786
|
)
|
|
750
787
|
return
|
|
751
788
|
|
|
752
789
|
provider = (
|
|
753
|
-
|
|
790
|
+
request_params.get("provider") if isinstance(request_params, dict) else None
|
|
754
791
|
) or "openai"
|
|
755
792
|
if provider != "openai":
|
|
756
793
|
await send_event(
|
|
757
794
|
writer,
|
|
758
795
|
{
|
|
759
|
-
"id":
|
|
796
|
+
"id": request_id,
|
|
760
797
|
"event": "error",
|
|
761
798
|
"error": {
|
|
762
799
|
"type": "UnsupportedProvider",
|
|
@@ -766,17 +803,27 @@ class ContainerRunner:
|
|
|
766
803
|
)
|
|
767
804
|
return
|
|
768
805
|
|
|
769
|
-
model =
|
|
770
|
-
messages =
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
806
|
+
model = request_params.get("model") if isinstance(request_params, dict) else None
|
|
807
|
+
messages = (
|
|
808
|
+
request_params.get("messages") if isinstance(request_params, dict) else None
|
|
809
|
+
)
|
|
810
|
+
stream = (
|
|
811
|
+
bool(request_params.get("stream", False))
|
|
812
|
+
if isinstance(request_params, dict)
|
|
813
|
+
else False
|
|
814
|
+
)
|
|
815
|
+
temperature = (
|
|
816
|
+
request_params.get("temperature") if isinstance(request_params, dict) else None
|
|
817
|
+
)
|
|
818
|
+
max_tokens = (
|
|
819
|
+
request_params.get("max_tokens") if isinstance(request_params, dict) else None
|
|
820
|
+
)
|
|
774
821
|
|
|
775
822
|
if not isinstance(model, str) or not model:
|
|
776
823
|
await send_event(
|
|
777
824
|
writer,
|
|
778
825
|
{
|
|
779
|
-
"id":
|
|
826
|
+
"id": request_id,
|
|
780
827
|
"event": "error",
|
|
781
828
|
"error": {
|
|
782
829
|
"type": "BadRequest",
|
|
@@ -789,7 +836,7 @@ class ContainerRunner:
|
|
|
789
836
|
await send_event(
|
|
790
837
|
writer,
|
|
791
838
|
{
|
|
792
|
-
"id":
|
|
839
|
+
"id": request_id,
|
|
793
840
|
"event": "error",
|
|
794
841
|
"error": {
|
|
795
842
|
"type": "BadRequest",
|
|
@@ -808,7 +855,7 @@ class ContainerRunner:
|
|
|
808
855
|
max_tokens=max_tokens,
|
|
809
856
|
stream=True,
|
|
810
857
|
)
|
|
811
|
-
|
|
858
|
+
accumulated_text = ""
|
|
812
859
|
async for chunk in stream_iter:
|
|
813
860
|
try:
|
|
814
861
|
delta = chunk.choices[0].delta
|
|
@@ -819,18 +866,23 @@ class ContainerRunner:
|
|
|
819
866
|
if not text:
|
|
820
867
|
continue
|
|
821
868
|
|
|
822
|
-
|
|
869
|
+
accumulated_text += text
|
|
823
870
|
await send_event(
|
|
824
|
-
writer,
|
|
871
|
+
writer,
|
|
872
|
+
{
|
|
873
|
+
"id": request_id,
|
|
874
|
+
"event": "delta",
|
|
875
|
+
"data": {"text": text},
|
|
876
|
+
},
|
|
825
877
|
)
|
|
826
878
|
|
|
827
879
|
await send_event(
|
|
828
880
|
writer,
|
|
829
881
|
{
|
|
830
|
-
"id":
|
|
882
|
+
"id": request_id,
|
|
831
883
|
"event": "done",
|
|
832
884
|
"data": {
|
|
833
|
-
"text":
|
|
885
|
+
"text": accumulated_text,
|
|
834
886
|
"usage": {
|
|
835
887
|
"prompt_tokens": 0,
|
|
836
888
|
"completion_tokens": 0,
|
|
@@ -857,7 +909,7 @@ class ContainerRunner:
|
|
|
857
909
|
await send_event(
|
|
858
910
|
writer,
|
|
859
911
|
{
|
|
860
|
-
"id":
|
|
912
|
+
"id": request_id,
|
|
861
913
|
"event": "done",
|
|
862
914
|
"data": {
|
|
863
915
|
"text": text,
|
|
@@ -874,7 +926,7 @@ class ContainerRunner:
|
|
|
874
926
|
await send_event(
|
|
875
927
|
writer,
|
|
876
928
|
{
|
|
877
|
-
"id":
|
|
929
|
+
"id": request_id,
|
|
878
930
|
"event": "error",
|
|
879
931
|
"error": {"type": type(e).__name__, "message": str(e)},
|
|
880
932
|
},
|
|
@@ -885,7 +937,9 @@ class ContainerRunner:
|
|
|
885
937
|
async def handle_broker_request(
|
|
886
938
|
writer: asyncio.StreamWriter, req: dict[str, Any]
|
|
887
939
|
) -> None:
|
|
888
|
-
raise RuntimeError(
|
|
940
|
+
raise RuntimeError(
|
|
941
|
+
"Broker requests are not expected in non-stdio transports"
|
|
942
|
+
) # pragma: no cover
|
|
889
943
|
|
|
890
944
|
# Start container process
|
|
891
945
|
process = await asyncio.create_subprocess_exec(
|
|
@@ -894,7 +948,7 @@ class ContainerRunner:
|
|
|
894
948
|
stdout=asyncio.subprocess.PIPE,
|
|
895
949
|
stderr=asyncio.subprocess.PIPE,
|
|
896
950
|
)
|
|
897
|
-
logger.debug(
|
|
951
|
+
logger.debug("[SANDBOX] Spawned container process pid=%s", process.pid)
|
|
898
952
|
|
|
899
953
|
stdout_task: asyncio.Task[None] | None = None
|
|
900
954
|
stderr_task: asyncio.Task[None] | None = None
|
|
@@ -909,7 +963,7 @@ class ContainerRunner:
|
|
|
909
963
|
request_line = (request.to_json() + "\n").encode("utf-8")
|
|
910
964
|
process.stdin.write(request_line)
|
|
911
965
|
await process.stdin.drain()
|
|
912
|
-
logger.debug(
|
|
966
|
+
logger.debug("[SANDBOX] Sent ExecutionRequest bytes=%s", len(request_line))
|
|
913
967
|
|
|
914
968
|
stdout_bytes = bytearray()
|
|
915
969
|
result_future: asyncio.Future[ExecutionResult] = (
|
|
@@ -968,7 +1022,7 @@ class ContainerRunner:
|
|
|
968
1022
|
continue
|
|
969
1023
|
|
|
970
1024
|
if line:
|
|
971
|
-
logger.info(
|
|
1025
|
+
logger.info("[container] %s", line)
|
|
972
1026
|
|
|
973
1027
|
stdout_task = asyncio.create_task(stdout_loop())
|
|
974
1028
|
stderr_task = asyncio.create_task(stderr_loop())
|
|
@@ -1075,7 +1129,7 @@ class ContainerRunner:
|
|
|
1075
1129
|
except Exception:
|
|
1076
1130
|
pass
|
|
1077
1131
|
for task in (stdout_task, stderr_task, wait_task):
|
|
1078
|
-
if task is None:
|
|
1132
|
+
if task is None: # pragma: no cover
|
|
1079
1133
|
continue
|
|
1080
1134
|
task.cancel()
|
|
1081
1135
|
try:
|