Skip to content
Closed
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
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
private/loopback/link-local IP rejection re-checked on every redirect hop,
size/time caps, and indirect-prompt-injection flagging. Reachable only from the
TUI, never from an MCP tool — the tool surface is unchanged. See `SECURITY.md`.
- **Optional per-Space retention caps** for the TUI's local store —
`PERPLEXITY_MAX_HISTORY_PER_SPACE` and `PERPLEXITY_MAX_TABS_PER_SPACE`. Unset
(the default) keeps everything: a deployed instance never deletes a user's
data unless its operator explicitly opts in. When set to N, only the N
most-recent rows per Space are kept; older rows are pruned on write.

### Security

Expand All @@ -28,6 +33,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the first differing byte, which leaks the token's length and prefix through
response timing. Comparing bytes also means a malformed (non-ASCII)
`Authorization` header still returns a clean `401` rather than erroring.
- The rate-limit + audit pairing is centralized in a single `RequestGuard`
(`security.py`) and applied uniformly to the MCP tools, the TUI commands, and
the background `/task` monitors. Previously the TUI built an audit logger it
never called and background probes bypassed the rate limiter entirely, so a new
surface can no longer acquire rate-limit tokens without also leaving an audit
trail.
- Untrusted page titles are escaped before they are rendered in the tab bar, so a
title containing console-markup metacharacters can neither crash the bar nor
inject styling.

### Changed

Expand All @@ -36,3 +50,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`disallow_any_generics` for `server.py`, because FastMCP only injects a tool's
`Context` when it is annotated as the bare `Context` — parameterizing it to
satisfy strict mode would silently break injection.
- CI runs lint, type-check, and dependency-audit across the full Python
3.11/3.12/3.13 matrix again (not just 3.12), adds a job that exercises the
**tui-less core install**, and no longer cancels in-progress runs on `main`
(so every commit that lands keeps a completed test/security record).
- The page fetcher enforces the response-size cap **while streaming** the body —
an oversized download is aborted before it is fully buffered into memory — and
resolves DNS off the event loop so a slow nameserver can't freeze the TUI.
- Saved tabs are keyed by URL within a Space: re-opening a page updates that tab
in place (one row per URL) instead of stacking duplicates, and the store
indexes its per-Space lookups.

### Fixed

- A background `/task` monitor no longer dies silently on the first transient
error — it reports the error once and keeps retrying — and quitting the TUI
awaits in-flight probes before closing the clients they share.
- A 3xx redirect with no `Location` header now reports the real problem instead
of a misleading "too many redirects".
- `perplexity-agent --transport http tui` now errors instead of silently
ignoring `--transport` and launching the interactive UI.
- `group_tabs` tolerates schema-nonconforming model output (and a malformed
response shape) instead of raising.
- A blank retention env var (e.g. `PERPLEXITY_MAX_HISTORY_PER_SPACE=`) is treated
as "unset" rather than crashing startup with a misleading "API key" error.

---

*Proudly Made in Nebraska. Go Big Red! 🌽 <https://xkcd.com/2347/>*
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<p align="center"><em>Proudly Made in Nebraska. Go Big Red! 🌽 <a href="https://xkcd.com/2347/">https://xkcd.com/2347/</a></em></p>

# PerplexityAgent

