|
| 1 | +# saas-sdk-python — agent instructions |
| 2 | + |
| 3 | +This repo is the **Python SDK for the saas accounts API**, the twin of |
| 4 | +[`saas-sdk-go`](https://github.com/codefly-dev/saas-sdk-go): a versioned, |
| 5 | +published client that solutions depend on instead of regenerating their own |
| 6 | +bindings. `README.md` documents the SDK for the people consuming it; this file |
| 7 | +is how to work *in* the repo. |
| 8 | + |
| 9 | +## What this repo owns — and does not |
| 10 | + |
| 11 | +- **Owns:** the generated message bindings (`saas_sdk._gen`); the gateway-bound |
| 12 | + `datasource` facade; the consumer-side halves of Work Context — the mint |
| 13 | + client and the verifier (`saas_sdk.work_context`); the current-revision client |
| 14 | + (`saas_sdk.authorization_revision`). |
| 15 | +- **Does not own:** the protos, which live in `module-saas-starter` |
| 16 | + (`module/services/accounts/proto`); the Connect transport, which the solution |
| 17 | + runtime owns behind `Gateway.unary`; the Work Context *signer*, which stays |
| 18 | + authority-side in `sdk-go`; the wire format, which is pinned to `sdk-go`. |
| 19 | +- **Never hand-edit `src/saas_sdk/_gen/`.** It is regenerated from a proto ref |
| 20 | + recorded in `SOURCE.txt` — see the `regenerate-bindings` skill. |
| 21 | +- Everything ships under the single top-level package `saas_sdk`, so installing |
| 22 | + it never claims a generic name in a consumer's import namespace. A new module |
| 23 | + goes inside that package, never beside it. |
| 24 | +- The runtime takes **no** dependency on this SDK. If a change here would |
| 25 | + require one, the change is in the wrong repo. |
| 26 | + |
| 27 | +## Build and test |
| 28 | + |
| 29 | +Transcribed from `.github/workflows/ci.yml`, which runs the matrix Python |
| 30 | +**3.10** and **3.12** on every pull request: |
| 31 | + |
| 32 | +```bash |
| 33 | +python -m pip install --upgrade pip |
| 34 | +pip install -e ".[test,revision-connect,revision-grpc]" |
| 35 | +python -m pytest -q |
| 36 | +``` |
| 37 | + |
| 38 | +Both networking extras are installed deliberately. `revision-connect` (httpx) |
| 39 | +and `revision-grpc` (grpcio) are optional for consumers but not for the suite: |
| 40 | +the tests import `grpc` and `httpx` at module scope, so a missing extra fails |
| 41 | +collection loudly instead of quietly skipping the transport tests. There are no |
| 42 | +`skipif` markers in `tests/` — keep it that way. |
| 43 | + |
| 44 | +Two floors move together and are not independently adjustable: |
| 45 | +`requires-python = ">=3.10"` in `pyproject.toml`, and `protobuf>=5.29.3`, which |
| 46 | +is the gencode line the bindings are generated with (`buf.gen.yaml` pins the |
| 47 | +plugins to `v29.3`). Raising one without the other makes the published SDK |
| 48 | +uninstallable for consumers still on protobuf 5.x. |
| 49 | + |
| 50 | +## Parity with saas-sdk-go is a cross-repo invariant |
| 51 | + |
| 52 | +Two fixtures are byte-identical to their Go counterparts, and CI in both repos |
| 53 | +verifies the same bytes: |
| 54 | + |
| 55 | +- `tests/fixtures/work_context_wire_golden.json` ↔ sdk-go's |
| 56 | + `TestWorkContextWireGolden`. The wire format is **not** a JWT; field order, |
| 57 | + `uint64`-as-decimal-string encoding, scope canonicalization, base64url and |
| 58 | + Ed25519 signing are pinned byte-for-byte. |
| 59 | +- `tests/fixtures/authorization_revision_projection.json` ↔ the Go SDK's |
| 60 | + `authorizationrevision/testdata/projection.json`. |
| 61 | + |
| 62 | +Editing a fixture to make a test here pass is not a fix — it is a silent break |
| 63 | +of the other implementation, which will keep minting or verifying the old bytes. |
| 64 | +A wire change is one coordinated pair of pull requests. See the |
| 65 | +`wire-format-parity` skill. |
| 66 | + |
| 67 | +## When something does not work |
| 68 | + |
| 69 | +These are the fleet's rules, not this repo's, and they are stated here because |
| 70 | +an agent reads this file and not the tracker: |
| 71 | +https://github.com/obin-ai/handbook/issues/68. |
| 72 | + |
| 73 | +- **A gap in the tooling is a bug in the tooling** — never a reason to reach |
| 74 | + around it. Not as a "workaround", not "just this once", not "until the verb |
| 75 | + lands". A missing capability in `codefly`, `buf` or the generation scripts is |
| 76 | + an issue against the thing that owns it. |
| 77 | +- **Never hack. Always provide the best fix, even when it spans repos.** The |
| 78 | + right fix living in `module-saas-starter`, `sdk-go` or the solution runtime is |
| 79 | + not a reason to work around it here — open the pull request there. When it |
| 80 | + genuinely cannot be fixed now, the deliverable is a precise issue against the |
| 81 | + owner plus a stopgap that says in the code and in the pull request body that |
| 82 | + it is one. Never an unlabelled one. |
| 83 | +- **Classify every change that makes something work**, in the pull request body: |
| 84 | + a *fix* at the place that owns the behaviour, or a *hack*. A hack does not |
| 85 | + become a fix by working, by being small, by being local, or by the real fix |
| 86 | + belonging elsewhere. |
| 87 | +- **Never hardcode what the system resolves.** This SDK exists to be the thing |
| 88 | + that resolves. `datasource.new(gateway)` routes through the runtime's |
| 89 | + `Gateway.unary` rather than typing an endpoint; `JWKSVerifier` discovers |
| 90 | + signing keys through the published JWKS rather than pinning a key; the |
| 91 | + revision client loads its credential through a caller-supplied async callback |
| 92 | + rather than reading one out of another component's config. A literal endpoint, |
| 93 | + token, port or key in this package is a defect, and a test fixture that needs |
| 94 | + one builds it locally. |
| 95 | +- **Diagnose, do not pattern-match.** "It started working when I set X" is not a |
| 96 | + diagnosis — set X back and confirm it breaks. Do not trust an error message |
| 97 | + before checking that it says what it appears to say. Nearly everything in |
| 98 | + `work_context` raises the one `WorkContextError` type, so the type discriminates |
| 99 | + nothing and only the message does; and `JWKSVerifier` refreshes at most once per |
| 100 | + cache generation on an unknown `key_id`, so a token that fails and then passes on |
| 101 | + a retry has told you about key rotation, not about flakiness. |
| 102 | +- **A silent skip is a failed test.** Nothing here may pass by not running. |
| 103 | + Never guard a test on an optional extra being importable — install the extra |
| 104 | + (CI does) so the test runs or the suite fails. |
| 105 | +- **Say what you did not verify.** Unverified is not the same as working. The |
| 106 | + matrix is 3.10 and 3.12; if you ran one interpreter, or could not exercise a |
| 107 | + transport, the pull request body says so in as many words. |
| 108 | + |
| 109 | +## Procedures |
| 110 | + |
| 111 | +Step-by-step procedures live in `.claude/skills/`, read only when the task needs |
| 112 | +them rather than carried here: |
| 113 | + |
| 114 | +<!-- skills --> |
| 115 | +- `regenerate-bindings` — regenerating `src/saas_sdk/_gen` from the accounts |
| 116 | + proto, including the option-stripping step and `SOURCE.txt`. |
| 117 | +- `wire-format-parity` — changing anything covered by a fixture shared |
| 118 | + byte-for-byte with `saas-sdk-go`. |
| 119 | +<!-- /skills --> |
| 120 | + |
| 121 | +`tests/test_agent_context.py` holds that list to the directories that exist, so |
| 122 | +a renamed or deleted skill cannot leave a pointer here aimed at nothing. It also |
| 123 | +holds this file to its length budget. |
| 124 | + |
| 125 | +Keep this file under ~200 lines. Context an agent needs only in one subtree |
| 126 | +belongs in a nested `AGENTS.md` beside that subtree, and a procedure belongs in |
| 127 | +a skill — never appended here. |
0 commit comments