From 89db8756f49ab029d38b1e405890ef4682522edf Mon Sep 17 00:00:00 2001 From: AJ Frio Date: Fri, 14 Aug 2026 12:10:53 -0600 Subject: [PATCH] Add an agent setup playbook and BildClient.verify() handshake. Co-Authored-By: Claude Opus 4.8 Co-authored-by: Cursor --- AGENTS.md | 1 + AGENT_SETUP.md | 166 +++++++++++++++++++++++++++++++ ARCHITECTURE.md | 1 + README.md | 4 + bild/client.py | 14 +++ docs/INDEX.md | 1 + docs/SECURITY.md | 3 + docs/design-docs/auth.md | 5 + docs/product-specs/python-sdk.md | 3 + tests/test_auth.py | 19 ++++ tests/test_client_routes.py | 5 + tests/test_live_api.py | 7 ++ tools/linters/docs_structure.py | 2 + 13 files changed, 231 insertions(+) create mode 100644 AGENT_SETUP.md diff --git a/AGENTS.md b/AGENTS.md index 1d0e05b..f393938 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,7 @@ The package is not on PyPI yet. | Security | [docs/SECURITY.md](docs/SECURITY.md) | | Reliability | [docs/RELIABILITY.md](docs/RELIABILITY.md) | | Full catalog | [docs/INDEX.md](docs/INDEX.md) | +| End-user agent setup | [AGENT_SETUP.md](AGENT_SETUP.md) | ## Layout diff --git a/AGENT_SETUP.md b/AGENT_SETUP.md new file mode 100644 index 0000000..c171962 --- /dev/null +++ b/AGENT_SETUP.md @@ -0,0 +1,166 @@ +# Bild Python — agent setup playbook + +You are an agent setting up the **Bild Python SDK** for a human. This file is +the only document you need. Follow it in order. Do not declare success until +the handshake at the end returns a value. + +Bild is a PDM/PLM product. This library talks to `https://api.getbild.com` +using a JWT personal access token. The package is used from source (not on +PyPI yet). Repo: `https://github.com/AJFrio/Bild-Python.git`. + +## Your job + +1. Ask the human for the values below. Wait for answers before installing. +2. Install the library into a virtualenv. +3. Save the token to a local `.env` (never commit it, never echo it back). +4. Call **`BildClient.verify()`** — this is the required handshake. +5. Tell the human you called that function and show the exact return value. + +## What to ask the human + +Ask these in the human's language. Do not invent answers. + +### 1. Bild API token (required) + +Prompt: + +> I need a Bild personal access token (JWT). An admin can create one in the +> Bild web app. Tokens shown there include issued-at, issued-by, and expiry. +> Paste the token here. I will save it to a local `.env` file and will not +> commit it or print it again. + +If they do not have a token, stop. Tell them to have an admin issue one in +the Bild app, then come back. Do not guess a token. Do not ask for their +Bild password — this SDK only accepts a JWT. + +### 2. Install location (required if you are not already in this repo) + +Prompt: + +> Do you already have [Bild-Python](https://github.com/AJFrio/Bild-Python) +> cloned on this machine? If yes, give me the folder path. If no, tell me +> which folder I should clone it into. + +Default clone URL: `https://github.com/AJFrio/Bild-Python.git` + +### 3. API host (optional) + +Default: `https://api.getbild.com` + +Only ask if they mention a custom or non-production host. If they give one, +you will pass `base_url=...` into `BildClient`. Otherwise omit it. + +## Install + +Use Python 3.10 or newer. + +```bash +git clone https://github.com/AJFrio/Bild-Python.git +cd Bild-Python +python3 -m venv .venv +``` + +Activate: + +- macOS / Linux: `source .venv/bin/activate` +- Windows: `.venv\Scripts\activate` + +If `pip` is blocked (uv-managed interpreter): + +```bash +uv pip install -e . --python .venv/Scripts/python.exe +``` + +Otherwise: + +```bash +pip install -e . +``` + +If the repo is already cloned, skip `git clone` and install in that folder. + +## Save the token + +Copy `.env.example` to `.env` in the repo root (`.env` is gitignored): + +```bash +BILD_API_KEY= +``` + +Rules: + +- Do not write the token into README, chat logs you control, or any tracked file. +- Do not `git add .env`. +- Prefer `.env` over putting the token in the shell, so later `BildClient()` + calls work without the human pasting it again. + +If they gave a custom host, you do not need to store it unless they ask; +pass it only when constructing the client. + +## Handshake (required) + +From the repo root, with the venv active, run this exact code: + +```python +from bild import BildClient + +client = BildClient() # reads BILD_API_KEY from .env +result = client.verify() +print(result) +``` + +If they gave a custom host: + +```python +client = BildClient(base_url="https://their-host.example") +result = client.verify() +``` + +`verify()` is read-only. It calls `users.list` and `projects.list` and +returns a dict shaped like: + +```python +{ + "ok": True, + "function": "BildClient.verify", + "base_url": "https://api.getbild.com", + "users": , + "projects": , +} +``` + +### What to tell the human after a success + +Use this shape. Include the real `result` value. Do not omit it. + +> Setup is complete. I called `BildClient.verify()` and it returned: +> +> ``` +> +> ``` +> +> The token works. I can use `client.api.` for further Bild work. + +### If it fails + +| Error | Meaning | What you do | +| --- | --- | --- | +| `ValueError: Missing token` | `.env` not loaded or empty | Fix `.env`, retry `verify()` | +| `BildAuthError` (401/403) | Token invalid, expired, or wrong host | Ask for a new token; do not retry blindly | +| Other `BildAPIError` | Host or API problem | Show `status_code` and `payload`; ask the human | + +Do not declare setup complete without a successful `verify()` return value. + +## After setup + +You may keep using the same `BildClient()` for the human's next request. + +Documented groups: `users`, `projects`, `project_users`, `branches`, +`commits`, `files`, `uploads`, `checkouts`, `shared_links`, `metadata`, +`feedback`, `packages`, `revisions`, `approvals`, `boms`, `search`, +`webhooks`. + +Do **not** invite users, upload, delete, checkout, or create webhooks unless +the human explicitly asked for that write. Prefer list/get/search. + +Escape hatch for an unwrapped path: `client.get("projects")`. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5c2f10c..22323a7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -41,6 +41,7 @@ BildClient token, base_url, timeout, session request / get / post / put / delete resolve_branch_id / resolve_file_version + verify() — read-only handshake (users + projects) api: _Resources users, projects, project_users, branches, commits, files, uploads, checkouts, shared_links, metadata, feedback, diff --git a/README.md b/README.md index a03c763..328f03f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Python library for the [Bild External API](https://bildexternalapi.portledocs.co > This repo is currently intended to be used from source (not published to PyPI yet). +To have an agent install this for you, give it [AGENT_SETUP.md](AGENT_SETUP.md). +It will ask for a Bild JWT, install the library, call `BildClient.verify()`, +and show you the return value. + ## 1) Clone and set up ```bash diff --git a/bild/client.py b/bild/client.py index b97efc7..58e6588 100644 --- a/bild/client.py +++ b/bild/client.py @@ -110,6 +110,20 @@ def __init__( webhooks=WebhooksAPI(self), ) + def verify(self) -> dict[str, Any]: + """Read-only setup handshake used by AGENT_SETUP.md. + + Lists users and projects so an installing agent can prove the token + works and show the human the exact return value. + """ + return { + "ok": True, + "function": "BildClient.verify", + "base_url": self.base_url, + "users": self.api.users.list(), + "projects": self.api.projects.list(), + } + def request(self, method: str, path: str, *, params=None, json=None) -> Any: url = f"{self.base_url}/{path.lstrip('/')}" kwargs: dict[str, Any] = { diff --git a/docs/INDEX.md b/docs/INDEX.md index b5ca446..6e35fa2 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -5,6 +5,7 @@ | Doc | Status | What it covers | | --- | --- | --- | | [../ARCHITECTURE.md](../ARCHITECTURE.md) | current | Layers, HTTP contract, test map | +| [../AGENT_SETUP.md](../AGENT_SETUP.md) | current | Playbook to hand another agent for end-user install + `verify()` | | [PRODUCT.md](PRODUCT.md) | current | Who the SDK is for and what "done" means | | [DESIGN.md](DESIGN.md) | current | Design principles and taste | | [CONVENTIONS.md](CONVENTIONS.md) | current | Naming, files, errors, JSON | diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 77a482b..b7e549b 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -19,6 +19,9 @@ Bild personal access tokens are JWTs. Treat them as secrets. ## Live tests +`BildClient.verify()` is read-only (`users.list` and `projects.list`). It is +the handshake in [AGENT_SETUP.md](../AGENT_SETUP.md). + `tests/test_live_api.py` and `TestLiveAuth` run only when a token is present. They must stay read-only (list/get/search). Do not add invite, upload, delete, checkout, or webhook-create calls to the live suite. diff --git a/docs/design-docs/auth.md b/docs/design-docs/auth.md index 6c0a110..f155963 100644 --- a/docs/design-docs/auth.md +++ b/docs/design-docs/auth.md @@ -9,6 +9,11 @@ A missing token raises `ValueError` at construct time. +`BildClient.verify()` is the consumer handshake. It is read-only +(`users.list` + `projects.list`). Installing agents follow +[AGENT_SETUP.md](../../AGENT_SETUP.md) and must show the human the exact +return value. Do not add writes to `verify()`. + ## Errors HTTP 401 and 403 raise `BildAuthError` (subclass of `BildAPIError`) with diff --git a/docs/product-specs/python-sdk.md b/docs/product-specs/python-sdk.md index 7cba149..e2a4caf 100644 --- a/docs/product-specs/python-sdk.md +++ b/docs/product-specs/python-sdk.md @@ -32,6 +32,9 @@ Must stay aligned with `BildClient.api` and the README "API groups" list: - Load `.env` without overriding existing env. - Auto-resolve default branch and latest file version where documented. - Omit `None` optional JSON fields. +- `BildClient.verify()` — read-only handshake (`users.list` + `projects.list`) + used by [AGENT_SETUP.md](../../AGENT_SETUP.md). Return shape is + `{ok, function, base_url, users, projects}`. ## Convenience (not allowed without a new spec) diff --git a/tests/test_auth.py b/tests/test_auth.py index 0920e5c..ddd120c 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -113,6 +113,25 @@ def test_403_raises_auth_error(self): with self.assertRaises(BildAuthError): client.api.projects.list() + def test_verify_lists_users_and_projects(self): + session = RecordingSession() + client = BildClient(token="jwt-token", session=session) + result = client.verify() + self.assertEqual( + result, + { + "ok": True, + "function": "BildClient.verify", + "base_url": DEFAULT_BASE_URL, + "users": {"ok": True}, + "projects": {"ok": True}, + }, + ) + paths = [call["path"] for call in session.calls] + self.assertEqual(paths, ["/users", "/projects"]) + self.assertTrue(all(call["method"] == "GET" for call in session.calls)) + self.assertTrue(all(call["json"] is None for call in session.calls)) + @unittest.skipUnless(os.getenv("BILD_API_KEY"), "BILD_API_KEY not set") class TestLiveAuth(unittest.TestCase): diff --git a/tests/test_client_routes.py b/tests/test_client_routes.py index 04919ac..ea6df24 100644 --- a/tests/test_client_routes.py +++ b/tests/test_client_routes.py @@ -61,6 +61,11 @@ def last(self): def test_full_route_coverage(self): c = self.client + verified = c.verify() + self.assertEqual(verified["function"], "BildClient.verify") + self.assertTrue(verified["ok"]) + self.assertTrue(self.last()["path"].endswith("/projects")) + c.api.users.list() self.assertTrue(self.last()["path"].endswith("/users")) c.api.users.invite(["a@example.com"], projects=[{"id": "p1"}]) diff --git a/tests/test_live_api.py b/tests/test_live_api.py index e08d91d..2281f2d 100644 --- a/tests/test_live_api.py +++ b/tests/test_live_api.py @@ -111,6 +111,13 @@ def setUpClass(cls): data.get("fileVersionID") or data.get("fileVersion") or data.get("versionId") ) + def test_verify_handshake(self): + result = self.client.verify() + self.assertTrue(result["ok"]) + self.assertEqual(result["function"], "BildClient.verify") + self.assertTrue(result.get("users")) + self.assertTrue(result.get("projects")) + def test_list_users(self): users = _as_list(self.client.api.users.list()) self.assertGreater(len(users), 0) diff --git a/tools/linters/docs_structure.py b/tools/linters/docs_structure.py index 13c431f..6826dd9 100644 --- a/tools/linters/docs_structure.py +++ b/tools/linters/docs_structure.py @@ -6,6 +6,7 @@ REQUIRED_DOCS = ( "AGENTS.md", + "AGENT_SETUP.md", "ARCHITECTURE.md", "docs/INDEX.md", "docs/PRODUCT.md", @@ -33,6 +34,7 @@ "ARCHITECTURE.md", "docs/CONVENTIONS.md", "docs/INDEX.md", + "AGENT_SETUP.md", "tools/check.py", "Content-Type", )