Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/agents/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Agents

VibePod manages each agent as a Docker or Podman container. Credentials and config are persisted to `~/.config/vibepod/agents/<agent>/` on your host and mounted into the container on every run, so you only need to authenticate once.
VibePod manages each agent as a Docker or Podman container. Credentials and config are persisted to `~/.config/vibepod/agents/<agent>/` on your host and mounted into the container on every run, so you only need to authenticate once. To keep several credential sets per agent (subscription vs. API key vs. Ollama), see [Credential Profiles](../profiles.md).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify that the displayed path is default-profile-only.

Line 3 says credentials persist at ~/.config/vibepod/agents/<agent>/ on every run, but named profiles use ~/.config/vibepod/profiles/<name>/agents/<agent>/. Describe the shown path as the default profile location.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/agents/index.md` at line 3, Update the documentation paragraph in the
agent overview to identify ~/.config/vibepod/agents/<agent>/ as the location for
the default profile only, while retaining the existing explanation that
credentials are persisted and mounted. Keep the Credential Profiles link and
mention of named profile storage unchanged.


## Supported Agents

Expand Down
5 changes: 5 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ These variables override the corresponding config keys without editing any file:
| `VP_LLM_API_KEY` | `llm.api_key` | `VP_LLM_API_KEY=ollama` |
| `VP_LLM_MODEL` | `llm.model` | `VP_LLM_MODEL=qwen3:14b` |
| `VP_CONFIG_DIR` | *(config root)* | `VP_CONFIG_DIR=/custom/path` |
| `VP_PROFILE` | `profile` | `VP_PROFILE=work` |

The `profile` key selects the active [credential profile](profiles.md); the
`--profile` flag on `vp run`, `vp task create`, and `vp doctor claude` takes
precedence over both the variable and the config key.

### Image overrides

Expand Down
70 changes: 70 additions & 0 deletions docs/profiles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Credential Profiles

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).

## Layout

```
~/.config/vibepod/
agents/<agent>/ # the built-in "default" profile
profiles/<name>/agents/<agent>/ # named profiles
```
Comment on lines +15 to +19

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify the layout fence language.

Line 15 violates markdownlint MD040. Use ```text for the directory-tree block.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 15-15: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/profiles.md` around lines 15 - 19, Update the directory-tree fenced code
block in the profiles documentation to specify the text language using the
```text fence, while preserving its existing layout and contents.

Source: Linters/SAST tools


Your existing credentials in `~/.config/vibepod/agents/` are the `default`
profile — nothing moves when you start using profiles.

## Managing profiles

```bash
vp profile list # list profiles; * marks the active one, agents with
# stored data (config, caches, credentials) in parentheses
vp profile create work # create an empty profile
vp profile remove work # delete a profile and its credentials (asks first)
```

Profile names are lowercase slugs: letters, digits, `-` and `_`.
The `default` profile always exists and cannot be removed.

## Using a profile

```bash
vp run claude --profile work
vp task create claude "summarize the diff" --profile work
vp doctor claude --profile work
```

The first run with a fresh profile starts unauthenticated — log in once and the
credentials are persisted inside that profile.

## Selecting a profile without the flag

Resolution order (first match wins):

1. `--profile` flag
2. `VP_PROFILE` environment variable
3. `profile:` key in the merged config (global `~/.config/vibepod/config.yaml`
or project `.vibepod/config.yaml`)
4. `default`

Pinning a profile per project pairs well with per-project env vars:

```yaml
# .vibepod/config.yaml — a project wired to a local Ollama
version: 1
profile: ollama
agents:
claude:
env:
ANTHROPIC_BASE_URL: http://host.docker.internal:11434
```

