104 lines
5.8 KiB
Markdown
104 lines
5.8 KiB
Markdown
|
|
# CLAUDE.md
|
||
|
|
|
||
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
|
|
||
|
|
## Commands
|
||
|
|
|
||
|
|
```bash
|
||
|
|
pip install -e ".[dev]" # dev install
|
||
|
|
scripts/check.sh # full local CI gate (ruff + mypy + pytest) — mirrors .forgejo/workflows/ci.yml
|
||
|
|
python -m pytest -q # tests
|
||
|
|
python -m pytest tests/test_prompt_urls.py -q # one file
|
||
|
|
python -m pytest tests/test_actions.py::test_do_start -q # one test
|
||
|
|
ruff check src/ tests/ build_archive.py
|
||
|
|
mypy # config in pyproject.toml (files = src/llamacppctl)
|
||
|
|
```
|
||
|
|
|
||
|
|
Always use `python -m pytest`, never the `pytest` console script: the test modules do
|
||
|
|
`from tests.test_docker_ops import ...`, which needs the repo root on `sys.path`.
|
||
|
|
|
||
|
|
`git config core.hooksPath .githooks` (once per clone) enables the pre-push hook that runs
|
||
|
|
`scripts/check.sh`. CI runs on Python 3.10 and 3.12; `requires-python = ">=3.10"`.
|
||
|
|
|
||
|
|
Other entry points:
|
||
|
|
- `python3 build_archive.py` — builds and self-verifies `llamacppctl-installable.tar.gz`
|
||
|
|
(has its own required-file manifest; adding a top-level file that must ship means updating it).
|
||
|
|
- `SMOKE_GPU=1 scripts/smoke.sh` — opt-in end-to-end test against a real Docker + GPU + GGUF model.
|
||
|
|
Not part of the pytest suite.
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
A single CLI (`llamacppctl`) that starts/checks/stops/switches llama.cpp servers running as Docker
|
||
|
|
containers, plus a security-hardened input layer for system/user prompts from text, file, or URL.
|
||
|
|
|
||
|
|
`main.py` is a thin pipeline: parse (`cli.build_parser`) → validate (`cli.validate_args`) →
|
||
|
|
`docker_available()` → resolve prompt sources (`prompt_io`) → resolve config
|
||
|
|
(`config.resolve_effective_config`) → dispatch to `actions.do_*`.
|
||
|
|
|
||
|
|
Layering is deliberate and must be preserved — it keeps the security-critical input validation
|
||
|
|
independently testable from orchestration:
|
||
|
|
- `prompt_io.py` knows nothing about argparse; it works only with `PromptSource` + `InputPolicy`.
|
||
|
|
- `config.py` knows nothing about Docker or HTTP.
|
||
|
|
- `docker_ops.py` / `http_ops.py` know nothing about CLI semantics.
|
||
|
|
- `actions.py` orchestrates; `schema.py` holds the shared dataclasses (`ServerConfig`,
|
||
|
|
`PromptConfig`, `ChatReply`, `CheckResult`).
|
||
|
|
|
||
|
|
Two cross-module couplings to know about:
|
||
|
|
- `main._resolve_prompts()` stashes loaded prompts on the argparse namespace as
|
||
|
|
`args._resolved_system_prompt` / `args._resolved_user_prompt`; `config.py` picks them up there.
|
||
|
|
- `main.run()` calls `docker_available()` *before* config resolution, so `--dry-run` still
|
||
|
|
needs a reachable Docker daemon. Only `--print-effective-config` skips that check — it is a
|
||
|
|
standalone action in the mutually-exclusive group, never a modifier, so it cannot start
|
||
|
|
anything.
|
||
|
|
|
||
|
|
### Config resolution
|
||
|
|
|
||
|
|
INI file (`llama.cpp.config`, template in `llama.cpp.config.example`), lowest to highest priority:
|
||
|
|
builtin defaults (`config.builtin_defaults()`) → `[default]` → `[model.<profile>]` (via `--profile`)
|
||
|
|
→ CLI overrides. `[prompt.<name>]` sections resolve separately and only supply a *fallback* system
|
||
|
|
prompt — an explicit `-s`/`--system-file`/`--system-url` always wins.
|
||
|
|
|
||
|
|
`container_name` is mandatory (empty → `ConfigError`). It is the only identity anchor: Docker
|
||
|
|
`--name`, the `--stop`/`--check` target, and the derived lock path
|
||
|
|
`/tmp/llamacppctl.<container_name>.lock`. Profiles meant to run in parallel need distinct
|
||
|
|
`container_name` *and* `host_port`, otherwise a second `--start` silently replaces the first
|
||
|
|
container.
|
||
|
|
|
||
|
|
`max_tokens` / `chat_temperature` / `connect_timeout` / `read_timeout` affect only the chat request
|
||
|
|
(`--chat` and the reply printed after `--start`), never the container.
|
||
|
|
|
||
|
|
### Exit codes (`actions.py`)
|
||
|
|
|
||
|
|
`0` ok · `1` general/config/prompt error · `2` argparse validation · `3` model path missing ·
|
||
|
|
`4` docker error · `5` HTTP not ready (also `--check` failing) · `6` lock busy.
|
||
|
|
`--check` is meant to be scriptable; keep those codes stable.
|
||
|
|
|
||
|
|
## Invariants worth not breaking
|
||
|
|
|
||
|
|
- All Docker calls go through `subprocess.run()` with **argument lists** — never `shell=True`,
|
||
|
|
never composed shell strings.
|
||
|
|
- The port publishes to `127.0.0.1` by default (`docker_ops._port_publish()`); the llama.cpp
|
||
|
|
OpenAI endpoint is unauthenticated unless `api_key` is set. `--expose` binds all interfaces.
|
||
|
|
- Every HTTP request in `prompt_io` — including each redirect hop — passes `validate_url_target()`:
|
||
|
|
HTTPS-only, no IP literals, no embedded credentials, no `localhost`, all resolved addresses
|
||
|
|
classified (private/loopback/link-local/multicast/reserved/unspecified → reject), metadata IP
|
||
|
|
`169.254.169.254` blocked. `_pin_dns()` then restricts `getaddrinfo` to the already-validated IPs
|
||
|
|
for the duration of the request, closing the TOCTOU/DNS-rebinding window. Size limits are enforced
|
||
|
|
both from `Content-Length` and while streaming.
|
||
|
|
- Escape hatches (`--allow-private-url`, `--allow-insecure-http`, `--allow-ip-host`,
|
||
|
|
`--allow-symlinks`, `--follow-redirects`, `--allow-html-input`) are opt-in and off by default.
|
||
|
|
`--allow-private-url` and `--url-allow-host` are mutually exclusive.
|
||
|
|
- `--start` and `--change` hold an exclusive non-blocking `fcntl.flock`; `--change` validates the
|
||
|
|
model path *before* removing the running container. `--dry-run` takes no lock and touches nothing.
|
||
|
|
|
||
|
|
Tests mock `subprocess` and `requests` (and DNS resolution for the SSRF cases); nothing in the
|
||
|
|
pytest suite requires Docker or the network.
|
||
|
|
|
||
|
|
## Conventions
|
||
|
|
|
||
|
|
- Docs, comments in `docs/`, and the example prompts are in German; source code, docstrings, and
|
||
|
|
commit subjects are in English. Commits follow Conventional Commits (`feat(chat):`, `docs(prompts):`).
|
||
|
|
- Line length 100, `from __future__ import annotations` in every module, `py.typed` is shipped —
|
||
|
|
keep annotations complete enough for `mypy` to pass.
|
||
|
|
- Behavior changes should be reflected in `man/llamacppctl.1`, `CHANGELOG.md`, and — for security-
|
||
|
|
relevant ones — `docs/SECURITY_AND_OPERATIONS.md`.
|