# The Adduce Contract for Agents

You are installing and driving Adduce on your user's machine. This document is
the whole procedure and the whole interface. Read it before running anything.

Part one gets a working binary onto the machine and proves it works. Part two is
what every command returns once it is there.

## The one rule

**Adduce is distributed as signed binaries.** There is no source distribution,
so there is nothing to build and no build step to fall back on. Download the
artefact for the platform you are on.

If your platform is not in the table below, stop and tell the user. Do not
substitute a different triple.

## Supported platforms

| OS | Triple | Artefact |
|---|---|---|
| Linux x86_64 (glibc) | `x86_64-unknown-linux-gnu` | `adduce-x86_64-unknown-linux-gnu.tar.gz` |
| Windows x86_64 | `x86_64-pc-windows-msvc` | `adduce-x86_64-pc-windows-msvc.tar.gz` |
| macOS Apple Silicon | `aarch64-apple-darwin` | `adduce-aarch64-apple-darwin.tar.gz` |

The macOS build is Apple Silicon only, from 0.3.4. It records a minimum system
version of 11.0, so every Apple Silicon Mac runs it. An **Intel** Mac is not
covered, and neither is ARM Linux. Say so plainly rather than improvising.

Linux builds link glibc, so they need a glibc distribution. Alpine and the
other musl distributions fall outside the table.

**Three requirements the Linux build imposes:**

| Requirement | Check | Fix |
|---|---|---|
| glibc 2.31 or newer | `ldd --version` | none; an older system cannot run the build |
| `libopenblas.so.0` | `ldd $(command -v adduce)` | `apt install libopenblas0`, `dnf install openblas-serial` |
| `libdbus-1.so.3` | same | `apt install libdbus-1-3`, `dnf install dbus-libs` |

glibc 2.31 is Debian 11 and Ubuntu 20.04, and RHEL 9 ships 2.34, so every
supported distribution clears the floor. `install.sh` reads the loader's own error
and reports which of the three failed. When it is the glibc one, say so plainly
and stop: no package installs a newer glibc.

**0.3.6 is the exception, and it is the reason the floor is stated here.** That
release was built on the maintainer's own machine and needs glibc 2.43, so
Debian 13 (2.41), Ubuntu 24.04 (2.39) and Fedora 43 (2.42) all refuse it. Later
releases are built in a container against glibc 2.31 and the release refuses to
publish an artefact above that floor.

## 0. One command

On Linux, macOS and Windows there is one command, and it does sections 1
through 3 and the `PATH` step for you:

```bash
curl --proto '=https' --tlsv1.2 -sSf https://adduce.dev/install.sh | sh
```

```powershell
powershell -c "irm https://adduce.dev/install.ps1 | iex"
```

It reads the same manifest section 1 describes, verifies the tarball against the
`sha256` the manifest states, and overlays the tree onto `~/.adduce/bin`,
leaving files it does not ship alone. That last part matters on a machine that
has already run `adduce pdf-ml setup`: the provisioned environment sits inside
the install tree and a reinstall must not delete it.

Flags: `--prefix <dir>` for a different location, `--no-modify-path` to write no
shell profile hook, `--version <v>` to fail unless the published release is that
version. `ADDUCE_INSTALL_DIR` is the environment form of `--prefix`.

It does not verify the Ed25519 signature, because the key would have to travel
in a script fetched from the host serving the artefacts, which defends against
nothing that host compromise does not already defeat. The signature chain lives
in `adduce update apply`, where the key sits inside a binary the user already
trusts. Sections 1 to 3 are the route that verifies a signature before anything
is unpacked, and they stay the documented procedure for a machine where that
matters.

Fall through to section 1 when the installer refuses the platform, when `curl`
or `tar` is absent, or when the user asks for the artefact rather than an
install.

## 1. Fetch the manifest

Every release is described by one JSON document:

```bash
curl -fsSL https://api.adduce.dev/releases/latest.json
```

```json
{
  "version": "0.2.0",
  "git_sha": "2d94ffb",
  "targets": {
    "x86_64-pc-windows-msvc": {
      "tarball": "adduce-x86_64-pc-windows-msvc.tar.gz",
      "sig": "adduce-x86_64-pc-windows-msvc.tar.gz.sig",
      "sha256": "e93500a2…",
      "size": 27931795
    }
  }
}
```

