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
68 changes: 68 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ proxy:
db_path: ~/.config/vibepod/proxy/proxy.db
ca_dir: ~/.config/vibepod/proxy/mitmproxy
ca_path: ~/.config/vibepod/proxy/mitmproxy/mitmproxy-ca-cert.pem
filter:
mode: open # open | allow | deny
allow: []
deny: []
```

## Environment variables
Expand All @@ -193,6 +197,7 @@ These variables override the corresponding config keys without editing any file:
| `VP_NO_COLOR` | `no_color` | `VP_NO_COLOR=true` |
| `VP_DATASETTE_PORT` | `logging.ui_port` | `VP_DATASETTE_PORT=9001` |
| `VP_PROXY_ENABLED` | `proxy.enabled` | `VP_PROXY_ENABLED=false` |
| `VP_PROXY_FILTER_MODE` | `proxy.filter.mode` | `VP_PROXY_FILTER_MODE=allow` |
| `VP_LLM_ENABLED` | `llm.enabled` | `VP_LLM_ENABLED=true` |
| `VP_LLM_BASE_URL` | `llm.base_url` | `VP_LLM_BASE_URL=http://localhost:11434` |
| `VP_LLM_API_KEY` | `llm.api_key` | `VP_LLM_API_KEY=ollama` |
Expand Down Expand Up @@ -332,3 +337,66 @@ VP_PROXY_ENABLED=false vp run claude
network. If you use Podman with the CNI backend, install the `dnsname`
plugin (e.g. `podman-plugins` on Fedora, `golang-github-containernetworking-plugin-dnsname`
on Debian/Ubuntu) and recreate the network. See [Quickstart — Using Podman](quickstart.md#using-podman-instead-of-docker) for full instructions.

### Allow/deny filtering

Filtering is opt-in; the default mode `open` passes and logs everything
(today's behavior). In `allow` mode only listed hosts pass; in `deny` mode
everything passes except listed hosts. Patterns: `example.com` matches that
host exactly, `*.example.com` matches subdomains (not the apex).

```bash
vp proxy filter status
vp proxy filter mode allow
vp proxy filter allow add api.anthropic.com
vp proxy filter allow add "*.github.com"
vp proxy filter deny add example.com
vp proxy filter mode open # back to no filtering; lists are kept
```

One shared proxy evaluates a separate policy for every VibePod agent container.
Each launch receives an opaque policy identity and is also bound to its source
container after startup. This prevents a later launch from replacing the rules
of an already-running agent. Unidentified clients continue to use the global
filter.

Profile changes apply immediately to every running container using that
profile—no proxy restart is needed. Project filter settings and
`VP_PROXY_FILTER_MODE` are captured when each container starts, so changing
either affects new launches only. The effective profile and live mode are shown
by `vp list` in the `PROFILE` and `PROXY MODE` columns and in its JSON rows as
`profile` and `proxy_mode`.

Blocked requests return `403` (HTTPS tunnels are refused at `CONNECT`) and are
logged with `blocked = 1` in the proxy database. Invalid policy configuration
is rejected instead of silently falling back to `open`; an identified launch
whose policy files are missing or malformed fails closed.

The `vp proxy filter` commands act on the **active profile**. For the
`default` profile they write the global config; for a named profile they write
`profiles/<name>/filter.yaml` (seeded from the global settings on first
write), so switching profiles also switches the filter mode and lists. Pass
`--profile <name>` to manage another profile's filter without switching.

A project-level `.vibepod/config.yaml` filter section or
`VP_PROXY_FILTER_MODE` takes precedence over the global values (for a profile
with its own `filter.yaml`, only `VP_PROXY_FILTER_MODE` still overrides the
mode). Project and environment overrides are launch-specific and never replace
the shared global fallback.

Custom proxy images must implement per-source policies and expose this exact
OCI image label:

```dockerfile
LABEL io.vibepod.proxy.policy-schema="2"
```

VibePod checks the configured image—and an already-running proxy—before
launching a proxied agent. An image with a missing or different schema label is
rejected with an upgrade error.

!!! warning "Filtering is not a network sandbox"
The filter controls requests that use the injected proxy. A container can
override its proxy environment or attempt a direct connection unless a
separate network-control layer prevents that. VibePod warns when explicit
`HTTP_PROXY` or `HTTPS_PROXY` values bypass its identified proxy URL.
41 changes: 36 additions & 5 deletions docs/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ Profiles let you keep multiple credential sets per agent and switch between them
at run time — for example a Claude subscription login, a separate API-key setup,
and an environment prepared for Ollama.

A profile only switches the **credential directories** that get mounted into the
agent container. Everything else (skills, allowed directories, proxy, logging)
stays shared. Environment variables such as `ANTHROPIC_API_KEY` are still
configured via `agents.<agent>.env` or `-e` flags — combine them with a profile
via a project config (see below).
A profile switches the **credential directories** that get mounted into the
agent container and, when the profile has its own filter settings, the
**proxy allow/deny filter** (see below). Everything else (skills, allowed
directories, proxy, logging) stays shared. Environment variables such as
`ANTHROPIC_API_KEY` are still configured via `agents.<agent>.env` or `-e`
flags — combine them with a profile via a project config (see below).

## Layout

```text
~/.config/vibepod/
agents/<agent>/ # the built-in "default" profile
profiles/<name>/agents/<agent>/ # named profiles
profiles/<name>/filter.yaml # optional per-profile proxy filter
```

Your existing credentials in `~/.config/vibepod/agents/` are the `default`
Expand Down Expand Up @@ -68,3 +70,32 @@ agents:

Referencing a profile that does not exist is a hard error — create it first
with `vp profile create <name>`.

## Per-profile proxy filter

Each named profile can carry its own proxy filter mode and allow/deny lists in
`profiles/<name>/filter.yaml`. The `vp proxy filter` commands act on the
active profile, or on an explicit one via `--profile`:

```bash
vp proxy filter mode allow --profile work
vp proxy filter allow add api.anthropic.com --profile work
vp proxy filter status --profile work
```

The file is created on first write, seeded from the global filter settings.
A profile without `filter.yaml` inherits the global `proxy.filter` config and
then any project filter captured for that launch. An explicit profile file is
a complete replacement for the global/project base. In both cases, the
launch-time `VP_PROXY_FILTER_MODE` value wins last.

The proxy keeps one materialized profile base and a small record for each
container. Editing a profile filter hot-reloads every running container that
uses it, while each container retains the project and environment overrides it
started with. This means two agents using different profiles—or different
projects with the same inherited profile—can safely share one proxy.

`vp list` shows the selected profile and live effective proxy mode for each
running container. Removing a profile deletes its credentials, but VibePod
retains its materialized filter while any existing container (including a
stopped container) still references it.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies = [
"rich>=13.0.0",
"docker>=7.0.0",
"pyyaml>=6.0.0",
"ruamel.yaml>=0.18.0",
"platformdirs>=4.0.0",
"tomli>=2.0.1; python_version < '3.11'",
]
Expand Down
28 changes: 25 additions & 3 deletions src/vibepod/commands/list_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from vibepod.core.config import get_config
from vibepod.core.docker import DockerClientError, DockerManager
from vibepod.core.launch import overlay_enabled
from vibepod.core.proxy_filter import resolve_container_policy
from vibepod.utils.console import console, error


Expand All @@ -30,18 +31,30 @@ def _configured_agent_rows() -> list[dict[str, str]]:
return rows


def _running_rows(containers: list[Any]) -> list[dict[str, str]]:
def _running_rows(containers: list[Any], config: dict[str, Any]) -> list[dict[str, str]]:
rows: list[dict[str, str]] = []
for container in containers:
labels = getattr(container, "labels", {}) or {}
agent = labels.get("vibepod.agent")
status = getattr(container, "status", "-")
if not agent or status != "running":
continue
profile = labels.get("vibepod.profile", "-")
policy_id = labels.get("vibepod.proxy-policy")
proxy_mode = "-"
if policy_id:
try:
settings = resolve_container_policy(config, policy_id)
mode = settings.get("mode")
proxy_mode = mode if isinstance(mode, str) else "unavailable"
except (OSError, ValueError):
proxy_mode = "unavailable"
rows.append(
{
"agent": agent,
"container": getattr(container, "name", "-"),
"profile": profile,
"proxy_mode": proxy_mode,
"context": labels.get("vibepod.workspace", "-"),
},
)
Expand Down Expand Up @@ -124,7 +137,8 @@ def list_agents(
manager = None
containers = []

running_rows = _running_rows(containers)
config = get_config()
running_rows = _running_rows(containers, config)
configured_rows = _configured_agent_rows()
overlay_rows = [] if running else _overlay_rows(manager)

Expand All @@ -141,11 +155,19 @@ def list_agents(
running_table = Table(title="Running Agents", title_justify="left")
running_table.add_column("AGENT", style="cyan")
running_table.add_column("CONTAINER", style="magenta")
running_table.add_column("PROFILE")
running_table.add_column("PROXY MODE")
running_table.add_column("CONTEXT")

if running_rows:
for row in running_rows:
running_table.add_row(row["agent"], row["container"], row["context"])
running_table.add_row(
row["agent"],
row["container"],
row["profile"],
row["proxy_mode"],
row["context"],
)
console.print(running_table)
else:
console.print("No running agents.")
Expand Down
18 changes: 17 additions & 1 deletion src/vibepod/commands/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from __future__ import annotations

import os
from typing import Annotated
from typing import Annotated, Any

import typer

from vibepod.constants import SUPPORTED_AGENTS
from vibepod.core.config import get_config
from vibepod.core.docker import DockerClientError, DockerManager
from vibepod.core.launch import managed_proxy_policy_ids
from vibepod.core.profiles import (
DEFAULT_PROFILE,
create_profile,
Expand All @@ -19,6 +21,7 @@
resolve_profile,
validate_profile_name,
)
from vibepod.core.proxy_filter import cleanup_orphan_policies
from vibepod.utils.console import console, error, success, warning

app = typer.Typer(help="Manage credential profiles (separate agent logins per environment)")
Expand All @@ -44,6 +47,17 @@ def _active_profile() -> str | None:
return None


def _cleanup_removed_profile_policy(config: dict[str, Any]) -> None:
"""Sweep only when the complete managed-container set is available."""
try:
manager = DockerManager()
except DockerClientError:
return
referenced = managed_proxy_policy_ids(manager)
if referenced is not None:
cleanup_orphan_policies(config, referenced)


@app.command("list")
def list_() -> None:
"""List profiles, the active one, and which agents have stored data."""
Expand Down Expand Up @@ -90,6 +104,7 @@ def remove(
raise typer.Exit(code=1)
if not yes:
typer.confirm(f"Remove profile '{name}' and all credentials stored in it?", abort=True)
config = get_config()
try:
remove_profile(name)
except ValueError as exc:
Expand All @@ -104,4 +119,5 @@ def remove(
raise typer.Exit(code=1) from exc
if os.environ.get("VP_PROFILE") == name:
console.print(f"Note: VP_PROFILE still points at removed profile '{name}'.")
_cleanup_removed_profile_policy(config)
success(f"Removed profile '{name}'")
Loading
Loading