Metadata-Version: 2.4
Name: hank-model-authority
Version: 1.2.0
Summary: Hank model authority: the model-authority/2.x artifact, its loader and capability resolver
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: PyYAML<7,>=6.0
Requires-Dist: pydantic<3,>=2.10

# hank-model-authority

The model authority every product repository reads: which models exist (registry
entries with their execution identity and per-family request facts), which model
each capability uses (selection policy), and what each model costs (rate tables).
The distribution depends on PyYAML and pydantic only, so Risk, Investment and Edgar
take it without the gateway.

Design and rollout: `docs/design/model-authority-registry-as-data-artifact-design-2026-09-29.md`
and its implementation plan.

## Layout

| Module | Owns |
|---|---|
| `authority/model-authority.yaml`, `authority/rates/*.json` | the packaged artifact: one `model-authority/2.x` document (registry `models`, `selection.capabilities`, one `revision`) and per-provider prices |
| `schema.py` | the version rule (`SCHEMA_MAJOR`/`SCHEMA_MINOR`), `ModelAuthorityError` and its message, protocol families and each family's `compat:` key set |
| `loader.py` | `HANK_MODEL_AUTHORITY_DIR` resolution, the strict load, the directory digest |
| `current.py` | the process's one import-time load: `INITIAL_MODEL_REGISTRY`, `INITIAL_MODEL_SELECTION_POLICY`, `RATE_TABLES`, `HEALTH_FACT`, `health_fact()` |
| `registry.py`, `selection.py` | registry entries and selection policy |
| `capabilities.py` | capability IDs and the process that executes each (a release, not data) |
| `binding.py`, `bind.py` | the single resolver and `CapabilityBind` |
| `rates.py`, `thinking.py` | rate tables; effort vocabulary |

The package root imports types and the loader, never `current`, so
`python -m model_authority check <dir>` can check a candidate directory while the
host directory is broken.

## Which directory a process loads

`HANK_MODEL_AUTHORITY_DIR` set: that directory, resolved once at start; missing or
unreadable fails the start. Unset: the packaged `authority/` directory. Every
loading unit sets it — `/etc/hank/model-authority` in production,
`~/.config/hank/model-authority` on this Mac — so the packaged copy is what tests
and unwired tools load. Every process reports what it admitted through
`health_fact()`: `{"revision", "schema", "sha256", "source", "loader", "admits"}`;
`source` is the resolved release path (`…/model-authority.d/<revision>-<sha12>/…`)
or `packaged:<path>`.

## Schema

This loader implements `model-authority/2.1` and admits 2.0 and 2.1 artifacts
(`health_fact()["admits"]` is `model-authority/2.0-2.1`). Each entry's `compat:`
key set is its protocol family's (`schema.py`, design §4d). 2.1 adds two
`anthropic.messages` keys, each derived when absent so 2.0 artifacts load
unchanged, and each refused when it contradicts the 2.0 key it refines:

| Key | Values | Absent | Must agree with |
|---|---|---|---|
| `thinking.off_value` | `disabled`, `between_tools`, null (cannot be turned off) | `disabled` if `can_disable`, else null | `can_disable` is true exactly when `off_value` is `disabled` |
| `tool_choice_forcing` | `any`, `thinking_off_only`, `never` | `thinking_off_only` if `forced_tool_choice_requires_thinking_off`, else `any` | that flag is false exactly when forcing is `any` |

The 2.0 keys stay required (a minor removes nothing), so an adapter that still
reads `can_disable` or `forced_tool_choice_requires_thinking_off` keeps working;
the gateway's Anthropic adapter reads both new keys. Sonnet 5.5 uses
`can_disable: false, off_value: between_tools`: the legacy bool still means
whether the literal `thinking.type: disabled` is legal.

## Changing it

- **A data change** (a model, a default, a price, a compat value) edits
  `authority/` and ships by `scripts/deploy_model_authority.sh`, not by a release
  ([runbook](../../docs/runbooks/model-authority-deploy.md)). Declare the lowest
  schema minor that covers the fields the artifact uses.
- **A schema change** is a release: a new optional field or literal is a minor
  (`SCHEMA_MINOR`), a removal or redefinition a major. Deploy loaders before data
  that uses a new minor: a loader refuses an artifact whose minor is newer than its
  own, naming both.
- **A capability or its executing process** is a release (`capabilities.py`).

Check a candidate directory with the ordinary load:

```bash
python -m model_authority check <dir>   # prints the health fact, or exits 1 with the refusal
```

Tests: `.venv/bin/python3 -m pytest packages/model-authority/tests -q`
(`make test-area AREA=model_authority`).
