Metadata-Version: 2.5
Name: pykeepassgate
Version: 0.0.1
Summary: The reference client for KeePassGate: pairing, and everything the gate serves.
Project-URL: Homepage, https://github.com/mfrnd/KeePassGate
Author: KeePassGate contributors
License: MIT
License-File: LICENSE
Keywords: keepass,keepassgate,mtls,password-manager
Requires-Python: >=3.11
Requires-Dist: cryptography>=42
Description-Content-Type: text/markdown

# PyKeePassGate

The reference client for [KeePassGate](https://github.com/mfrnd/KeePassGate), a KeePass 2.x plugin
that serves a small specification-defined API over mutual TLS so that automation and AI agents can
each be given a narrow, revocable, audited slice of a database.

**Reference implementation** means the code here is the second copy of the specification. Where the
plugin and this disagree, one of them is wrong and a test should already have said so. It also means
what is missing is a statement: a route with no method here is a route the gate does not serve.

## Install

```shell
uv add pykeepassgate
```

One dependency, `cryptography`, because the gate hands over a PKCS#12 and Python's `ssl` reads PEM.
Everything else, the command line included, is the standard library.

## Pair

The operator presses **Renew invite** on the **KPG** tab of the database's settings dialog and sends
the invitation over a second channel. What they see while doing it is
[`docs/pairing.md`](https://github.com/mfrnd/KeePassGate/blob/master/docs/pairing.md), and worked examples of everything below are
[`docs/pykeepassgate.md`](https://github.com/mfrnd/KeePassGate/blob/master/docs/pykeepassgate.md). It is spent by the first client to use it
and it does not survive KeePass closing.

```shell
pykeepassgate pair --port 45678 --name agent-invented --profile-name reader
```

Leave `--invitation` off and the token is read from standard input, so it stays out of the shell
history and out of the process list.

The same thing from a program:

```python
from pykeepassgate import api

credentials = api.pair(
    port=45678,
    invitation="...",
    name="agent-invented",
    profiles=["reader"],
)

print(credentials.profiles)  # what the operator approved, not what was asked for
```

## Use

```python
from pykeepassgate import GateClient

client = GateClient.open()
print(client.status())
```

```shell
pykeepassgate status
pykeepassgate whoami
```

## What to catch

Every exception says what to do next rather than what went wrong, because that is the part a caller
has to decide.

| Exception | What it means, and what to do |
| --- | --- |
| `DatabaseLocked` | The database is locked. Wait, and try again. |
| `GateBusy` | The gate cannot write right now, usually because a group dialog is open. Retry. |
| `NotAdmitted` | This database holds no record for this certificate. It was never admitted, or it was revoked. Pair again. |
| `ProofRefused` | During pairing: the token is wrong, or whatever answered is not the KeePass whose operator holds it. Stop. |
| `PairingRefused` | The operator said no. The invitation is spent, so ask for another. |
| `PairingUnanswered` | Nobody answered the dialog. The invitation is **not** spent, so this can be tried again. |
| `RouteRefused` | Everything else the gate refuses, carrying the status and the one word it used. A `404` is deliberately both "no such node" and "not yours", so a client cannot map a database by asking: if the node should be there, the rule is too narrow and the operator has to widen it. |
| `MalformedToken` | The text handed over is not an invitation, so nothing was sent. Check what was pasted. |
| `ProtocolError` | Something on the wire is not what the protocol allows: an answer this client cannot read, or a request it will not send. Do not retry an answer it cannot read: the two halves disagree about the wire, which `tests/pykeepassgate/test_protocol.py` exists to stop. |
| `StorageError` | What is stored for this client will not read, or could not be written. Look at the directory `status` names. |
| `TransportError` | Nothing is listening, or the handshake failed. |

## What it does not do

**It does not open a `.kdbx`.** There is no file and no master password on this side of the API, by
design. What that means for anybody expecting a `pykeepass` object is measured on the
`spikes/pykeepass-compatibility` branch: 23 of its 56 public members are servable over the routes the
gate already has, and the object itself is not one of them.

**It defends nothing against local code execution as the user.** The client's key is on the disk,
because a client has to still be the same client tomorrow. The whole design concedes this
throughout, and so does this.