Select the entry matching the user's platform. If the key is absent, that
platform has no build in this release; see above.

## 2. Download and verify

Artefacts live beside the manifest, so a URL is the manifest base plus the
`tarball` field.

```bash
BASE=https://api.adduce.dev/releases
curl -fsSLO "$BASE/adduce-x86_64-unknown-linux-gnu.tar.gz"
```

**Verify the SHA-256 against the manifest before unpacking.** This is not
optional ceremony: you are about to put an executable on your user's PATH.

```bash
sha256sum -c <<< "<sha256 from manifest>  adduce-x86_64-unknown-linux-gnu.tar.gz"
```

On Windows PowerShell:

```powershell
(Get-FileHash adduce-x86_64-pc-windows-msvc.tar.gz -Algorithm SHA256).Hash -eq '<SHA256 FROM MANIFEST>'
```

A mismatch means a corrupt or tampered download. Delete it and stop. Do not
unpack it, and do not retry more than once.

Each tarball also has a detached Ed25519 signature (`.sig`, 64 raw bytes) over
the tarball. You cannot check it without the public key, which is embedded in
the binaries rather than published as a file, so the SHA-256 is your check at
install time. `adduce update` verifies the signature on every later
self-update.

## 3. Unpack

The Linux tarball:

```
adduce
adduce-daemon
libonnxruntime.so.1
AGENTS.md
adduce-ocr-worker/
adduce-ml-worker/
```

The Windows tarball adds one file:

```
adduce.exe
adduce-daemon.exe
DirectML.dll
AGENTS.md
adduce-ocr-worker/
adduce-ml-worker/
```

**Keep the whole tree together.** The two worker directories are Python
sidecars that `adduce` and `adduce-daemon` spawn as subprocesses, and both
resolve them as siblings of the running executable. Unpack only the binaries
and `adduce pdf-ml setup` has nothing to install into, so PDF ingestion fails
while everything else works.

The workers ship without their environments. `uv` builds those on the user's
machine on first use, which is why the tarball is megabytes rather than
gigabytes.

**On Windows, all three files must end up in the same directory.**
`DirectML.dll` is named in the import tables of both executables, so the loader
resolves it at process start. Split them up and neither program starts, with an
error that looks like a corrupt binary rather than a missing file.

**Linux keeps `libonnxruntime.so.1` in that directory too.** Both executables
are linked with an `$ORIGIN` rpath, so the loader looks for it beside the
running binary and nowhere else. This is Microsoft's own ONNX
Runtime, shipped rather than required as a package because the alternative
raises the glibc floor by seven minor versions.

Put the directory on `PATH`. Reasonable choices:

- Linux and macOS: `~/.local/bin` (already on `PATH` for most users)
- Windows: a folder such as `%LOCALAPPDATA%\Programs\adduce`, added to the user
  `PATH`

Tar entries carry mode `0755`, so the Unix binaries are already executable.

## 4. Windows prerequisite: the Visual C++ Redistributable

The Windows build links the Microsoft C runtime dynamically. It imports
`VCRUNTIME140.dll`, `VCRUNTIME140_1.dll`, `MSVCP140.dll`, and `MSVCP140_1.dll`,
which come from the **Microsoft Visual C++ Redistributable for Visual Studio
2015-2022 (x64)**. That is not part of a clean Windows install, though most
machines have it because many applications ship it.

If `adduce.exe` fails to start naming any of those DLLs, install it:

```
https://aka.ms/vs/17/release/vc_redist.x64.exe
```

The `api-ms-win-crt-*.dll` imports are the Universal CRT, which *is* part of
Windows 10 and later. Those need nothing.

## 4c. macOS: the quarantine attribute

A file downloaded with `curl` has no `com.apple.quarantine` attribute and runs
immediately, which is the path these instructions take. A file the user
downloaded in a browser has one, and macOS refuses it by name on first launch.

If `adduce` is refused that way, the user downloaded it themselves. Clear the
attribute rather than telling them to disable a security setting:

```bash
xattr -d com.apple.quarantine ./adduce ./adduce-daemon
```

The binaries are not yet signed with a Developer ID, so this is expected rather
than a sign of a corrupt download.

