Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1972c8c6e9 | |||
| 5d109ea290 |
@@ -35,6 +35,7 @@ backend remains available with `BOT_BOTTLE_BACKEND=docker` or
|
|||||||
- `.bot-bottle/` — per-repo agent and bottle manifests (YAML markdown format).
|
- `.bot-bottle/` — per-repo agent and bottle manifests (YAML markdown format).
|
||||||
- `examples/` — example bottles and agents showing the manifest format.
|
- `examples/` — example bottles and agents showing the manifest format.
|
||||||
- `docs/README.md` — docs overview; when to write which document.
|
- `docs/README.md` — docs overview; when to write which document.
|
||||||
|
- `docs/glossary.md` — canonical term definitions (Agent Provider, Bottle, Sealed Bottle, etc.).
|
||||||
- `docs/prds/` — product requirement docs (see `docs/prds/README.md` for format).
|
- `docs/prds/` — product requirement docs (see `docs/prds/README.md` for format).
|
||||||
- `docs/research/` — research notes (see `docs/research/README.md`).
|
- `docs/research/` — research notes (see `docs/research/README.md`).
|
||||||
- `docs/decisions/` — decision records (ADR-lite).
|
- `docs/decisions/` — decision records (ADR-lite).
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ backend exposes five methods:
|
|||||||
|
|
||||||
enumerate_active() -> Sequence[ActiveAgent]
|
enumerate_active() -> Sequence[ActiveAgent]
|
||||||
Return every currently-running bottle on this backend, with
|
Return every currently-running bottle on this backend, with
|
||||||
enough metadata for callers (CLI `list active`, dashboard
|
enough metadata for callers (CLI `active`, dashboard
|
||||||
agents pane) to render a row.
|
agents pane) to render a row.
|
||||||
|
|
||||||
Selection is driven by `--backend` on `start` or BOT_BOTTLE_BACKEND
|
Selection is driven by `--backend` on `start` or BOT_BOTTLE_BACKEND
|
||||||
@@ -200,7 +200,7 @@ class ExecResult:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ActiveAgent:
|
class ActiveAgent:
|
||||||
"""One currently-running agent, as the CLI `list active` and
|
"""One currently-running agent, as the CLI `active` and
|
||||||
dashboard agents pane render it. ("Agent" is the project's
|
dashboard agents pane render it. ("Agent" is the project's
|
||||||
consistent name for the thing running inside a bottle — the
|
consistent name for the thing running inside a bottle — the
|
||||||
bottle is the container, the agent is what runs in it.)
|
bottle is the container, the agent is what runs in it.)
|
||||||
@@ -593,7 +593,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
|||||||
Linux + KVM. Used by the cross-backend
|
Linux + KVM. Used by the cross-backend
|
||||||
`enumerate_active_agents` / `cmd_cleanup` to skip backends
|
`enumerate_active_agents` / `cmd_cleanup` to skip backends
|
||||||
the operator hasn't installed, so a docker-only host
|
the operator hasn't installed, so a docker-only host
|
||||||
doesn't fail when `cli.py list active` walks past
|
doesn't fail when `cli.py active` walks past
|
||||||
firecracker."""
|
firecracker."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -813,7 +813,7 @@ def has_backend(name: str) -> bool:
|
|||||||
|
|
||||||
def enumerate_active_agents() -> list[ActiveAgent]:
|
def enumerate_active_agents() -> list[ActiveAgent]:
|
||||||
"""All currently-running agents, across every available
|
"""All currently-running agents, across every available
|
||||||
backend. Used by CLI `list active` and the dashboard's agents
|
backend. Used by CLI `active` and the dashboard's agents
|
||||||
pane so neither has to know which backends exist. Skips
|
pane so neither has to know which backends exist. Skips
|
||||||
backends whose `is_available()` reports False.
|
backends whose `is_available()` reports False.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Active-agent enumeration for the docker backend.
|
"""Active-agent enumeration for the docker backend.
|
||||||
|
|
||||||
Returns `ActiveAgent` records the CLI `list active` command and the
|
Returns `ActiveAgent` records the CLI `active` command and the
|
||||||
dashboard agents pane consume. Empty when docker isn't reachable
|
dashboard agents pane consume. Empty when docker isn't reachable
|
||||||
— gated by `has_backend('docker')` at the cross-backend caller
|
— gated by `has_backend('docker')` at the cross-backend caller
|
||||||
so this module trusts that docker is available when called.
|
so this module trusts that docker is available when called.
|
||||||
@@ -60,7 +60,7 @@ def _parse_services_by_project(stdout: str) -> dict[str, set[str]]:
|
|||||||
|
|
||||||
def _query_services_by_project() -> dict[str, set[str]]:
|
def _query_services_by_project() -> dict[str, set[str]]:
|
||||||
"""One `docker ps` call → `{project: {service, ...}}`. Used
|
"""One `docker ps` call → `{project: {service, ...}}`. Used
|
||||||
by the CLI's `list active` and the dashboard's agents pane —
|
by the CLI's `active` and the dashboard's agents pane —
|
||||||
one subprocess per refresh tick, not one per bottle."""
|
one subprocess per refresh tick, not one per bottle."""
|
||||||
try:
|
try:
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Main CLI dispatcher.
|
"""Main CLI dispatcher.
|
||||||
|
|
||||||
Commands: backend, cleanup, commit, edit, info, init, list, resume, start, supervise
|
Commands: active, backend, cleanup, commit, edit, init, list, resume, start, supervise
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -13,11 +13,11 @@ from ..manifest import ManifestError
|
|||||||
from ..store_manager import StoreManager
|
from ..store_manager import StoreManager
|
||||||
from ._common import PROG
|
from ._common import PROG
|
||||||
from . import list as _list_mod
|
from . import list as _list_mod
|
||||||
|
from .active import cmd_active
|
||||||
from .backend import cmd_backend
|
from .backend import cmd_backend
|
||||||
from .cleanup import cmd_cleanup
|
from .cleanup import cmd_cleanup
|
||||||
from .commit import cmd_commit
|
from .commit import cmd_commit
|
||||||
from .edit import cmd_edit
|
from .edit import cmd_edit
|
||||||
from .info import cmd_info
|
|
||||||
from .init import cmd_init
|
from .init import cmd_init
|
||||||
from .login import cmd_login
|
from .login import cmd_login
|
||||||
from .resume import cmd_resume
|
from .resume import cmd_resume
|
||||||
@@ -27,11 +27,11 @@ from .supervise import cmd_supervise
|
|||||||
cmd_list = _list_mod.cmd_list
|
cmd_list = _list_mod.cmd_list
|
||||||
|
|
||||||
COMMANDS = {
|
COMMANDS = {
|
||||||
|
"active": cmd_active,
|
||||||
"backend": cmd_backend,
|
"backend": cmd_backend,
|
||||||
"cleanup": cmd_cleanup,
|
"cleanup": cmd_cleanup,
|
||||||
"commit": cmd_commit,
|
"commit": cmd_commit,
|
||||||
"edit": cmd_edit,
|
"edit": cmd_edit,
|
||||||
"info": cmd_info,
|
|
||||||
"init": cmd_init,
|
"init": cmd_init,
|
||||||
"list": cmd_list,
|
"list": cmd_list,
|
||||||
"login": cmd_login,
|
"login": cmd_login,
|
||||||
@@ -51,13 +51,13 @@ NO_MIGRATION_COMMANDS = frozenset({"backend", "login"})
|
|||||||
def usage() -> None:
|
def usage() -> None:
|
||||||
sys.stderr.write(f"usage: {PROG} <command> [args...]\n\n")
|
sys.stderr.write(f"usage: {PROG} <command> [args...]\n\n")
|
||||||
sys.stderr.write("Commands:\n")
|
sys.stderr.write("Commands:\n")
|
||||||
|
sys.stderr.write(" active list currently-running bot-bottle bottles\n")
|
||||||
sys.stderr.write(" backend set up / check / undo a backend's host prerequisites (setup|status|teardown)\n")
|
sys.stderr.write(" backend set up / check / undo a backend's host prerequisites (setup|status|teardown)\n")
|
||||||
sys.stderr.write(" cleanup stop and remove all active bot-bottle containers\n")
|
sys.stderr.write(" cleanup stop and remove all active bot-bottle containers\n")
|
||||||
sys.stderr.write(" commit snapshot a running bottle's container state to a Docker image\n")
|
sys.stderr.write(" commit snapshot a running bottle's container state to a Docker image\n")
|
||||||
sys.stderr.write(" edit open an agent in vim for editing\n")
|
sys.stderr.write(" edit open an agent in vim for editing\n")
|
||||||
sys.stderr.write(" info print env, skills, and prompt details for a named agent\n")
|
|
||||||
sys.stderr.write(" init interactively create a new agent and add it to bot-bottle.json\n")
|
sys.stderr.write(" init interactively create a new agent and add it to bot-bottle.json\n")
|
||||||
sys.stderr.write(" list list available agents or active containers\n")
|
sys.stderr.write(" list list available agents from bot-bottle.json\n")
|
||||||
sys.stderr.write(" login register this host with a bot-bottle console\n")
|
sys.stderr.write(" login register this host with a bot-bottle console\n")
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
" resume re-launch a bottle by its identity "
|
" resume re-launch a bottle by its identity "
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""active: list currently-running bot-bottle bottles."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ..backend import enumerate_active_agents
|
||||||
|
from ._common import PROG
|
||||||
|
|
||||||
|
_ANSI_COLOR_CODES: dict[str, str] = {
|
||||||
|
"red": "\033[91m",
|
||||||
|
"green": "\033[92m",
|
||||||
|
"yellow": "\033[93m",
|
||||||
|
"blue": "\033[94m",
|
||||||
|
"magenta": "\033[95m",
|
||||||
|
}
|
||||||
|
_ANSI_RESET = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def _ansi_label(text: str, color: str) -> str:
|
||||||
|
if not color:
|
||||||
|
return text
|
||||||
|
if not sys.stdout.isatty():
|
||||||
|
return text
|
||||||
|
term = os.environ.get("TERM", "")
|
||||||
|
if term in ("dumb", ""):
|
||||||
|
return text
|
||||||
|
code = _ANSI_COLOR_CODES.get(color)
|
||||||
|
if not code:
|
||||||
|
return text
|
||||||
|
return f"{code}{text}{_ANSI_RESET}"
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_active(argv: list[str]) -> int:
|
||||||
|
if argv and argv[0] in ("-h", "--help"):
|
||||||
|
sys.stderr.write(f"usage: {PROG} active\n")
|
||||||
|
sys.stderr.write("\nList all currently-running bot-bottle bottles.\n")
|
||||||
|
sys.stderr.write("Output: <backend>\\t<slug>\\t<label>\\t<services>\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
active = enumerate_active_agents()
|
||||||
|
if not active:
|
||||||
|
print("no active bot-bottle bottles", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
# One line per bottle: `<backend>\t<slug>\t<label>\t<services>`.
|
||||||
|
# Tab-separated keeps the format stable for shell pipelines.
|
||||||
|
for b in active:
|
||||||
|
services = ",".join(b.services) if b.services else "-"
|
||||||
|
display_name = f"{b.label} ({b.agent_name})" if b.label else b.agent_name
|
||||||
|
colored_name = _ansi_label(display_name, b.color)
|
||||||
|
print(f"{b.backend_name}\t{b.slug}\t{colored_name}\t{services}")
|
||||||
|
return 0
|
||||||
@@ -27,7 +27,7 @@ def cmd_commit(argv: list[str]) -> int:
|
|||||||
nargs="?",
|
nargs="?",
|
||||||
default=None,
|
default=None,
|
||||||
help=(
|
help=(
|
||||||
"bottle slug from `cli.py list active` "
|
"bottle slug from `cli.py active` "
|
||||||
"(omit to pick interactively)"
|
"(omit to pick interactively)"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
"""info: print env, skills, and prompt details for a named agent."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from ..log import info
|
|
||||||
from ..manifest import ManifestIndex
|
|
||||||
from ._common import PROG, USER_CWD
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_info(argv: list[str]) -> int:
|
|
||||||
parser = argparse.ArgumentParser(prog=f"{PROG} info", add_help=True)
|
|
||||||
parser.add_argument("name", help="agent name defined in bot-bottle.json")
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
|
|
||||||
names = ManifestIndex.resolve(USER_CWD)
|
|
||||||
names.require_agent(args.name)
|
|
||||||
manifest = names.load_for_agent(args.name)
|
|
||||||
|
|
||||||
agent = manifest.agent
|
|
||||||
bottle = manifest.bottle
|
|
||||||
env_names = list(bottle.env.keys())
|
|
||||||
prompt_first_line = agent.prompt.splitlines()[0] if agent.prompt else ""
|
|
||||||
|
|
||||||
print()
|
|
||||||
info(f"agent : {args.name}")
|
|
||||||
info(f"env (names only): {', '.join(env_names) if env_names else '(none)'}")
|
|
||||||
info(f"skills : {' '.join(agent.skills) if agent.skills else '(none)'}")
|
|
||||||
info(
|
|
||||||
f"prompt : {len(agent.prompt)} chars; "
|
|
||||||
f"first line: {prompt_first_line or '(empty)'}"
|
|
||||||
)
|
|
||||||
info(f"bottle : {agent.bottle}")
|
|
||||||
identity = manifest.git_identity_summary()
|
|
||||||
if identity:
|
|
||||||
info(f" git identity : {identity}")
|
|
||||||
if bottle.git:
|
|
||||||
for e in bottle.git:
|
|
||||||
info(
|
|
||||||
f" git remote : {e.Name} -> {e.Upstream} "
|
|
||||||
f"(IdentityFile={e.IdentityFile})"
|
|
||||||
)
|
|
||||||
if e.KnownHostKey:
|
|
||||||
info(f" KnownHostKey: {e.KnownHostKey}")
|
|
||||||
else:
|
|
||||||
info(" git remotes : (none)")
|
|
||||||
print()
|
|
||||||
return 0
|
|
||||||
+7
-49
@@ -1,62 +1,20 @@
|
|||||||
"""list: list available agents or active bottles."""
|
"""list: list available agents."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ..backend import enumerate_active_agents
|
|
||||||
from ..manifest import ManifestIndex
|
from ..manifest import ManifestIndex
|
||||||
from ._common import PROG, USER_CWD
|
from ._common import PROG, USER_CWD
|
||||||
|
|
||||||
_ANSI_COLOR_CODES: dict[str, str] = {
|
|
||||||
"red": "\033[91m",
|
|
||||||
"green": "\033[92m",
|
|
||||||
"yellow": "\033[93m",
|
|
||||||
"blue": "\033[94m",
|
|
||||||
"magenta": "\033[95m",
|
|
||||||
}
|
|
||||||
_ANSI_RESET = "\033[0m"
|
|
||||||
|
|
||||||
|
|
||||||
def _ansi_label(text: str, color: str) -> str:
|
|
||||||
if not color:
|
|
||||||
return text
|
|
||||||
if not sys.stdout.isatty():
|
|
||||||
return text
|
|
||||||
term = os.environ.get("TERM", "")
|
|
||||||
if term in ("dumb", ""):
|
|
||||||
return text
|
|
||||||
code = _ANSI_COLOR_CODES.get(color)
|
|
||||||
if not code:
|
|
||||||
return text
|
|
||||||
return f"{code}{text}{_ANSI_RESET}"
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_list(argv: list[str]) -> int:
|
def cmd_list(argv: list[str]) -> int:
|
||||||
parser = argparse.ArgumentParser(prog=f"{PROG} list", add_help=True)
|
if argv and argv[0] in ("-h", "--help"):
|
||||||
parser.add_argument("scope", choices=["available", "active"])
|
sys.stderr.write(f"usage: {PROG} list\n")
|
||||||
args = parser.parse_args(argv)
|
sys.stderr.write("\nList all available agents from bot-bottle.json.\n")
|
||||||
|
|
||||||
if args.scope == "available":
|
|
||||||
manifest = ManifestIndex.resolve(USER_CWD)
|
|
||||||
for name in manifest.all_agent_names:
|
|
||||||
print(name)
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# `active` enumerates every backend (docker, firecracker,
|
manifest = ManifestIndex.resolve(USER_CWD)
|
||||||
# macos-container) so non-docker bottles aren't hidden behind
|
for name in manifest.all_agent_names:
|
||||||
# the env var.
|
print(name)
|
||||||
active = enumerate_active_agents()
|
|
||||||
if not active:
|
|
||||||
print("no active bot-bottle bottles", file=sys.stderr)
|
|
||||||
return 0
|
|
||||||
# One line per bottle: `<backend>\t<slug>\t<label>\t<services>`.
|
|
||||||
# Tab-separated keeps the format stable for shell pipelines.
|
|
||||||
for b in active:
|
|
||||||
services = ",".join(b.services) if b.services else "-"
|
|
||||||
display_name = f"{b.label} ({b.agent_name})" if b.label else b.agent_name
|
|
||||||
colored_name = _ansi_label(display_name, b.color)
|
|
||||||
print(f"{b.backend_name}\t{b.slug}\t{colored_name}\t{services}")
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ picking the right document for what you're capturing.
|
|||||||
|
|
||||||
| Artifact | For |
|
| Artifact | For |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| **Glossary** (`docs/glossary.md`) | Canonical term definitions — what words mean in this project. |
|
||||||
| **PRD** (`docs/prds/`) | A feature: what to build, scope, success criteria. |
|
| **PRD** (`docs/prds/`) | A feature: what to build, scope, success criteria. |
|
||||||
| **Research note** (`docs/research/`) | A landscape/tradeoff investigation. |
|
| **Research note** (`docs/research/`) | A landscape/tradeoff investigation. |
|
||||||
| **Decision record** (`docs/decisions/`) | A decision that isn't itself a feature — a policy, a convention, a "we will / won't do this," or a load-bearing choice made inside a larger PRD that deserves to be discoverable on its own. |
|
| **Decision record** (`docs/decisions/`) | A decision that isn't itself a feature — a policy, a convention, a "we will / won't do this," or a load-bearing choice made inside a larger PRD that deserves to be discoverable on its own. |
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Glossary
|
||||||
|
|
||||||
|
Canonical terminology for bot-bottle. Prefer these names in docs, comments, and UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Provider
|
||||||
|
|
||||||
|
The component that connects to an external model provider and sets up the model
|
||||||
|
harness inside the agent runtime. Configured in a bottle manifest under
|
||||||
|
`agent_provider:`; built-in templates are `claude` and `codex`. Responsible for
|
||||||
|
provider-specific auth, startup args, and egress routes.
|
||||||
|
|
||||||
|
## Agent Runtime
|
||||||
|
|
||||||
|
The OCI image (and the container or VM it runs in) that houses the model
|
||||||
|
harness. On the macOS-container backend this is an Apple Container; on
|
||||||
|
Firecracker it is a microVM; on the legacy Docker backend it is a Docker
|
||||||
|
container. The agent runtime is built from the agent provider's Dockerfile
|
||||||
|
(built-in or custom).
|
||||||
|
|
||||||
|
## Agent / Agent Definition
|
||||||
|
|
||||||
|
A Markdown file with YAML frontmatter that declares the system prompt and
|
||||||
|
identity of the model harness. Lives under `~/.bot-bottle/agents/` or a repo's
|
||||||
|
`.bot-bottle/agents/`. Specifies which bottle to run under (`bottle:`) and
|
||||||
|
which skills to load. Agent definitions are safe to commit; they contain no
|
||||||
|
secrets or egress policy.
|
||||||
|
|
||||||
|
## Bottle / Bottle Definition
|
||||||
|
|
||||||
|
A Markdown file with YAML frontmatter that declares the security and runtime
|
||||||
|
boundaries for one agent runtime: egress allowlist, git remotes, env vars,
|
||||||
|
nested-container flag, and agent provider config. Lives under
|
||||||
|
`~/.bot-bottle/bottles/`. Bottles are scoped to `$HOME` so a cloned repo
|
||||||
|
cannot override host egress policy.
|
||||||
|
|
||||||
|
## Sealed Bottle
|
||||||
|
|
||||||
|
The fully-resolved bottle after all `extends:` inheritance is applied and every
|
||||||
|
field has been validated. The sealed bottle is the immutable boundary spec that
|
||||||
|
the launcher enforces — no further overrides are possible once it is sealed.
|
||||||
|
|
||||||
|
## Bottled Agent
|
||||||
|
|
||||||
|
The combination of an Agent Definition and a Sealed Bottle, representing a
|
||||||
|
single deployable agent with a fixed identity and fixed boundaries. A bottled
|
||||||
|
agent has two observable states:
|
||||||
|
|
||||||
|
- **Active** — the agent runtime is running and the agent is executing.
|
||||||
|
- **Frozen** — the agent runtime has been snapshotted (Firecracker committed
|
||||||
|
image); the agent is not running but can be resumed from the snapshot.
|
||||||
|
|
||||||
|
## Active Bottle
|
||||||
|
|
||||||
|
Shorthand for an active (running) Bottled Agent. Used in the supervisor TUI
|
||||||
|
and discovery layer to mean "a bottle whose agent runtime is currently up."
|
||||||
Reference in New Issue
Block a user