Metadata-Version: 2.4
Name: ozzylabs-opshub
Version: 0.2.4
Summary: Local-first operational memory and execution hub for humans and AI agents
Project-URL: Homepage, https://github.com/ozzy-labs/opshub
Project-URL: Issues, https://github.com/ozzy-labs/opshub/issues
Author: ozzy-labs
License: MIT
License-File: LICENSE
Keywords: ai-agents,event-sourcing,local-first,operational-memory
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.13
Requires-Dist: alembic>=1.13
Requires-Dist: jinja2>=3.1
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: pydantic>=2.7
Requires-Dist: pyyaml>=6
Requires-Dist: sqlalchemy>=2.0
Requires-Dist: sqlite-vec>=0.1
Requires-Dist: structlog>=24.1
Requires-Dist: typer>=0.12
Provides-Extra: api-embedding-openai
Requires-Dist: openai>=1.40; extra == 'api-embedding-openai'
Provides-Extra: api-embedding-voyage
Requires-Dist: voyageai>=0.2; extra == 'api-embedding-voyage'
Provides-Extra: connectors-box
Requires-Dist: boxsdk<4,>=3.10; extra == 'connectors-box'
Provides-Extra: connectors-github
Requires-Dist: httpx>=0.27; extra == 'connectors-github'
Requires-Dist: pygithub>=2.3; extra == 'connectors-github'
Provides-Extra: connectors-ms365
Requires-Dist: httpx>=0.27; extra == 'connectors-ms365'
Requires-Dist: msal>=1.30; extra == 'connectors-ms365'
Provides-Extra: connectors-slack
Requires-Dist: slack-sdk>=3.30; extra == 'connectors-slack'
Provides-Extra: connectors-teams
Requires-Dist: httpx>=0.27; extra == 'connectors-teams'
Requires-Dist: msal>=1.30; extra == 'connectors-teams'
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pyright>=1.1.380; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest-xdist>=3.6; extra == 'dev'
Requires-Dist: pytest>=8.2; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-jsonschema>=4.21; extra == 'dev'
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
Provides-Extra: encryption
Requires-Dist: keyring>=24; extra == 'encryption'
Requires-Dist: sqlcipher3-binary>=0.5; extra == 'encryption'
Provides-Extra: llm-anthropic
Requires-Dist: anthropic>=0.40; extra == 'llm-anthropic'
Provides-Extra: llm-ollama
Requires-Dist: httpx>=0.27; extra == 'llm-ollama'
Provides-Extra: llm-openai
Requires-Dist: openai>=1.50; extra == 'llm-openai'
Provides-Extra: local-embedding
Requires-Dist: sentence-transformers>=3.0; extra == 'local-embedding'
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Provides-Extra: office
Requires-Dist: markitdown[docx,pptx,xlsx]>=0.1; extra == 'office'
Provides-Extra: secrets
Requires-Dist: keyring>=24; extra == 'secrets'
Provides-Extra: vector
Requires-Dist: numpy>=2.0; extra == 'vector'
Description-Content-Type: text/markdown

# OpsHub