## 4b. Installing uv for PDF ingestion

The workers run under [uv](https://docs.astral.sh/uv/). Adduce resolves it from
`ADDUCE_UV_BIN`, then `PATH`, then `%USERPROFILE%\.local\bin\uv.exe` on Windows
or `~/.local/bin/uv` on Unix. It does **not** install uv, so a machine without
it gets an error from `adduce pdf-ml setup` and nothing else.

```powershell
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"     # Windows
```

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh          # Linux and macOS
```

Everything except `adduce pdf-ml` works without it, so treat this as a
prerequisite for ingestion rather than for the install.

`adduce pdf-ml setup` then downloads a Python ML stack, which takes several
minutes and a few GB on first run. That is expected, not a hang.

## 4d. What the tarball leaves out, and how to get it

The tarball is two binaries, the two worker source trees, and this document. It
carries no ML runtime, no model weights and no GPU library. Roughly 30 MB, not
several GB, and every heavy thing is fetched on the user's machine when they
ask for it.

Transcription needs one command:

```
adduce pdf-ml setup --backend mineru
```

That resolves a Python environment through uv from
`adduce-ocr-worker/pyproject.toml` and downloads the MinerU weights. Several
minutes and a few GB the first time. `adduce pdf-ml status` reports whether a
backend is provisioned, and `adduce status` names this among the subsystems it
checks. Weights land under the Adduce root at `pdf-ml/hf-cache`, so a second
workspace on the same machine reuses them.

Provisioning records a marker. When the worker's dependencies change, a later
release reports `marker mismatch for backend mineru` and names the same setup
command as the repair. Re-run it; nothing is corrupt.

### Acceleration

MinerU uses the GPU wherever the machine has one. Its device selection reads
`MINERU_DEVICE_MODE` first, then CUDA, then Metal, and settles on the CPU only
when none is present.

| Platform | What MinerU uses |
|---|---|
| Apple Silicon, macOS 14 or later | Metal, through PyTorch MPS |
| Linux with an NVIDIA driver | the GPU, through the CUDA build of PyTorch |
| Anything else | the CPU |

Apple Silicon needs `PYTORCH_ENABLE_MPS_FALLBACK`, which the worker sets for
you. A few operators in the layout and formula heads have no Metal
implementation, and PyTorch raises on the first one it meets unless that
variable is set. The fallback moves those operators alone to the CPU and leaves
everything else on the GPU. Report a Mac that transcribes on the CPU as a fault
rather than as the expected behaviour.

The worker prints the device it settled on when it warms:

```
[adduce-ocr] mineru device: mps
```

and records it on each transcription, so a stored document says what produced
it. Set `ADDUCE_OCR_DEVICE` to force one: `cpu` rules the accelerator out of a
comparison, and `mps` or `cuda` makes a machine fail loudly rather than fall
back silently.

The two Rust ML paths, the embedder and the cross-encoder, are separate and
run on the CPU in every shipped binary. They are small next to transcription,
and a user asking why a PDF is slow is asking about MinerU.

## 5. Sign in, or activate a licence

There are two ways to authenticate, and most users need the first.

### 5a. Sign in (`adduce login`)

```bash
adduce login
```

This opens the approval page in a browser with the code already filled in, and
waits. The person approves it, and the machine stays signed in until
`adduce logout`. The token goes into the OS keychain, or into
`<adduce root>/credentials.json` with owner-only permissions when no keychain is
reachable, which is what happens on a headless box or inside a container.

The code is good for **30 minutes**. If it expires unapproved, the command exits
**51**, which is a sign-in failure and says nothing about the licence. Do not
respond to a 51 by touching the licence.

**If you are an agent, do not sit and wait on a sign-in you cannot complete.**
You have no browser and no way to approve anything. Use:

```bash
adduce login --emit-code --format json
```

That prints the URL, the code, and a `user_instruction` sentence, then exits 0
without polling. Relay `user_instruction` to the user word for word, and ask
them to run `adduce login` themselves in their own terminal, or to open the URL
you were given. Poll `adduce license status --format json` afterwards to see
when they are done.

To check the state of a machine without starting anything, use:

```bash
adduce whoami --format json
```

It reports the email, the plan, and which credential answered (`device-login`
or `environment`, the latter meaning `ADDUCE_API_KEY`). It exits **51** when the
machine holds no usable credential, so `if adduce whoami >/dev/null` is a sound
test for "is this machine signed in".

Two more flags: `--no-browser` prints the URL instead of opening one,
and is implied over SSH and on a Linux machine with no display.
`ADDUCE_NO_BROWSER=1` does the same thing.

### 5b. Activate a licence JWT

Engine commands refuse to run without a valid key, and the daemon exits **50**
if you start it unlicensed. Enterprise users were sent a JWT out of band,
usually as `license-key.txt`.

```bash
adduce license activate "<the full JWT>"
adduce license status --format json
```

The JWT is one long line. Copy it from the file, never from a PDF, where line
wrapping silently corrupts it.

Verification is offline, so this works without a network. The key is stored in
the OS keychain: Windows Credential Manager, the login keychain on macOS, or
the platform keyring on Linux. On a headless Linux box with no keyring daemon,
and on macOS when the user dismisses the keychain prompt, activation fails with
a keychain error rather than an invalid-licence error; those are deliberately
distinct, so report which one you saw.

`adduce license status` reports the plan, expiry, and revocation state.
`adduce license deactivate` removes it. `adduce license refresh` pulls the
revocation list.

### 5c. An unlockable keychain

Set `ADDUCE_NO_KEYCHAIN=1` and both credentials go to owner-only files under
the Adduce root instead: the licence to `license.jwt`, the device-login token
to `credentials.json`. Nothing reads or writes the OS keychain in that mode.

This is for one situation: a keychain the user cannot unlock at all. The usual
cause is a macOS login keychain whose password drifted from the account
password after a reset, so the login keychain never unlocks and every read and
every write is refused. The licence has no other home, so the product is
unusable on that machine however valid the token is.

It is deliberately not automatic. A failed keychain read and an empty keychain
are indistinguishable through the `keyring` crate, so falling back on failure
would move every user's credential to disk the first time one call was
refused.

Say what it gives up when you set it. A file at 0600 is readable by anything
running as that user, and it survives in backups and in snapshots, which is
what a keychain avoids. Set it for that machine rather than globally, and prefer
repairing the keychain (Keychain Access, then Reset My Default Keychain) where
the user still can.

The variable has to be set for every command, so put it in the shell profile
rather than passing it once:

```bash
echo 'export ADDUCE_NO_KEYCHAIN=1' >> ~/.zshrc
```

`adduce license status` names the store it read, so a machine in this mode says
so rather than looking like any other.

## 6. Prove the install works

Run these in order and stop at the first failure.

```bash
adduce --version                     # binary loads at all
adduce doctor --format json          # paths resolve, root is writable
adduce license status --format json  # data.status == "active"
```

`adduce doctor` runs even when the licence is denied, which is exactly when you
need it. Check `data.root_writable` is `true` and that `data.adduce_root` is
where you expect:

- Linux and macOS: `~/.adduce`
- Windows: `%LOCALAPPDATA%\adduce`

Override with `ADDUCE_ROOT` if the user wants it elsewhere. `ADDUCE_LIBRARY`
overrides only the citation library.

## 7. Register the MCP server (optional)

Steps 7 and 8 are one command each, or one plugin for both:

```bash
claude plugin marketplace add https://api.adduce.dev/releases/marketplace.json
claude plugin install adduce@adduce
```

That registers the MCP server and the backed-research skill together. The rest
of this section and section 8 are the manual equivalents.

The `adduce` entry is a command source: Claude Code runs
`adduce claude-plugin-path`, which stages a plugin directory beside the library
and prints where it is, so the MCP server names the daemon of the running build
at its absolute path. The client re-runs that once per session in the
background, and the plugin follows the install with nothing further to do.

**Acceptance happens once, in a terminal, by the user.** Claude Code
refuses to accept a command source from inside a session, whatever `--yes` says.
Tell the user to run `claude plugin install adduce@adduce` themselves the first
time and to accept the command it shows them. Every later update reuses that
acceptance.

Two administrative settings, `disableCommandPluginSources` and
`allowManagedHooksOnly`, block command sources outright. Install
`adduce-archive@adduce` on a machine under either. It ships the same skill and
resolves `adduce-daemon` through `PATH`, so a cold install needs one client
restart before the name resolves.

A git checkout of the repository is also a plugin and its own marketplace, which
is what `claude plugin marketplace add /path/to/adduce` uses. A release tarball
is not: it has no `.claude-plugin/` directory.

To give a Claude Code session Adduce's tools directly:

```bash
claude mcp add -s user adduce -- adduce-daemon --mcp
claude mcp list
```

`-s user` registers the server for every project on the machine rather than for
the current directory alone. The daemon needs an activated licence to start, so
do step 5 first.

No API key belongs on that command line. `adduce login` writes a device token
and `resolve_api_credential` reads it before anything else, so after a login the
server authenticates itself. `ADDUCE_API_KEY` remains the fallback for CI,
containers and headless boxes with no keychain; on one of those, and only there,
add `--env ADDUCE_API_KEY=<key>`. The key answers for `report_feedback` alone,
which files bugs against the user's account. Every other tool works without one.

## 8. The backed-research skill (optional)

`skills/backed-research` searches the user's local Adduce library and returns
claims with verified page locators. Install it by placing the directory in the
Claude Code skills directory, or symlinking it.

Note for Windows: the skill's helper is invoked as `py`, not `python3`. A stock
Windows install has no `python3` on `PATH`, and the name resolves to a Microsoft
Store stub that prints an advert and exits without running. The helper needs
nothing beyond the standard library.

The skill is only useful once the user has a library. A fresh install has an
empty one, and the skill will correctly report that nothing backs any claim.
That is the right answer rather than a broken install.

## 9. Updating

```bash
adduce update          # same as `update check`: reports, installs nothing
adduce update check
adduce update apply
adduce update info
```

`apply` fetches the manifest, downloads the artefact for its own triple,
verifies the Ed25519 signature against the key embedded at build time, and
replaces the running binary. Prefer it to a manual re-download: it is the only
path that checks the signature.

`apply` writes progress lines to stderr and finishes by naming the one restart
command for the platform it is running on. On Windows with no service
registered there is nothing to restart: an MCP client starts the daemon itself,
so reconnecting the client is enough.

---

# What every command returns

Build against this section rather than against `--help` text or message
strings.

## One shape over two transports

The CLI under `--format json` and the MCP tool result carry the **same**
envelope. An agent that parses it once can drive either.

```json
{
  "ok":          true,
  "verdict":     { "kind": "clean" | "flagged" | "unknown", "reason": "..." },
  "data":        { },
  "diagnostics": [ ],
  "evidence":    "namespace:check:version",
  "meta": {
    "command":        "cite.verify",
    "schema_version": "1",
    "adduce_version": "0.2.8",
    "elapsed_ms":     42,
    "timestamp":      "2026-08-12T01:14:00Z"
  }
}
```

- **Branch on `ok` first.** It is `true` for a clean or flagged verdict, and
  `false` for unknown and for any uncaught error.
- `verdict` is present on verification commands and absent on pure-data ones.
- `data` is command-specific. `adduce schema` emits the whole CLI surface,
  every subcommand and argument, as JSON.
- `diagnostics` is the findings array.
- `evidence` names the check that produced the verdict.
- `meta.elapsed_ms` and `meta.timestamp` are omitted under
  `ADDUCE_DETERMINISTIC=1`, which is how you get byte-stable output.

## A finding

```json
{
  "range": {
    "file":  "paper.tex",
    "start": { "line": 12, "character": 0 },
    "end":   { "line": 12, "character": 0 }
  },
  "severity": "error" | "warning" | "information" | "hint",
  "code":     "E3002",
  "source":   "cite-verify",
  "message":  "drift on title: stored vs upstream differ"
}
```

The shape follows the Language Server Protocol's diagnostic, so an editor
integration has nothing to translate. `line` and `character` are 0-based, and
`range` is omitted when a finding has no position.

## Three states

| Verdict | `ok` | Exit | Meaning |
|---|---|---|---|
| clean | `true` | 0 | the check ran end to end and found nothing |
| flagged | `true` | 10 | the check ran end to end and produced findings |
| unknown | `false` | 11 | the check could not run |

**Unknown is never collapsed into clean.** If a check could not run end to end,
that gap reaches you as its own state, over both transports, and the MCP
transport returns it as an error carrying the same envelope.

## Error codes

Every error carries a stable `Exxxx` code. A code never changes meaning between
versions, a new code gets a new number, and a retired one stays reserved.

| Range | Domain |
|---|---|
| `E0xxx` | arguments and runtime |
| `E1xxx` | parsing |
| `E2xxx` | filesystem |
| `E3xxx` | citations and bibliography |
| `E4xxx` | local storage |
| `E5xxx` | network |
| `E6xxx` | proof tooling |
| `E7xxx` | verification doctrine |
| `E8xxx` | licence and authentication |
| `E9xxx` | internal invariants |

`adduce explain E3002` prints the full explanation of any code, and
`adduce explain --list` indexes them. The MCP server returns the same codes.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | success |
| 1 | generic error |
| 2 | usage or argument error |
| 10 | verification produced findings |
| 11 | verification could not complete |
| 20 | a file, key or socket was not found |
| 30 | drift or conflict detected |
| 40 | network failure |
| 50 | licence invalid, expired or revoked |
| 51 | sign-in expired, denied, or absent; the licence is fine |
| 60 | daemon unreachable, or a protocol mismatch |
| 61 | a required component is missing or its hash differs |
| 70 | internal invariant violation |

A script branching on the exit code alone needs 0, 10, 11, and the network and
licence categories. Anything finer comes from `diagnostics[].code`.

## Global flags and environment

| Flag | Effect |
|---|---|
| `--format FORMAT` | `human` (default on a terminal), `json` (default when piped), `ndjson` |
| `--quiet`, `-q` | suppress non-error output |
| `--verbose`, `-v` | `-v` debug, `-vv` trace |

| Variable | Purpose |
|---|---|
| `ADDUCE_FORMAT` | default for `--format` |
| `ADDUCE_DETERMINISTIC` | when `1`, omits `timestamp` and `elapsed_ms` |
| `ADDUCE_ROOT` | data root |
| `ADDUCE_LIBRARY` | citation library, alone |
| `ADDUCE_API_KEY` | credential, consulted after a device login |

## Discovery

| Command | Purpose |
|---|---|
| `adduce schema` | the whole CLI surface as JSON |
| `adduce explain <CODE>` | one error code, explained |
| `adduce explain --list` | every code in the taxonomy |
| `adduce status --format json` | every subsystem, each with a remedy, and one `ready` field |
| `adduce completions <SHELL>` | shell completions |

## Stability

The envelope carries `schema_version`, currently `"1"`, and its shape will not
change in a breaking way without that number changing. Error codes are stable
across versions. Exit code categories are stable. `adduce schema` is itself part
of the contract, and it reports every breaking change to date as
`breaking_changes`.

## Failure modes

| Symptom | Cause |
|---|---|
| Windows: process does not start, no output | `DirectML.dll` missing or in another directory |
| `adduce pdf-ml setup` cannot find its worker directory | the `adduce-ocr-worker` and `adduce-ml-worker` directories were not unpacked beside the binaries |
| `adduce pdf-ml setup` reports uv missing | install uv; see above |
| Windows: error naming `VCRUNTIME140.dll` or `MSVCP140.dll` | Visual C++ Redistributable not installed |
| Exit 50 from any engine command or the daemon | no licence activated, or it expired |
| Exit 51 from `adduce login` or `adduce whoami` | the sign-in code expired unapproved, was refused, or this machine holds no credential. Run `adduce login` again; the licence is unaffected |
| Activation reports a keychain error | no keyring available, which is its own failure, distinct from an invalid licence |
| `adduce daemon status` says no service is registered | normal for an MCP-only install, which starts the daemon over stdio. `adduce daemon install` registers one if you want the service |
| Exit 60 | daemon not running, or a protocol mismatch after a partial update |
| `adduce doctor` reports `root_writable: false` | permissions on the data root |
| SHA-256 mismatch | corrupt or tampered download; stop |

## What not to do

- Do not install a different platform's artefact.
- Do not separate the Windows files, or copy only `adduce.exe`.
- Do not paste the licence JWT out of a PDF.
- Do not report an install as working without running step 6.