Referencing a profile that does not exist is a hard error — create it first
with `vp profile create <name>`.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ nav:
- Quickstart: quickstart.md
- Development: development.md
- Agents: agents/index.md
- Profiles: profiles.md
- Skills:
- Overview: skills/index.md
- Locator format: skills/locators.md
Expand Down
12 changes: 12 additions & 0 deletions src/vibepod/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
doctor,
list_cmd,
logs,
profile,
proxy,
run,
skills,
Expand Down Expand Up @@ -77,6 +78,10 @@ def run_command(
help="I Know What I'm Doing: enable auto-approval / skip permission prompts",
),
] = False,
profile: Annotated[
str | None,
typer.Option("--profile", help="Credential profile to use (see `vp profile list`)"),
] = None,
) -> None:
"""Start an agent container."""
run.run(
Expand All @@ -91,6 +96,7 @@ def run_command(
network=network,
paste_images=paste_images,
ikwid=ikwid,
profile=profile,
passthrough_args=_context_args(ctx),
)

Expand All @@ -106,6 +112,7 @@ def run_command(

app.add_typer(logs.app, name="logs")
app.add_typer(config.app, name="config")
app.add_typer(profile.app, name="profile")
app.add_typer(proxy.app, name="proxy")
app.add_typer(doctor.app, name="doctor")
app.add_typer(skills.app, name="skills")
Expand Down Expand Up @@ -155,6 +162,10 @@ def _alias(
help="I Know What I'm Doing: enable auto-approval / skip permission prompts",
),
] = False,
profile: Annotated[
str | None,
typer.Option("--profile", help="Credential profile to use (see `vp profile list`)"),
] = None,
) -> None:
run.run(
agent=agent_name,
Expand All @@ -168,6 +179,7 @@ def _alias(
network=network,
paste_images=paste_images,
ikwid=ikwid,
profile=profile,
passthrough_args=_context_args(ctx),
)

Expand Down
20 changes: 16 additions & 4 deletions src/vibepod/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from typing import Annotated, Any

import typer

from vibepod.core.agents import agent_config_dir
from vibepod.core.config import get_config
from vibepod.core.profiles import resolve_profile
from vibepod.utils.console import console, error, success, warning

app = typer.Typer(help="Inspect agent auth and config state")
Expand Down Expand Up @@ -64,10 +66,20 @@ def _file_ownership(path: Path) -> str:


@app.command("claude")
def claude() -> None:
def claude(
profile: Annotated[
str | None,
typer.Option("--profile", help="Credential profile to inspect (see `vp profile list`)"),
] = None,
) -> None:
"""Inspect Claude Code credential state for diagnosing auth/refresh issues."""
cfg_dir = agent_config_dir("claude")
console.print(f"[bold]Claude config dir:[/bold] {cfg_dir}")
try:
active_profile = resolve_profile(profile, get_config())
except ValueError as exc:
error(str(exc))
raise typer.Exit(1) from exc
cfg_dir = agent_config_dir("claude", active_profile)
console.print(f"[bold]Claude config dir:[/bold] {cfg_dir} (profile: {active_profile})")

if not cfg_dir.exists():
error(f"Config dir does not exist: {cfg_dir}")
Expand Down
109 changes: 109 additions & 0 deletions src/vibepod/commands/profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Profile subcommands: manage named agent credential environments."""

from __future__ import annotations

import os
from typing import Annotated

import typer

from vibepod.constants import SUPPORTED_AGENTS
from vibepod.core.config import get_config
from vibepod.core.profiles import (
DEFAULT_PROFILE,
create_profile,
list_profiles,
profile_agents_root,
profile_exists,
remove_profile,
resolve_profile,
validate_profile_name,
)
from vibepod.utils.console import console, error, success, warning

app = typer.Typer(help="Manage credential profiles (separate agent logins per environment)")


def _agents_with_data(profile: str) -> list[str]:
"""Agents with any stored data (config, caches, credentials) in the profile."""
root = profile_agents_root(profile)
found: list[str] = []
for agent in sorted(SUPPORTED_AGENTS):
agent_dir = root / agent
if agent_dir.is_dir() and any(agent_dir.iterdir()):
found.append(agent)
Comment on lines +33 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish credentials from arbitrary agent state

When a fresh unauthenticated agent writes any cache, settings, session, or Xauthority file into its profile directory, this nonempty-directory check reports that agent under vp profile list as having stored credentials. Users can therefore be told a profile is authenticated after exiting before login; check agent-specific credential artifacts or describe the output as stored agent data instead.

Useful? React with 👍 / 👎.

return found


def _active_profile() -> str | None:
"""Resolved active profile, or None when the current selection is broken."""
try:
return resolve_profile(None, get_config())
except ValueError as exc:
warning(str(exc))
return None


@app.command("list")
def list_() -> None:
"""List profiles, the active one, and which agents have stored data."""
active = _active_profile()
for name in list_profiles():
marker = "*" if name == active else " "
agents = ", ".join(_agents_with_data(name))
suffix = f" ({agents})" if agents else ""
console.print(f"{marker} {name}{suffix}")


@app.command("create")
def create(
name: Annotated[str, typer.Argument(help="Profile name (lowercase slug)")],
) -> None:
"""Create a new empty profile."""
try:
path = create_profile(name)
except ValueError as exc:
error(str(exc))
raise typer.Exit(code=1) from exc
except OSError as exc:
error(f"Could not create profile '{name}': {exc}")
raise typer.Exit(code=1) from exc
success(f"Created profile '{name}' at {path.parent}")


@app.command("remove")
def remove(
name: Annotated[str, typer.Argument(help="Profile to remove")],
yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
) -> None:
"""Remove a profile and its stored credentials."""
if name == DEFAULT_PROFILE:
error("Profile 'default' cannot be removed.")
raise typer.Exit(code=1)
try:
validate_profile_name(name)
except ValueError as exc:
error(str(exc))
raise typer.Exit(code=1) from exc
if not profile_exists(name):
error(f"Profile '{name}' does not exist.")
raise typer.Exit(code=1)
if not yes:
typer.confirm(
f"Remove profile '{name}' and all credentials stored in it?", abort=True
)
try:
remove_profile(name)
except ValueError as exc:
error(str(exc))
raise typer.Exit(code=1) from exc
except OSError as exc:
error(
f"Could not remove profile '{name}': {exc}. The profile may be partially "
"deleted. Files created by agent containers can be owned by another user; "
"fix ownership (e.g. `sudo chown -R $USER ...`) and retry."
)
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}'.")
success(f"Removed profile '{name}'")
14 changes: 12 additions & 2 deletions src/vibepod/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
from vibepod.core.launch import (
x11_volumes_and_env as _x11_volumes_and_env,
)
from vibepod.core.profiles import resolve_profile
from vibepod.core.session_logger import SessionLogger
from vibepod.utils.console import error, info, success, warning

Expand Down Expand Up @@ -277,6 +278,10 @@ def run(
help="I Know What I'm Doing: enable auto-approval / skip permission prompts",
),
] = False,
profile: Annotated[
str | None,
typer.Option("--profile", help="Credential profile to use (see `vp profile list`)"),
] = None,
passthrough_args: list[str] | None = None,
) -> None:
"""Start an agent container.
Expand All @@ -287,6 +292,11 @@ def run(
"""
passthrough_args = passthrough_args or []
config = get_config()
try:
active_profile = resolve_profile(profile, config)
except ValueError as exc:
error(str(exc))
raise typer.Exit(1) from exc
selected_agent_input = agent or str(config.get("default_agent", "claude"))
selected_agent = resolve_agent_name(selected_agent_input)
if selected_agent is None:
Expand Down Expand Up @@ -358,7 +368,7 @@ def run(
and "ANTHROPIC_API_KEY" not in merged_env
and "setup-token" not in passthrough_args
):
stored_token = _read_claude_stored_token(agent_config_dir(selected_agent))
stored_token = _read_claude_stored_token(agent_config_dir(selected_agent, active_profile))
if stored_token:
merged_env["CLAUDE_CODE_OAUTH_TOKEN"] = stored_token
info("Using stored Claude OAuth token (from `vp run claude setup-token`)")
Expand Down Expand Up @@ -455,7 +465,7 @@ def run(
raise typer.Exit(1) from exc
command = list(command or []) + passthrough_args

config_dir = agent_config_dir(selected_agent)
config_dir = agent_config_dir(selected_agent, active_profile)
config_dir.mkdir(parents=True, exist_ok=True)

proxy_cfg = config.get("proxy", {})
Expand Down
Loading
Loading