-
Notifications
You must be signed in to change notification settings - Fork 72
M5: Python agent sidecar + SwiftUI agent panel #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kalil0321
wants to merge
45
commits into
claude/proxy-monitor-m4-alpn-har
Choose a base branch
from
claude/proxy-monitor-m5-agent-sidecar
base: claude/proxy-monitor-m4-alpn-har
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
a7e2537
M5: Python agent sidecar + SwiftUI agent panel
claude c8e663d
M5 review fixes + tests
claude 77e7ddd
fix(macos): stabilize m5 agent sessions
kalil0321 5eba296
fix(macos): trust ca before device capture
kalil0321 17f5c18
feat(macos): add theme palette, shared pill controls, markdown renderer
kalil0321 43eebb5
feat(macos): redesign main window with near-black UI and command palette
kalil0321 861602e
feat(macos): inspector preview tab and native segmented tabs
kalil0321 92ada6e
feat(macos): markdown rendering and cleaner agent panel
kalil0321 83ecf55
chore(macos): commit Package.resolved to pin dependencies
kalil0321 7051bbd
refactor(macos): drop keyboard shortcuts and agent panel animation
kalil0321 52ed957
fix(macos): use AppKit text field wrappers for palette and agent inputs
kalil0321 3426e42
fix(macos): set .regular activation policy so swift run inputs work
kalil0321 341a790
fix(macos): force explicit white on text views to stop invisible text
kalil0321 9c50a52
feat(macos): self-contained agent sidecar — embed python runtime in .app
kalil0321 4d56456
fix(macos): wrap agent composer text instead of overflowing horizontally
kalil0321 d0c5887
refactor(macos): drop bottom status bar, surface state + CA in action…
kalil0321 e154b38
feat(agent): stream assistant replies incrementally instead of bulk
kalil0321 4e83c6a
feat(macos): show user messages in the agent timeline and hide noisy …
kalil0321 89dc19b
fix(agent): import StreamEvent from claude_agent_sdk.types submodule
kalil0321 3167f20
refactor(macos): render assistant markdown with swift-markdown-ui
kalil0321 be4c010
feat(macos): support deleting captured flows
kalil0321 052758c
feat(macos): pick which flows go to the agent + delete from row conte…
kalil0321 aa0aeb9
fix(agent): run sidecar with bypassPermissions so reads don't prompt
kalil0321 1e26106
feat(macos): refine tool call / result rows in the agent timeline
kalil0321 1516b09
feat(macos): tap an agent-written file to open it in a viewer sheet
kalil0321 5155b46
feat(macos): native syntax highlighter for assistant code blocks
kalil0321 cf1d8f3
feat(macos): full markdown theme — tables, code header, lists, task l…
kalil0321 ac46267
feat(agent): persist sessions to disk and resume via Claude SDK sessi…
kalil0321 a6b115f
feat(macos): cards layout + agent sessions history view
kalil0321 fddb287
refactor(macos): redesign traffic + inspector rows for narrow widths,…
kalil0321 ade6a80
refactor(macos): drop divider between action bar and cards
kalil0321 11ab9bf
fix(macos): align card headers, unify backgrounds, raise traffic card…
kalil0321 78f5fb6
fix(macos): traffic card auto-grows when inspector opens to prevent o…
kalil0321 c06ea11
refactor(macos): slim down ActionBar, move filter + delete-all into t…
kalil0321 c886f62
style(macos): warmer dark grey palette instead of near-black
kalil0321 e710e64
refactor(macos): replace outer HSplitView with custom transparent split
kalil0321 89c7118
feat(macos): inline filter chips + custom text filter inside the traf…
kalil0321 6499a48
refactor(macos): move filter chips back inside a single popover button
kalil0321 d58bab3
feat(macos): show request duration next to the timestamp on each row
kalil0321 d10fd1d
fix(macos): harden AgentSidecar port discovery — require newline, rec…
kalil0321 51db0f4
fix(agent): emit TextBlocks when StreamEvent SDK fallback is in use
kalil0321 de5fdb8
fix(macos): keep in-memory + persisted flow state in sync on delete/c…
kalil0321 6f401a9
fix(macos): inspector — nil HTML baseURL + don't gate text copy on co…
kalil0321 70bc059
fix(macos): truncate bodies on UTF-8 boundary + scope history record …
kalil0321 ea7a00d
refactor(macos): drop dead palette footer + dedupe HTTP color helpers
kalil0321 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [project] | ||
| name = "rae-agent" | ||
| version = "0.1.0" | ||
| description = "ReverseAPI agent sidecar" | ||
| requires-python = ">=3.11" | ||
| dependencies = [ | ||
| "websockets>=13.0", | ||
| "claude-agent-sdk>=0.1.48", | ||
| ] | ||
|
|
||
| [project.scripts] | ||
| rae-agent = "rae_agent.server:main" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["rae_agent"] | ||
|
|
||
| [tool.ruff] | ||
| line-length = 100 | ||
| target-version = "py311" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| from rae_agent.protocol import ( | ||
| AgentEvent, | ||
| ChatRequest, | ||
| FlowSummary, | ||
| ProtocolError, | ||
| TargetLanguage, | ||
| sanitize_session_id, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "AgentEvent", | ||
| "ChatRequest", | ||
| "FlowSummary", | ||
| "ProtocolError", | ||
| "TargetLanguage", | ||
| "sanitize_session_id", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from rae_agent.protocol import ChatRequest, TargetLanguage | ||
|
|
||
| LANGUAGE_HINTS = { | ||
| TargetLanguage.PYTHON: """ | ||
| - Idiomatic Python 3.11+ | ||
| - Use `httpx` for HTTP, prefer async if the flows look paginated or interactive | ||
| - Type hints with `from __future__ import annotations` | ||
| - Pydantic models when response shapes are stable | ||
| """, | ||
| TargetLanguage.TYPESCRIPT: """ | ||
| - TypeScript 5+, ESM | ||
| - Use the global `fetch` API (no axios) | ||
| - Strict types, no `any` | ||
| - Export plain async functions; group them in a class only if state is required | ||
| """, | ||
| TargetLanguage.GO: """ | ||
| - Modern Go 1.22+ | ||
| - net/http standard client; no third-party HTTP libraries | ||
| - Errors wrapped with `%w` | ||
| - Structs with json tags | ||
| """, | ||
| } | ||
|
|
||
|
|
||
| SYSTEM_PROMPT = """You are ReverseAPI, an expert at reverse-engineering HTTP APIs from captured traffic. | ||
|
|
||
| You will be given: | ||
| - A user request describing the API client they want | ||
| - A JSON file at a known path containing captured HTTP flows (request + response, headers and bodies) | ||
| - A target language | ||
|
|
||
| Your job: | ||
| 1. Read the flows file with the Read tool. | ||
| 2. Identify the API endpoints involved in the user's request. | ||
| 3. Detect authentication patterns (Bearer, cookies, custom headers). | ||
| 4. Detect content negotiation, pagination, retries, rate limit headers. | ||
| 5. Synthesise a clean, production-shaped client library in the target language. | ||
| 6. Write each generated file using the Write tool, into the current working directory. | ||
| 7. Briefly summarise what you produced and any assumptions you made. | ||
|
|
||
| Do NOT execute any code or make outbound HTTP calls. Do NOT install packages. | ||
| Keep generated code dependency-light and readable.""" | ||
|
|
||
|
|
||
| def build_user_prompt(request: ChatRequest, flows_path: str) -> str: | ||
| hints = LANGUAGE_HINTS.get(request.target, "").strip() | ||
| lines = [ | ||
| f"User request: {request.user_message}", | ||
| "", | ||
| f"Captured flows file: {flows_path}", | ||
| f"Number of flows: {len(request.flows)}", | ||
| f"Target language: {request.target.value}", | ||
| "", | ||
| "Language guidelines:", | ||
| hints, | ||
| ] | ||
| if request.history: | ||
| lines.extend([ | ||
| "", | ||
| "Recent conversation:", | ||
| *[f"- {item['role']}: {item['content']}" for item in request.history[-6:]], | ||
| ]) | ||
| return "\n".join(lines) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| from dataclasses import dataclass, field | ||
| from enum import Enum | ||
| from typing import Any | ||
|
|
||
|
|
||
| class TargetLanguage(str, Enum): | ||
| PYTHON = "python" | ||
| TYPESCRIPT = "typescript" | ||
| GO = "go" | ||
|
|
||
|
|
||
| class ProtocolError(Exception): | ||
| pass | ||
|
|
||
|
|
||
| _SAFE_ID_PATTERN = re.compile(r"^[A-Za-z0-9._\-]+$") | ||
|
|
||
|
|
||
| def sanitize_session_id(raw: str, fallback: str) -> str: | ||
| if not raw: | ||
| return fallback | ||
| candidate = raw.strip() | ||
| if not candidate or candidate in {".", ".."}: | ||
| return fallback | ||
| if "/" in candidate or "\\" in candidate or "\x00" in candidate: | ||
| return fallback | ||
| if not _SAFE_ID_PATTERN.fullmatch(candidate): | ||
| return fallback | ||
| return candidate[:128] | ||
|
|
||
|
|
||
| def _optional_float(value: Any) -> float | None: | ||
| if value is None: | ||
| return None | ||
| try: | ||
| return float(value) | ||
| except (TypeError, ValueError) as exc: | ||
| raise ProtocolError(f"expected float, got {value!r}") from exc | ||
|
|
||
|
|
||
| @dataclass | ||
| class FlowSummary: | ||
| id: str | ||
| scheme: str | ||
| method: str | ||
| url: str | ||
| request_headers: list[tuple[str, str]] | ||
| request_body: str | None | ||
| response_status: int | None | ||
| response_headers: list[tuple[str, str]] | ||
| response_body: str | None | ||
| started_at: float | ||
| finished_at: float | None | ||
|
|
||
| @classmethod | ||
| def from_payload(cls, payload: dict[str, Any]) -> "FlowSummary": | ||
| try: | ||
| return cls( | ||
| id=str(payload["id"]), | ||
| scheme=str(payload["scheme"]), | ||
| method=str(payload["method"]), | ||
| url=str(payload["url"]), | ||
| request_headers=[(str(k), str(v)) for k, v in payload.get("requestHeaders", [])], | ||
| request_body=payload.get("requestBody"), | ||
| response_status=payload.get("responseStatus"), | ||
| response_headers=[(str(k), str(v)) for k, v in payload.get("responseHeaders", [])], | ||
| response_body=payload.get("responseBody"), | ||
| started_at=float(payload.get("startedAt", 0.0)), | ||
| finished_at=_optional_float(payload.get("finishedAt")), | ||
| ) | ||
| except (KeyError, TypeError, ValueError) as exc: | ||
| raise ProtocolError(f"invalid flow payload: {exc}") from exc | ||
|
|
||
|
|
||
| @dataclass | ||
| class ChatRequest: | ||
| id: str | ||
| user_message: str | ||
| target: TargetLanguage | ||
| flows: list[FlowSummary] = field(default_factory=list) | ||
| history: list[dict[str, str]] = field(default_factory=list) | ||
| # The Claude Agent SDK session id from a previous turn. When present we | ||
| # pass it as ClaudeAgentOptions.resume so the SDK can re-attach to its | ||
| # own persisted state instead of replaying our local history. | ||
| claude_session_id: str | None = None | ||
|
|
||
| @classmethod | ||
| def from_payload(cls, payload: dict[str, Any]) -> "ChatRequest": | ||
| if payload.get("type") != "chat": | ||
| raise ProtocolError("expected type=chat") | ||
| try: | ||
| target_value = str(payload.get("target", "python")).lower() | ||
| target = TargetLanguage(target_value) | ||
| except ValueError as exc: | ||
| raise ProtocolError(f"unsupported target language: {payload.get('target')!r}") from exc | ||
| flows = [FlowSummary.from_payload(f) for f in payload.get("flows", [])] | ||
| history = [ | ||
| {"role": str(item.get("role", "user")), "content": str(item.get("content", ""))} | ||
| for item in payload.get("history", []) | ||
| ] | ||
| raw_sid = payload.get("claudeSessionId") or payload.get("claude_session_id") | ||
| claude_session_id = str(raw_sid) if isinstance(raw_sid, str) and raw_sid else None | ||
| return cls( | ||
| id=str(payload.get("id", "")), | ||
| user_message=str(payload.get("message", "")), | ||
| target=target, | ||
| flows=flows, | ||
| history=history, | ||
| claude_session_id=claude_session_id, | ||
| ) | ||
|
|
||
|
|
||
| @dataclass | ||
| class AgentEvent: | ||
| type: str | ||
| payload: dict[str, Any] | ||
|
|
||
| def to_dict(self) -> dict[str, Any]: | ||
| return {"type": self.type, **self.payload} | ||
|
|
||
| @classmethod | ||
| def assistant_text(cls, chat_id: str, text: str) -> "AgentEvent": | ||
| return cls(type="assistant_text", payload={"id": chat_id, "text": text}) | ||
|
|
||
| @classmethod | ||
| def session_started(cls, chat_id: str, claude_session_id: str) -> "AgentEvent": | ||
| return cls( | ||
| type="session_started", | ||
| payload={"id": chat_id, "claudeSessionId": claude_session_id}, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def assistant_text_chunk(cls, chat_id: str, text: str) -> "AgentEvent": | ||
| return cls(type="assistant_text_chunk", payload={"id": chat_id, "text": text}) | ||
|
|
||
| @classmethod | ||
| def tool_use(cls, chat_id: str, name: str, tool_input: dict[str, Any]) -> "AgentEvent": | ||
| return cls(type="tool_use", payload={"id": chat_id, "name": name, "input": tool_input}) | ||
|
|
||
| @classmethod | ||
| def tool_result(cls, chat_id: str, name: str, output: str, is_error: bool) -> "AgentEvent": | ||
| return cls( | ||
| type="tool_result", | ||
| payload={"id": chat_id, "name": name, "output": output, "is_error": is_error}, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def file_written(cls, chat_id: str, path: str) -> "AgentEvent": | ||
| return cls(type="file_written", payload={"id": chat_id, "path": path}) | ||
|
|
||
| @classmethod | ||
| def complete(cls, chat_id: str, workdir: str, files: list[str]) -> "AgentEvent": | ||
| return cls( | ||
| type="complete", | ||
| payload={"id": chat_id, "workdir": workdir, "files": files}, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def error(cls, chat_id: str | None, message: str) -> "AgentEvent": | ||
| payload: dict[str, Any] = {"message": message} | ||
| if chat_id is not None: | ||
| payload["id"] = chat_id | ||
| return cls(type="error", payload=payload) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import json | ||
| import logging | ||
| import os | ||
| import sys | ||
| import tempfile | ||
| from pathlib import Path | ||
|
|
||
| import websockets | ||
|
|
||
| from rae_agent.protocol import AgentEvent, ChatRequest, ProtocolError | ||
| from rae_agent.session import run_chat | ||
|
|
||
| logger = logging.getLogger("rae_agent") | ||
|
|
||
|
|
||
| async def handle_connection(websocket, base_dir: Path) -> None: | ||
| try: | ||
| async for raw in websocket: | ||
| await _process(websocket, raw, base_dir) | ||
| except websockets.ConnectionClosed: | ||
| return | ||
|
|
||
|
|
||
| async def _process(websocket, raw: str | bytes, base_dir: Path) -> None: | ||
| try: | ||
| payload = json.loads(raw) | ||
| except json.JSONDecodeError as exc: | ||
| await websocket.send(json.dumps(AgentEvent.error(None, f"invalid JSON: {exc}").to_dict())) | ||
| return | ||
|
|
||
| try: | ||
| request = ChatRequest.from_payload(payload) | ||
| except ProtocolError as exc: | ||
| await websocket.send(json.dumps(AgentEvent.error(None, str(exc)).to_dict())) | ||
| return | ||
|
|
||
| try: | ||
| async for event in run_chat(request, base_dir): | ||
| await websocket.send(json.dumps(event.to_dict())) | ||
| except ValueError as exc: | ||
| await websocket.send(json.dumps(AgentEvent.error(request.id, str(exc)).to_dict())) | ||
| except Exception as exc: | ||
| logger.exception("agent run failed") | ||
| await websocket.send(json.dumps(AgentEvent.error(request.id, str(exc)).to_dict())) | ||
|
|
||
|
|
||
| async def serve(host: str, port: int, base_dir: Path) -> None: | ||
| async def handler(websocket): | ||
| await handle_connection(websocket, base_dir) | ||
|
|
||
| async with websockets.serve(handler, host, port, max_size=64 * 1024 * 1024) as server: | ||
| sockets = list(server.sockets or []) | ||
| bound_port = sockets[0].getsockname()[1] if sockets else port | ||
| print(f"RAE_AGENT_LISTENING:{bound_port}", flush=True) | ||
| await asyncio.Future() | ||
|
|
||
|
|
||
| def resolve_base_dir() -> Path: | ||
| raw = os.environ.get("RAE_AGENT_WORKDIR") | ||
| if raw: | ||
| return Path(raw) | ||
| return Path(tempfile.gettempdir()) / "rae-agent-sessions" | ||
|
|
||
|
|
||
| def main() -> None: | ||
| logging.basicConfig(level=os.environ.get("RAE_AGENT_LOG", "INFO")) | ||
|
|
||
| host = os.environ.get("RAE_AGENT_HOST", "127.0.0.1") | ||
| port = int(os.environ.get("RAE_AGENT_PORT", "0")) | ||
| base_dir = resolve_base_dir() | ||
| base_dir.mkdir(parents=True, exist_ok=True) | ||
|
|
||
| try: | ||
| asyncio.run(serve(host, port, base_dir)) | ||
| except KeyboardInterrupt: | ||
| sys.exit(0) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Validate
request.idbefore callingrun_chat; path traversal sequences can escapebase_dirwhen session directories are created.Prompt for AI agents