Metadata-Version: 2.5
Name: hermes-yandex-disk
Version: 0.5.1
Summary: Yandex Disk plugin for Hermes Agent — browse, read, write and share files on Yandex Disk.
Project-URL: Homepage, https://github.com/akinfold/hermes-yandex-disk
Project-URL: Repository, https://github.com/akinfold/hermes-yandex-disk
Project-URL: Issues, https://github.com/akinfold/hermes-yandex-disk/issues
Author: Roman Akinfeev
License-Expression: MIT
License-File: LICENSE
Keywords: cloud-storage,hermes,hermes-agent,plugin,yandex,yandex-disk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Utilities
Requires-Python: <3.14,>=3.11
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: bandit>=1.7; extra == 'dev'
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: radon>=6.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# hermes-yandex-disk

[![PyPI version](https://img.shields.io/pypi/v/hermes-yandex-disk.svg)](https://pypi.org/project/hermes-yandex-disk/)
[![CI](https://github.com/akinfold/hermes-yandex-disk/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/akinfold/hermes-yandex-disk/actions/workflows/ci.yml)
[![E2E (live)](https://github.com/akinfold/hermes-yandex-disk/actions/workflows/e2e.yml/badge.svg)](https://github.com/akinfold/hermes-yandex-disk/actions/workflows/e2e.yml)
[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/akinfold/hermes-yandex-disk/badges/coverage.json&v=1)](https://github.com/akinfold/hermes-yandex-disk/actions/workflows/ci.yml)
[![CodeFactor](https://www.codefactor.io/repository/github/akinfold/hermes-yandex-disk/badge)](https://www.codefactor.io/repository/github/akinfold/hermes-yandex-disk)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

**Your Yandex Disk, handed to the agent.** It can walk the folders, read a file into the
conversation, write the answer back, and give you a share link — without you opening a
browser.

> "Read me the March invoice in /Documents/Accounting."
> "Save these meeting notes to /Notes/2026/ and give me a link."
> "What's eating my Disk space? Move the old videos into /Archive."
> "Restore that file I deleted this morning."

- 🔒 **`YANDEX_DISK_ROOT` sandboxes the agent to one folder** — every path is checked against
  it, including ones the model invents and ones it copies back from an earlier result.
- 🛟 **Nothing is overwritten or destroyed by accident** — writes, copies, moves and uploads
  refuse an existing destination unless you pass `overwrite`, deletes go to the bin by default,
  and the bin can be restored from. Replacing is never done by clearing the way first: an
  upload from a `url` is staged beside the target and moved over it once Yandex has finished
  fetching, and a download writes beside the local file and moves over it once the transfer is
  complete. A transfer that fails costs you the new copy, never the one you already had.
- 🎚 **`YANDEX_DISK_ACTIONS` decides what the agent may do at all** — a tool outside the
  allow-list is never registered, so it cannot be called or talked into being called.
- 📂 **Fourteen tools over one credential** — browse, read, write, upload, download, copy,
  move, share and restore, all through the REST API — plus a fifteenth, `yadisk_search`,
  wherever the token is allowed to use Yandex's search endpoint.
- 🔑 **One OAuth token, nothing proxied** — the plugin talks to `cloud-api.yandex.net`
  directly; no third-party service sees your files or your token.

Tested against Hermes 0.19.x, Python 3.11–3.13.

## Quick start

```bash
# 1. Install the plugin into your Hermes environment (or from PyPI — see
#    Installing, B). It asks for your token; issue one at
#    https://yandex.ru/dev/disk/poligon/
#    (scopes: cloud_api:disk.info, cloud_api:disk.read, cloud_api:disk.write)
hermes plugins install akinfold/hermes-yandex-disk/hermes_yandex_disk --enable

# 2. Skipped the question, or installed another way? Add the token yourself:
echo 'YANDEX_DISK_OAUTH_TOKEN=y0_your_token_here' >> ~/.hermes/.env
```

```yaml
# 3. `--enable` already wrote this into ~/.hermes/config.yaml
plugins:
  enabled: [yandex-disk]
```

Restart Hermes and ask it: **"How much space is left on my Yandex Disk?"**

> Not ready to hand over write access? Add `YANDEX_DISK_ACTIONS=read` to `~/.hermes/.env` and
> the agent gets a read-only Disk. See [Choosing what the agent may do](#choosing-what-the-agent-may-do).

## The tools

| Tool | What it does |
|---|---|
| `yadisk_disk_info` | Quota, used and bin space, account login |
| `yadisk_list` | List a folder, or show one file's metadata |
| `yadisk_read_file` | Read a text file's contents into the conversation |
| `yadisk_trash_list` | What is in the bin, and where each item came from |
| `yadisk_download` | Save a file from the Disk to the local machine |
| `yadisk_mkdir` | Create a folder, parents included |
| `yadisk_write_file` | Create or replace a text file from the agent's own output |
| `yadisk_upload` | Upload a local file, or have Yandex fetch a public URL |
| `yadisk_copy` | Server-side copy — no bytes pass through your machine |
| `yadisk_move` | Move or rename |
| `yadisk_trash_restore` | Put something back where it was deleted from |
| `yadisk_publish` | Create or revoke a public share link |
| `yadisk_delete` | Delete to the bin, or permanently with `permanently=true` |
| `yadisk_trash_empty` | Destroy one bin item, or the whole bin |

Every read result carries the `path` the write tools take as input, so the agent can chain
them without guessing.

> **Changed in 0.5.0.** `yadisk_upload` from a `url` used to ignore `overwrite`. Yandex'
> fetch-from-internet endpoint has no such parameter and the plugin did not stand in for it,
> so an upload onto a path that was already taken did whatever the server decided — the API
> documentation does not say what. It is now refused unless you pass `overwrite=true`, the
> same as every other write. If anything of yours uploads by URL over a file that is already
> there, add the flag before upgrading.

## Configuration

| Variable | Required | Default | Meaning |
|---|---|---|---|
| `YANDEX_DISK_OAUTH_TOKEN` | yes | — | OAuth token for the account (`YANDEX_DISK_API_KEY` is also accepted) |
| `YANDEX_DISK_ACTIONS` | no | all tools | Which tools get registered — see below |
| `YANDEX_DISK_ROOT` | no | whole disk | Confine every tool to this folder |
| `YANDEX_DISK_MAX_READ_BYTES` | no | `1048576` | Largest file `yadisk_read_file` will pull into context |
| `YANDEX_DISK_MAX_DOWNLOAD_BYTES` | no | `268435456` | Largest file `yadisk_download` will write locally |
| `YANDEX_DISK_TIMEOUT` | no | `30` | Per-request timeout, in seconds |
| `YANDEX_DISK_BASE_URL` | no | `https://cloud-api.yandex.net/v1/disk` | API base, for testing against a stub |

Put them in `~/.hermes/.env` — Hermes reads it in gateway and subprocess runs too — or export
them in the environment.

The two byte limits take a whole number of bytes and the timeout a number of seconds, fractions
allowed. A value that is not a number, or is zero or negative — `5MB`, `60s`, `0` — is ignored
without an error and the default applies.

Two further limits are fixed and cannot be configured. A request answered with 429, 500, 502,
503 or 504, or one that fails to connect, is retried up to three times (honouring `Retry-After`,
capped at 30 seconds), so a single call can take longer than `YANDEX_DISK_TIMEOUT`. Large copies,
moves, deletes, restores and bin operations run as deferred operations on Yandex's side; the
plugin waits up to 60 seconds for one to finish and then reports that it is still being
processed. Such an operation usually completes on its own, so check with `yadisk_list` before
retrying.

### Confining the agent to one folder

```dotenv
YANDEX_DISK_ROOT=/Hermes
```

Every path the agent gives is then interpreted relative to `disk:/Hermes`, and anything that
resolves outside it — an absolute `disk:/Private/...`, a `../` climb, a path pasted from
somewhere else — is refused before a request is made. Results are rewritten so the agent never
sees the prefix: every path it gets back is relative to that folder — `disk:/Hermes/notes.md`
comes back as `/notes.md` — and can be handed straight to the next tool. The agent sees a disk
whose root *is* that folder. The bin is filtered the same way: items deleted from outside the
sandbox are neither listed nor restorable, and emptying the whole bin is refused, because the
bin is shared with the rest of the account.

### Choosing what the agent may do

```dotenv
YANDEX_DISK_ACTIONS=read,write      # no deleting, no sharing
YANDEX_DISK_ACTIONS=read            # a read-only Disk
YANDEX_DISK_ACTIONS=list,read_file  # exactly two tools
```

| Group | Tools |
|---|---|
| `read` | `disk_info`, `list`, `read_file`, `trash_list`, and `search` when the token may use it |
| `download` | `download` (writes to the **local** machine) |
| `write` | `mkdir`, `write_file`, `upload`, `copy`, `move`, `trash_restore` |
| `share` | `publish` |
| `delete` | `delete`, `trash_empty` |
| `all` | everything — the default when the variable is unset |

`search` is capability-gated: Yandex grants its search endpoint per application rather than
per token scope, so the plugin probes it once at load and registers the tool only if the probe
succeeds. Granting `read` therefore also grants a whole-disk search wherever the token is
allowed to run one; name the tools individually to withhold it.

Groups and individual tool names mix freely, with or without the `yadisk_` prefix, so you can
paste straight from the tool table above. A name that matches nothing is dropped: a typo can
only ever withhold a tool, never grant one, and a value naming nothing valid (`none`, say)
registers nothing at all. An empty or blank value counts as unset and therefore registers
every tool. Filtering happens when the plugin loads, so **restart Hermes** after changing it.

## Getting the credential

1. Open <https://yandex.ru/dev/disk/poligon/> signed in as the account you want the agent to
   use, and issue a debug token — the fastest route, and what it gives you is an ordinary
   OAuth token.
2. For a long-lived setup, register an app at <https://oauth.yandex.ru/> with the scopes
   `cloud_api:disk.info`, `cloud_api:disk.read` and `cloud_api:disk.write`, then run the usual
   OAuth flow.

Yandex Disk is free up to 5 GB and the API costs nothing to use. The token is the only
credential needed — see [Why REST and not WebDAV](#why-rest-and-not-webdav) for why there is
no app password to store.

## Installing

**A. From Git (recommended)**

```bash
hermes plugins install akinfold/hermes-yandex-disk/hermes_yandex_disk --enable
```

Note the `/hermes_yandex_disk` at the end. The plugin lives in that directory, not
at the repository root, and Hermes reads the manifest from whatever you point it
at. Name the directory and the install is a plugin: Hermes prompts for
`YANDEX_DISK_OAUTH_TOKEN`, installs under the manifest name `yandex-disk`, and
`--enable` enables that name. It also scans only that directory, so the tests and
workflows in this repository stay out of the security report.

Point it at the repository root instead and the install still appears to succeed,
but it copies a directory with no manifest and no `register(ctx)` in it: Hermes
warns that it "may not be a valid Hermes plugin", asks for nothing, and enables the
repository name, which nothing answers to. `hermes plugins list` then still shows
`yandex-disk` — the package nested in the clone is found — but shows it as **not
enabled**, which is the symptom to look for. If you installed that way, remove
`~/.hermes/plugins/hermes-yandex-disk` and install again with the directory named.

**B. From PyPI** — install the package into the virtualenv Hermes runs from, then
enable it. With the standard Hermes install that virtualenv is
`~/.hermes/hermes-agent/venv` (`/usr/local/lib/hermes-agent/venv` if the installer ran
as root on Linux), and Hermes keeps its own `uv` in `~/.hermes/bin`:

```bash
~/.hermes/bin/uv pip install --python ~/.hermes/hermes-agent/venv/bin/python hermes-yandex-disk
hermes plugins enable yandex-disk
```

A bare `pip install hermes-yandex-disk` does not get there: the installer builds that
virtualenv with `uv` and without `pip`, so the `pip` on your `PATH` belongs to some other
Python, and Hermes never sees the plugin. If you installed Hermes another way, install the
package into whichever environment the `hermes` command runs from. Hermes finds it through
the `hermes_agent.plugins` entry point. Nothing asks for the token on this path — add it to
`~/.hermes/.env` as in the [Quick start](#quick-start).

**C. Drop-in** — download `hermes-yandex-disk-plugin-<version>.zip` from a
[release](https://github.com/akinfold/hermes-yandex-disk/releases), then unzip it into
`~/.hermes/plugins/` and enable it:

```bash
unzip hermes-yandex-disk-plugin-<version>.zip -d ~/.hermes/plugins/
hermes plugins enable yandex-disk
```

You should end up with `~/.hermes/plugins/yandex-disk/plugin.yaml`. As with B, add the
token to `~/.hermes/.env` yourself. The archive brings no dependencies; the one the plugin
needs, `httpx`, is a Hermes dependency already.

All three are checked before every release by installing the build into a real Hermes —
the latest release and `main` — exactly as written here; see
[Checking the install paths](#checking-the-install-paths).

## Why REST and not WebDAV

Yandex Disk exposes both a REST API and a WebDAV endpoint. This plugin is REST-only, because
WebDAV covers strictly less: no bin, no public links, and no reporting for the deferred
operations that large folder copies and deletes turn into.
rclone's Yandex backend made the same call. The one thing WebDAV adds — an app-password login
— is not worth a second stored credential for capabilities the REST API already has.

## Development

```bash
python -m venv .venv && . .venv/bin/activate
pip install -e '.[dev]'

pytest --cov=hermes_yandex_disk --cov-report=term-missing   # unit tests, no network
ruff check . && ruff format --check .
radon cc -s -n C hermes_yandex_disk                         # must print nothing
```

## Running the live E2E tests

> These create, publish and permanently delete real files. **Use a throwaway Yandex account.**
> Each test works inside its own `hermes-e2e-<id>` folder, permanently deleted in
> teardown even when the test fails. Two things reach outside it: the bin tests pass
> their files through the account-wide bin and remove their own entries, and — only
> when the token is allowed to search — one test creates a single marker file in the
> disk root and permanently deletes it again.

Locally, put the token in `~/.yandex-disk-oauth` (and optionally the login in
`~/.yandex-disk-login`) and run:

```bash
pytest -m e2e -v
```

The suite skips itself when no credentials are available, and an environment variable wins over
the file: `YANDEX_DISK_OAUTH_TOKEN` for the token and, optionally, `YANDEX_DISK_E2E_LOGIN` for the
account login the tests check `yadisk_disk_info` against. On GitHub Actions, run the **E2E (live)**
workflow manually; it reads the secrets `YANDEX_DISK_OAUTH_TOKEN` and, optionally,
`YANDEX_DISK_E2E_LOGIN` from the `yandex-disk-e2e` environment. It runs the plugin inside a real
Hermes, set up the way the Hermes installer sets it up, so the real credential resolver is
exercised: the latest Hermes release by default, and its `hermes` input switches to Hermes `main` or
to no Hermes at all. With Hermes, the run fails outright if the plugin cannot import it, rather than
testing the plugin's stand-ins instead.

## Checking the install paths

The `install`-marked tests in `tests/install/` install the built plugin into a real Hermes,
set up the way the official installer sets it up, by each route in [Installing](#installing)
— running the README's own commands — and then ask Hermes what it loaded: the plugin must be
listed as enabled, load without error, and give the agent its tools. They also check that
installing from the repository root still looks the way this README describes. A fast unit
test keeps the commands in the tests and in this README identical.

The **Install check** workflow runs them against the latest Hermes release and against
Hermes `main` on every pull request, and on every release tag before anything is published:
the GitHub Release and the PyPI upload both wait for it. To run them locally, see the
docstring of `tests/install/test_install.py`.

## Related Hermes plugins

Part of a family of Yandex plugins for Hermes Agent:

- [hermes-yandex-mail](https://github.com/akinfold/hermes-yandex-mail) — search, read, flag, move, and delete Yandex Mail messages over IMAP, and send over SMTP when sending is switched on.
- [hermes-yandex-calendar](https://github.com/akinfold/hermes-yandex-calendar) — list, create, update, respond to, move, and delete Yandex Calendar events (CalDAV).
- [hermes-yandex-search-api](https://github.com/akinfold/hermes-yandex-search-api) — Yandex web search backend and generative, cited answers for Hermes (Yandex Search API).

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). Please report security issues privately rather than as
a public issue.

## License

MIT — see [LICENSE](LICENSE).
