ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"""Fail-closed Docker boundary for model-generated policy code."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import threading
|
||||
from typing import Any, Iterable
|
||||
import uuid
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
DOCKERFILE = ROOT / "Dockerfile.sandbox"
|
||||
RUNNER = ROOT / "sandbox_runner.py"
|
||||
DEFAULT_TIMEOUT_SECONDS = 8.0
|
||||
MAX_REQUEST_BYTES = 1024 * 1024
|
||||
MAX_OUTPUT_BYTES = 1024 * 1024
|
||||
MAX_SOURCE_BYTES = 256 * 1024
|
||||
|
||||
|
||||
class SandboxError(RuntimeError):
|
||||
"""The candidate sandbox could not produce a trusted response."""
|
||||
|
||||
|
||||
def _default_image() -> str:
|
||||
digest = hashlib.sha256(DOCKERFILE.read_bytes() + RUNNER.read_bytes()).hexdigest()[:12]
|
||||
return f"ai-agent-book/self-modifying-agent-sandbox:{digest}"
|
||||
|
||||
|
||||
def sandbox_image() -> str:
|
||||
"""Return an operator-supplied image or the content-addressed local image."""
|
||||
return os.environ.get("SELF_MODIFY_SANDBOX_IMAGE", _default_image())
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _ensure_image(image: str) -> None:
|
||||
try:
|
||||
inspect_result = subprocess.run(
|
||||
["docker", "image", "inspect", image],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
|
||||
raise SandboxError("Docker is required for candidate evaluation") from exc
|
||||
if inspect_result.returncode == 0:
|
||||
return
|
||||
if os.environ.get("SELF_MODIFY_SANDBOX_IMAGE"):
|
||||
raise SandboxError(f"Configured sandbox image is unavailable: {image}")
|
||||
try:
|
||||
build = subprocess.run(
|
||||
[
|
||||
"docker", "build", "--file", str(DOCKERFILE),
|
||||
"--tag", image, str(ROOT),
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=180,
|
||||
check=False,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
|
||||
raise SandboxError("Docker is required to build the candidate sandbox") from exc
|
||||
if build.returncode != 0:
|
||||
detail = build.stderr[-2000:].strip()
|
||||
raise SandboxError(f"Could not build candidate sandbox: {detail}")
|
||||
|
||||
|
||||
def _docker_command(image: str, name: str) -> list[str]:
|
||||
return [
|
||||
"docker", "run", "--rm", "--interactive", "--name", name,
|
||||
"--hostname", "candidate-sandbox",
|
||||
"--network", "none",
|
||||
"--ipc", "none",
|
||||
"--read-only",
|
||||
"--cap-drop", "ALL",
|
||||
"--security-opt", "no-new-privileges:true",
|
||||
"--user", "65534:65534",
|
||||
"--pids-limit", "16",
|
||||
"--memory", "64m",
|
||||
"--memory-swap", "64m",
|
||||
"--cpus", "0.5",
|
||||
"--ulimit", "cpu=2:2",
|
||||
"--ulimit", "nofile=64:64",
|
||||
"--ulimit", "core=0:0",
|
||||
"--tmpfs", "/tmp:rw,noexec,nosuid,nodev,size=16m,mode=1777",
|
||||
"--workdir", "/tmp",
|
||||
"--log-driver", "none",
|
||||
"--env", "PYTHONDONTWRITEBYTECODE=1",
|
||||
image,
|
||||
]
|
||||
|
||||
|
||||
def _bounded_reader(stream: Any, destination: bytearray, overflow: list[bool]) -> None:
|
||||
while chunk := stream.read(8192):
|
||||
remaining = MAX_OUTPUT_BYTES - len(destination)
|
||||
if remaining > 0:
|
||||
destination.extend(chunk[:remaining])
|
||||
if len(chunk) > remaining:
|
||||
overflow[0] = True
|
||||
|
||||
|
||||
def _remove_container(name: str) -> None:
|
||||
try:
|
||||
subprocess.run(
|
||||
["docker", "rm", "--force", name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
|
||||
def run_in_sandbox(
|
||||
action: str,
|
||||
source: str,
|
||||
trajectories: Iterable[dict[str, Any]],
|
||||
*,
|
||||
stable_source: str | None = None,
|
||||
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
|
||||
) -> dict[str, Any]:
|
||||
"""Evaluate source in a disposable, resource-limited Docker container."""
|
||||
try:
|
||||
if len(source.encode("utf-8")) > MAX_SOURCE_BYTES:
|
||||
raise SandboxError("Candidate source exceeds 256 KiB")
|
||||
if stable_source is not None and len(stable_source.encode("utf-8")) > MAX_SOURCE_BYTES:
|
||||
raise SandboxError("Stable source exceeds 256 KiB")
|
||||
request = json.dumps({
|
||||
"action": action,
|
||||
"source": source,
|
||||
"trajectories": list(trajectories),
|
||||
"stable_source": stable_source,
|
||||
}).encode("utf-8")
|
||||
except (TypeError, UnicodeError, ValueError) as exc:
|
||||
raise SandboxError("Candidate sandbox request is not valid JSON data") from exc
|
||||
if len(request) > MAX_REQUEST_BYTES:
|
||||
raise SandboxError("Candidate sandbox request exceeds 1 MiB")
|
||||
|
||||
image = sandbox_image()
|
||||
_ensure_image(image)
|
||||
name = f"agent-candidate-{uuid.uuid4().hex}"
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
_docker_command(image, name),
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise SandboxError("Docker is required for candidate evaluation") from exc
|
||||
|
||||
stdout = bytearray()
|
||||
stderr = bytearray()
|
||||
stdout_overflow = [False]
|
||||
stderr_overflow = [False]
|
||||
stdout_thread = threading.Thread(
|
||||
target=_bounded_reader,
|
||||
args=(process.stdout, stdout, stdout_overflow),
|
||||
daemon=True,
|
||||
)
|
||||
stderr_thread = threading.Thread(
|
||||
target=_bounded_reader,
|
||||
args=(process.stderr, stderr, stderr_overflow),
|
||||
daemon=True,
|
||||
)
|
||||
stdout_thread.start()
|
||||
stderr_thread.start()
|
||||
assert process.stdin is not None
|
||||
try:
|
||||
process.stdin.write(request)
|
||||
process.stdin.close()
|
||||
returncode = process.wait(timeout=timeout_seconds)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
_remove_container(name)
|
||||
process.kill()
|
||||
process.wait()
|
||||
if isinstance(exc, subprocess.TimeoutExpired):
|
||||
raise SandboxError("Candidate evaluation exceeded the wall-clock limit") from exc
|
||||
raise SandboxError("Candidate sandbox closed its input unexpectedly") from exc
|
||||
finally:
|
||||
stdout_thread.join(timeout=1)
|
||||
stderr_thread.join(timeout=1)
|
||||
|
||||
if stdout_overflow[0] or stderr_overflow[0]:
|
||||
raise SandboxError("Candidate sandbox output exceeded 1 MiB")
|
||||
if returncode != 0:
|
||||
detail = stderr.decode("utf-8", errors="replace").strip()[-1000:]
|
||||
raise SandboxError(f"Candidate sandbox exited with {returncode}: {detail}")
|
||||
try:
|
||||
response = json.loads(stdout.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SandboxError("Candidate sandbox returned an invalid response") from exc
|
||||
if not isinstance(response, dict) or response.get("ok") is not True:
|
||||
raise SandboxError("Candidate sandbox rejected the evaluation request")
|
||||
result = response.get("result")
|
||||
if not isinstance(result, dict):
|
||||
raise SandboxError("Candidate sandbox response has no result object")
|
||||
return result
|
||||
Reference in New Issue
Block a user