Metadata-Version: 2.4
Name: hermes-plugin-pushary
Version: 0.5.2
Summary: Pushary push notifications plugin for Hermes Agent
Author-email: RalphNex OU <business@pushary.com>
License: MIT
Project-URL: Homepage, https://pushary.com
Project-URL: Issues, https://pushary.com/support
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# Pushary Plugin for Hermes Agent

Push notifications and human-in-the-loop for [Hermes Agent](https://hermes-agent.nousresearch.com/) via [Pushary](https://pushary.com).

## Install

```bash
npx @pushary/agent-hooks@latest setup --agents hermes
```

That installs the plugin into the interpreter Hermes runs in, enables it, and
selects Pushary as the approval transport. To do it by hand:

```bash
~/.hermes/hermes-agent/venv/bin/python -m pip install hermes-plugin-pushary
hermes plugins enable pushary
```

Hermes runs in its own virtualenv, so `pip install` must target that interpreter
rather than your system Python.

## Setup

```bash
export PUSHARY_API_KEY="pk_xxx.sk_xxx"
```

Get a key at [pushary.com/sign-up](https://pushary.com/sign-up?from=hermes).

## Tools

| Tool | Description |
|------|-------------|
| `pushary_notify` | Send a push notification with optional rich context |
| `pushary_ask` | Ask a question via push (yes/no, multiple choice, or free text) |
| `pushary_wait` | Poll once for the answer to a question created with `wait=false` |
| `pushary_cancel` | Cancel a pending question |
| `pushary_propose_scope` | Agree what a multi-step run may touch, in one tap, before starting |
| `pushary_enroll` | Connect one of your own end-users' phones (Partner plan) |
| `pushary_ask_end_user` | Ask one of your own end-users, fail-closed (Partner plan) |

## Approving from your lock screen

Hermes already detects dangerous commands and asks a human before running them.
This plugin registers `pushary` as an **approval transport**, so that question
goes to your phone instead of a terminal nobody is watching.

```yaml
# ~/.hermes/config.yaml
security:
  approval:
    transport: pushary
    transport_fallback: builtin
```

You get the same four choices Hermes offers in the terminal, and Hermes
remembers the last two exactly as it would have:

| Choice | Effect |
|--------|--------|
| Allow once | Runs this command |
| Allow for this session | Runs it, and stops asking for the rest of the session |
| Always allow | Runs it, and writes a standing allow rule |
| Deny | Blocks it, and the agent is told the user did not consent |

`transport_fallback: builtin` is what makes this safe to leave on: if your API
key is missing, Pushary is unreachable, or no device is connected, Hermes falls
back to the terminal prompt rather than blocking work. Drop the fallback line and
an unreachable Pushary becomes a denial instead.

The approval window is Hermes' own `approvals.timeout` (300 seconds by default),
so an answer is still yours to give minutes after the notification lands.

## Per-tool gating (without the transport)

To gate specific tools rather than Hermes' dangerous-command set, set
`PUSHARY_GATE_TOOLS`. This runs inside the `pre_tool_call` hook.

```bash
export PUSHARY_GATE_TOOLS="terminal,write_file"
export PUSHARY_GATE_TIMEOUT_MS=25000
```

| Outcome | Result |
|---------|--------|
| You approve | Tool runs |
| You deny | Tool is blocked, the agent is told why |
| No answer in the window | Tool is blocked (fail-closed) |
| Pushary unreachable / no API key | Tool is blocked (fail-closed) |

When no phone, browser, or Slack channel is connected, Pushary says so on the create call rather than waiting, and the gate blocks with that as the reason instead of something that reads like a refusal.

The window here is bounded by Hermes, not by us. Hermes abandons any
`pre_tool_call` callback that outlives `plugins.hook_callback_timeout` (30
seconds by default) and fails it closed, so the plugin clamps its own wait to fit
inside that budget and returns a real answer rather than being cut off mid-wait.
Raising `plugins.hook_callback_timeout` raises the window; the plugin reads it
and follows. A Hermes old enough to have no callback timeout at all runs the hook
to completion, and there the plugin uses the full window rather than shortening
it for a deadline that does not exist.

When both are configured the transport wins: the hook escalates to Hermes' own
approval gate, which gets the full 300-second window and the once/session/always
choices, instead of holding a 30-second hook open on a network call.

## Auto-notifications

Errors returned by any tool are pushed automatically, capped at three per
session. Set `PUSHARY_AUTO_NOTIFY_SESSION_END=1` to also get one notification
when a session finishes, with its tool and error counts.

Set `PUSHARY_AGENT_NAME` to identify this Hermes instance in notifications (e.g.
`"Hermes - daily-briefing"`). It defaults to `Hermes`.

## Tests

```bash
PYTHONPATH=. python3 -m unittest discover -s tests
```

`tests/test_hermes_contract.py` checks this plugin's assumptions against the
Hermes it is running inside: that every hook it registers is one Hermes fires,
that the callback budget it clamps to is the one Hermes enforces, and that the
approval request still carries what the transport reads. It skips where Hermes
is not importable, so run it with the interpreter Hermes uses, and after a
Hermes upgrade:

```bash
PYTHONPATH=. ~/.hermes/hermes-agent/venv/bin/python3 -m unittest discover -s tests
```

## License

MIT
