agentguard-spend-hermes 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentguard_spend_hermes-0.1.4/.gitignore +70 -0
- agentguard_spend_hermes-0.1.4/LICENSE +87 -0
- agentguard_spend_hermes-0.1.4/PKG-INFO +275 -0
- agentguard_spend_hermes-0.1.4/README.md +162 -0
- agentguard_spend_hermes-0.1.4/agentguard_spend_hermes/__init__.py +349 -0
- agentguard_spend_hermes-0.1.4/pyproject.toml +55 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
.vercel
|
|
2
|
+
.next/
|
|
3
|
+
node_modules/
|
|
4
|
+
|
|
5
|
+
/generated/prisma
|
|
6
|
+
|
|
7
|
+
# clerk configuration (can include secrets)
|
|
8
|
+
/.clerk/
|
|
9
|
+
|
|
10
|
+
# Environment files (NEVER commit secrets!)
|
|
11
|
+
.env
|
|
12
|
+
.env.local
|
|
13
|
+
.env.*.local
|
|
14
|
+
agent/.env
|
|
15
|
+
*.pem
|
|
16
|
+
|
|
17
|
+
# Hardhat artifacts
|
|
18
|
+
cache/
|
|
19
|
+
artifacts/
|
|
20
|
+
.env*.local
|
|
21
|
+
.env.production
|
|
22
|
+
supabase/.temp/
|
|
23
|
+
|
|
24
|
+
# Git worktrees
|
|
25
|
+
.worktrees/
|
|
26
|
+
|
|
27
|
+
# Sentinela ML model artifacts (large binary files — stored in GCS, not git)
|
|
28
|
+
models/sentinela/l1-xlmr-v1/
|
|
29
|
+
models/sentinela/l2-xgboost-v1/
|
|
30
|
+
models/sentinela/training_data.jsonl
|
|
31
|
+
!models/sentinela/.gitkeep
|
|
32
|
+
*.onnx
|
|
33
|
+
*.ubj
|
|
34
|
+
|
|
35
|
+
# Sentinela L1 Cloud Run -- large artifacts (downloaded in CI, never committed)
|
|
36
|
+
agents/sentinela-l1/models/*.onnx
|
|
37
|
+
agents/sentinela-l1/tokenizer/
|
|
38
|
+
!agents/sentinela-l1/models/.gitkeep
|
|
39
|
+
!agents/sentinela-l1/tokenizer/.gitkeep
|
|
40
|
+
|
|
41
|
+
# Python artifacts
|
|
42
|
+
__pycache__/
|
|
43
|
+
*.egg-info/
|
|
44
|
+
dist/
|
|
45
|
+
|
|
46
|
+
# Playwright MCP cache
|
|
47
|
+
.playwright-mcp/
|
|
48
|
+
|
|
49
|
+
# Vercel check artifacts
|
|
50
|
+
.env.vercel-check
|
|
51
|
+
|
|
52
|
+
# Build/test artifacts
|
|
53
|
+
*.tsbuildinfo
|
|
54
|
+
.turbo/
|
|
55
|
+
|
|
56
|
+
# Debug/log files
|
|
57
|
+
firebase-debug.log
|
|
58
|
+
|
|
59
|
+
# Script output artifacts
|
|
60
|
+
scripts/*.json
|
|
61
|
+
list-all-products.js
|
|
62
|
+
find-starter-product.js
|
|
63
|
+
create-starter-product.js
|
|
64
|
+
test-stripe-key.js
|
|
65
|
+
broadcast/
|
|
66
|
+
.DS_Store
|
|
67
|
+
|
|
68
|
+
# TypeScript build cache (auto-generated)
|
|
69
|
+
tsconfig.tsbuildinfo
|
|
70
|
+
*.tsbuildinfo
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
AgentGuard(TM) Spend SDK — Alpha License
|
|
2
|
+
Copyright (c) 2026 Dunecrest Ventures Inc.
|
|
3
|
+
|
|
4
|
+
1. SCOPE.
|
|
5
|
+
This software, including all files under packages/agentguard-spend-python/agentguard_spend/, is
|
|
6
|
+
licensed by Dunecrest Ventures Inc. ("Licensor") subject to the following
|
|
7
|
+
thresholds:
|
|
8
|
+
|
|
9
|
+
(a) Evaluation Use. Internal evaluation, prototyping, and non-commercial
|
|
10
|
+
development at any call volume.
|
|
11
|
+
|
|
12
|
+
(b) Free Production Threshold. Production deployments processing 10,000
|
|
13
|
+
or fewer enforcement calls per calendar month, in aggregate across
|
|
14
|
+
all instances operated by the licensee, are permitted under this
|
|
15
|
+
License without additional fee.
|
|
16
|
+
|
|
17
|
+
(c) Commercial License Required. Production deployments processing more
|
|
18
|
+
than 10,000 enforcement calls per calendar month, deployments
|
|
19
|
+
operated for the benefit of third parties as a service, redistribution,
|
|
20
|
+
sublicensing, public hosting, and republication each require a
|
|
21
|
+
separate commercial license agreement with Licensor.
|
|
22
|
+
|
|
23
|
+
Commercial-license inquiries: invest@agentguard.run
|
|
24
|
+
|
|
25
|
+
2. NO PATENT LICENSE GRANTED.
|
|
26
|
+
Nothing in this License grants, expressly or by implication, any patent license
|
|
27
|
+
to any patent, patent application, or other intellectual property right of
|
|
28
|
+
Licensor. All patent rights are expressly reserved. The patent applications
|
|
29
|
+
identified in Section 7 are not licensed by this License.
|
|
30
|
+
|
|
31
|
+
3. SEPARATE GRANT FOR DEMONSTRATION ASSETS.
|
|
32
|
+
The following assets, and ONLY these assets, are released under the Apache
|
|
33
|
+
License, Version 2.0, the text of which is reproduced or available at
|
|
34
|
+
https://www.apache.org/licenses/LICENSE-2.0:
|
|
35
|
+
|
|
36
|
+
- The test vectors under packages/agentguard-spend-python/test_vectors/
|
|
37
|
+
- The documentation examples under packages/agentguard-spend-python/examples/
|
|
38
|
+
- The contents of packages/agentguard-spend-python/README.md
|
|
39
|
+
|
|
40
|
+
The source code under packages/agentguard-spend-python/agentguard_spend/ is NOT included in this
|
|
41
|
+
Apache License 2.0 grant. The Python type definitions, policy engine,
|
|
42
|
+
decision log, store implementation, cost table, and wrapper code under
|
|
43
|
+
agentguard_spend/ are licensed only under the alpha evaluation terms of
|
|
44
|
+
Section 1 above.
|
|
45
|
+
|
|
46
|
+
4. WARRANTY DISCLAIMER.
|
|
47
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
48
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
49
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
50
|
+
DUNECREST VENTURES INC. BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY
|
|
51
|
+
ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
52
|
+
DEALINGS IN THE SOFTWARE.
|
|
53
|
+
|
|
54
|
+
5. SUCCESSORS AND ASSIGNS.
|
|
55
|
+
This License binds and benefits the parties' respective successors and assigns.
|
|
56
|
+
In the event of an asset sale, merger, change of control, or other transfer of
|
|
57
|
+
the Licensor's rights in this software, all rights and obligations under this
|
|
58
|
+
License inure to the benefit of and are binding upon Licensor's successor or
|
|
59
|
+
assignee. Outstanding evaluation grants survive change-of-control, but the
|
|
60
|
+
successor or assignee may, upon thirty (30) days' written notice, terminate
|
|
61
|
+
ongoing evaluation grants in favor of a commercial-license requirement.
|
|
62
|
+
|
|
63
|
+
6. TERMINATION.
|
|
64
|
+
Licensor may terminate this License with thirty (30) days' written notice for
|
|
65
|
+
any reason or no reason. Upon termination, Licensee shall cease all use of the
|
|
66
|
+
software under agentguard_spend/ and shall destroy all copies in Licensee's possession.
|
|
67
|
+
|
|
68
|
+
7. PATENT NOTICE (35 U.S.C. § 287).
|
|
69
|
+
Protected by U.S. patent-pending technology, including the following
|
|
70
|
+
provisional patent applications filed with the United States Patent and
|
|
71
|
+
Trademark Office:
|
|
72
|
+
|
|
73
|
+
- Application No. 63/983,615 (filed February 15, 2026)
|
|
74
|
+
- Application No. 63/983,621 (filed February 15, 2026)
|
|
75
|
+
- Application No. 63/983,843 (filed February 16, 2026)
|
|
76
|
+
- Application No. 63/984,626 (filed February 17, 2026)
|
|
77
|
+
- Application No. 64/071,781 (filed May 21, 2026)
|
|
78
|
+
- Application No. 64/071,789 (filed May 21, 2026)
|
|
79
|
+
|
|
80
|
+
Additional patents pending. All patent rights expressly reserved per
|
|
81
|
+
Section 2 above.
|
|
82
|
+
|
|
83
|
+
AgentGuard(TM) is a trademark of Dunecrest Ventures Inc. (USPTO Serial
|
|
84
|
+
No. 99462472, pending). MerchantGuard(TM) is a trademark of Dunecrest
|
|
85
|
+
Ventures Inc. (USPTO Serial No. 99051215, pending).
|
|
86
|
+
|
|
87
|
+
For commercial licensing: invest@agentguard.run
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentguard-spend-hermes
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: AgentGuard Spend plugin for Hermes Agent (Nous Research). One-step install of local spend caps + signed audit logs for every Anthropic/OpenAI call your Hermes agent makes. No proxy, prompts never leave your machine.
|
|
5
|
+
Project-URL: Homepage, https://agentguard.run
|
|
6
|
+
Project-URL: AgentGuard SDK (PyPI), https://pypi.org/project/agentguard-spend/
|
|
7
|
+
Project-URL: AgentGuard SDK (npm), https://www.npmjs.com/package/@agentguard-run/spend
|
|
8
|
+
Project-URL: Hermes Agent, https://github.com/NousResearch/hermes-agent
|
|
9
|
+
Project-URL: Source, https://github.com/MerchantGuard/merchantguard-website/tree/main/packages/agentguard-spend-hermes
|
|
10
|
+
Author-email: "Dunecrest Ventures Inc." <hello@agentguard.run>
|
|
11
|
+
License: AgentGuard(TM) Spend SDK — Alpha License
|
|
12
|
+
Copyright (c) 2026 Dunecrest Ventures Inc.
|
|
13
|
+
|
|
14
|
+
1. SCOPE.
|
|
15
|
+
This software, including all files under packages/agentguard-spend-python/agentguard_spend/, is
|
|
16
|
+
licensed by Dunecrest Ventures Inc. ("Licensor") subject to the following
|
|
17
|
+
thresholds:
|
|
18
|
+
|
|
19
|
+
(a) Evaluation Use. Internal evaluation, prototyping, and non-commercial
|
|
20
|
+
development at any call volume.
|
|
21
|
+
|
|
22
|
+
(b) Free Production Threshold. Production deployments processing 10,000
|
|
23
|
+
or fewer enforcement calls per calendar month, in aggregate across
|
|
24
|
+
all instances operated by the licensee, are permitted under this
|
|
25
|
+
License without additional fee.
|
|
26
|
+
|
|
27
|
+
(c) Commercial License Required. Production deployments processing more
|
|
28
|
+
than 10,000 enforcement calls per calendar month, deployments
|
|
29
|
+
operated for the benefit of third parties as a service, redistribution,
|
|
30
|
+
sublicensing, public hosting, and republication each require a
|
|
31
|
+
separate commercial license agreement with Licensor.
|
|
32
|
+
|
|
33
|
+
Commercial-license inquiries: invest@agentguard.run
|
|
34
|
+
|
|
35
|
+
2. NO PATENT LICENSE GRANTED.
|
|
36
|
+
Nothing in this License grants, expressly or by implication, any patent license
|
|
37
|
+
to any patent, patent application, or other intellectual property right of
|
|
38
|
+
Licensor. All patent rights are expressly reserved. The patent applications
|
|
39
|
+
identified in Section 7 are not licensed by this License.
|
|
40
|
+
|
|
41
|
+
3. SEPARATE GRANT FOR DEMONSTRATION ASSETS.
|
|
42
|
+
The following assets, and ONLY these assets, are released under the Apache
|
|
43
|
+
License, Version 2.0, the text of which is reproduced or available at
|
|
44
|
+
https://www.apache.org/licenses/LICENSE-2.0:
|
|
45
|
+
|
|
46
|
+
- The test vectors under packages/agentguard-spend-python/test_vectors/
|
|
47
|
+
- The documentation examples under packages/agentguard-spend-python/examples/
|
|
48
|
+
- The contents of packages/agentguard-spend-python/README.md
|
|
49
|
+
|
|
50
|
+
The source code under packages/agentguard-spend-python/agentguard_spend/ is NOT included in this
|
|
51
|
+
Apache License 2.0 grant. The Python type definitions, policy engine,
|
|
52
|
+
decision log, store implementation, cost table, and wrapper code under
|
|
53
|
+
agentguard_spend/ are licensed only under the alpha evaluation terms of
|
|
54
|
+
Section 1 above.
|
|
55
|
+
|
|
56
|
+
4. WARRANTY DISCLAIMER.
|
|
57
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
58
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
59
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
60
|
+
DUNECREST VENTURES INC. BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY
|
|
61
|
+
ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
62
|
+
DEALINGS IN THE SOFTWARE.
|
|
63
|
+
|
|
64
|
+
5. SUCCESSORS AND ASSIGNS.
|
|
65
|
+
This License binds and benefits the parties' respective successors and assigns.
|
|
66
|
+
In the event of an asset sale, merger, change of control, or other transfer of
|
|
67
|
+
the Licensor's rights in this software, all rights and obligations under this
|
|
68
|
+
License inure to the benefit of and are binding upon Licensor's successor or
|
|
69
|
+
assignee. Outstanding evaluation grants survive change-of-control, but the
|
|
70
|
+
successor or assignee may, upon thirty (30) days' written notice, terminate
|
|
71
|
+
ongoing evaluation grants in favor of a commercial-license requirement.
|
|
72
|
+
|
|
73
|
+
6. TERMINATION.
|
|
74
|
+
Licensor may terminate this License with thirty (30) days' written notice for
|
|
75
|
+
any reason or no reason. Upon termination, Licensee shall cease all use of the
|
|
76
|
+
software under agentguard_spend/ and shall destroy all copies in Licensee's possession.
|
|
77
|
+
|
|
78
|
+
7. PATENT NOTICE (35 U.S.C. § 287).
|
|
79
|
+
Protected by U.S. patent-pending technology, including the following
|
|
80
|
+
provisional patent applications filed with the United States Patent and
|
|
81
|
+
Trademark Office:
|
|
82
|
+
|
|
83
|
+
- Application No. 63/983,615 (filed February 15, 2026)
|
|
84
|
+
- Application No. 63/983,621 (filed February 15, 2026)
|
|
85
|
+
- Application No. 63/983,843 (filed February 16, 2026)
|
|
86
|
+
- Application No. 63/984,626 (filed February 17, 2026)
|
|
87
|
+
- Application No. 64/071,781 (filed May 21, 2026)
|
|
88
|
+
- Application No. 64/071,789 (filed May 21, 2026)
|
|
89
|
+
|
|
90
|
+
Additional patents pending. All patent rights expressly reserved per
|
|
91
|
+
Section 2 above.
|
|
92
|
+
|
|
93
|
+
AgentGuard(TM) is a trademark of Dunecrest Ventures Inc. (USPTO Serial
|
|
94
|
+
No. 99462472, pending). MerchantGuard(TM) is a trademark of Dunecrest
|
|
95
|
+
Ventures Inc. (USPTO Serial No. 99051215, pending).
|
|
96
|
+
|
|
97
|
+
For commercial licensing: invest@agentguard.run
|
|
98
|
+
License-File: LICENSE
|
|
99
|
+
Keywords: agent-plugin,agentguard,ai-agent-security,hermes,hermes-agent,llm-governance,nous-research,spend-control
|
|
100
|
+
Classifier: Development Status :: 4 - Beta
|
|
101
|
+
Classifier: Intended Audience :: Developers
|
|
102
|
+
Classifier: License :: Other/Proprietary License
|
|
103
|
+
Classifier: Programming Language :: Python :: 3
|
|
104
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
105
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
106
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
107
|
+
Classifier: Topic :: Security
|
|
108
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
109
|
+
Requires-Python: >=3.10
|
|
110
|
+
Requires-Dist: agentguard-spend>=0.1.4
|
|
111
|
+
Requires-Dist: pyyaml>=6.0
|
|
112
|
+
Description-Content-Type: text/markdown
|
|
113
|
+
|
|
114
|
+
# agentguard-spend-hermes
|
|
115
|
+
|
|
116
|
+
> One-step install of [AgentGuard™ Spend](https://pypi.org/project/agentguard-spend/) into [Hermes Agent](https://github.com/NousResearch/hermes-agent) (Nous Research).
|
|
117
|
+
|
|
118
|
+
Adds **local spend caps + Ed25519-signed audit logs** to every Anthropic and OpenAI call your Hermes agent makes. No proxy. No cloud component. Prompts and provider keys never leave your machine.
|
|
119
|
+
|
|
120
|
+
Built for Hermes's unattended-by-default workflow: cron jobs, sleep loops, multi-platform messaging bridges (Telegram, Discord, Slack, WhatsApp, Signal).
|
|
121
|
+
|
|
122
|
+
## Install
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# In the same virtualenv as Hermes
|
|
126
|
+
uv pip install agentguard-spend-hermes
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
That's it. Hermes's plugin manager auto-discovers the plugin via the `hermes_agent.plugins` entry point on next startup.
|
|
130
|
+
|
|
131
|
+
## First-run auto-bootstrap
|
|
132
|
+
|
|
133
|
+
On first Hermes startup after install, the plugin:
|
|
134
|
+
|
|
135
|
+
1. Generates `~/.hermes/agentguard/private.key` + `public.key` (Ed25519)
|
|
136
|
+
2. Writes a default `policy.yaml` ($5/day soft cap downgrades Opus → Sonnet, $20/day hard cap blocks, $2/min burst guard)
|
|
137
|
+
3. Monkey-patches the Anthropic and OpenAI client constructors so every LLM call is policy-checked + signed
|
|
138
|
+
|
|
139
|
+
You'll see in the Hermes log:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
agentguard-spend: wrote default policy to ~/.hermes/agentguard/policy.yaml (locale: en-US)
|
|
143
|
+
agentguard-spend: generated signing keypair (private=..., public=...)
|
|
144
|
+
agentguard-spend: active (anthropic=True, openai=early, policy=...)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The policy file is localized to your system locale at first-write (`en-US`, `es-419`, or `pt-BR`).
|
|
148
|
+
|
|
149
|
+
## When a cap fires
|
|
150
|
+
|
|
151
|
+
**Downgrade (soft cap):**
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
[hermes] anthropic.messages.create model=claude-opus-4-7
|
|
155
|
+
[agentguard] downgrade: claude-opus-4-7 -> claude-sonnet-4-6 (daily soft cap)
|
|
156
|
+
[hermes] continuing with sonnet
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Block (hard cap):**
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
[hermes] anthropic.messages.create model=claude-opus-4-7
|
|
163
|
+
[agentguard] block: hard daily ceiling exceeded
|
|
164
|
+
[hermes] AgentGuardBlockedError raised — agent halts this turn
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Configure
|
|
168
|
+
|
|
169
|
+
Edit `~/.hermes/agentguard/policy.yaml` and restart Hermes. Example:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
id: hermes-default-v1
|
|
173
|
+
name: Hermes default caps
|
|
174
|
+
version: 1
|
|
175
|
+
effectiveFrom: "2026-01-01T00:00:00Z"
|
|
176
|
+
mode: enforce
|
|
177
|
+
|
|
178
|
+
scope:
|
|
179
|
+
tenantId: hermes-personal
|
|
180
|
+
|
|
181
|
+
caps:
|
|
182
|
+
- amountCents: 500 # $5/day soft cap
|
|
183
|
+
window: per_day
|
|
184
|
+
action: downgrade
|
|
185
|
+
downgradeTo: claude-sonnet-4-6
|
|
186
|
+
- amountCents: 2000 # $20/day hard cap
|
|
187
|
+
window: per_day
|
|
188
|
+
action: block
|
|
189
|
+
- amountCents: 200 # $2/minute burst guard
|
|
190
|
+
window: per_minute
|
|
191
|
+
action: block
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Disable temporarily
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
export AGENTGUARD_DISABLE=1 # plugin loads but does nothing
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Per-platform spend caps
|
|
201
|
+
|
|
202
|
+
Set `HERMES_AGENT_ID` differently in each Hermes process to scope caps:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Telegram bridge process
|
|
206
|
+
export HERMES_AGENT_ID=hermes-telegram
|
|
207
|
+
|
|
208
|
+
# Discord bridge process
|
|
209
|
+
export HERMES_AGENT_ID=hermes-discord
|
|
210
|
+
|
|
211
|
+
# Cron job
|
|
212
|
+
export HERMES_AGENT_ID=hermes-cron
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Then write per-agent caps in `policy.yaml`:
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
caps:
|
|
219
|
+
- amountCents: 1000
|
|
220
|
+
window: per_day
|
|
221
|
+
scope: {agentId: hermes-telegram}
|
|
222
|
+
action: block
|
|
223
|
+
- amountCents: 500
|
|
224
|
+
window: per_day
|
|
225
|
+
scope: {agentId: hermes-cron}
|
|
226
|
+
action: block
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Verify the audit log
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from agentguard_spend import verify_chain
|
|
233
|
+
import pathlib, json
|
|
234
|
+
|
|
235
|
+
public_key = pathlib.Path("~/.hermes/agentguard/public.key").expanduser().read_bytes()
|
|
236
|
+
entries_path = pathlib.Path("~/.hermes/agentguard/decisions.ndjson").expanduser()
|
|
237
|
+
entries = [json.loads(line) for line in entries_path.read_text().splitlines()]
|
|
238
|
+
result = verify_chain(entries, public_key)
|
|
239
|
+
print("OK" if result.ok else f"Chain broken at #{result.sequence}: {result.reason}")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## What this plugin does NOT change in Hermes
|
|
243
|
+
|
|
244
|
+
- Hermes's request retry logic
|
|
245
|
+
- Hermes's credential refresh flow
|
|
246
|
+
- Hermes's interrupt / cancel pattern
|
|
247
|
+
- Hermes's MCP / tool-call layer
|
|
248
|
+
- Any of the existing skills
|
|
249
|
+
|
|
250
|
+
Strictly an LLM-call-intercept layer between Hermes and the Anthropic/OpenAI wire.
|
|
251
|
+
|
|
252
|
+
## Localization
|
|
253
|
+
|
|
254
|
+
Block traces render in the active locale:
|
|
255
|
+
- 🇺🇸 `en-US` (default)
|
|
256
|
+
- 🇲🇽 🇦🇷 🇨🇴 🇨🇱 `es-419` (Latin American Spanish)
|
|
257
|
+
- 🇧🇷 `pt-BR` (Brazilian Portuguese)
|
|
258
|
+
|
|
259
|
+
Resolution chain: explicit `AGENTGUARD_LOCALE` env var → `LC_ALL` / `LC_MESSAGES` / `LANG` → system locale → en-US fallback.
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
BUSL-1.1. Free for production deployments processing **up to 10,000 enforcement calls per calendar month**. Commercial licensing for higher volumes or service redistribution: `invest@agentguard.run`.
|
|
264
|
+
|
|
265
|
+
## Patents
|
|
266
|
+
|
|
267
|
+
Protected by 7 U.S. patent-pending applications (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626; 64/071,781; 64/071,789; plus DV-2026-007 in active filing). See https://agentguard.run/patents.
|
|
268
|
+
|
|
269
|
+
## Links
|
|
270
|
+
|
|
271
|
+
- **Homepage:** https://agentguard.run
|
|
272
|
+
- **Underlying SDK:** [`agentguard-spend`](https://pypi.org/project/agentguard-spend/) (PyPI)
|
|
273
|
+
- **TypeScript counterpart:** [`@agentguard-run/spend`](https://www.npmjs.com/package/@agentguard-run/spend) (npm)
|
|
274
|
+
- **Hermes Agent:** https://github.com/NousResearch/hermes-agent
|
|
275
|
+
- **Contact:** `invest@agentguard.run`
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# agentguard-spend-hermes
|
|
2
|
+
|
|
3
|
+
> One-step install of [AgentGuard™ Spend](https://pypi.org/project/agentguard-spend/) into [Hermes Agent](https://github.com/NousResearch/hermes-agent) (Nous Research).
|
|
4
|
+
|
|
5
|
+
Adds **local spend caps + Ed25519-signed audit logs** to every Anthropic and OpenAI call your Hermes agent makes. No proxy. No cloud component. Prompts and provider keys never leave your machine.
|
|
6
|
+
|
|
7
|
+
Built for Hermes's unattended-by-default workflow: cron jobs, sleep loops, multi-platform messaging bridges (Telegram, Discord, Slack, WhatsApp, Signal).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# In the same virtualenv as Hermes
|
|
13
|
+
uv pip install agentguard-spend-hermes
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
That's it. Hermes's plugin manager auto-discovers the plugin via the `hermes_agent.plugins` entry point on next startup.
|
|
17
|
+
|
|
18
|
+
## First-run auto-bootstrap
|
|
19
|
+
|
|
20
|
+
On first Hermes startup after install, the plugin:
|
|
21
|
+
|
|
22
|
+
1. Generates `~/.hermes/agentguard/private.key` + `public.key` (Ed25519)
|
|
23
|
+
2. Writes a default `policy.yaml` ($5/day soft cap downgrades Opus → Sonnet, $20/day hard cap blocks, $2/min burst guard)
|
|
24
|
+
3. Monkey-patches the Anthropic and OpenAI client constructors so every LLM call is policy-checked + signed
|
|
25
|
+
|
|
26
|
+
You'll see in the Hermes log:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
agentguard-spend: wrote default policy to ~/.hermes/agentguard/policy.yaml (locale: en-US)
|
|
30
|
+
agentguard-spend: generated signing keypair (private=..., public=...)
|
|
31
|
+
agentguard-spend: active (anthropic=True, openai=early, policy=...)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The policy file is localized to your system locale at first-write (`en-US`, `es-419`, or `pt-BR`).
|
|
35
|
+
|
|
36
|
+
## When a cap fires
|
|
37
|
+
|
|
38
|
+
**Downgrade (soft cap):**
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
[hermes] anthropic.messages.create model=claude-opus-4-7
|
|
42
|
+
[agentguard] downgrade: claude-opus-4-7 -> claude-sonnet-4-6 (daily soft cap)
|
|
43
|
+
[hermes] continuing with sonnet
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Block (hard cap):**
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
[hermes] anthropic.messages.create model=claude-opus-4-7
|
|
50
|
+
[agentguard] block: hard daily ceiling exceeded
|
|
51
|
+
[hermes] AgentGuardBlockedError raised — agent halts this turn
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configure
|
|
55
|
+
|
|
56
|
+
Edit `~/.hermes/agentguard/policy.yaml` and restart Hermes. Example:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
id: hermes-default-v1
|
|
60
|
+
name: Hermes default caps
|
|
61
|
+
version: 1
|
|
62
|
+
effectiveFrom: "2026-01-01T00:00:00Z"
|
|
63
|
+
mode: enforce
|
|
64
|
+
|
|
65
|
+
scope:
|
|
66
|
+
tenantId: hermes-personal
|
|
67
|
+
|
|
68
|
+
caps:
|
|
69
|
+
- amountCents: 500 # $5/day soft cap
|
|
70
|
+
window: per_day
|
|
71
|
+
action: downgrade
|
|
72
|
+
downgradeTo: claude-sonnet-4-6
|
|
73
|
+
- amountCents: 2000 # $20/day hard cap
|
|
74
|
+
window: per_day
|
|
75
|
+
action: block
|
|
76
|
+
- amountCents: 200 # $2/minute burst guard
|
|
77
|
+
window: per_minute
|
|
78
|
+
action: block
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Disable temporarily
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
export AGENTGUARD_DISABLE=1 # plugin loads but does nothing
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Per-platform spend caps
|
|
88
|
+
|
|
89
|
+
Set `HERMES_AGENT_ID` differently in each Hermes process to scope caps:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Telegram bridge process
|
|
93
|
+
export HERMES_AGENT_ID=hermes-telegram
|
|
94
|
+
|
|
95
|
+
# Discord bridge process
|
|
96
|
+
export HERMES_AGENT_ID=hermes-discord
|
|
97
|
+
|
|
98
|
+
# Cron job
|
|
99
|
+
export HERMES_AGENT_ID=hermes-cron
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Then write per-agent caps in `policy.yaml`:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
caps:
|
|
106
|
+
- amountCents: 1000
|
|
107
|
+
window: per_day
|
|
108
|
+
scope: {agentId: hermes-telegram}
|
|
109
|
+
action: block
|
|
110
|
+
- amountCents: 500
|
|
111
|
+
window: per_day
|
|
112
|
+
scope: {agentId: hermes-cron}
|
|
113
|
+
action: block
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Verify the audit log
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from agentguard_spend import verify_chain
|
|
120
|
+
import pathlib, json
|
|
121
|
+
|
|
122
|
+
public_key = pathlib.Path("~/.hermes/agentguard/public.key").expanduser().read_bytes()
|
|
123
|
+
entries_path = pathlib.Path("~/.hermes/agentguard/decisions.ndjson").expanduser()
|
|
124
|
+
entries = [json.loads(line) for line in entries_path.read_text().splitlines()]
|
|
125
|
+
result = verify_chain(entries, public_key)
|
|
126
|
+
print("OK" if result.ok else f"Chain broken at #{result.sequence}: {result.reason}")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## What this plugin does NOT change in Hermes
|
|
130
|
+
|
|
131
|
+
- Hermes's request retry logic
|
|
132
|
+
- Hermes's credential refresh flow
|
|
133
|
+
- Hermes's interrupt / cancel pattern
|
|
134
|
+
- Hermes's MCP / tool-call layer
|
|
135
|
+
- Any of the existing skills
|
|
136
|
+
|
|
137
|
+
Strictly an LLM-call-intercept layer between Hermes and the Anthropic/OpenAI wire.
|
|
138
|
+
|
|
139
|
+
## Localization
|
|
140
|
+
|
|
141
|
+
Block traces render in the active locale:
|
|
142
|
+
- 🇺🇸 `en-US` (default)
|
|
143
|
+
- 🇲🇽 🇦🇷 🇨🇴 🇨🇱 `es-419` (Latin American Spanish)
|
|
144
|
+
- 🇧🇷 `pt-BR` (Brazilian Portuguese)
|
|
145
|
+
|
|
146
|
+
Resolution chain: explicit `AGENTGUARD_LOCALE` env var → `LC_ALL` / `LC_MESSAGES` / `LANG` → system locale → en-US fallback.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
BUSL-1.1. Free for production deployments processing **up to 10,000 enforcement calls per calendar month**. Commercial licensing for higher volumes or service redistribution: `invest@agentguard.run`.
|
|
151
|
+
|
|
152
|
+
## Patents
|
|
153
|
+
|
|
154
|
+
Protected by 7 U.S. patent-pending applications (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626; 64/071,781; 64/071,789; plus DV-2026-007 in active filing). See https://agentguard.run/patents.
|
|
155
|
+
|
|
156
|
+
## Links
|
|
157
|
+
|
|
158
|
+
- **Homepage:** https://agentguard.run
|
|
159
|
+
- **Underlying SDK:** [`agentguard-spend`](https://pypi.org/project/agentguard-spend/) (PyPI)
|
|
160
|
+
- **TypeScript counterpart:** [`@agentguard-run/spend`](https://www.npmjs.com/package/@agentguard-run/spend) (npm)
|
|
161
|
+
- **Hermes Agent:** https://github.com/NousResearch/hermes-agent
|
|
162
|
+
- **Contact:** `invest@agentguard.run`
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentGuard Spend — Hermes plugin.
|
|
3
|
+
|
|
4
|
+
Adds local spend cap enforcement + Ed25519-signed audit logs to every
|
|
5
|
+
Anthropic / OpenAI call this Hermes agent makes.
|
|
6
|
+
|
|
7
|
+
Activation model
|
|
8
|
+
----------------
|
|
9
|
+
On register() we monkey-patch the two LLM client construction sites Hermes
|
|
10
|
+
uses:
|
|
11
|
+
|
|
12
|
+
1. agent.anthropic_adapter.build_anthropic_client (Anthropic chokepoint)
|
|
13
|
+
2. run_agent.AIAgent._create_request_openai_client (OpenAI chokepoint)
|
|
14
|
+
|
|
15
|
+
Both patched functions wrap the freshly-constructed client with AgentGuard's
|
|
16
|
+
spend guard before returning it. Hermes's normal call sites (messages.create,
|
|
17
|
+
chat.completions.create) are untouched.
|
|
18
|
+
|
|
19
|
+
No-op when AGENTGUARD_POLICY_PATH is not set, so the plugin is safe to ship
|
|
20
|
+
by default.
|
|
21
|
+
|
|
22
|
+
Auto-bootstrap on first use
|
|
23
|
+
---------------------------
|
|
24
|
+
If the user installs the plugin but hasn't configured a policy:
|
|
25
|
+
- Generate ~/.hermes/agentguard/private.key + public.key (one-time)
|
|
26
|
+
- Write ~/.hermes/agentguard/policy.yaml with conservative defaults
|
|
27
|
+
($5/day soft cap downgrades Opus to Sonnet, $20/day hard cap blocks)
|
|
28
|
+
- Log where everything was written, then continue running
|
|
29
|
+
|
|
30
|
+
That way the user goes from `hermes plugins install agentguard-spend` to
|
|
31
|
+
working spend caps in one step with sensible defaults.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
import pathlib
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
_DEFAULT_HERMES_HOME = "~/.hermes"
|
|
43
|
+
_AG_DIR_NAME = "agentguard"
|
|
44
|
+
_POLICY_FILENAME = "policy.yaml"
|
|
45
|
+
_PRIVATE_KEY_FILENAME = "private.key"
|
|
46
|
+
_PUBLIC_KEY_FILENAME = "public.key"
|
|
47
|
+
|
|
48
|
+
_POLICY_YAML_TEMPLATE = """\
|
|
49
|
+
# AgentGuard Spend default policy (auto-generated on first install).
|
|
50
|
+
# {edit_hint}
|
|
51
|
+
id: hermes-default-v1
|
|
52
|
+
name: {policy_name}
|
|
53
|
+
version: 1
|
|
54
|
+
effectiveFrom: "2026-01-01T00:00:00Z"
|
|
55
|
+
mode: enforce
|
|
56
|
+
|
|
57
|
+
scope:
|
|
58
|
+
tenantId: hermes-personal
|
|
59
|
+
|
|
60
|
+
caps:
|
|
61
|
+
- amountCents: 500
|
|
62
|
+
window: per_day
|
|
63
|
+
action: downgrade
|
|
64
|
+
downgradeTo: claude-sonnet-4-6
|
|
65
|
+
reason: "{soft_cap_reason}"
|
|
66
|
+
|
|
67
|
+
- amountCents: 2000
|
|
68
|
+
window: per_day
|
|
69
|
+
action: block
|
|
70
|
+
reason: "{hard_cap_reason}"
|
|
71
|
+
|
|
72
|
+
- amountCents: 200
|
|
73
|
+
window: per_minute
|
|
74
|
+
action: block
|
|
75
|
+
reason: "{burst_reason}"
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_POLICY_LOCALE_STRINGS = {
|
|
80
|
+
"en-US": {
|
|
81
|
+
"edit_hint": "Edit this file to change your caps. All amounts are integer cents.",
|
|
82
|
+
"policy_name": "Hermes default caps",
|
|
83
|
+
"soft_cap_reason": "Soft daily cap ($5/day) — downgrading Opus to Sonnet",
|
|
84
|
+
"hard_cap_reason": "Hard daily ceiling ($20/day)",
|
|
85
|
+
"burst_reason": "Per-minute burst guard ($2/min)",
|
|
86
|
+
},
|
|
87
|
+
"es-419": {
|
|
88
|
+
"edit_hint": "Edite este archivo para cambiar sus límites. Todos los montos son enteros en centavos.",
|
|
89
|
+
"policy_name": "Límites predeterminados de Hermes",
|
|
90
|
+
"soft_cap_reason": "Límite diario blando ($5/día) — degradando Opus a Sonnet",
|
|
91
|
+
"hard_cap_reason": "Tope diario duro ($20/día)",
|
|
92
|
+
"burst_reason": "Protección de ráfaga por minuto ($2/min)",
|
|
93
|
+
},
|
|
94
|
+
"pt-BR": {
|
|
95
|
+
"edit_hint": "Edite este arquivo para alterar seus limites. Todos os valores são números inteiros em centavos.",
|
|
96
|
+
"policy_name": "Limites padrão do Hermes",
|
|
97
|
+
"soft_cap_reason": "Limite diário leve ($5/dia) — rebaixando Opus para Sonnet",
|
|
98
|
+
"hard_cap_reason": "Teto diário rígido ($20/dia)",
|
|
99
|
+
"burst_reason": "Proteção de pico por minuto ($2/min)",
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _resolve_install_locale() -> str:
|
|
105
|
+
"""Resolve locale for the auto-bootstrap policy at install time.
|
|
106
|
+
|
|
107
|
+
Uses the same priority chain as the agentguard-spend SDK runtime,
|
|
108
|
+
so the bootstrapped policy reasons match the locale the runtime
|
|
109
|
+
will later render block traces in.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
from agentguard_spend import resolve_locale
|
|
113
|
+
return resolve_locale()
|
|
114
|
+
except ImportError:
|
|
115
|
+
return "en-US"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _build_default_policy_yaml() -> str:
|
|
119
|
+
locale = _resolve_install_locale()
|
|
120
|
+
strings = _POLICY_LOCALE_STRINGS.get(locale, _POLICY_LOCALE_STRINGS["en-US"])
|
|
121
|
+
return _POLICY_YAML_TEMPLATE.format(**strings)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _ag_dir() -> pathlib.Path:
|
|
125
|
+
home = os.environ.get("HERMES_HOME", os.path.expanduser(_DEFAULT_HERMES_HOME))
|
|
126
|
+
return pathlib.Path(home).expanduser() / _AG_DIR_NAME
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _bootstrap_if_needed() -> dict[str, pathlib.Path] | None:
|
|
130
|
+
"""Create policy + keys on first run if absent. Returns paths dict or None on failure."""
|
|
131
|
+
d = _ag_dir()
|
|
132
|
+
try:
|
|
133
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
except OSError as exc:
|
|
135
|
+
logger.warning("agentguard-spend: cannot create %s (%s); plugin disabled", d, exc)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
policy_path = d / _POLICY_FILENAME
|
|
139
|
+
private_key_path = d / _PRIVATE_KEY_FILENAME
|
|
140
|
+
public_key_path = d / _PUBLIC_KEY_FILENAME
|
|
141
|
+
|
|
142
|
+
if not policy_path.exists():
|
|
143
|
+
policy_path.write_text(_build_default_policy_yaml())
|
|
144
|
+
logger.info(
|
|
145
|
+
"agentguard-spend: wrote default policy to %s (locale: %s)",
|
|
146
|
+
policy_path,
|
|
147
|
+
_resolve_install_locale(),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not private_key_path.exists() or not public_key_path.exists():
|
|
151
|
+
try:
|
|
152
|
+
from agentguard_spend import generate_keypair
|
|
153
|
+
except ImportError:
|
|
154
|
+
logger.warning(
|
|
155
|
+
"agentguard-spend: package not installed — run `uv pip install agentguard-spend` "
|
|
156
|
+
"in the Hermes virtualenv to enable. Plugin disabled until then."
|
|
157
|
+
)
|
|
158
|
+
return None
|
|
159
|
+
priv, pub = generate_keypair()
|
|
160
|
+
private_key_path.write_bytes(priv)
|
|
161
|
+
public_key_path.write_bytes(pub)
|
|
162
|
+
logger.info(
|
|
163
|
+
"agentguard-spend: generated signing keypair (private=%s, public=%s)",
|
|
164
|
+
private_key_path,
|
|
165
|
+
public_key_path,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"policy": policy_path,
|
|
170
|
+
"private_key": private_key_path,
|
|
171
|
+
"public_key": public_key_path,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _load_policy(policy_path: pathlib.Path) -> Any | None:
|
|
176
|
+
try:
|
|
177
|
+
import yaml
|
|
178
|
+
from agentguard_spend import SpendPolicy, SpendScope, SpendCap
|
|
179
|
+
except ImportError:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
raw = yaml.safe_load(policy_path.read_text())
|
|
183
|
+
return SpendPolicy(
|
|
184
|
+
id=raw["id"],
|
|
185
|
+
name=raw.get("name", raw["id"]),
|
|
186
|
+
scope=SpendScope(**raw.get("scope", {})),
|
|
187
|
+
caps=[SpendCap(**c) for c in raw.get("caps", [])],
|
|
188
|
+
mode=raw.get("mode", "enforce"),
|
|
189
|
+
version=raw.get("version", 1),
|
|
190
|
+
effectiveFrom=raw["effectiveFrom"],
|
|
191
|
+
requiredCapability=raw.get("requiredCapability"),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _build_config(policy: Any, private_key: bytes, public_key: bytes) -> Any:
|
|
196
|
+
from agentguard_spend import (
|
|
197
|
+
SpendGuardConfig,
|
|
198
|
+
SigningKeys,
|
|
199
|
+
InMemorySpendStore,
|
|
200
|
+
InMemoryDecisionLogStore,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return SpendGuardConfig(
|
|
204
|
+
policy=policy,
|
|
205
|
+
spendStore=InMemorySpendStore(),
|
|
206
|
+
logStore=InMemoryDecisionLogStore(),
|
|
207
|
+
signingKeys=SigningKeys(privateKey=private_key, publicKey=public_key),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _runtime_scope() -> Any:
|
|
212
|
+
from agentguard_spend import SpendScope
|
|
213
|
+
return SpendScope(
|
|
214
|
+
tenantId=os.environ.get("HERMES_TENANT", "hermes-personal"),
|
|
215
|
+
agentId=os.environ.get("HERMES_AGENT_ID", "hermes-agent"),
|
|
216
|
+
userId=os.environ.get("HERMES_USER_ID") or None,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _install_anthropic_patch(policy: Any, private_key: bytes, public_key: bytes) -> bool:
|
|
221
|
+
"""Patch agent.anthropic_adapter.build_anthropic_client to wrap on construction."""
|
|
222
|
+
try:
|
|
223
|
+
from agent import anthropic_adapter
|
|
224
|
+
from agentguard_spend.bindings import with_anthropic_spend_guard
|
|
225
|
+
except ImportError as exc:
|
|
226
|
+
logger.warning("agentguard-spend: anthropic patch unavailable (%s)", exc)
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
original = anthropic_adapter.build_anthropic_client
|
|
230
|
+
|
|
231
|
+
def patched(api_key: str, base_url: str = None):
|
|
232
|
+
client = original(api_key, base_url)
|
|
233
|
+
if client is None:
|
|
234
|
+
return client
|
|
235
|
+
try:
|
|
236
|
+
return with_anthropic_spend_guard(
|
|
237
|
+
client,
|
|
238
|
+
policy=policy,
|
|
239
|
+
scope=_runtime_scope(),
|
|
240
|
+
config=_build_config(policy, private_key, public_key),
|
|
241
|
+
)
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
logger.warning(
|
|
244
|
+
"agentguard-spend: anthropic wrap failed (%s) — returning raw client",
|
|
245
|
+
exc,
|
|
246
|
+
)
|
|
247
|
+
return client
|
|
248
|
+
|
|
249
|
+
anthropic_adapter.build_anthropic_client = patched
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
_openai_patch_installed = False
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _install_openai_patch(policy: Any, private_key: bytes, public_key: bytes) -> bool:
|
|
257
|
+
"""Patch AIAgent._create_request_openai_client to wrap on construction.
|
|
258
|
+
|
|
259
|
+
Called both from register() (early-load attempt) and from on_session_start
|
|
260
|
+
(deferred fallback). Early-load may fail with AIAgent=None because
|
|
261
|
+
run_agent.py is mid-import when register() runs; the deferred call
|
|
262
|
+
succeeds because by then run_agent has fully loaded.
|
|
263
|
+
"""
|
|
264
|
+
global _openai_patch_installed
|
|
265
|
+
if _openai_patch_installed:
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
import sys
|
|
270
|
+
run_agent = sys.modules.get("run_agent")
|
|
271
|
+
if run_agent is None:
|
|
272
|
+
import run_agent # noqa: F811
|
|
273
|
+
from agentguard_spend.bindings import with_openai_spend_guard
|
|
274
|
+
except ImportError as exc:
|
|
275
|
+
logger.warning("agentguard-spend: openai patch unavailable (%s)", exc)
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
AIAgent = getattr(run_agent, "AIAgent", None)
|
|
279
|
+
if AIAgent is None:
|
|
280
|
+
# Silent during register() because run_agent is mid-import; the
|
|
281
|
+
# deferred on_session_start call will install successfully.
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
original = AIAgent._create_request_openai_client
|
|
285
|
+
|
|
286
|
+
def patched(self, *, reason: str):
|
|
287
|
+
client = original(self, reason=reason)
|
|
288
|
+
if client is None:
|
|
289
|
+
return client
|
|
290
|
+
try:
|
|
291
|
+
return with_openai_spend_guard(
|
|
292
|
+
client,
|
|
293
|
+
policy=policy,
|
|
294
|
+
scope=_runtime_scope(),
|
|
295
|
+
config=_build_config(policy, private_key, public_key),
|
|
296
|
+
)
|
|
297
|
+
except Exception as exc:
|
|
298
|
+
logger.warning(
|
|
299
|
+
"agentguard-spend: openai wrap failed (%s) — returning raw client",
|
|
300
|
+
exc,
|
|
301
|
+
)
|
|
302
|
+
return client
|
|
303
|
+
|
|
304
|
+
AIAgent._create_request_openai_client = patched
|
|
305
|
+
_openai_patch_installed = True
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _log_completed_call(session_id: str = "", **kwargs) -> None:
|
|
310
|
+
"""Optional post-llm telemetry hook. Currently a noop — reserved for v0.2."""
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def register(ctx) -> None:
|
|
315
|
+
"""Called by Hermes plugin manager when the plugin is loaded."""
|
|
316
|
+
if os.environ.get("AGENTGUARD_DISABLE", "").lower() in {"1", "true", "yes"}:
|
|
317
|
+
logger.info("agentguard-spend: disabled via AGENTGUARD_DISABLE env var")
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
paths = _bootstrap_if_needed()
|
|
321
|
+
if paths is None:
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
policy = _load_policy(paths["policy"])
|
|
325
|
+
if policy is None:
|
|
326
|
+
logger.warning("agentguard-spend: policy load failed; plugin disabled")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
private_key = paths["private_key"].read_bytes()
|
|
330
|
+
public_key = paths["public_key"].read_bytes()
|
|
331
|
+
|
|
332
|
+
anthropic_ok = _install_anthropic_patch(policy, private_key, public_key)
|
|
333
|
+
|
|
334
|
+
# Try the openai patch now; if AIAgent isn't yet defined (partial import),
|
|
335
|
+
# the on_session_start hook will retry once run_agent has fully loaded.
|
|
336
|
+
openai_ok_early = _install_openai_patch(policy, private_key, public_key)
|
|
337
|
+
|
|
338
|
+
def _retry_openai(**_):
|
|
339
|
+
_install_openai_patch(policy, private_key, public_key)
|
|
340
|
+
|
|
341
|
+
ctx.register_hook("on_session_start", _retry_openai)
|
|
342
|
+
ctx.register_hook("post_llm_call", _log_completed_call)
|
|
343
|
+
|
|
344
|
+
logger.info(
|
|
345
|
+
"agentguard-spend: active (anthropic=%s, openai=%s, policy=%s)",
|
|
346
|
+
anthropic_ok,
|
|
347
|
+
"early" if openai_ok_early else "deferred-to-session-start",
|
|
348
|
+
paths["policy"],
|
|
349
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentguard-spend-hermes"
|
|
7
|
+
version = "0.1.4"
|
|
8
|
+
description = "AgentGuard Spend plugin for Hermes Agent (Nous Research). One-step install of local spend caps + signed audit logs for every Anthropic/OpenAI call your Hermes agent makes. No proxy, prompts never leave your machine."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Dunecrest Ventures Inc.", email = "hello@agentguard.run" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"hermes",
|
|
15
|
+
"hermes-agent",
|
|
16
|
+
"nous-research",
|
|
17
|
+
"agentguard",
|
|
18
|
+
"ai-agent-security",
|
|
19
|
+
"spend-control",
|
|
20
|
+
"llm-governance",
|
|
21
|
+
"agent-plugin",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: Other/Proprietary License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Topic :: Security",
|
|
32
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
|
+
]
|
|
34
|
+
dependencies = [
|
|
35
|
+
"agentguard-spend>=0.1.4",
|
|
36
|
+
"pyyaml>=6.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://agentguard.run"
|
|
41
|
+
"AgentGuard SDK (PyPI)" = "https://pypi.org/project/agentguard-spend/"
|
|
42
|
+
"AgentGuard SDK (npm)" = "https://www.npmjs.com/package/@agentguard-run/spend"
|
|
43
|
+
"Hermes Agent" = "https://github.com/NousResearch/hermes-agent"
|
|
44
|
+
"Source" = "https://github.com/MerchantGuard/merchantguard-website/tree/main/packages/agentguard-spend-hermes"
|
|
45
|
+
|
|
46
|
+
# Hermes plugin auto-discovery entry point.
|
|
47
|
+
# When agentguard-spend-hermes is pip-installed in the same Python
|
|
48
|
+
# environment as Hermes Agent, Hermes's plugin manager finds the
|
|
49
|
+
# `register` callable via this entry point and loads the plugin
|
|
50
|
+
# automatically on next startup.
|
|
51
|
+
[project.entry-points."hermes_agent.plugins"]
|
|
52
|
+
agentguard-spend = "agentguard_spend_hermes:register"
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["agentguard_spend_hermes"]
|