Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ __pycache__/
*.pyo
.env
.venv/
.venv-*/
venv/

# Local-run artifacts — autostart stdout/stderr logs + memory middleware
# fallback directory when /sandbox is not available.
logs/
.proto/
*.egg-info/
dist/
build/
Expand Down
23 changes: 22 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ RUN useradd -m -s /bin/bash -u ${SANDBOX_UID} sandbox
# auth, add them here. The ddgs + beautifulsoup4 pair powers the
# starter web_search / fetch_url tools; drop them if you strip those.
RUN pip install --no-cache-dir \
gradio httpx uvicorn langfuse prometheus-client pyyaml \
gradio httpx uvicorn langfuse prometheus-client pyyaml 'ruamel.yaml>=0.18' \
langchain langchain-openai langgraph websockets \
ddgs beautifulsoup4

Expand All @@ -40,6 +40,27 @@ RUN chmod +x /opt/protoagent/entrypoint.sh
RUN mkdir -p /sandbox /tmp/sandbox /sandbox/audit /sandbox/knowledge \
&& chown -R sandbox:sandbox /sandbox /tmp/sandbox

# Make /opt/protoagent/config writable by the sandbox user so the
# drawer and setup wizard can persist edits from inside the container.
RUN chown -R sandbox:sandbox /opt/protoagent/config

