Metadata-Version: 2.4
Name: llm-1password
Version: 0.1.0
Summary: Resolve llm model API keys from 1Password op:// references
Author-email: Dallas Crilley <dallasdotjs@gmail.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/dallascrilley/llm-1password
Project-URL: Repository, https://github.com/dallascrilley/llm-1password
Project-URL: Issues, https://github.com/dallascrilley/llm-1password/issues
Keywords: llm,plugin,1password
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: llm>=0.32
Requires-Dist: httpx
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: httpx; extra == "test"
Dynamic: license-file

# llm-1password

[![test](https://github.com/dallascrilley/llm-1password/actions/workflows/test.yml/badge.svg)](https://github.com/dallascrilley/llm-1password/actions/workflows/test.yml)

An [llm](https://llm.datasette.io/en/stable/) plugin that keeps model API keys in 1Password.

I wrote this so `llm` can resolve keys from `op://` references at call time. The durable store holds the reference only.

```text
$ llm 1password doctor
op: /usr/local/bin/op
auth: SERVICE_ACCOUNT

$ llm 1password check
openai OK
```

`refs.json` is mode `0600` in a `0700` directory. The wrap that owns resolution is `_wrapped_get_key` in `llm_1password.py`.

## Install

Requires Python 3.10+, [llm](https://llm.datasette.io/en/stable/) 0.32 or newer, and the [1Password CLI](https://www.1password.dev/cli/).

```bash
llm install llm-1password
```

From a checkout:

```bash
git clone https://github.com/dallascrilley/llm-1password.git
cd llm-1password
llm install -e .
```

If `llm` itself fails with `No module named 'httpx'` before any plugin loads, that is an llm 0.32 packaging gap:

```bash
pipx inject llm httpx
```

Confirm the plugin loaded:

```bash
llm plugins
llm 1password doctor
llm 1password check
```

`doctor` prints the chosen `op` binary and its `user_type`. `check` prints `No references registered.` until you `set` one.

## Use

```bash
llm 1password set openai 'op://Vault/Item/credential'
llm 1password check
llm 1password doctor
llm prompt 'hello'
llm 1password unset openai
```

`set` writes the reference under `llm`'s user directory at `1password/refs.json`. It does not write `keys.json`. The `llm prompt` line needs a reference `op read` can resolve. Run `check` first and look for `OK`.

If you previously stored a plaintext key for the same alias, remove that alias (`llm keys path`) so this plugin owns the name.

`check` exits 1 if any registered reference fails to resolve.

## How resolution works

1. If `$HOME/.local/bin/op` exists, is executable, and is a non-world-writable regular file, the plugin uses it. That lets a local wrapper sit in front of the official CLI.
2. Otherwise it uses `op` on `PATH`, with the same file checks.
3. It runs `op read -- <ref>`. A missing binary, a non-zero exit, a timeout, empty output, or a non-UTF-8 payload aborts the model call.

`llm 1password doctor` prints the chosen binary and the `op whoami` `user_type` field (`SERVICE_ACCOUNT`, or whatever `op` reports).

An explicit `--key` / `input=` value is not replaced by a registered alias unless that value itself is the alias. Unregistered `op://` strings in `keys.json` or the environment are rejected rather than fetched or sent to the provider (`_reject_bare_op_uri`).

## Security model

| Surface | Holds |
|---|---|
| `refs.json` | `op://` references only, mode `0600` in a `0700` directory |
| Process memory | Secret value for the life of that `llm` process |
| `check` / `doctor` / errors | Alias and OK/FAIL or binary/auth mode. Never the secret, never the reference path |

If `op read` fails, the plugin raises an error and does not call the model.

## What this package does not do

- Resolve `op://` inside prompts, templates, or fragments.
- Hide the `op://` URI from `ps` while `op read` is running.
- Wrap assignments such as `model.key = "op://..."`. Those bypass `llm.get_key`.
- Uninstall leftover plaintext keys from `keys.json`.

See [SECURITY.md](SECURITY.md) for reporting and the guarantees the package aims for.

## License

Apache-2.0
