-
Notifications
You must be signed in to change notification settings - Fork 80
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
20f3e56
M5: Python agent sidecar + SwiftUI agent panel
claude fc2c3bb
M5 review fixes + tests
claude a41ebd5
fix(macos): stabilize m5 agent sessions
kalil0321 26c7503
fix(macos): trust ca before device capture
kalil0321 2fbd902
feat(macos): add theme palette, shared pill controls, markdown renderer
kalil0321 50579b4
feat(macos): redesign main window with near-black UI and command palette
kalil0321 5085483
feat(macos): inspector preview tab and native segmented tabs
kalil0321 5eac633
feat(macos): markdown rendering and cleaner agent panel
kalil0321 6987a04
chore(macos): commit Package.resolved to pin dependencies
kalil0321 563c54c
refactor(macos): drop keyboard shortcuts and agent panel animation
kalil0321 f697305
fix(macos): use AppKit text field wrappers for palette and agent inputs
kalil0321 82d6a71
fix(macos): set .regular activation policy so swift run inputs work
kalil0321 78d96ed
fix(macos): force explicit white on text views to stop invisible text
kalil0321 f3da02e
feat(macos): self-contained agent sidecar — embed python runtime in .app
kalil0321 c50daa9
fix(macos): wrap agent composer text instead of overflowing horizontally
kalil0321 264f36c
refactor(macos): drop bottom status bar, surface state + CA in action…
kalil0321 a6f5da7
feat(agent): stream assistant replies incrementally instead of bulk
kalil0321 c57e4ba
feat(macos): show user messages in the agent timeline and hide noisy …
kalil0321 88ddc3a
fix(agent): import StreamEvent from claude_agent_sdk.types submodule
kalil0321 9f13a8e
refactor(macos): render assistant markdown with swift-markdown-ui
kalil0321 d8edc3a
feat(macos): support deleting captured flows
kalil0321 049a224
feat(macos): pick which flows go to the agent + delete from row conte…
kalil0321 70b1480
fix(agent): run sidecar with bypassPermissions so reads don't prompt
kalil0321 1327ad6
feat(macos): refine tool call / result rows in the agent timeline
kalil0321 07171b8
feat(macos): tap an agent-written file to open it in a viewer sheet
kalil0321 c71ba4d
feat(macos): native syntax highlighter for assistant code blocks
kalil0321 89acb57
feat(macos): full markdown theme — tables, code header, lists, task l…
kalil0321 5ce6476
feat(agent): persist sessions to disk and resume via Claude SDK sessi…
kalil0321 d5cefa6
feat(macos): cards layout + agent sessions history view
kalil0321 ede144b
refactor(macos): redesign traffic + inspector rows for narrow widths,…
kalil0321 18dbcd2
refactor(macos): drop divider between action bar and cards
kalil0321 737177e
fix(macos): align card headers, unify backgrounds, raise traffic card…
kalil0321 25fccf6
fix(macos): traffic card auto-grows when inspector opens to prevent o…
kalil0321 dfbd01b
refactor(macos): slim down ActionBar, move filter + delete-all into t…
kalil0321 ab3c718
style(macos): warmer dark grey palette instead of near-black
kalil0321 b541052
refactor(macos): replace outer HSplitView with custom transparent split
kalil0321 a52382e
feat(macos): inline filter chips + custom text filter inside the traf…
kalil0321 746511b
refactor(macos): move filter chips back inside a single popover button
kalil0321 88e879f
feat(macos): show request duration next to the timestamp on each row
kalil0321 0e8627b
fix(macos): harden AgentSidecar port discovery — require newline, rec…
kalil0321 f7bce0a
fix(agent): emit TextBlocks when StreamEvent SDK fallback is in use
kalil0321 5d29963
fix(macos): keep in-memory + persisted flow state in sync on delete/c…
kalil0321 76d19ae
fix(macos): inspector — nil HTML baseURL + don't gate text copy on co…
kalil0321 8f4e40c
fix(macos): truncate bodies on UTF-8 boundary + scope history record …
kalil0321 584bb86
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