From 958a8845a6f88c3261a18d4d384c47aedab854a8 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 24 May 2026 22:19:44 -0400 Subject: [PATCH] docs: rewrite README manifest section + ship MD examples (PRD 0011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Manifest" section now describes the per-file MD layout under ~/.claude-bottle/{bottles,agents}/, the filename-as-key convention, the YAML subset constraints, and the trust boundary (bottles are home-only by filesystem layout). Includes a working bottle example with comments inside the frontmatter and a working agent example showing the Markdown body as the system prompt. Drops claude-bottle.example.json. The new examples/ tree — examples/bottles/dev.md, examples/agents/implementer.md, examples/agents/researcher.md — verifies the parser end-to-end via Manifest.from_md_dirs(examples/, None). --- README.md | 199 ++++++++++++++++++++------------- claude-bottle.example.json | 105 ----------------- examples/agents/implementer.md | 20 ++++ examples/agents/researcher.md | 15 +++ examples/bottles/dev.md | 38 +++++++ 5 files changed, 194 insertions(+), 183 deletions(-) delete mode 100644 claude-bottle.example.json create mode 100644 examples/agents/implementer.md create mode 100644 examples/agents/researcher.md create mode 100644 examples/bottles/dev.md diff --git a/README.md b/README.md index 32b2469..e3048bd 100644 --- a/README.md +++ b/README.md @@ -186,87 +186,130 @@ left running; remove it with `docker rm -f `. ## Manifest -Agents and the bottles they run in are declared in `claude-bottle.json` -in your project root or `$HOME` (both files merge if present, with -project entries overriding home entries on key conflict). +Bottles and agents live as Markdown files with YAML frontmatter under +`~/.claude-bottle/`. Each bottle is one file in `bottles/`, each agent +is one file in `agents/`: -```jsonc -{ - "bottles": { - "gitea-dev": { - "env": { - "GITEA_TOKEN": "?paste your Gitea API token", - "GITHUB_TOKEN": "${GH_PAT}", - "GIT_AUTHOR_NAME": "didericis" - }, - - "git": [ - { - "Name": "claude-bottle", - "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git", - "IdentityFile": "/Users/didericis/.ssh/id_ed25519_gitea", - "KnownHostKey": "ssh-ed25519 AAAA..." - } - ], - - // Routes declared here are held by a per-bottle cred-proxy - // sidecar, not the agent. Each route names a path the agent - // dials, the upstream the proxy forwards to, an auth_scheme, - // and a token_ref (host env var). The value goes into the - // sidecar's environ via `docker create -e`, never touches - // argv or disk. Optional `role` tags drive agent-side - // rewrites: `anthropic-base-url` (sets ANTHROPIC_BASE_URL), - // `npm-registry` (writes ~/.npmrc), `git-insteadof` (writes - // ~/.gitconfig), `tea-login` (writes ~/.config/tea/config.yml). - // See `docs/prds/0010-cred-proxy.md`. - "cred_proxy": { - "routes": [ - { "path": "/anthropic/", "upstream": "https://api.anthropic.com", - "auth_scheme": "Bearer", "token_ref": "CLAUDE_BOTTLE_OAUTH_TOKEN", - "role": "anthropic-base-url" }, - { "path": "/gh-api/", "upstream": "https://api.github.com", - "auth_scheme": "Bearer", "token_ref": "GITHUB_PAT" }, - { "path": "/gh-git/", "upstream": "https://github.com", - "auth_scheme": "Bearer", "token_ref": "GITHUB_PAT", - "role": "git-insteadof" }, - { "path": "/npm/", "upstream": "https://registry.npmjs.org", - "auth_scheme": "Bearer", "token_ref": "NPM_TOKEN", - "role": "npm-registry" } - ] - }, - - // Egress is forced through a per-agent - // [pipelock](https://github.com/luckyPipewrench/pipelock) sidecar - // on a Docker `--internal` network — without the proxy the agent - // has no route off-box. The effective allowlist is the union of - // baked-in defaults (api.anthropic.com, claude.ai, ...) and the - // hostnames listed here. Pipelock also runs DLP scanning and - // detects URL-embedded high-entropy secrets. The resolved - // allowlist is shown in the y/N preflight before launch. - "egress": { - "allowlist": [ - "github.com", - "registry.npmjs.org", - "pypi.org" - ] - } - } - }, - - "agents": { - "gitea-helper": { - "bottle": "gitea-dev", - "skills": ["init-prd"], - "prompt": "You help maintain Gitea-hosted projects." - } - } -} +``` +~/.claude-bottle/ +├── bottles/ +│ ├── dev.md +│ └── gitea-dev.md +└── agents/ + ├── implementer.md + └── researcher.md ``` -Comments are illustrative; the file itself must be valid JSON. See -`claude-bottle.example.json` for a working starting point. Pipelock's -design lives in `docs/prds/0001-per-agent-egress-proxy-via-pipelock.md` -and the rationale in `docs/research/pipelock-assessment.md`. +The filename (without `.md`) is the entity's name. Filenames must +match `[a-z][a-z0-9-]*`; files that don't are skipped with a warning. + +A repo can ship its own agent files alongside its code at +`/.claude-bottle/agents/.md`. Those agents reference +bottles defined in `~/.claude-bottle/bottles/` (the only place +bottles can come from); a `bottles/` subdir in a repo is ignored +with a warning. **This is the trust boundary**: bottle infrastructure +— credentials, egress allowlists, git remotes — comes from your home +directory only. A cloned repo cannot redirect a host env var to an +attacker-named upstream because it has no way to declare a bottle. + +### Example bottle (`~/.claude-bottle/bottles/gitea-dev.md`) + +````markdown +--- +env: + GIT_AUTHOR_NAME: didericis + +git: + - Name: claude-bottle + Upstream: ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git + IdentityFile: /Users/didericis/.ssh/id_ed25519_gitea + KnownHostKey: ssh-ed25519 AAAA... + +# Routes declared here are held by a per-bottle cred-proxy sidecar, +# not the agent. Each route names a path the agent dials, the +# upstream the proxy forwards to, an auth_scheme, and a token_ref +# (host env var). The value goes into the sidecar's environ via +# `docker create -e`, never touches argv or disk. Optional `role` +# tags drive agent-side rewrites: anthropic-base-url (sets +# ANTHROPIC_BASE_URL), npm-registry (writes ~/.npmrc), git-insteadof +# (writes ~/.gitconfig), tea-login (writes ~/.config/tea/config.yml). +# See docs/prds/0010-cred-proxy.md. +cred_proxy: + routes: + - path: /anthropic/ + upstream: https://api.anthropic.com + auth_scheme: Bearer + token_ref: CLAUDE_BOTTLE_OAUTH_TOKEN + role: anthropic-base-url + - path: /gh-api/ + upstream: https://api.github.com + auth_scheme: Bearer + token_ref: GH_PAT + - path: /gh-git/ + upstream: https://github.com + auth_scheme: Bearer + token_ref: GH_PAT + role: git-insteadof + - path: /npm/ + upstream: https://registry.npmjs.org + auth_scheme: Bearer + token_ref: NPM_TOKEN + role: npm-registry + +# Egress is forced through a per-agent pipelock sidecar on a Docker +# `--internal` network — without the proxy the agent has no route +# off-box. The effective allowlist is the union of baked-in defaults +# (api.anthropic.com, claude.ai, ...) and the hostnames listed here. +# Pipelock also runs DLP scanning and detects URL-embedded +# high-entropy secrets. The resolved allowlist is shown in the y/N +# preflight before launch. +egress: + allowlist: + - github.com + - registry.npmjs.org + - pypi.org +--- + +The `gitea-dev` bottle. Backs my work on personal projects: Anthropic +OAuth via cred-proxy, gitea.dideric.is over SSH (with PAT for tea +API), and npm for publishing scoped packages. +```` + +### Example agent (`~/.claude-bottle/agents/gitea-helper.md`) + +````markdown +--- +bottle: gitea-dev +skills: + - init-prd +--- + +You help maintain Gitea-hosted projects. +```` + +The agent's Markdown body is its system prompt (whitespace +stripped). The frontmatter declares the bottle to launch in and any +skills to mount. You can also include Claude Code subagent fields +(`name`, `description`, `model`, `color`, `memory`) in the +frontmatter — claude-bottle ignores them at launch but doesn't +reject them, so the same file can drop into `~/.claude/agents/` as a +Claude Code subagent. + +Unknown top-level frontmatter keys die at load with a "did you mean" +pointer; typos don't silently ghost into an empty config. + +The YAML subset the frontmatter accepts is bounded (flat keys, +strings / ints / true-or-false bools / null / lists / one-level +nested dicts). Anchors, multi-line block scalars, tags, and +ambiguous bare strings (`yes` / `NO` / `2026-05-24` / +`0x...`) all die with a clear pointer at the spec — quote your +strings when in doubt. The full schema lives in +`claude_bottle/yaml_subset.py` (~450 lines, stdlib-only, no PyYAML). + +Working examples live under `examples/`. Pipelock's design lives in +`docs/prds/0001-per-agent-egress-proxy-via-pipelock.md` and the +rationale in `docs/research/pipelock-assessment.md`. The trust +boundary rationale lives in `docs/prds/0011-per-file-md-manifest.md`. ## Auth: OAuth token, not API key diff --git a/claude-bottle.example.json b/claude-bottle.example.json deleted file mode 100644 index c6be907..0000000 --- a/claude-bottle.example.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "bottles": { - "default": { - "env": {}, - "egress": { - "allowlist": [ - "github.com", - "objects.githubusercontent.com", - "registry.npmjs.org" - ] - } - }, - - "gitea-dev": { - "env": { - "GITEA_TOKEN": "?paste your Gitea API token", - "GITHUB_TOKEN": "${GH_PAT}", - "GIT_AUTHOR_NAME": "Eric Diderich", - "NODE_ENV": "development" - }, - "git": [ - { - "Name": "claude-bottle", - "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git", - "IdentityFile": "/Users/didericis/.ssh/id_ed25519_gitea", - "KnownHostKey": "ssh-ed25519 AAAA...", - "ExtraHosts": { "gitea.dideric.is": "100.78.141.42" } - } - ], - "egress": { - "allowlist": [ - "github.com", - "objects.githubusercontent.com", - "registry.npmjs.org", - "pypi.org", - "files.pythonhosted.org" - ] - } - }, - - "agentic": { - "env": { - "GIT_AUTHOR_NAME": "Eric Diderich", - "NODE_ENV": "development" - }, - "cred_proxy": { - "routes": [ - { "path": "/anthropic/", - "upstream": "https://api.anthropic.com", - "auth_scheme": "Bearer", - "token_ref": "CLAUDE_BOTTLE_OAUTH_TOKEN", - "role": "anthropic-base-url" }, - - { "path": "/gh-api/", - "upstream": "https://api.github.com", - "auth_scheme": "Bearer", - "token_ref": "GH_PAT" }, - { "path": "/gh-git/", - "upstream": "https://github.com", - "auth_scheme": "Bearer", - "token_ref": "GH_PAT", - "role": "git-insteadof" }, - - { "path": "/gitea/dideric/", - "upstream": "https://gitea.dideric.is", - "auth_scheme": "token", - "token_ref": "GITEA_TOKEN", - "role": ["git-insteadof", "tea-login"] }, - - { "path": "/npm/", - "upstream": "https://registry.npmjs.org", - "auth_scheme": "Bearer", - "token_ref": "NPM_TOKEN", - "role": "npm-registry" } - ] - } - } - }, - - "agents": { - "researcher": { - "bottle": "default", - "skills": [], - "prompt": "You are a research assistant. Read widely, summarise concisely, and cite sources by URL. Do not write code unless explicitly asked." - }, - - "gitea-helper": { - "bottle": "gitea-dev", - "skills": ["init-prd"], - "prompt": "You help maintain Gitea-hosted projects. Prefer small, focused commits. Follow Conventional Commits. Run tests before pushing." - }, - - "agentic-helper": { - "bottle": "agentic", - "skills": [], - "prompt": "You operate against APIs whose credentials live in a per-bottle cred-proxy sidecar. Your environ carries only proxy URLs." - }, - - "minimal": { - "bottle": "default", - "skills": [], - "prompt": "" - } - } -} diff --git a/examples/agents/implementer.md b/examples/agents/implementer.md new file mode 100644 index 0000000..4880eef --- /dev/null +++ b/examples/agents/implementer.md @@ -0,0 +1,20 @@ +--- +name: implementer +description: Implements features against PRDs in this repo. +model: opus +bottle: dev +skills: + - init-prd +--- + +You are a feature-implementation agent running inside an ephemeral +claude-bottle sandbox. Treat the workspace's CLAUDE.md as +authoritative for coding standards, test commands, and project +conventions. Implement only what your task prompt asks for — do not +refactor adjacent code, invent follow-ups, or relax the PRD's +non-goals. Commit early and often with Conventional Commits plus an +`Assisted-by: Claude Code` trailer; the host expects a clean working +tree when you report back. Do not open, merge, or comment on the PR +— the host drives those steps. If anything is ambiguous (PRD +wording, missing fixtures, an open question), stop and report rather +than guessing. diff --git a/examples/agents/researcher.md b/examples/agents/researcher.md new file mode 100644 index 0000000..0d728eb --- /dev/null +++ b/examples/agents/researcher.md @@ -0,0 +1,15 @@ +--- +name: researcher +description: Investigates questions and writes well-cited research notes. +model: opus +bottle: dev +--- + +You are a research assistant. Read widely, summarise concisely, and +cite sources by URL. Do not write code unless explicitly asked. + +When given a research question, decompose it into sub-questions, +investigate systematically, evaluate sources critically (primary vs +secondary, recency, reliability), and synthesise findings with +appropriate confidence levels. Flag contradictions between sources +and note where additional evidence would change your answer. diff --git a/examples/bottles/dev.md b/examples/bottles/dev.md new file mode 100644 index 0000000..584b8c3 --- /dev/null +++ b/examples/bottles/dev.md @@ -0,0 +1,38 @@ +--- +env: + GIT_AUTHOR_NAME: Eric Diderich + NODE_ENV: development + +cred_proxy: + routes: + - path: /anthropic/ + upstream: https://api.anthropic.com + auth_scheme: Bearer + token_ref: CLAUDE_BOTTLE_OAUTH_TOKEN + role: anthropic-base-url + - path: /gh-api/ + upstream: https://api.github.com + auth_scheme: Bearer + token_ref: GH_PAT + - path: /gh-git/ + upstream: https://github.com + auth_scheme: Bearer + token_ref: GH_PAT + role: git-insteadof + - path: /gitea/dideric/ + upstream: https://gitea.dideric.is + auth_scheme: token + token_ref: GITEA_TOKEN + role: [git-insteadof, tea-login] + - path: /npm/ + upstream: https://registry.npmjs.org + auth_scheme: Bearer + token_ref: NPM_TOKEN + role: npm-registry +--- + +The `dev` bottle — backs a generic development workflow. + +Holds tokens for Anthropic, GitHub, a self-hosted Gitea, and npm. +Drop this file into `~/.claude-bottle/bottles/dev.md` and any agent +referencing `bottle: dev` will launch against this infrastructure.