A simple, **security-hardened** [Model Context Protocol](https://modelcontextprotocol.io)
Expand Down Expand Up @@ -273,8 +275,11 @@ reaches Sonar. See [`SECURITY.md`](SECURITY.md). The MCP tool surface is unchang

All optional knobs are environment variables (see [`.env.example`](.env.example)):
timeouts, response-size cap, retry count, rate limits, an optional JSON audit-log
path, and (for the TUI) the fetch User-Agent, `PERPLEXITY_FETCH_ALLOW_PRIVATE`, and
`PERPLEXITY_STORE_PATH`.
path, and (for the TUI) the fetch User-Agent, `PERPLEXITY_FETCH_ALLOW_PRIVATE`,
`PERPLEXITY_STORE_PATH`, and optional per-Space retention caps
(`PERPLEXITY_MAX_HISTORY_PER_SPACE` / `PERPLEXITY_MAX_TABS_PER_SPACE` — unset means
keep everything; set a positive integer to keep only the N most-recent rows per
Space).

## Development

Expand Down
19 changes: 14 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ operating-system account the server runs under.
| **Choose supported MCP projects** | Built on the official `mcp` Python SDK (FastMCP). Dependencies are pinned and fully locked in `uv.lock`. |
| **Design for boundaries / least privilege** | Default **stdio** transport runs locally with no network exposure. No shell execution, no filesystem writes (except an optional, explicitly-configured audit-log path). Egress is only to `api.perplexity.ai`. The API key lives only in the client layer and is **never** returned by a tool. The MCP server's only egress is `api.perplexity.ai`; the optional TUI adds an SSRF-guarded page fetcher (see below) that is never reachable from a tool. |
| **Validate parameters** | Every tool input is validated against a strict `pydantic` model (`schemas.py`) with bounded string lengths (≤ 4 KB), numeric ranges (`max_results` 1–20, `num_subquestions` 1–8), and a `model` enum. Unknown fields are rejected (`extra="forbid"`), preventing parameter smuggling. |
| **Constrain & sandbox tool execution** | Per-request timeouts, a hard response-size cap, and capped retries with jittered backoff (`client.py`). Run the process under seccomp/AppArmor/SELinux or in a container for OS-level isolation (see below). |
| **Constrain & sandbox tool execution** | Per-request timeouts, a hard response-size cap (shared `enforce_size_cap`; the TUI page fetcher enforces it **while streaming**, aborting an oversized download before it is fully buffered), and capped retries with jittered backoff (`client.py`). Run the process under seccomp/AppArmor/SELinux or in a container for OS-level isolation (see below). |
| **Sign & verify messages (transport)** | stdio mode is local-trusted. The optional HTTP transport **refuses to start without a bearer token**, binds to localhost by default, and rejects any request lacking the exact `Authorization: Bearer <token>` header. Terminate TLS at a reverse proxy in front of it. |
| **Filter & monitor outputs / chained execution** | Perplexity responses are treated as **untrusted input** to the next stage. `deep_research` scans retrieved snippets for indirect-prompt-injection patterns and surfaces them in `security_flags`. Citations are taken only from API metadata, never from model free-text. |
| **Instrument for logging & detection** | A structured JSON audit log (`security.py`) records every tool call and result with redacted parameters, a result hash, and validation status — suitable for SIEM ingestion. |
| **Instrument for logging & detection** | A structured JSON audit log (`security.py`) records every tool call and result with redacted parameters, a result hash, and validation status — suitable for SIEM ingestion. A single `RequestGuard` pairs the rate limiter with the audit log, so the TUI's commands and its background `/task` monitors are logged on the same path as the MCP tools rather than slipping past it. |
| **Track & patch vulnerabilities** | Pinned deps + `uv.lock`; CI runs `pip-audit` on every push. Enable Renovate/Dependabot on the GitHub mirror to automate updates. |
| **DoS / fatigue resistance** | A token-bucket rate limiter (`rate_per_minute` / `rate_burst`), bounded sub-question counts, input-size caps, and request timeouts. |
| **DoS / fatigue resistance** | A token-bucket rate limiter (`rate_per_minute` / `rate_burst`) that — via `RequestGuard` — meters the MCP tools, the TUI commands, and background `/task` probes alike, plus bounded sub-question counts, input-size caps, and request timeouts. The TUI's local store can be bounded with per-Space retention caps (`PERPLEXITY_MAX_HISTORY_PER_SPACE` / `PERPLEXITY_MAX_TABS_PER_SPACE`). |
| **Access control / token security** | `PERPLEXITY_API_KEY` is loaded server-side from the environment only; the server fails fast if it is absent. No token passthrough. |

## Interactive TUI page fetcher (added egress surface)
Expand All @@ -52,14 +52,19 @@ fetcher applies SSRF + DoS controls mirroring the API client:
- **Untrusted-output handling** — extracted page text is run through the same
indirect-prompt-injection scan as `deep_research`; matches are surfaced to the user
before the text is sent to Sonar.
- **Audit** — fetches share the TUI's `TokenBucket` rate limiter and audit logger.
- **Audit & rate limit** — the TUI's commands, page fetches, and background
`/task` probes all pass through one `RequestGuard` that charges the
`TokenBucket` and writes a redacted audit record, so the interactive surface is
metered and logged just like the MCP tools.

**Residual risk:** a DNS-rebinding TOCTOU window exists between resolution and
connect (we validate the resolved IPs, then connect by hostname for correct TLS).
With `fetch_allow_private=False` the blast radius is "publicly routable internet
only". Operators who fetch untrusted URLs should still run the TUI behind a filtering
egress proxy. The TUI's local SQLite store (history/tabs/Spaces) holds only the
user's own artifacts — no secrets.
user's own artifacts — no secrets — and its growth can be bounded with the
per-Space retention caps (`PERPLEXITY_MAX_HISTORY_PER_SPACE` /
`PERPLEXITY_MAX_TABS_PER_SPACE`; default: unbounded, i.e. nothing is deleted).

## Secret handling

Expand All @@ -83,3 +88,7 @@ Please report security issues privately to the maintainer rather than opening a
public issue: open a confidential issue on the canonical Codeberg repo, or contact
the maintainer directly. Include reproduction steps and affected version. Please
allow reasonable time for a fix before public disclosure.

---

*Proudly Made in Nebraska. Go Big Red! 🌽 <https://xkcd.com/2347/>*
4 changes: 4 additions & 0 deletions docs/perplexity-research-agent-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,7 @@ A practical validation layer should enforce three checks:
## Deployment notes

Perplexity’s quickstart materials describe OpenAI SDK compatibility for Sonar and standard authenticated HTTPS access for the broader API platform, which makes local integration straightforward through a small backend wrapper.[cite:30][cite:37] The cleanest deployment pattern is to keep Perplexity keys on the server, expose only internal endpoints to local apps, and put caching plus validation in the same control plane.[cite:6][cite:17]

---

*Proudly Made in Nebraska. Go Big Red! 🌽 <https://xkcd.com/2347/>*
Loading