# Declare config as a volume so setup completion (``.setup-complete``
# marker + any YAML / SOUL.md edits) survives ``docker run`` without
# a -v flag.
#
# Lifecycle note: without an explicit mount, Docker creates an
# ANONYMOUS volume on every ``docker run``. Those accumulate and the
# volume is NOT removed when the container is removed unless you pass
# ``--rm -v``. For long-lived deployments, use a named volume or a
# host mount so upgrades don't silently carry stale config forward:
#
# docker run -v my-agent-config:/opt/protoagent/config my-agent:latest
#
# or a bind mount:
#
# docker run -v /srv/my-agent/config:/opt/protoagent/config my-agent:latest
VOLUME ["/opt/protoagent/config"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

ENV PYTHONPATH=/opt/protoagent

USER sandbox
Expand Down
43 changes: 26 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ close to a rewrite of `SOUL.md`, `graph/prompts.py`, and
Quinn was the first agent built on this template — it's a good
example of what a filled-in fork looks like end-to-end.

Start a new agent by clicking **"Use this template"** at the top
of the GitHub repo. See [TEMPLATE.md](./TEMPLATE.md) for the
step-by-step fork checklist.
**Try it in 5 minutes:** clone, `pip install -r requirements.txt`,
`python server.py`, open <http://localhost:7870>, and walk the
setup wizard — no forking, no `sed`, no Docker required to get
your first agent talking. See the [first-agent tutorial](./docs/tutorials/first-agent.md).

**When you're ready to ship your own:** click **"Use this template"**
at the top of the GitHub repo, then follow [Customize &
deploy](./docs/guides/customize-and-deploy.md) for the fork /
rename / release-pipeline wiring.

## What you get out of the box

Expand All @@ -31,28 +37,31 @@ step-by-step fork checklist.
| UI | `chat_ui.py`, `static/` | Gradio chat with PWA shell, dark theme, offline fallback |
| Release pipeline | `.github/workflows/*.yml` | Autonomous semver bumps, GHCR image push, GitHub release with filtered notes, optional Discord post |

## Quickstart
## Quickstart — from zero to chatting in 5 minutes

```bash
# 1. Click "Use this template" on GitHub, or:
gh repo create protoLabsAI/my-agent \
--template protoLabsAI/protoAgent \
--public --clone

# 1. Get the code (no fork needed for a first run)
git clone https://github.com/protoLabsAI/protoAgent.git my-agent
cd my-agent

# 2. Rename the agent (one env var, read by server.py, metrics, tracing)
export AGENT_NAME=my-agent
# 2. Install deps into a venv
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

# 3. Boot the container
docker build -t my-agent:local .
docker run --rm -p 7870:7870 -e AGENT_NAME=my-agent my-agent:local
# 3. Run the server — no env vars required
python server.py

# 4. Hit the agent card
curl http://localhost:7870/.well-known/agent-card.json
# 4. Open the wizard — pick your endpoint, pick a model, name the
# agent, pick a persona preset, hit Launch. The chat UI appears
# on the same page.
open http://localhost:7870
```

See [TEMPLATE.md](./TEMPLATE.md) for the full fork checklist.
[First-agent tutorial](./docs/tutorials/first-agent.md) walks
through every wizard step with screenshots.

Once you're happy and want to ship it as your own image in your
own GHCR: [Customize & deploy](./docs/guides/customize-and-deploy.md).

## Architecture

Expand Down
11 changes: 11 additions & 0 deletions TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Fork checklist

> **Most of what used to be in this file is now a runtime wizard**
> that runs on first page load. Model, tools, persona, name, auth,
> autostart — all captured without editing code. See
> [first-agent tutorial](./docs/tutorials/first-agent.md).
>
> This checklist is only for forks that want to ship their own
> container image under their own GitHub org — the structural
> changes the wizard can't do. For most of that, the new
> [Customize & deploy](./docs/guides/customize-and-deploy.md)
> guide is the canonical source. This file stays for back-compat.

You clicked "Use this template" (or ran `gh repo create --template`).
Now what?

Expand Down
48 changes: 39 additions & 9 deletions a2a_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,36 +919,66 @@ def _check_auth(request: Request, api_key: str) -> None:
# ── Route factory ─────────────────────────────────────────────────────────────


# Module-level mutable holder for the bearer token so hosts can
# update it at runtime without re-registering routes (e.g. when the
# setup wizard captures a token post-boot). ``register_a2a_routes``
# seeds this from its ``auth_token`` argument (or ``A2A_AUTH_TOKEN``
# env as fallback); ``set_a2a_token`` updates it live. Closures inside
# ``register_a2a_routes`` read ``_A2A_TOKEN[0]`` on every request, so
# a mutation is picked up by the next incoming call.
_A2A_TOKEN: list[str | None] = [None]


def set_a2a_token(token: str | None) -> None:
"""Update the active A2A bearer token at runtime.

Called by the host (e.g. ``server.py``) after the wizard / drawer
changes ``auth.token`` in the YAML — without this, bearer auth
captured at register time would stay stale until process restart.
"""
_A2A_TOKEN[0] = (token or "").strip() or None


def register_a2a_routes(
app: FastAPI,
chat_stream_fn_factory: Callable[..., AsyncGenerator],
chat_fn: Callable, # kept for potential future use / testing
api_key: str,
agent_card: dict,
register_card_route: bool = True,
auth_token: str = "",
) -> None:
"""Register all A2A routes on *app* and update *agent_card* capabilities.

Host apps that already serve the agent card themselves (e.g. at multiple
well-known paths for sdk compat) should pass ``register_card_route=False``
so FastAPI does not raise on a duplicate route registration.

``auth_token`` seeds the bearer-token check. When empty, falls
back to the ``A2A_AUTH_TOKEN`` env var. Hosts can update the
active token post-registration via ``set_a2a_token(...)`` (e.g.
after a wizard-driven config reload) without needing a restart.
"""

# ── Bearer token authentication ───────────────────────────────────────────
_raw_a2a_token = os.environ.get("A2A_AUTH_TOKEN", "")
_a2a_token: str | None = _raw_a2a_token.strip() or None
if not _a2a_token:
# Seed order: explicit arg > env. Stored in the module-level holder
# so mutations propagate to the closure below.
seed = (auth_token or os.environ.get("A2A_AUTH_TOKEN", "") or "").strip()
_A2A_TOKEN[0] = seed or None
if _A2A_TOKEN[0] is None:
logger.warning(
"[a2a] A2A auth token not configured — endpoint is open"
)

def _check_bearer_auth(request: Request) -> None:
"""Validate Authorization: Bearer <token> against A2A_AUTH_TOKEN.
"""Validate Authorization: Bearer <token> against the active
token. No-ops when unset. Raises HTTP 401 on missing/invalid.

No-ops when A2A_AUTH_TOKEN is unset (open mode).
Raises HTTP 401 on missing or invalid token.
Reads ``_A2A_TOKEN[0]`` on every call so runtime updates via
``set_a2a_token`` are honored without route re-registration.
"""
if not _a2a_token:
active = _A2A_TOKEN[0]
if not active:
return
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
Expand All @@ -957,7 +987,7 @@ def _check_bearer_auth(request: Request) -> None:
detail="Unauthorized: expected 'Authorization: Bearer <token>'",
)
provided = auth_header[len("Bearer "):]
if not hmac.compare_digest(provided, _a2a_token):
if not hmac.compare_digest(provided, active):
raise HTTPException(status_code=401, detail="Unauthorized: invalid bearer token")

# ── Origin verification for SSE/streaming endpoints ───────────────────────
Expand Down Expand Up @@ -989,7 +1019,7 @@ def _check_origin(request: Request) -> None:
agent_card.setdefault("capabilities", {})
agent_card["capabilities"]["streaming"] = True
agent_card["capabilities"]["pushNotifications"] = True
if _a2a_token:
if _A2A_TOKEN[0]:
agent_card.setdefault("securitySchemes", {})
agent_card["securitySchemes"]["bearer"] = {
"type": "http",
Expand Down
Loading
Loading