docs: rewrite README manifest section + ship MD examples (PRD 0011)
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).
This commit is contained in:
@@ -186,87 +186,130 @@ left running; remove it with `docker rm -f <container-name>`.
|
||||
|
||||
## 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
|
||||
`<repo>/.claude-bottle/agents/<name>.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
|
||||
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user