runagents 0.2.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.
Files changed (38) hide show
  1. runagents-0.2.0/PKG-INFO +150 -0
  2. runagents-0.2.0/README.md +123 -0
  3. runagents-0.2.0/pyproject.toml +50 -0
  4. runagents-0.2.0/runagents/__init__.py +8 -0
  5. runagents-0.2.0/runagents/agent.py +143 -0
  6. runagents-0.2.0/runagents/cli/__init__.py +1 -0
  7. runagents-0.2.0/runagents/cli/binary.py +121 -0
  8. runagents-0.2.0/runagents/cli/dev_cmd.py +308 -0
  9. runagents-0.2.0/runagents/cli/init_cmd.py +57 -0
  10. runagents-0.2.0/runagents/cli/main.py +71 -0
  11. runagents-0.2.0/runagents/cli/templates/agent.py.tmpl +21 -0
  12. runagents-0.2.0/runagents/cli/templates/agents_md.tmpl +22 -0
  13. runagents-0.2.0/runagents/cli/templates/claude_md.tmpl +37 -0
  14. runagents-0.2.0/runagents/cli/templates/cursorrules.tmpl +14 -0
  15. runagents-0.2.0/runagents/cli/templates/gitignore.tmpl +8 -0
  16. runagents-0.2.0/runagents/cli/templates/mcp_json.tmpl +9 -0
  17. runagents-0.2.0/runagents/cli/templates/requirements.txt.tmpl +1 -0
  18. runagents-0.2.0/runagents/cli/templates/runagents.yaml.tmpl +7 -0
  19. runagents-0.2.0/runagents/client.py +253 -0
  20. runagents-0.2.0/runagents/config.py +70 -0
  21. runagents-0.2.0/runagents/mcp/__init__.py +5 -0
  22. runagents-0.2.0/runagents/mcp/__main__.py +5 -0
  23. runagents-0.2.0/runagents/mcp/server.py +254 -0
  24. runagents-0.2.0/runagents/runtime.py +1163 -0
  25. runagents-0.2.0/runagents/types.py +166 -0
  26. runagents-0.2.0/runagents.egg-info/PKG-INFO +150 -0
  27. runagents-0.2.0/runagents.egg-info/SOURCES.txt +36 -0
  28. runagents-0.2.0/runagents.egg-info/dependency_links.txt +1 -0
  29. runagents-0.2.0/runagents.egg-info/entry_points.txt +3 -0
  30. runagents-0.2.0/runagents.egg-info/requires.txt +6 -0
  31. runagents-0.2.0/runagents.egg-info/top_level.txt +2 -0
  32. runagents-0.2.0/runagents_runtime.py +13 -0
  33. runagents-0.2.0/setup.cfg +4 -0
  34. runagents-0.2.0/tests/test_agent.py +69 -0
  35. runagents-0.2.0/tests/test_cli_init.py +47 -0
  36. runagents-0.2.0/tests/test_client.py +63 -0
  37. runagents-0.2.0/tests/test_config.py +87 -0
  38. runagents-0.2.0/tests/test_types.py +77 -0
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: runagents
3
+ Version: 0.2.0
4
+ Summary: RunAgents Python SDK — deploy, manage, and build AI agents
5
+ Author-email: RunAgents <try@runagents.io>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://runagents.io
8
+ Project-URL: Documentation, https://docs.runagents.io
9
+ Project-URL: Repository, https://github.com/runagents-io/runagents
10
+ Project-URL: Issues, https://github.com/runagents-io/runagents/issues
11
+ Keywords: ai,agents,runagents,llm,tool-calling
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: mcp
24
+ Requires-Dist: mcp>=1.0; extra == "mcp"
25
+ Provides-Extra: dev
26
+ Requires-Dist: watchdog>=3.0; extra == "dev"
27
+
28
+ # RunAgents Python SDK
29
+
30
+ Everything you need to build, test, and deploy AI agents on [RunAgents](https://runagents.io) — in one package.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install runagents # Core SDK + CLI + runtime (zero deps)
36
+ pip install runagents[mcp] # + MCP server for AI coding assistants
37
+ pip install runagents[dev] # + hot-reload for local dev
38
+ ```
39
+
40
+ ## Quickstart
41
+
42
+ ```bash
43
+ runagents init my-agent # Scaffold a project
44
+ cd my-agent
45
+ runagents dev # Local dev server with mock tools
46
+ runagents deploy # Ship to the platform
47
+ ```
48
+
49
+ ## Python API
50
+
51
+ ### Client — manage platform resources
52
+
53
+ ```python
54
+ from runagents import Client
55
+
56
+ client = Client() # reads ~/.runagents/config.json + env vars
57
+
58
+ agents = client.agents.list()
59
+ tools = client.tools.list()
60
+ runs = client.runs.list(agent="payment-agent")
61
+
62
+ result = client.agents.deploy(
63
+ name="my-agent",
64
+ source_files={"agent.py": open("agent.py").read()},
65
+ required_tools=["stripe-api"],
66
+ llm_configs=[{"provider": "openai", "model": "gpt-4o-mini", "role": "default"}],
67
+ )
68
+ ```
69
+
70
+ ### Agent — write agent code
71
+
72
+ ```python
73
+ from runagents import Agent, tool
74
+
75
+ agent = Agent() # reads TOOL_URL_*, LLM_GATEWAY_URL, LLM_MODEL from env
76
+
77
+ # Call a platform tool (mesh handles auth)
78
+ result = agent.call_tool("echo-tool", "/echo", {"message": "hello"})
79
+
80
+ # Chat via LLM gateway
81
+ response = agent.chat("What is 2+2?", tools=[...])
82
+
83
+ # Custom handler (Tier 2)
84
+ def handler(request, ctx):
85
+ message = request["message"]
86
+ result = agent.chat(message)
87
+ return result["choices"][0]["message"]["content"]
88
+ ```
89
+
90
+ ### @tool decorator
91
+
92
+ ```python
93
+ from runagents import tool
94
+
95
+ @tool(name="calculator", description="Evaluate math expressions")
96
+ def calculate(expression: str) -> str:
97
+ return str(eval(expression))
98
+ ```
99
+
100
+ ## CLI Commands
101
+
102
+ | Command | Description |
103
+ |---------|-------------|
104
+ | `runagents init [name]` | Scaffold a new agent project |
105
+ | `runagents dev` | Start local dev server with mock tools |
106
+ | `runagents deploy` | Deploy an agent (delegates to Go CLI) |
107
+ | `runagents agents list` | List agents |
108
+ | `runagents tools list` | List tools |
109
+ | `runagents runs list` | List runs |
110
+ | `runagents config` | Manage configuration |
111
+
112
+ ## Runtime
113
+
114
+ The runtime provides the HTTP server for deployed agents — tool calling loop, SSE streaming, health checks, OAuth consent, and JIT approvals. It runs automatically inside the platform; for local development use `runagents dev`.
115
+
116
+ ```python
117
+ # Backward compatible — still works
118
+ import runagents_runtime
119
+ ```
120
+
121
+ ## MCP Server
122
+
123
+ ```bash
124
+ pip install runagents[mcp]
125
+ runagents-mcp # starts on stdio
126
+ ```
127
+
128
+ 14 tools for AI coding assistants (Claude Code, Cursor, Codex). See [AI Assistant Setup](https://docs.runagents.io/cli/ai-assistant-setup/).
129
+
130
+ ## Configuration
131
+
132
+ Reads from `~/.runagents/config.json` with env var overrides:
133
+
134
+ | Variable | Description | Default |
135
+ |----------|-------------|---------|
136
+ | `RUNAGENTS_ENDPOINT` | Platform API URL | `http://localhost:8092` |
137
+ | `RUNAGENTS_API_KEY` | API key or workspace key | — |
138
+ | `RUNAGENTS_NAMESPACE` | Target namespace | `default` |
139
+
140
+ ## Documentation
141
+
142
+ - [RunAgents Docs](https://docs.runagents.io)
143
+ - [Writing Agents](https://docs.runagents.io/getting-started/writing-agents/)
144
+ - [Agent Runtime](https://docs.runagents.io/platform/agent-runtime/)
145
+ - [AI Assistant Setup](https://docs.runagents.io/cli/ai-assistant-setup/)
146
+ - [CLI Commands](https://docs.runagents.io/cli/commands/)
147
+
148
+ ## License
149
+
150
+ Apache-2.0
@@ -0,0 +1,123 @@
1
+ # RunAgents Python SDK
2
+
3
+ Everything you need to build, test, and deploy AI agents on [RunAgents](https://runagents.io) — in one package.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install runagents # Core SDK + CLI + runtime (zero deps)
9
+ pip install runagents[mcp] # + MCP server for AI coding assistants
10
+ pip install runagents[dev] # + hot-reload for local dev
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```bash
16
+ runagents init my-agent # Scaffold a project
17
+ cd my-agent
18
+ runagents dev # Local dev server with mock tools
19
+ runagents deploy # Ship to the platform
20
+ ```
21
+
22
+ ## Python API
23
+
24
+ ### Client — manage platform resources
25
+
26
+ ```python
27
+ from runagents import Client
28
+
29
+ client = Client() # reads ~/.runagents/config.json + env vars
30
+
31
+ agents = client.agents.list()
32
+ tools = client.tools.list()
33
+ runs = client.runs.list(agent="payment-agent")
34
+
35
+ result = client.agents.deploy(
36
+ name="my-agent",
37
+ source_files={"agent.py": open("agent.py").read()},
38
+ required_tools=["stripe-api"],
39
+ llm_configs=[{"provider": "openai", "model": "gpt-4o-mini", "role": "default"}],
40
+ )
41
+ ```
42
+
43
+ ### Agent — write agent code
44
+
45
+ ```python
46
+ from runagents import Agent, tool
47
+
48
+ agent = Agent() # reads TOOL_URL_*, LLM_GATEWAY_URL, LLM_MODEL from env
49
+
50
+ # Call a platform tool (mesh handles auth)
51
+ result = agent.call_tool("echo-tool", "/echo", {"message": "hello"})
52
+
53
+ # Chat via LLM gateway
54
+ response = agent.chat("What is 2+2?", tools=[...])
55
+
56
+ # Custom handler (Tier 2)
57
+ def handler(request, ctx):
58
+ message = request["message"]
59
+ result = agent.chat(message)
60
+ return result["choices"][0]["message"]["content"]
61
+ ```
62
+
63
+ ### @tool decorator
64
+
65
+ ```python
66
+ from runagents import tool
67
+
68
+ @tool(name="calculator", description="Evaluate math expressions")
69
+ def calculate(expression: str) -> str:
70
+ return str(eval(expression))
71
+ ```
72
+
73
+ ## CLI Commands
74
+
75
+ | Command | Description |
76
+ |---------|-------------|
77
+ | `runagents init [name]` | Scaffold a new agent project |
78
+ | `runagents dev` | Start local dev server with mock tools |
79
+ | `runagents deploy` | Deploy an agent (delegates to Go CLI) |
80
+ | `runagents agents list` | List agents |
81
+ | `runagents tools list` | List tools |
82
+ | `runagents runs list` | List runs |
83
+ | `runagents config` | Manage configuration |
84
+
85
+ ## Runtime
86
+
87
+ The runtime provides the HTTP server for deployed agents — tool calling loop, SSE streaming, health checks, OAuth consent, and JIT approvals. It runs automatically inside the platform; for local development use `runagents dev`.
88
+
89
+ ```python
90
+ # Backward compatible — still works
91
+ import runagents_runtime
92
+ ```
93
+
94
+ ## MCP Server
95
+
96
+ ```bash
97
+ pip install runagents[mcp]
98
+ runagents-mcp # starts on stdio
99
+ ```
100
+
101
+ 14 tools for AI coding assistants (Claude Code, Cursor, Codex). See [AI Assistant Setup](https://docs.runagents.io/cli/ai-assistant-setup/).
102
+
103
+ ## Configuration
104
+
105
+ Reads from `~/.runagents/config.json` with env var overrides:
106
+
107
+ | Variable | Description | Default |
108
+ |----------|-------------|---------|
109
+ | `RUNAGENTS_ENDPOINT` | Platform API URL | `http://localhost:8092` |
110
+ | `RUNAGENTS_API_KEY` | API key or workspace key | — |
111
+ | `RUNAGENTS_NAMESPACE` | Target namespace | `default` |
112
+
113
+ ## Documentation
114
+
115
+ - [RunAgents Docs](https://docs.runagents.io)
116
+ - [Writing Agents](https://docs.runagents.io/getting-started/writing-agents/)
117
+ - [Agent Runtime](https://docs.runagents.io/platform/agent-runtime/)
118
+ - [AI Assistant Setup](https://docs.runagents.io/cli/ai-assistant-setup/)
119
+ - [CLI Commands](https://docs.runagents.io/cli/commands/)
120
+
121
+ ## License
122
+
123
+ Apache-2.0
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "runagents"
7
+ version = "0.2.0"
8
+ description = "RunAgents Python SDK — deploy, manage, and build AI agents"
9
+ readme = "README.md"
10
+ license = {text = "Apache-2.0"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "RunAgents", email = "try@runagents.io"},
14
+ ]
15
+ keywords = ["ai", "agents", "runagents", "llm", "tool-calling"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: Apache Software License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
26
+ ]
27
+ dependencies = []
28
+
29
+ [project.optional-dependencies]
30
+ mcp = ["mcp>=1.0"]
31
+ dev = ["watchdog>=3.0"]
32
+
33
+ [project.scripts]
34
+ runagents = "runagents.cli.main:main"
35
+ runagents-mcp = "runagents.mcp:main"
36
+
37
+ [project.urls]
38
+ Homepage = "https://runagents.io"
39
+ Documentation = "https://docs.runagents.io"
40
+ Repository = "https://github.com/runagents-io/runagents"
41
+ Issues = "https://github.com/runagents-io/runagents/issues"
42
+
43
+ [tool.setuptools.packages.find]
44
+ include = ["runagents*"]
45
+
46
+ [tool.setuptools]
47
+ py-modules = ["runagents_runtime"]
48
+
49
+ [tool.setuptools.package-data]
50
+ "runagents.cli" = ["templates/*"]
@@ -0,0 +1,8 @@
1
+ """RunAgents — Python SDK for the RunAgents AI agent platform."""
2
+
3
+ from runagents.client import Client
4
+ from runagents.agent import Agent, tool
5
+ from runagents.runtime import RunContext
6
+
7
+ __version__ = "0.2.0"
8
+ __all__ = ["Client", "Agent", "tool", "RunContext"]
@@ -0,0 +1,143 @@
1
+ """Agent SDK for writing RunAgents agents.
2
+
3
+ Provides a high-level Agent class that reads operator-injected env vars
4
+ and exposes ``call_tool()`` and ``chat()`` helpers. Stdlib only.
5
+
6
+ Usage::
7
+
8
+ from runagents import Agent
9
+
10
+ agent = Agent()
11
+ result = agent.chat("What is 2+2?", tools=[...])
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import functools
19
+ from typing import Any, Callable
20
+
21
+
22
+ class Agent:
23
+ """High-level agent helper that reads operator-injected env vars.
24
+
25
+ Attributes:
26
+ system_prompt: The agent's system prompt.
27
+ model: LLM model name (e.g. ``gpt-4o-mini``).
28
+ llm_gateway_url: URL for the LLM gateway chat completions endpoint.
29
+ tool_urls: Mapping of tool name → base URL.
30
+ """
31
+
32
+ def __init__(self):
33
+ self.system_prompt = os.environ.get("SYSTEM_PROMPT", "You are a helpful assistant.")
34
+ self.model = os.environ.get("LLM_MODEL", "gpt-4o-mini")
35
+ self.llm_gateway_url = os.environ.get(
36
+ "LLM_GATEWAY_URL",
37
+ "http://llm-gateway.agent-system.svc:8080/v1/chat/completions",
38
+ )
39
+ self.tool_urls: dict[str, str] = {}
40
+ for key, val in os.environ.items():
41
+ if key.startswith("TOOL_URL_"):
42
+ name = key[len("TOOL_URL_"):].lower().replace("_", "-")
43
+ self.tool_urls[name] = val
44
+
45
+ def call_tool(
46
+ self,
47
+ name: str,
48
+ path: str = "/",
49
+ payload: dict | None = None,
50
+ method: str = "POST",
51
+ ) -> dict:
52
+ """Call a platform tool by name.
53
+
54
+ The Istio mesh handles auth and policy — this just makes the HTTP call.
55
+
56
+ Args:
57
+ name: Tool name (must be in ``self.tool_urls``).
58
+ path: HTTP path on the tool (default ``/``).
59
+ payload: JSON body (for POST/PUT/PATCH).
60
+ method: HTTP method.
61
+
62
+ Returns:
63
+ Parsed JSON response.
64
+
65
+ Raises:
66
+ KeyError: If tool name is not found in env vars.
67
+ """
68
+ from runagents.runtime import execute_tool_call
69
+
70
+ base = self.tool_urls.get(name)
71
+ if base is None:
72
+ env_key = "TOOL_URL_" + name.upper().replace("-", "_")
73
+ raise KeyError(f"Tool {name!r} not found. Set {env_key} or check requiredTools.")
74
+
75
+ url = base.rstrip("/") + path
76
+ body = json.dumps(payload) if payload else None
77
+ result_str = execute_tool_call(method, url, body=body)
78
+ try:
79
+ return json.loads(result_str)
80
+ except (json.JSONDecodeError, TypeError):
81
+ return {"raw": result_str}
82
+
83
+ def chat(
84
+ self,
85
+ message: str,
86
+ tools: list[dict] | None = None,
87
+ history: list[dict] | None = None,
88
+ ) -> dict:
89
+ """Send a chat completion request through the LLM gateway.
90
+
91
+ Args:
92
+ message: User message.
93
+ tools: OpenAI-format tool definitions (optional).
94
+ history: Prior conversation messages (optional).
95
+
96
+ Returns:
97
+ Full OpenAI-format response dict.
98
+ """
99
+ from runagents.runtime import call_llm
100
+
101
+ messages = list(history) if history else []
102
+ messages.append({"role": "user", "content": message})
103
+
104
+ return call_llm(
105
+ messages,
106
+ gateway_url=self.llm_gateway_url,
107
+ model=self.model,
108
+ system_prompt=self.system_prompt if not history else None,
109
+ tools=tools,
110
+ )
111
+
112
+
113
+ def tool(fn: Callable | None = None, *, name: str | None = None, description: str = "") -> Any:
114
+ """Decorator to mark a function as a tool handler.
115
+
116
+ The decorated function gains ``.tool_name`` and ``.tool_description``
117
+ attributes for discovery by frameworks.
118
+
119
+ Usage::
120
+
121
+ @tool
122
+ def calculator(expression: str) -> str:
123
+ return str(eval(expression))
124
+
125
+ @tool(name="weather-lookup", description="Get current weather")
126
+ def weather(city: str) -> dict:
127
+ ...
128
+ """
129
+ def decorator(f: Callable) -> Callable:
130
+ f.tool_name = name or f.__name__.replace("_", "-") # type: ignore[attr-defined]
131
+ f.tool_description = description or (f.__doc__ or "").strip().split("\n")[0] # type: ignore[attr-defined]
132
+
133
+ @functools.wraps(f)
134
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
135
+ return f(*args, **kwargs)
136
+
137
+ wrapper.tool_name = f.tool_name # type: ignore[attr-defined]
138
+ wrapper.tool_description = f.tool_description # type: ignore[attr-defined]
139
+ return wrapper
140
+
141
+ if fn is not None:
142
+ return decorator(fn)
143
+ return decorator
@@ -0,0 +1 @@
1
+ """RunAgents CLI — Python entry point that delegates to Go binary."""
@@ -0,0 +1,121 @@
1
+ """Go CLI binary downloader — mirrors cli/npm/install.js logic.
2
+
3
+ Downloads from S3, verifies SHA256, caches at ~/.runagents/bin/.
4
+ Stdlib only: urllib.request, tarfile, hashlib, platform.
5
+ """
6
+
7
+ import hashlib
8
+ import os
9
+ import platform
10
+ import shutil
11
+ import stat
12
+ import sys
13
+ import tarfile
14
+ import tempfile
15
+ import urllib.error
16
+ import urllib.request
17
+ from pathlib import Path
18
+
19
+ CLI_VERSION = "1.0.6"
20
+ S3_BASE = "https://runagents-releases.s3.amazonaws.com/cli"
21
+
22
+ PLATFORM_MAP = {"Darwin": "darwin", "Linux": "linux", "Windows": "windows"}
23
+ ARCH_MAP = {"x86_64": "amd64", "AMD64": "amd64", "arm64": "arm64", "aarch64": "arm64"}
24
+
25
+ _BIN_DIR = Path.home() / ".runagents" / "bin"
26
+
27
+
28
+ def ensure_binary(version: str = CLI_VERSION) -> Path | None:
29
+ """Return path to Go binary, downloading if needed. Returns None on failure."""
30
+ # 1. Check cached binary
31
+ cached = _BIN_DIR / f"runagents-{version}"
32
+ if cached.exists() and os.access(cached, os.X_OK):
33
+ return cached
34
+
35
+ # 2. Check PATH
36
+ on_path = shutil.which("runagents")
37
+ if on_path:
38
+ return Path(on_path)
39
+
40
+ # 3. Download
41
+ try:
42
+ return _download(version)
43
+ except Exception as e:
44
+ print(f"Warning: could not download CLI binary: {e}", file=sys.stderr)
45
+ return None
46
+
47
+
48
+ def _download(version: str) -> Path:
49
+ plat = PLATFORM_MAP.get(platform.system())
50
+ arch = ARCH_MAP.get(platform.machine())
51
+ if not plat or not arch:
52
+ raise RuntimeError(f"Unsupported platform: {platform.system()}/{platform.machine()}")
53
+
54
+ ext = ".zip" if plat == "windows" else ".tar.gz"
55
+ asset = f"runagents_{plat}_{arch}{ext}"
56
+ url = f"{S3_BASE}/v{version}/{asset}"
57
+ checksums_url = f"{S3_BASE}/v{version}/checksums.txt"
58
+
59
+ _BIN_DIR.mkdir(parents=True, exist_ok=True)
60
+
61
+ with tempfile.TemporaryDirectory() as tmpdir:
62
+ archive_path = Path(tmpdir) / asset
63
+
64
+ # Download archive
65
+ print(f"Downloading runagents v{version} for {plat}/{arch}...")
66
+ urllib.request.urlretrieve(url, archive_path)
67
+
68
+ # Verify checksum
69
+ try:
70
+ with urllib.request.urlopen(checksums_url, timeout=10) as resp:
71
+ checksums_text = resp.read().decode()
72
+ expected_hash = _find_hash(checksums_text, asset)
73
+ if expected_hash:
74
+ actual_hash = _sha256(archive_path)
75
+ if actual_hash != expected_hash:
76
+ raise RuntimeError(
77
+ f"SHA256 mismatch for {asset}: expected {expected_hash}, got {actual_hash}"
78
+ )
79
+ except urllib.error.URLError:
80
+ pass # Skip verification if checksums unavailable
81
+
82
+ # Extract
83
+ bin_name = "runagents.exe" if plat == "windows" else "runagents"
84
+ if ext == ".tar.gz":
85
+ with tarfile.open(archive_path, "r:gz") as tar:
86
+ # Find the binary in the archive
87
+ for member in tar.getmembers():
88
+ if member.name.endswith(bin_name):
89
+ member.name = bin_name
90
+ tar.extract(member, tmpdir)
91
+ break
92
+ else:
93
+ import zipfile
94
+ with zipfile.ZipFile(archive_path) as zf:
95
+ for name in zf.namelist():
96
+ if name.endswith(bin_name):
97
+ zf.extract(name, tmpdir)
98
+ break
99
+
100
+ src = Path(tmpdir) / bin_name
101
+ dst = _BIN_DIR / f"runagents-{version}"
102
+ shutil.move(str(src), str(dst))
103
+ dst.chmod(dst.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
104
+ print(f"Installed runagents v{version} to {dst}")
105
+ return dst
106
+
107
+
108
+ def _sha256(path: Path) -> str:
109
+ h = hashlib.sha256()
110
+ with open(path, "rb") as f:
111
+ for chunk in iter(lambda: f.read(8192), b""):
112
+ h.update(chunk)
113
+ return h.hexdigest()
114
+
115
+
116
+ def _find_hash(checksums_text: str, asset_name: str) -> str | None:
117
+ for line in checksums_text.strip().splitlines():
118
+ parts = line.split()
119
+ if len(parts) == 2 and parts[1] == asset_name:
120
+ return parts[0]
121
+ return None