-
Notifications
You must be signed in to change notification settings - Fork 69
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
d85da95
M5: Python agent sidecar + SwiftUI agent panel
claude ceaf3f2
M5 review fixes + tests
claude 1d8eb1f
fix(macos): stabilize m5 agent sessions
kalil0321 8a6f72a
fix(macos): trust ca before device capture
kalil0321 2863877
feat(macos): add theme palette, shared pill controls, markdown renderer
kalil0321 88e3dc3
feat(macos): redesign main window with near-black UI and command palette
kalil0321 fba86ae
feat(macos): inspector preview tab and native segmented tabs
kalil0321 e856ab9
feat(macos): markdown rendering and cleaner agent panel
kalil0321 0d59f8a
chore(macos): commit Package.resolved to pin dependencies
kalil0321 2b3fb48
refactor(macos): drop keyboard shortcuts and agent panel animation
kalil0321 7407155
fix(macos): use AppKit text field wrappers for palette and agent inputs
kalil0321 92d995b
fix(macos): set .regular activation policy so swift run inputs work
kalil0321 6177d9c
fix(macos): force explicit white on text views to stop invisible text
kalil0321 0aeb228
feat(macos): self-contained agent sidecar — embed python runtime in .app
kalil0321 695ea2d
fix(macos): wrap agent composer text instead of overflowing horizontally
kalil0321 b39af3c
refactor(macos): drop bottom status bar, surface state + CA in action…
kalil0321 9abdb13
feat(agent): stream assistant replies incrementally instead of bulk
kalil0321 7c5727d
feat(macos): show user messages in the agent timeline and hide noisy …
kalil0321 a978470
fix(agent): import StreamEvent from claude_agent_sdk.types submodule
kalil0321 ff7a97e
refactor(macos): render assistant markdown with swift-markdown-ui
kalil0321 2fef6ec
feat(macos): support deleting captured flows
kalil0321 da73591
feat(macos): pick which flows go to the agent + delete from row conte…
kalil0321 8c20030
fix(agent): run sidecar with bypassPermissions so reads don't prompt
kalil0321 4b3f96e
feat(macos): refine tool call / result rows in the agent timeline
kalil0321 978696c
feat(macos): tap an agent-written file to open it in a viewer sheet
kalil0321 23c0e97
feat(macos): native syntax highlighter for assistant code blocks
kalil0321 1fb23dd
feat(macos): full markdown theme — tables, code header, lists, task l…
kalil0321 67cce06
feat(agent): persist sessions to disk and resume via Claude SDK sessi…
kalil0321 e46d031
feat(macos): cards layout + agent sessions history view
kalil0321 4e46823
refactor(macos): redesign traffic + inspector rows for narrow widths,…
kalil0321 ae5b326
refactor(macos): drop divider between action bar and cards
kalil0321 afcee6d
fix(macos): align card headers, unify backgrounds, raise traffic card…
kalil0321 da71445
fix(macos): traffic card auto-grows when inspector opens to prevent o…
kalil0321 e0b1249
refactor(macos): slim down ActionBar, move filter + delete-all into t…
kalil0321 640b582
style(macos): warmer dark grey palette instead of near-black
kalil0321 bb3502d
refactor(macos): replace outer HSplitView with custom transparent split
kalil0321 a8dbbdf
feat(macos): inline filter chips + custom text filter inside the traf…
kalil0321 7a1f3bc
refactor(macos): move filter chips back inside a single popover button
kalil0321 f1d3d49
feat(macos): show request duration next to the timestamp on each row
kalil0321 d3eea63
fix(macos): harden AgentSidecar port discovery — require newline, rec…
kalil0321 e2c9c22
fix(agent): emit TextBlocks when StreamEvent SDK fallback is in use
kalil0321 b29f248
fix(macos): keep in-memory + persisted flow state in sync on delete/c…
kalil0321 73c0046
fix(macos): inspector — nil HTML baseURL + don't gate text copy on co…
kalil0321 539ab40
fix(macos): truncate bodies on UTF-8 boundary + scope history record …
kalil0321 a04add5
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