react-agent-harness 0.5.1__tar.gz → 0.5.2__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.
- {react_agent_harness-0.5.1/react_agent_harness.egg-info → react_agent_harness-0.5.2}/PKG-INFO +1 -1
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/README.md +57 -25
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/cli.py +9 -2
- react_agent_harness-0.5.2/harness/oauth_browser.py +150 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/pyproject.toml +1 -1
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2/react_agent_harness.egg-info}/PKG-INFO +1 -1
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/react_agent_harness.egg-info/SOURCES.txt +2 -0
- react_agent_harness-0.5.2/tests/test_mcp_auth.py +368 -0
- react_agent_harness-0.5.2/tests/test_oauth_browser.py +111 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_steering.py +43 -14
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tools/mcp/adapter.py +12 -2
- react_agent_harness-0.5.2/tools/mcp/auth.py +437 -0
- react_agent_harness-0.5.1/tests/test_mcp_auth.py +0 -185
- react_agent_harness-0.5.1/tools/mcp/auth.py +0 -191
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/LICENSE +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/agents/__init__.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/agents/base.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/__init__.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/annotation.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/checkpoint.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/console.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/events.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/executor_bridge.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/hitl.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/llm/__init__.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/llm/_streaming.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/llm/anthropic.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/llm/auth.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/llm/claude_code.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/llm/openai.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/llm/openai_codex.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/otel.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/runtime.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/steering.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/tool_policy.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/harness/utils.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/memory/__init__.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/memory/episodic_lance.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/memory/manager.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/memory/redis_store.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/memory/stores.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/memory/working.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/orchestrator/__init__.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/orchestrator/planner.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/react_agent_harness.egg-info/dependency_links.txt +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/react_agent_harness.egg-info/entry_points.txt +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/react_agent_harness.egg-info/requires.txt +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/react_agent_harness.egg-info/top_level.txt +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/setup.cfg +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_agents_base.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_annotation.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_anthropic_llm.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_checkpoint_resume.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_claude_code_llm.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_cli.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_console_renderer.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_executor_bridge.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_http_fetch.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_llm_auth.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_mcp_adapter.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_memory.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_openai_codex_llm.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_openai_llm.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_orchestrator.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_otel.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_parse_action_json.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_redis_store.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_streaming.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_tool_policy.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_utils.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_vision.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tests/test_working_memory.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tools/__init__.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tools/builtin/__init__.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tools/builtin/fetch_image.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tools/builtin/http_fetch.py +0 -0
- {react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/tools/mcp/__init__.py +0 -0
|
@@ -44,6 +44,7 @@ harness/steering.py Async steering — agent.steer(text), StdinRouter pu
|
|
|
44
44
|
harness/checkpoint.py CheckpointStore + _ResumeHint + maybe_resume_key — pluggable run-state persistence (file + Redis); auto-resume built into dispatch_stream / run_stream
|
|
45
45
|
harness/otel.py OTELHook — OpenTelemetry span exporter (opt-in)
|
|
46
46
|
harness/executor_bridge.py ExecutorBridge + ExecutorTool — controlled subprocess launcher with optional Docker sandboxing
|
|
47
|
+
harness/oauth_browser.py Localhost OAuth callback server + open_or_print_url — shared by MCP browser-OAuth and LLM login flows
|
|
47
48
|
orchestrator/planner.py Hybrid DAG orchestrator — plan, replan, synthesize
|
|
48
49
|
agents/base.py Generic BaseAgent — ReAct loop, no subclassing needed
|
|
49
50
|
memory/manager.py MemoryManager — semantic KV + episodic vector
|
|
@@ -592,47 +593,78 @@ async with MCPServerConnection(params, server_name="filesystem") as conn:
|
|
|
592
593
|
result = await runtime.run("list files in /tmp")
|
|
593
594
|
```
|
|
594
595
|
|
|
595
|
-
Supports **stdio** and **
|
|
596
|
-
manager handles the full lifecycle —
|
|
596
|
+
Supports **stdio**, **SSE**, and **streamable-HTTP** transports. The
|
|
597
|
+
`MCPServerConnection` context manager handles the full lifecycle —
|
|
598
|
+
connect, discover, and cleanup.
|
|
597
599
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
+
### Auth options
|
|
601
|
+
|
|
602
|
+
Pick the provider that matches how your MCP server authenticates:
|
|
603
|
+
|
|
604
|
+
| Provider | When to use |
|
|
605
|
+
|---|---|
|
|
606
|
+
| `StaticMCPAuth` | Literal header/env values you have in hand |
|
|
607
|
+
| `BearerMCPAuth` | A single bearer token string |
|
|
608
|
+
| `ApiKeyMCPAuth` | API-key headers backed by environment variables |
|
|
609
|
+
| `OAuthMCPAuth` | Bearer token cached in the shared `auth.json` file |
|
|
610
|
+
| `BrowserOAuthMCPAuth` | Full OAuth 2.0 + PKCE flow with browser login |
|
|
611
|
+
|
|
612
|
+
**API keys backed by env vars** — generic, no vendor coupling:
|
|
600
613
|
|
|
601
614
|
```python
|
|
602
615
|
import os
|
|
603
|
-
from tools.mcp import
|
|
616
|
+
from tools.mcp.auth import ApiKeyMCPAuth, StreamableHttpServerParams
|
|
617
|
+
from tools.mcp import MCPServerConnection
|
|
618
|
+
|
|
619
|
+
auth = ApiKeyMCPAuth({
|
|
620
|
+
"DD-Api-Key": "DD_API_KEY",
|
|
621
|
+
"DD-Application-Key": "DD_APP_KEY",
|
|
622
|
+
})
|
|
623
|
+
params = StreamableHttpServerParams(url="https://mcp.datadoghq.com/")
|
|
624
|
+
|
|
625
|
+
async with MCPServerConnection(params, server_name="datadog", auth=auth) as conn:
|
|
626
|
+
conn.register_tools(tool_registry)
|
|
627
|
+
```
|
|
604
628
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
629
|
+
**Browser-based OAuth (PKCE) for hosted MCP servers**:
|
|
630
|
+
|
|
631
|
+
```python
|
|
632
|
+
from tools.mcp.auth import BrowserOAuthMCPAuth, StreamableHttpServerParams
|
|
633
|
+
from tools.mcp import MCPServerConnection
|
|
634
|
+
|
|
635
|
+
auth = BrowserOAuthMCPAuth(
|
|
636
|
+
server_url="https://mcp.example.com/",
|
|
637
|
+
provider_name="mcp:example",
|
|
638
|
+
client_id="abc123", # from the provider's developer console
|
|
639
|
+
client_secret="shh", # optional (PKCE-only flows omit)
|
|
640
|
+
scopes=["read", "write"],
|
|
610
641
|
)
|
|
642
|
+
params = StreamableHttpServerParams(url="https://mcp.example.com/")
|
|
611
643
|
|
|
612
|
-
async with MCPServerConnection(
|
|
613
|
-
{"url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp"},
|
|
614
|
-
server_name="datadog",
|
|
615
|
-
auth=auth,
|
|
616
|
-
) as conn:
|
|
644
|
+
async with MCPServerConnection(params, auth=auth) as conn:
|
|
617
645
|
conn.register_tools(tool_registry)
|
|
618
646
|
```
|
|
619
647
|
|
|
620
|
-
|
|
648
|
+
First connect opens the browser, captures the redirect on
|
|
649
|
+
`http://127.0.0.1:8765/callback`, persists tokens to
|
|
650
|
+
`~/.agent-harness/auth/auth.json` (chmod 0600), and refreshes them
|
|
651
|
+
transparently on every subsequent run. Register your OAuth app with that
|
|
652
|
+
redirect URI.
|
|
653
|
+
|
|
654
|
+
Servers that support RFC 7591 dynamic client registration work without
|
|
655
|
+
supplying `client_id` — the MCP SDK registers a fresh client on first
|
|
656
|
+
connect.
|
|
657
|
+
|
|
658
|
+
**Cached OAuth from the auth.json file** (for tokens you already minted
|
|
659
|
+
elsewhere):
|
|
621
660
|
|
|
622
661
|
```python
|
|
623
|
-
from tools.mcp import
|
|
662
|
+
from tools.mcp import OAuthMCPAuth, MCPServerConnection
|
|
624
663
|
|
|
625
664
|
auth = OAuthMCPAuth.from_auth_file(
|
|
626
665
|
"~/.agent-harness/auth/auth.json",
|
|
627
|
-
provider="
|
|
666
|
+
provider="my-service",
|
|
628
667
|
)
|
|
629
|
-
|
|
630
|
-
async with MCPServerConnection(
|
|
631
|
-
{"url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp"},
|
|
632
|
-
server_name="datadog",
|
|
633
|
-
auth=auth,
|
|
634
|
-
) as conn:
|
|
635
|
-
conn.register_tools(tool_registry)
|
|
636
668
|
```
|
|
637
669
|
|
|
638
670
|
See `examples/mcp_demo.py` for local stdio MCP and `examples/mcp_auth_demo.py`
|
|
@@ -79,11 +79,13 @@ def main() -> int:
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
async def _login_openai_codex(path: Path) -> int:
|
|
82
|
+
from harness.oauth_browser import open_or_print_url
|
|
83
|
+
|
|
82
84
|
client = OpenAICodexOAuthClient()
|
|
83
85
|
try:
|
|
84
86
|
device = await client.request_device_code()
|
|
85
87
|
print("OpenAI Codex login")
|
|
86
|
-
|
|
88
|
+
open_or_print_url(device.verification_uri, prefix="Open:")
|
|
87
89
|
print(f"Code: {device.user_code}")
|
|
88
90
|
print("Waiting for authorization...")
|
|
89
91
|
cred = await client.poll_device_code(device)
|
|
@@ -95,11 +97,16 @@ async def _login_openai_codex(path: Path) -> int:
|
|
|
95
97
|
|
|
96
98
|
|
|
97
99
|
async def _login_claude_code(path: Path) -> int:
|
|
100
|
+
from harness.oauth_browser import open_or_print_url
|
|
101
|
+
|
|
98
102
|
client = AnthropicClaudeCodeOAuthClient()
|
|
99
103
|
try:
|
|
100
104
|
login = client.begin_login()
|
|
101
105
|
print("Claude Code login")
|
|
102
|
-
|
|
106
|
+
# Anthropic owns the redirect URI (console.anthropic.com), so we
|
|
107
|
+
# can't auto-capture the callback here. Best we can do is open the
|
|
108
|
+
# browser for the user and let them paste the result.
|
|
109
|
+
open_or_print_url(login.url, prefix="Open:")
|
|
103
110
|
print("Paste the final callback URL, or the code#state value.")
|
|
104
111
|
callback_input = input("Callback: ")
|
|
105
112
|
cred = await client.finish_login(login, callback_input)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Browser-based OAuth helpers shared across providers.
|
|
2
|
+
|
|
3
|
+
Two utilities here:
|
|
4
|
+
|
|
5
|
+
- ``open_or_print_url(url)`` — try to open a URL in the user's default
|
|
6
|
+
browser; fall back to printing it so headless / SSH sessions still work.
|
|
7
|
+
|
|
8
|
+
- ``wait_for_oauth_callback(port, path, timeout)`` — spin up a one-shot
|
|
9
|
+
localhost HTTP server, block until the OAuth provider redirects the
|
|
10
|
+
browser back, and return the ``(code, state)`` pair from the query
|
|
11
|
+
string. Used by ``BrowserOAuthMCPAuth`` and any future browser-based
|
|
12
|
+
login flow whose redirect URI we control.
|
|
13
|
+
|
|
14
|
+
Stdlib only — no new dependencies. The callback server uses
|
|
15
|
+
``http.server.HTTPServer`` in a background thread so the asyncio caller can
|
|
16
|
+
``await`` on a future that resolves when the request arrives.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
import urllib.parse
|
|
25
|
+
import webbrowser
|
|
26
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_HTML_OK = b"""<!doctype html>
|
|
33
|
+
<html><body style="font-family:sans-serif;text-align:center;padding:3em">
|
|
34
|
+
<h2>Authorization complete</h2>
|
|
35
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
36
|
+
</body></html>
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
_HTML_ERROR = b"""<!doctype html>
|
|
40
|
+
<html><body style="font-family:sans-serif;text-align:center;padding:3em">
|
|
41
|
+
<h2>Authorization failed</h2>
|
|
42
|
+
<p>%s</p>
|
|
43
|
+
<p>Check the terminal for details.</p>
|
|
44
|
+
</body></html>
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def open_or_print_url(url: str, *, prefix: str = "Open in browser:") -> None:
|
|
49
|
+
"""Try to open ``url`` in the default browser; always print it as a fallback."""
|
|
50
|
+
print(f"{prefix} {url}")
|
|
51
|
+
try:
|
|
52
|
+
webbrowser.open(url, new=2)
|
|
53
|
+
except Exception as e: # noqa: BLE001 — best-effort UX nicety
|
|
54
|
+
logger.debug("webbrowser.open failed: %s", e)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def wait_for_oauth_callback(
|
|
58
|
+
*,
|
|
59
|
+
port: int = 0,
|
|
60
|
+
path: str = "/callback",
|
|
61
|
+
timeout: float = 300.0,
|
|
62
|
+
bind_host: str = "127.0.0.1",
|
|
63
|
+
) -> tuple[str, str | None]:
|
|
64
|
+
"""Run a localhost HTTP server until a redirect with ``code`` arrives.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
port: Port to bind. ``0`` lets the OS pick a free port — read it back
|
|
68
|
+
from ``actual_port`` after construction if you need it (this
|
|
69
|
+
helper does not return the bound port; callers that need it
|
|
70
|
+
should use :func:`bind_callback_server` instead).
|
|
71
|
+
path: Expected redirect path. Other paths return 404.
|
|
72
|
+
timeout: Seconds to wait before raising :class:`TimeoutError`.
|
|
73
|
+
bind_host: Address to bind on. Keep ``127.0.0.1`` for security —
|
|
74
|
+
anything else makes the auth code observable on the LAN.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
``(code, state)`` from the query string. ``state`` is ``None`` when
|
|
78
|
+
the provider does not echo it back.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
TimeoutError: No callback arrived within ``timeout`` seconds.
|
|
82
|
+
RuntimeError: The redirect carried an ``error`` query parameter.
|
|
83
|
+
"""
|
|
84
|
+
server, actual_port, future = bind_callback_server(port=port, path=path, bind_host=bind_host)
|
|
85
|
+
try:
|
|
86
|
+
return await asyncio.wait_for(future, timeout=timeout)
|
|
87
|
+
finally:
|
|
88
|
+
server.shutdown()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def bind_callback_server(
|
|
92
|
+
*,
|
|
93
|
+
port: int = 0,
|
|
94
|
+
path: str = "/callback",
|
|
95
|
+
bind_host: str = "127.0.0.1",
|
|
96
|
+
) -> tuple[HTTPServer, int, asyncio.Future[tuple[str, str | None]]]:
|
|
97
|
+
"""Start the callback server and return (server, port, future).
|
|
98
|
+
|
|
99
|
+
Callers that need the bound port up front (to construct the redirect URI
|
|
100
|
+
before opening the browser) use this and then ``await future``. Callers
|
|
101
|
+
that already know the port should prefer :func:`wait_for_oauth_callback`.
|
|
102
|
+
|
|
103
|
+
The server runs in a daemon thread and shuts down on the first valid
|
|
104
|
+
callback or when ``server.shutdown()`` is called.
|
|
105
|
+
"""
|
|
106
|
+
loop = asyncio.get_running_loop()
|
|
107
|
+
future: asyncio.Future[tuple[str, str | None]] = loop.create_future()
|
|
108
|
+
|
|
109
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
110
|
+
def do_GET(self) -> None: # noqa: N802 — stdlib API
|
|
111
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
112
|
+
if parsed.path != path:
|
|
113
|
+
self.send_response(404)
|
|
114
|
+
self.end_headers()
|
|
115
|
+
return
|
|
116
|
+
qs = urllib.parse.parse_qs(parsed.query)
|
|
117
|
+
err = qs.get("error", [None])[0]
|
|
118
|
+
if err:
|
|
119
|
+
desc = qs.get("error_description", [""])[0]
|
|
120
|
+
msg = f"{err}: {desc}".strip(": ")
|
|
121
|
+
self.send_response(400)
|
|
122
|
+
self.send_header("Content-Type", "text/html")
|
|
123
|
+
self.end_headers()
|
|
124
|
+
self.wfile.write(_HTML_ERROR % msg.encode("utf-8", "replace"))
|
|
125
|
+
if not future.done():
|
|
126
|
+
loop.call_soon_threadsafe(
|
|
127
|
+
future.set_exception, RuntimeError(f"OAuth callback error: {msg}")
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
code = qs.get("code", [None])[0]
|
|
131
|
+
state = qs.get("state", [None])[0]
|
|
132
|
+
if not code:
|
|
133
|
+
self.send_response(400)
|
|
134
|
+
self.end_headers()
|
|
135
|
+
return
|
|
136
|
+
self.send_response(200)
|
|
137
|
+
self.send_header("Content-Type", "text/html")
|
|
138
|
+
self.end_headers()
|
|
139
|
+
self.wfile.write(_HTML_OK)
|
|
140
|
+
if not future.done():
|
|
141
|
+
loop.call_soon_threadsafe(future.set_result, (code, state))
|
|
142
|
+
|
|
143
|
+
def log_message(self, *_args: Any) -> None: # silence stdlib's stderr noise
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
server = HTTPServer((bind_host, port), _Handler)
|
|
147
|
+
actual_port = server.server_address[1]
|
|
148
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
149
|
+
thread.start()
|
|
150
|
+
return server, actual_port, future
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "react-agent-harness"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.2"
|
|
8
8
|
description = "Multi-agent LLM orchestration: hybrid DAG planning, two-tier memory, streaming"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
{react_agent_harness-0.5.1 → react_agent_harness-0.5.2}/react_agent_harness.egg-info/SOURCES.txt
RENAMED
|
@@ -11,6 +11,7 @@ harness/console.py
|
|
|
11
11
|
harness/events.py
|
|
12
12
|
harness/executor_bridge.py
|
|
13
13
|
harness/hitl.py
|
|
14
|
+
harness/oauth_browser.py
|
|
14
15
|
harness/otel.py
|
|
15
16
|
harness/runtime.py
|
|
16
17
|
harness/steering.py
|
|
@@ -50,6 +51,7 @@ tests/test_llm_auth.py
|
|
|
50
51
|
tests/test_mcp_adapter.py
|
|
51
52
|
tests/test_mcp_auth.py
|
|
52
53
|
tests/test_memory.py
|
|
54
|
+
tests/test_oauth_browser.py
|
|
53
55
|
tests/test_openai_codex_llm.py
|
|
54
56
|
tests/test_openai_llm.py
|
|
55
57
|
tests/test_orchestrator.py
|