[![PyPI](https://img.shields.io/pypi/v/ozzylabs-opshub.svg)](https://pypi.org/project/ozzylabs-opshub/)
[![CI](https://github.com/ozzy-labs/opshub/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/ozzy-labs/opshub/actions/workflows/ci.yaml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue.svg)](https://www.python.org/downloads/)

English | [日本語](README.ja.md)

**Local-first secretary agent platform — auditable operational memory for humans and AI agents.**

OpsHub is the local-first **secretary agent platform**: an auditable, event-sourced operational memory that an AI agent host (Claude Code / Codex CLI / Gemini CLI / GitHub Copilot CLI) can drive on your behalf. Ask your agent "what should I do next?" or "draft a reply to that Slack thread" and it talks to OpsHub over MCP to recall, summarise, and propose — never sending your state to a cloud service.

Three layers ([ADR-0004](docs/adr/0004-agent-runtime-boundary.md)):

1. **You (human)** — ask in natural language.
2. **Secretary agent** — runs in your editor / terminal (Claude Code etc.). OpsHub ships **MCP server (`opshub mcp serve`) + Agent Skills (SKILL.md)** but no LLM runtime; the agent host owns the brain.
3. **OpsHub core (CLI)** — append-only event log + projections + body store + connectors. Same surface, whether you call it from the agent or from the CLI directly.

## Install

OpsHub is distributed on PyPI under the name **`ozzylabs-opshub`** (PyPI has no
namespace concept, so we follow the PEP 423 `<owner>-<package>` convention).
The CLI command stays `opshub`.

```bash
uv tool install ozzylabs-opshub
# or
pipx install ozzylabs-opshub
```

Optional extras (pulled only if needed):

```bash
uv tool install "ozzylabs-opshub[llm-anthropic,connectors-github]"
```

See [Optional dependencies](#optional-dependencies) below for the full extras
matrix.

### Alternative: install directly from GitHub

You can also install from a tagged git ref (no PyPI involvement):

```bash
uv tool install git+https://github.com/ozzy-labs/opshub.git@v0.1.0
```

This is useful for pre-release tags, unreleased fixes on `main`, or air-gapped
environments where PyPI isn't reachable.

## Quickstart

```bash
opshub init                                           # one-time DB + workspace setup
opshub task create "Write blog post about OpsHub"     # create a task
opshub task list                                       # see open tasks

# Once the LLM backend is configured (next section):
opshub brief "current priorities"                      # LLM-summarised briefing
opshub propose generate "what's next?"                 # LLM-proposed next-actions
opshub propose apply <proposal-id> 0                  # operator-approved entity creation
```

All state lives under XDG directories; override via `OPSHUB_*` env vars
(e.g. `OPSHUB_STORAGE__DB_PATH=/custom/path.sqlite`).

## Ask your secretary

Once you wire OpsHub into an agent host over MCP (see [Connect an agent host](#connect-an-agent-host-mcp) below), you can talk to your secretary in plain language. The agent calls the right OpsHub commands behind the scenes.

Phase 12 (2026-05-31) widened the secretary skill repertoire from 5 to **14** (10 read / 4 HITL write). The catalog below shows the most common triggers; the full responsibility map lives in [`docs/secretary-agent.md`](docs/secretary-agent.md).

| You ask | Skill that fires | What it does |
|---|---|---|
| "What should I do next?" / "今日のまとめ" / "今週どうなってる" | `personal-brief` / `next-actions` | Signals over the requested window (今日 / 今週 / 今月 / 先週 / 先月) + active tasks + untriaged inbox |
| "Draft a reply to that Slack thread" / "返信案考えて" | `reply-draft` | LLM-generated draft grounded in your past sending style (HITL apply, idempotent — OpsHub never sends) |
| "Review PR #123" | `pr-review` | Pulls related decisions / tasks / past discussion so the agent can review with context |
| "Find that Box file about X" / "Word/Excel/PPT 探して" | `find-document` | Full-text + semantic search across Slack / Box / GitHub / MS365 / Teams / Box Drive / OneDrive Drive (incl. Office body extraction, Phase 11; FTS5 over MCP since Phase 12 H1) |
| "Summarise that Teams thread" / "Teams スレッド要約して" | `personal-brief` / `find-document` | Body-based recall over Teams chat history (Phase 11) |
| "Prep me for tomorrow's meeting" / "明日の会議準備" | `meeting-prep` (Phase 12) | Purpose + prior discussion + related decisions / sources for the upcoming calendar event |
| "Research X end-to-end" / "<X> について調べて" | `research` (Phase 12) | Cross-cutting topical research (semantic recall + FTS5 + graph expand + briefing) |
| "Weekly status for my manager" / "上司向け週次報告" | `external-brief` (Phase 12) | Outward-facing report (completed tasks + confirmed decisions, restrained tone) — pair of personal-brief |
| "Why did we choose X?" / "あの決定はなぜ" | `decision-rationale` (Phase 12) | Decision + source + prior decisions traced via `graph.trace` |
| "Triage my inbox" / "受信箱整理して" | `inbox-triage` (Phase 12, HITL) | Generate per-item action candidates over open inbox items, you approve each one |
| "Extract tasks from this doc" / "この資料から task 抽出" | `source-extract` (Phase 12, HITL) | Pull task / decision candidates from one source body |
| "Action items from yesterday's meeting" / "会議後のフォロー" | `meeting-followup` (Phase 12, HITL) | Action items extracted from recent calendar events — pair of meeting-prep |
| "Write the handoff doc" / "引き継ぎ書作って" | `handoff-draft` (Phase 12) | Markdown handoff text built from in-progress tasks + decisions + recall (text-only, no persist) |
| "Draft the release announcement" / "リリース告知文書いて" | `announcement-draft` (Phase 12) | Markdown announcement built from `recall.search` + recent decisions + briefing (text-only) |

The 14 secretary skills live under [`docs/skills/<name>/SKILL.md`](docs/skills/) (Phase 12 H1 made opshub the SSOT). The original 5 skills were renamed (`daily-brief` → `personal-brief`, `file-lookup` → `find-document`); the other 9 are new in Phase 12 H2-H5. The `@ozzylabs/skills` Renovate preset distribution (handbook ADR-0016) is deferred to Phase 13+; in Phase 12 you copy them into the host loader manually (see [Install the secretary skills](#install-the-secretary-skills) below). See [`docs/secretary-agent.md`](docs/secretary-agent.md) for the responsibility map, MCP tool dependency matrix, pair structure, HITL boundary, and what OpsHub deliberately does not do (no write-back to SaaS, no always-on runtime, no auto-apply).

## Connect an agent host (MCP)

```bash
uv tool install "ozzylabs-opshub[mcp]"
opshub init
opshub db migrate
opshub mcp tools           # inspect read / write tool surface
```

Then point your agent host (Claude Code etc.) at `opshub mcp serve` as a stdio MCP server. Example for Claude Code (`~/.claude/mcp_servers.json`):

```json
{
  "mcpServers": {
    "opshub": { "command": "opshub", "args": ["mcp", "serve"] }
  }
}
```

Full setup (other hosts, encryption, troubleshooting): [`docs/mcp-setup.md`](docs/mcp-setup.md).

## Install the secretary skills

Phase 12 ships **14 secretary skills** under [`docs/skills/<name>/SKILL.md`](docs/skills/) (opshub is the SSOT, [ADR-0004 §決定 (c)](docs/adr/0004-agent-runtime-boundary.md)). The `@ozzylabs/skills` Renovate preset distribution is deferred to Phase 13+; in Phase 12 copy the skills into the host loader manually:

```bash
# Claude Code (user-level)
cp -r path/to/opshub/docs/skills/* ~/.claude/skills/

# Codex CLI / GitHub Copilot CLI (user-level)
cp -r path/to/opshub/docs/skills/* ~/.agents/skills/

# Project-local install (any host)
cp -r path/to/opshub/docs/skills/* ./.claude/skills/   # or ./.agents/skills/
```

Skill descriptions include Japanese trigger phrases, so asking your agent "今日のまとめ" / "What should I do next?" routes to the right skill automatically. See [`docs/secretary-agent.md`](docs/secretary-agent.md) §8 (セットアップ) for the up-to-date install steps and Phase 13+ distribution outlook.

## Configure an LLM backend (optional)

OpsHub is functional without any LLM — `task` / `decision` / `inbox` /
`connector sync` all work standalone. To enable `brief` / `propose`, configure
one of:

```bash
opshub connector auth set llm:anthropic       # Anthropic Claude (recommended)
opshub connector auth set llm:openai          # OpenAI
# Or for local-only:
ollama serve && ollama pull llama3.2:3b
# Then set [llm] backend = "ollama" in ~/.config/opshub/config.toml
```

See [`docs/principles.md`](docs/principles.md) §1 (Local-first) for the design
rationale.

## What's in OpsHub today

Phases 1–8 shipped (2026-05-17, v0.1.0). Phase 9 shipped 2026-05-23. Phase 10 + Phase 11 + Phase 12 shipped 2026-05-31:

| Phase | Layer | Highlights |
|---|---|---|
| 1 | Foundation | Event store, tasks, CLI, markdown workspace |
| 2 | Coordination | Inbox / decisions / sessions / locks / handoffs |
| 3 | Connectors framework | GitHub connector + workspace file ingest |
| 4 | Semantic recall | Pluggable Embedder (local / OpenAI / Voyage) + sqlite-vec + `recall` + duplicate detection |
| 5 | Briefing | Pluggable LLM (Anthropic / OpenAI) + `brief` + prompt injection mitigation |
| 6 | Action loop | Structured output + Ollama backend + `propose` (human-in-the-loop) |
| 7 | Connectors wave 2 | Slack + Microsoft 365 + Box |
| 8 | Knowledge graph | `links` projection + auto-extraction + `graph` + `--expand-graph` |
| 9 | Local-FS connectors | `box_drive` (Box Drive desktop client → local FS scan, ADR-0019) |
| 10 | Secretary agent platform | Full local body retention (ADR-0020) + encryption at rest (ADR-0021) + MCP server (ADR-0022) + `opshub search` (FTS5) + `opshub mcp serve` + secretary 5 Skills (renamed in Phase 12 H1 to `personal-brief` / `next-actions` / `reply-draft` / `pr-review` / `find-document`) + reply-draft (ADR-0016 §決定 (i)) + ADR-0004 revision (form-A: no agent runtime in core) + ADR-0010 revision (write-back ban) + ADR-0017 revision (reply_draft link types) |
| 11 | MS Office deep-dive | Office content extraction (ADR-0025, markitdown for `.docx`/`.xlsx`/`.pptx`, 50 MB / 500K chars cap, fail-safe) + ADR-0019 revision (`content_extraction` opt-in exception + `onedrive_drive` pattern generalisation) + ADR-0010 revision (Teams connector + body-extraction contract + delta-link cursor + invalidated-token fallback + Teams User Token principal) + new `teams` connector (Microsoft Graph chat delta + `Chat.Read`) + new `onedrive_drive` connector (FS scan, WSL2 `/mnt/onedrive` / macOS `~/OneDrive`) + `box_drive` Office extraction hook + Outlook body deep retention |
| 12 | Secretary Skills expansion | Secretary skill repertoire grows from 5 to **14** (10 read / 4 HITL write) — `meeting-prep` / `research` / `inbox-triage` / `external-brief` / `decision-rationale` / `handoff-draft` / `announcement-draft` / `meeting-followup` / `source-extract` are new; `daily-brief` / `file-lookup` renamed to `personal-brief` / `find-document`. 4 new MCP tools (`search` FTS5 + `propose.apply` HITL idempotent + physical-column time filters on `task.list` / `inbox.list` / `decision.list` / `source.list`). The original 5 skills now call MCP directly (CLI fallback dropped). ADR-0004 revision (Skills SSOT moves into opshub `docs/skills/`, distribution deferred to Phase 13+) + ADR-0022 revision (4 new MCP tools contract) + ADR-0016 revision (draft-family unified policy: persist boundary by reply-source presence, `mode` arg scope, triage scope, Candidate union freeze). `docs/secretary-agent.md` becomes the 14-skill catalog SSOT (responsibility map + HITL boundary + MCP tool dependency matrix + pair structure) |

Next: **Phase 13+ candidates** — multi-machine sync, proactive secretary (cron-delegated commands), image OCR (PPT figures / slide images), additional connectors (Google Workspace via markitdown reuse / Notion / Jira), external write-back (Teams reply send with HITL), `ozzy-labs/skills` distribution completion. Longer phase-by-phase narrative lives in
[`docs/architecture.md`](docs/architecture.md) §9 (Phased Delivery).

## Commands

```bash
# Foundation (Phase 1)
opshub task create "draft phase 2 plan"
opshub task list --format md                          # formats: table / md / json

# Coordination (Phase 2)
opshub inbox add "triage the failing nightly build"
opshub inbox triage <id> --to-task "fix nightly build"
opshub inbox list --format md
opshub decision record "adopt sqlite-vec for Phase 4"
opshub decision list
opshub lock acquire task:<task-ulid>                  # ADR-0013 coordination lock
opshub lock release <lock-id>
opshub lock list
opshub session start --scope "phase-3 design"         # work session bracket
opshub agent run begin claude                         # auto-injected into agent runs
opshub agent run end <run-id> --summary "drafted ADR-0017"
opshub session end --summary "EOD wrap"
opshub handoff open --from agent:claude --to ozzy --topic "review"
opshub handoff close <handoff-id> --note "merged"

# Connectors (Phase 3 + Phase 7, ADR-0010 / ADR-0014)
opshub connector auth set github                      # store GitHub PAT in OS keychain
opshub connector sync github                          # incremental sync (OPSHUB_CONNECTOR_GITHUB_REPO=owner/repo)
opshub connector auth set connector:slack             # store Slack OAuth token in OS keychain (User Token preferred, Bot Token also accepted — ADR-0018)
opshub connector sync slack                           # incremental sync ([connectors.slack] channels)
opshub connector auth set connector:ms365             # OAuth paste-code (Microsoft Graph Calendar / OneDrive / Outlook)
opshub connector sync ms365                           # incremental sync per endpoint
opshub connector auth set connector:box               # OAuth paste-code (Box Events API)
opshub connector sync box                             # incremental sync (Box stream_position cursor)
opshub connector sync box_drive                       # Phase 9: scan local Box Drive mount (see docs/box-drive-setup.md)
opshub connector sync onedrive_drive                  # Phase 11: scan local OneDrive Desktop mount (see docs/onedrive-drive-setup.md)
opshub connector auth set connector:teams             # Phase 11: store Microsoft Graph User Token (Chat.Read, see docs/teams-setup.md)
opshub connector sync teams                           # Phase 11: Graph chat delta + invalidated-token fallback
opshub connector list                                 # show registered connectors

# Workspace + projections
opshub workspace ingest                               # ingest workspace/inbox/*.md (Phase 3)
opshub workspace ingest --dry-run                     # scan only, no writes
opshub workspace generate                             # regenerate markdown workspace from projections
opshub projections rebuild                            # rebuild projections from the event store (idempotent)

# Semantic recall (Phase 4, ADR-0012)
opshub connector auth set embedder:openai             # store OpenAI API key in OS keychain
opshub embeddings rebuild                             # bulk-embed task/decision/inbox/source bodies (Phase 10: now body-based, ADR-0020)
opshub embeddings status                              # show backend + per-entity-type embedded vs pending
opshub embeddings drain                               # retry pending embeddings (auto-embed hook backup)
opshub embeddings find-duplicates -t 0.92             # offline near-duplicate scan
opshub recall "recent decisions about authentication" # semantic search across all entities

# Full-text search across source bodies (Phase 10, ADR-0012 改訂 §4 + ADR-0020)
opshub search "ticket-1234"                           # SQLite FTS5 across Slack / GitHub / Box / MS365 / Box Drive bodies
opshub search "deploy AND failure" --raw              # opt into FTS5 boolean / phrase / prefix syntax
opshub search "channel ID" --connector slack          # restrict to one connector

# Briefing (Phase 5, ADR-0015)
opshub connector auth set llm:anthropic               # store Anthropic API key in OS keychain
opshub brief "phase 5 progress"                       # LLM-backed briefing (markdown to stdout)
opshub brief "phase 5 progress" --save                # also persist under <workspace>/briefings/
opshub brief "phase 5 progress" --format json         # JSON with briefing_id / model / tokens / source_refs
opshub brief "phase 8 review" --expand-graph          # widen LLM context via 1-hop graph expansion

# Action loop (Phase 6, ADR-0016, Phase 10 reply-draft)
opshub propose generate "next steps"                  # LLM proposes task/decision candidates
opshub propose generate "next steps" --from-briefing <id>
opshub propose generate "next steps" --format json
opshub propose generate "next steps" --expand-graph   # 1-hop graph expansion for proposals
opshub propose generate "" --reply-to <source-id>     # Phase 10: reply-draft mode (topic is ignored; ADR-0016 §決定 (i))
opshub propose list                                   # recent proposals (markdown table)
opshub propose list --state pending --limit 10        # filter: pending / applied / rejected
opshub propose apply <proposal-id> <candidate-index>  # operator approval → creates entities (reply-draft saves locally, never sends)
opshub propose reject <proposal-id> <candidate-index> --reason "out of scope"

# Knowledge graph (Phase 8, ADR-0017)
opshub link add task:<task-id> source:<src-id> --type references
opshub link list --from task:<task-id>                # filter by --from / --to / --type
opshub link remove <link-id> --reason "wrong source"  # hard delete (emits LinkDeleted)
opshub graph related task:<task-id> --direction both  # 1-hop neighbours (md / json / dot)
opshub graph trace task:<task-id> --depth 3           # backward provenance walk (default 3, max 10)
opshub graph expand task:<task-id> --depth 2 --format dot

# MCP server surface (Phase 10, ADR-0022)
opshub mcp tools                                       # inspect read / write tool surface (auditable policy-as-data)
opshub mcp tools -f json                               # JSON for diff / scripting
opshub mcp serve                                       # stdio MCP server — agent host spawns this as a subprocess
```

## Optional dependencies

| Extras | Purpose | Heavy? |
|---|---|---|
| `vector` | sqlite-vec for semantic recall | Small |
| `local-embedding` | sentence-transformers (bge-m3, ~500MB) | Heavy |
| `api-embedding-openai` / `api-embedding-voyage` | API embedder backends | Small |
| `llm-anthropic` / `llm-openai` | API LLM backends | Small |
| `llm-ollama` | Ollama daemon client | Small |
| `connectors-github` / `connectors-slack` / `connectors-ms365` / `connectors-box` | SaaS connectors | Small |
| `connectors-teams` | Microsoft Teams connector (Phase 11, msal + httpx) | Small |
| `office` | Office document content extraction (Phase 11, ADR-0025). Pulls `markitdown` with the `[docx,xlsx,pptx]` sub-extras (i.e. `mammoth` / `openpyxl` / `python-pptx`) so only the three Office sub-formats opshub supports are installed | Small |
| `secrets` | OS keyring backend | Small |
| `encryption` | SQLCipher-backed at-rest encryption (Phase 10, ADR-0021) | Small |
| `mcp` | MCP server SDK for `opshub mcp serve` (Phase 10, ADR-0022) | Small |
| `dev` | Test + lint toolchain | Medium |

## Documentation

- [`docs/principles.md`](docs/principles.md) — design principles (local-first, event-sourced, full local content retention)
- [`docs/architecture.md`](docs/architecture.md) — layered architecture overview
- [`docs/secretary-agent.md`](docs/secretary-agent.md) — Phase 10 secretary agent platform usage (skills catalog, what it can / cannot do)
- [`docs/mcp-setup.md`](docs/mcp-setup.md) — Phase 10 MCP setup for agent hosts (Claude Code etc.)
- [`docs/adr/`](docs/adr/README.md) — Architecture Decision Records
- [`docs/box-drive-setup.md`](docs/box-drive-setup.md) — Phase 9 `box_drive` connector setup (WSL2 / macOS)
- [`docs/onedrive-drive-setup.md`](docs/onedrive-drive-setup.md) — Phase 11 `onedrive_drive` connector setup (WSL2 / macOS)
- [`docs/teams-setup.md`](docs/teams-setup.md) — Phase 11 `teams` connector setup (Azure app registration + User Token)
- [`docs/upgrading.md`](docs/upgrading.md) — version migration notes (when applicable)
- [`docs/release-notes-v0.1.0.md`](docs/release-notes-v0.1.0.md) — v0.1.0 narrative release notes
- [`docs/RELEASE_RUNBOOK.md`](docs/RELEASE_RUNBOOK.md) — how to cut a release (maintainers)
- [`CHANGELOG.md`](CHANGELOG.md) — release history
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — contribution guidelines
- [`SECURITY.md`](SECURITY.md) — vulnerability disclosure + Phase 10 body retention threat model

## License

MIT. See [LICENSE](LICENSE).
