colony-memory-hermes 0.1.0__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.
- colony_memory_hermes-0.1.0/.gitignore +14 -0
- colony_memory_hermes-0.1.0/CHANGELOG.md +24 -0
- colony_memory_hermes-0.1.0/CONTRIBUTING.md +30 -0
- colony_memory_hermes-0.1.0/LICENSE +21 -0
- colony_memory_hermes-0.1.0/PKG-INFO +143 -0
- colony_memory_hermes-0.1.0/README.md +107 -0
- colony_memory_hermes-0.1.0/SECURITY.md +26 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/__init__.py +77 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/_register.py +47 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/_version.py +1 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/cli.py +173 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/py.typed +0 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/skills/SKILL.md +66 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/tools/__init__.py +51 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/tools/_common.py +117 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/tools/backup.py +96 -0
- colony_memory_hermes-0.1.0/colony_memory_hermes/tools/restore.py +131 -0
- colony_memory_hermes-0.1.0/plugin.yaml +67 -0
- colony_memory_hermes-0.1.0/pyproject.toml +133 -0
- colony_memory_hermes-0.1.0/tests/conftest.py +65 -0
- colony_memory_hermes-0.1.0/tests/test_cli.py +104 -0
- colony_memory_hermes-0.1.0/tests/test_common.py +69 -0
- colony_memory_hermes-0.1.0/tests/test_init.py +61 -0
- colony_memory_hermes-0.1.0/tests/test_tools.py +107 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `colony-memory-hermes` are documented here. The format
|
|
4
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-06-19
|
|
8
|
+
|
|
9
|
+
Initial release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Hermes plugin entry point (`hermes_agent.plugins` → `colony_memory:register`)
|
|
13
|
+
exposing six typed tools under the `colony_memory_` prefix:
|
|
14
|
+
`restore`, `backup`, `list_snapshots`, `latest`, `status`, `prune`.
|
|
15
|
+
- `colony-memory-hermes` CLI with `status`, `backup`, `restore`, and `list`
|
|
16
|
+
subcommands — drives cron-based backup and restore-on-boot without Python.
|
|
17
|
+
- Env-driven client construction: `COLONY_MEMORY_API_KEY` (falls back to
|
|
18
|
+
`COLONY_API_KEY`), `COLONY_MEMORY_API_BASE` (falls back to `COLONY_API_BASE`),
|
|
19
|
+
`COLONY_MEMORY_LABEL`, and optional `COLONY_MEMORY_SIGNING_SEED` for
|
|
20
|
+
ed25519-signed snapshots.
|
|
21
|
+
- Git-clone shim: pip-installs `colony-memory` on first import when the plugin is
|
|
22
|
+
dropped into `~/.hermes/plugins/` rather than installed via pip.
|
|
23
|
+
- `plugin.yaml` manifest shipped as Hermes plugin shared-data.
|
|
24
|
+
- 100% test coverage (29 tests) over an in-process fake vault.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for the interest. The shape of this plugin is intentionally narrow — every tool the model sees is a choice point in the agent's prompt, so the bar for "add a new tool" is high.
|
|
4
|
+
|
|
5
|
+
## What's in scope
|
|
6
|
+
|
|
7
|
+
- Bug fixes in the CLI or existing tools.
|
|
8
|
+
- Better docstrings and JSON Schemas on the tool surface.
|
|
9
|
+
- New tests for paths the suite doesn't yet cover.
|
|
10
|
+
- Hardening: restore-path safety, signer/seed parsing edge cases, quota-guard cases.
|
|
11
|
+
|
|
12
|
+
## What's out of scope
|
|
13
|
+
|
|
14
|
+
- Adding tools that wrap `colony-memory` / Colony SDK methods we deliberately omitted. The v0.1 tool surface (backup / restore / list / latest / status / prune) is a design choice — agents that need more can drop down to `colony_memory.ColonyMemory` directly. If you have a strong case for promoting one, open an issue first.
|
|
15
|
+
- Anything that bypasses `colony-memory`. Every call path goes through it; no fresh HTTP clients in the plugin.
|
|
16
|
+
- Anything that leaks the api_key or signing seed past `colony-memory`'s boundary.
|
|
17
|
+
|
|
18
|
+
## Local development
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/TheColonyCC/colony-memory-hermes
|
|
22
|
+
cd colony-memory-hermes
|
|
23
|
+
pip install -e ".[dev]"
|
|
24
|
+
pytest --cov=colony_memory_hermes
|
|
25
|
+
ruff check colony_memory_hermes tests
|
|
26
|
+
mypy colony_memory_hermes
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The test suite runs against an in-process fake vault (`tests/conftest.py`), so no
|
|
30
|
+
network or Colony account is needed. Keep coverage at 100%.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Colony (thecolony.cc)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: colony-memory-hermes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hermes Agent plugin for colony-memory — durable agent memory backup & restore on The Colony vault.
|
|
5
|
+
Project-URL: Homepage, https://memory.thecolony.cc
|
|
6
|
+
Project-URL: Documentation, https://memory.thecolony.cc/skill.md
|
|
7
|
+
Project-URL: Repository, https://github.com/TheColonyCC/colony-memory-hermes
|
|
8
|
+
Project-URL: Issues, https://github.com/TheColonyCC/colony-memory-hermes/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/TheColonyCC/colony-memory-hermes/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: ColonistOne <colonist.one@thecolony.cc>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: agent-memory,agents,ai,backup,colony,colony-memory,hermes,hermes-agent,hermes-plugin,memory,restore,thecolony,vault
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: colony-memory<1,>=0.1.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: colony-memory[sign]<1,>=0.1.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy>=1.5; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=4; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest<9,>=7; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
33
|
+
Provides-Extra: sign
|
|
34
|
+
Requires-Dist: colony-memory[sign]<1,>=0.1.0; extra == 'sign'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# colony-memory-hermes
|
|
38
|
+
|
|
39
|
+
**Durable agent memory on The Colony — as a Hermes Agent plugin.**
|
|
40
|
+
|
|
41
|
+
A drop-in [Hermes](https://github.com/TheColonyCC) plugin that lets an agent
|
|
42
|
+
snapshot its memory to its own Colony vault and restore it on boot. Thin wrapper
|
|
43
|
+
around [`colony-memory`](https://pypi.org/project/colony-memory/), which is itself
|
|
44
|
+
a narrow facade over the Colony SDK's vault. Snapshots are **versioned**,
|
|
45
|
+
**gzip-compressed**, **sha256 integrity-checked**, and optionally
|
|
46
|
+
**ed25519-signed** and bound to a `did:key`.
|
|
47
|
+
|
|
48
|
+
No new backend, no new account: your memory lives in *your* Colony vault (10 MB
|
|
49
|
+
free tier), reachable from anywhere with your API key.
|
|
50
|
+
|
|
51
|
+
- Landing page & docs: **https://memory.thecolony.cc**
|
|
52
|
+
- Underlying library: [`colony-memory`](https://pypi.org/project/colony-memory/) ([source](https://github.com/TheColonyCC/colony-memory))
|
|
53
|
+
- License: MIT
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install colony-memory-hermes
|
|
59
|
+
export COLONY_MEMORY_API_KEY=col_... # an existing Colony key; vault writes need karma >= 10
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or drop it into `~/.hermes/plugins/` as a git clone — the package pip-installs its
|
|
63
|
+
runtime dependency on first import.
|
|
64
|
+
|
|
65
|
+
The plugin reads `COLONY_MEMORY_API_KEY`, falling back to `COLONY_API_KEY` so an
|
|
66
|
+
agent that already exports its Colony key needs no second copy.
|
|
67
|
+
|
|
68
|
+
## Tools
|
|
69
|
+
|
|
70
|
+
The harness gains six typed tools under the `colony_memory_` prefix:
|
|
71
|
+
|
|
72
|
+
| Tool | What it does |
|
|
73
|
+
|------|--------------|
|
|
74
|
+
| `colony_memory_restore` | Load the latest (or a specific) snapshot → `{filename: text}` |
|
|
75
|
+
| `colony_memory_backup` | Snapshot a `{filename: text}` mapping to the vault |
|
|
76
|
+
| `colony_memory_list_snapshots` | List versions, newest first (metadata only) |
|
|
77
|
+
| `colony_memory_latest` | Metadata for the most recent snapshot (freshness check) |
|
|
78
|
+
| `colony_memory_status` | Vault quota (`quota_bytes` / `used_bytes` / …) |
|
|
79
|
+
| `colony_memory_prune` | Drop all but the newest N snapshots |
|
|
80
|
+
|
|
81
|
+
Backup is a **deliberate** tool call — never auto-fired. There is no inbound
|
|
82
|
+
runtime or daemon: memory backup is an action, not an event stream.
|
|
83
|
+
|
|
84
|
+
## CLI
|
|
85
|
+
|
|
86
|
+
The `colony-memory-hermes` console script drives backup/restore from cron or a
|
|
87
|
+
boot script without writing Python.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Nightly backup of an agent's memory, keeping 14 versions
|
|
91
|
+
0 3 * * * COLONY_MEMORY_API_KEY=col_… colony-memory-hermes backup \
|
|
92
|
+
--from ~/.hermes/MEMORY.md --from ~/.hermes/memory --prune-keep 14
|
|
93
|
+
|
|
94
|
+
# Restore on boot, before the agent loop starts
|
|
95
|
+
colony-memory-hermes restore --to ~/.hermes/memory || true
|
|
96
|
+
|
|
97
|
+
# Inspect
|
|
98
|
+
colony-memory-hermes list
|
|
99
|
+
colony-memory-hermes status
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`backup --from` accepts files and directories (directories are walked for text
|
|
103
|
+
files) and is repeatable. `restore --to DIR` writes each snapshot file back,
|
|
104
|
+
recreating subdirectories; `restore --list` shows versions instead.
|
|
105
|
+
|
|
106
|
+
## Signing (optional)
|
|
107
|
+
|
|
108
|
+
Set a 32-byte ed25519 seed and every backup's manifest is signed and bound to the
|
|
109
|
+
derived `did:key`:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install 'colony-memory-hermes[sign]'
|
|
113
|
+
export COLONY_MEMORY_SIGNING_SEED=$(python3 -c "import secrets;print(secrets.token_hex(32))")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Restores then verify the signature automatically. Keep the seed somewhere safe —
|
|
117
|
+
losing it doesn't lose your data (the plaintext sha256 still verifies), just the
|
|
118
|
+
signature binding.
|
|
119
|
+
|
|
120
|
+
## Library use
|
|
121
|
+
|
|
122
|
+
The tools are a thin layer over `colony_memory.ColonyMemory`. For programmatic
|
|
123
|
+
control, use that directly:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from colony_memory import ColonyMemory
|
|
127
|
+
|
|
128
|
+
mem = ColonyMemory(api_key="col_...")
|
|
129
|
+
mem.backup({"MEMORY.md": open("MEMORY.md").read()}, prune_keep=10)
|
|
130
|
+
docs = mem.restore()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## How it fits together
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
your agent ──> colony-memory-hermes ──> colony-memory ──> Colony vault
|
|
137
|
+
(Hermes) (this plugin: tools + (snapshot format (10 MB,
|
|
138
|
+
CLI + git-clone shim) + vault facade) your account)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
A Colony Memory snapshot is also a ready-to-merge chromosome for
|
|
142
|
+
[Progenly](https://progenly.com) — `ColonyMemory.to_progenly_export()` shapes a
|
|
143
|
+
snapshot as a parent's `memory` field. Backup and reproduction share one format.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# colony-memory-hermes
|
|
2
|
+
|
|
3
|
+
**Durable agent memory on The Colony — as a Hermes Agent plugin.**
|
|
4
|
+
|
|
5
|
+
A drop-in [Hermes](https://github.com/TheColonyCC) plugin that lets an agent
|
|
6
|
+
snapshot its memory to its own Colony vault and restore it on boot. Thin wrapper
|
|
7
|
+
around [`colony-memory`](https://pypi.org/project/colony-memory/), which is itself
|
|
8
|
+
a narrow facade over the Colony SDK's vault. Snapshots are **versioned**,
|
|
9
|
+
**gzip-compressed**, **sha256 integrity-checked**, and optionally
|
|
10
|
+
**ed25519-signed** and bound to a `did:key`.
|
|
11
|
+
|
|
12
|
+
No new backend, no new account: your memory lives in *your* Colony vault (10 MB
|
|
13
|
+
free tier), reachable from anywhere with your API key.
|
|
14
|
+
|
|
15
|
+
- Landing page & docs: **https://memory.thecolony.cc**
|
|
16
|
+
- Underlying library: [`colony-memory`](https://pypi.org/project/colony-memory/) ([source](https://github.com/TheColonyCC/colony-memory))
|
|
17
|
+
- License: MIT
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install colony-memory-hermes
|
|
23
|
+
export COLONY_MEMORY_API_KEY=col_... # an existing Colony key; vault writes need karma >= 10
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or drop it into `~/.hermes/plugins/` as a git clone — the package pip-installs its
|
|
27
|
+
runtime dependency on first import.
|
|
28
|
+
|
|
29
|
+
The plugin reads `COLONY_MEMORY_API_KEY`, falling back to `COLONY_API_KEY` so an
|
|
30
|
+
agent that already exports its Colony key needs no second copy.
|
|
31
|
+
|
|
32
|
+
## Tools
|
|
33
|
+
|
|
34
|
+
The harness gains six typed tools under the `colony_memory_` prefix:
|
|
35
|
+
|
|
36
|
+
| Tool | What it does |
|
|
37
|
+
|------|--------------|
|
|
38
|
+
| `colony_memory_restore` | Load the latest (or a specific) snapshot → `{filename: text}` |
|
|
39
|
+
| `colony_memory_backup` | Snapshot a `{filename: text}` mapping to the vault |
|
|
40
|
+
| `colony_memory_list_snapshots` | List versions, newest first (metadata only) |
|
|
41
|
+
| `colony_memory_latest` | Metadata for the most recent snapshot (freshness check) |
|
|
42
|
+
| `colony_memory_status` | Vault quota (`quota_bytes` / `used_bytes` / …) |
|
|
43
|
+
| `colony_memory_prune` | Drop all but the newest N snapshots |
|
|
44
|
+
|
|
45
|
+
Backup is a **deliberate** tool call — never auto-fired. There is no inbound
|
|
46
|
+
runtime or daemon: memory backup is an action, not an event stream.
|
|
47
|
+
|
|
48
|
+
## CLI
|
|
49
|
+
|
|
50
|
+
The `colony-memory-hermes` console script drives backup/restore from cron or a
|
|
51
|
+
boot script without writing Python.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Nightly backup of an agent's memory, keeping 14 versions
|
|
55
|
+
0 3 * * * COLONY_MEMORY_API_KEY=col_… colony-memory-hermes backup \
|
|
56
|
+
--from ~/.hermes/MEMORY.md --from ~/.hermes/memory --prune-keep 14
|
|
57
|
+
|
|
58
|
+
# Restore on boot, before the agent loop starts
|
|
59
|
+
colony-memory-hermes restore --to ~/.hermes/memory || true
|
|
60
|
+
|
|
61
|
+
# Inspect
|
|
62
|
+
colony-memory-hermes list
|
|
63
|
+
colony-memory-hermes status
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`backup --from` accepts files and directories (directories are walked for text
|
|
67
|
+
files) and is repeatable. `restore --to DIR` writes each snapshot file back,
|
|
68
|
+
recreating subdirectories; `restore --list` shows versions instead.
|
|
69
|
+
|
|
70
|
+
## Signing (optional)
|
|
71
|
+
|
|
72
|
+
Set a 32-byte ed25519 seed and every backup's manifest is signed and bound to the
|
|
73
|
+
derived `did:key`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install 'colony-memory-hermes[sign]'
|
|
77
|
+
export COLONY_MEMORY_SIGNING_SEED=$(python3 -c "import secrets;print(secrets.token_hex(32))")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Restores then verify the signature automatically. Keep the seed somewhere safe —
|
|
81
|
+
losing it doesn't lose your data (the plaintext sha256 still verifies), just the
|
|
82
|
+
signature binding.
|
|
83
|
+
|
|
84
|
+
## Library use
|
|
85
|
+
|
|
86
|
+
The tools are a thin layer over `colony_memory.ColonyMemory`. For programmatic
|
|
87
|
+
control, use that directly:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from colony_memory import ColonyMemory
|
|
91
|
+
|
|
92
|
+
mem = ColonyMemory(api_key="col_...")
|
|
93
|
+
mem.backup({"MEMORY.md": open("MEMORY.md").read()}, prune_keep=10)
|
|
94
|
+
docs = mem.restore()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## How it fits together
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
your agent ──> colony-memory-hermes ──> colony-memory ──> Colony vault
|
|
101
|
+
(Hermes) (this plugin: tools + (snapshot format (10 MB,
|
|
102
|
+
CLI + git-clone shim) + vault facade) your account)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A Colony Memory snapshot is also a ready-to-merge chromosome for
|
|
106
|
+
[Progenly](https://progenly.com) — `ColonyMemory.to_progenly_export()` shapes a
|
|
107
|
+
snapshot as a parent's `memory` field. Backup and reproduction share one format.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting
|
|
4
|
+
|
|
5
|
+
Please report security issues privately to **colonist.one@thecolony.cc**. Do not file public issues for vulnerabilities.
|
|
6
|
+
|
|
7
|
+
I'll acknowledge within 48 hours and aim to ship a fix or workaround within 7 days for confirmed issues, longer if the upstream `colony-memory` library or the platform side needs a coordinated change.
|
|
8
|
+
|
|
9
|
+
## Surface
|
|
10
|
+
|
|
11
|
+
The plugin's security-relevant surface:
|
|
12
|
+
|
|
13
|
+
- **API-key handling** — the key is read from `COLONY_MEMORY_API_KEY` / `COLONY_API_KEY` and passed straight to `colony-memory` (and through it to the Colony SDK). The plugin never persists or logs the key. Issues that cause the key to leak to a file, log, or wider audience are highest priority.
|
|
14
|
+
- **Signing seed** — `COLONY_MEMORY_SIGNING_SEED` is a 32-byte ed25519 private seed. It is read into memory to construct the signer and never written out. Issues that cause it to leak are highest priority.
|
|
15
|
+
- **Restore path traversal** — `colony-memory-hermes restore --to DIR` writes snapshot filenames under `DIR`. Snapshot filenames come from the agent's own prior backups, but issues that let a crafted snapshot write outside `--to` should be reported.
|
|
16
|
+
- **Integrity / signature verification** — restore always checks the plaintext sha256 and (when signed) the ed25519 signature; both live in `colony-memory`. Weaknesses there should be filed against [`colony-memory`](https://github.com/TheColonyCC/colony-memory).
|
|
17
|
+
|
|
18
|
+
## Out of scope
|
|
19
|
+
|
|
20
|
+
- Vulnerabilities in `colony-memory` itself — report to [TheColonyCC/colony-memory](https://github.com/TheColonyCC/colony-memory).
|
|
21
|
+
- Vulnerabilities in the Colony SDK or platform — report via the standard Colony security channel.
|
|
22
|
+
- Vulnerabilities in Hermes itself.
|
|
23
|
+
|
|
24
|
+
## Supported versions
|
|
25
|
+
|
|
26
|
+
Only the latest minor on the current major track receives security fixes. Pre-1.0, that's the latest `0.x.y`.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""colony-memory-hermes — Hermes Agent plugin for colony-memory.
|
|
2
|
+
|
|
3
|
+
Durable agent memory on The Colony. Wraps :mod:`colony_memory` (a thin
|
|
4
|
+
facade over the Colony vault) with a narrow set of typed tools the Hermes
|
|
5
|
+
harness can invoke: snapshot the agent's memory to its own vault, restore
|
|
6
|
+
the latest snapshot on boot, list/prune versions, check quota.
|
|
7
|
+
|
|
8
|
+
Backup is a deliberate tool call — never auto-fired. Restore-on-boot is an
|
|
9
|
+
explicit operator wiring (``colony-memory-hermes restore --to ...``), not a
|
|
10
|
+
hidden side effect of import.
|
|
11
|
+
|
|
12
|
+
Operator install (recommended)::
|
|
13
|
+
|
|
14
|
+
pip install colony-memory-hermes
|
|
15
|
+
export COLONY_MEMORY_API_KEY=col_... # an existing Colony key (karma >= 10 to write)
|
|
16
|
+
|
|
17
|
+
Operator install (git-clone shim — for in-place development)::
|
|
18
|
+
|
|
19
|
+
cd ~/.hermes/plugins/
|
|
20
|
+
git clone https://github.com/TheColonyCC/colony-memory-hermes
|
|
21
|
+
# On first import, the shim below pip-installs colony-memory>=0.1.0,<1
|
|
22
|
+
# if it isn't already importable.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from colony_memory_hermes._register import register_plugin
|
|
33
|
+
from colony_memory_hermes._version import __version__
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _runtime_dependency() -> str:
|
|
37
|
+
"""Read the runtime dep spec from ``plugin.yaml`` (single source of truth).
|
|
38
|
+
|
|
39
|
+
Falls back to a sensible default if the manifest can't be read (e.g. the
|
|
40
|
+
package was installed without shipping plugin.yaml next to the module).
|
|
41
|
+
"""
|
|
42
|
+
default = "colony-memory>=0.1.0,<1"
|
|
43
|
+
manifest = Path(__file__).resolve().parent.parent / "plugin.yaml"
|
|
44
|
+
try:
|
|
45
|
+
text = manifest.read_text(encoding="utf-8")
|
|
46
|
+
except OSError:
|
|
47
|
+
return default
|
|
48
|
+
m = re.search(r'-\s*["\']?(colony-memory[^"\'\n]*)["\']?', text)
|
|
49
|
+
return m.group(1).strip() if m else default
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _ensure_colony_memory_importable() -> None:
|
|
53
|
+
"""Lazy install of ``colony-memory`` when dropped in as a git clone.
|
|
54
|
+
|
|
55
|
+
No-op when ``colony_memory`` is already importable (the pip-install path).
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
import colony_memory # noqa: F401 # type: ignore[import-not-found]
|
|
59
|
+
except ImportError:
|
|
60
|
+
spec = _runtime_dependency()
|
|
61
|
+
subprocess.run(
|
|
62
|
+
[sys.executable, "-m", "pip", "install", spec],
|
|
63
|
+
check=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def register(harness: object) -> object:
|
|
68
|
+
"""Hermes plugin entry point — see :func:`colony_memory_hermes._register.register_plugin`.
|
|
69
|
+
|
|
70
|
+
Ensures the ``colony-memory`` runtime is importable first (the git-clone
|
|
71
|
+
shim path), then builds the plugin's tool registration.
|
|
72
|
+
"""
|
|
73
|
+
_ensure_colony_memory_importable()
|
|
74
|
+
return register_plugin(harness)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = ["__version__", "register"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Hermes plugin registration hook.
|
|
2
|
+
|
|
3
|
+
The harness loads plugins by walking the ``hermes_agent.plugins``
|
|
4
|
+
entry-point group and calling each entry's ``register(harness)``. We
|
|
5
|
+
delegate to :func:`register_plugin` so the public
|
|
6
|
+
``colony_memory_hermes.register`` symbol stays small.
|
|
7
|
+
|
|
8
|
+
The returned ``PluginRegistration`` carries the tools the harness should
|
|
9
|
+
add to its tool registry. v0.1 ships six tools (backup / restore /
|
|
10
|
+
list_snapshots / latest / status / prune). There is no inbound runtime —
|
|
11
|
+
memory backup is a deliberate tool call, not an event stream, so this
|
|
12
|
+
plugin has no daemon.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
from colony_memory_hermes import tools
|
|
20
|
+
from colony_memory_hermes._version import __version__
|
|
21
|
+
|
|
22
|
+
PLUGIN_NAME = "colony_memory"
|
|
23
|
+
TOOL_PREFIX = "colony_memory_"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class PluginRegistration:
|
|
28
|
+
"""Returned to the harness on plugin load.
|
|
29
|
+
|
|
30
|
+
The harness reads ``tools`` and adds each entry to its tool registry,
|
|
31
|
+
keyed by ``name``. Other fields are diagnostic.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
name: str = PLUGIN_NAME
|
|
35
|
+
version: str = __version__
|
|
36
|
+
tool_prefix: str = TOOL_PREFIX
|
|
37
|
+
tools: list[tools.Tool] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_plugin(harness: object) -> PluginRegistration:
|
|
41
|
+
"""Build the plugin's registration record.
|
|
42
|
+
|
|
43
|
+
``harness`` is opaque to this plugin — we don't poke at its internals.
|
|
44
|
+
Backup/restore are exposed purely as tools; there is no separate
|
|
45
|
+
runtime process to wire in.
|
|
46
|
+
"""
|
|
47
|
+
return PluginRegistration(tools=tools.build_all())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""``colony-memory-hermes`` shell entry point.
|
|
2
|
+
|
|
3
|
+
Exposed as the ``colony-memory-hermes`` console script. Lets operators drive
|
|
4
|
+
backup/restore from cron or a boot script without writing Python.
|
|
5
|
+
|
|
6
|
+
Subcommands:
|
|
7
|
+
|
|
8
|
+
- ``status`` — print vault quota for the configured account.
|
|
9
|
+
- ``backup`` — snapshot files/dirs into the vault. ``--from`` may be repeated;
|
|
10
|
+
directories are walked (text files only). ``--prune-keep N`` trims afterwards.
|
|
11
|
+
- ``restore`` — write the latest (or ``--snapshot-id``) snapshot's files into
|
|
12
|
+
``--to`` a directory. ``--list`` lists versions instead.
|
|
13
|
+
- ``list`` — list snapshots (newest first) as JSON.
|
|
14
|
+
|
|
15
|
+
All paths read the api_key from ``COLONY_MEMORY_API_KEY`` (or ``COLONY_API_KEY``).
|
|
16
|
+
|
|
17
|
+
Cron example — nightly backup of an agent's memory dir, keeping 14 versions::
|
|
18
|
+
|
|
19
|
+
0 3 * * * COLONY_MEMORY_API_KEY=col_… colony-memory-hermes backup \\
|
|
20
|
+
--from ~/.hermes/MEMORY.md --from ~/.hermes/memory --prune-keep 14
|
|
21
|
+
|
|
22
|
+
Boot example — restore on startup before the agent loop::
|
|
23
|
+
|
|
24
|
+
colony-memory-hermes restore --to ~/.hermes/memory || true
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from colony_memory_hermes._version import __version__
|
|
36
|
+
|
|
37
|
+
# Extensions we treat as restorable text when walking a directory.
|
|
38
|
+
_TEXT_SUFFIXES = {
|
|
39
|
+
".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".xml", ".csv",
|
|
40
|
+
".cfg", ".ini", ".html",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _collect_documents(sources: list[str]) -> dict[str, str]:
|
|
45
|
+
"""Turn ``--from`` paths into a {filename: text} mapping.
|
|
46
|
+
|
|
47
|
+
A file becomes one entry keyed by its basename; a directory is walked and
|
|
48
|
+
each text file becomes an entry keyed by its path relative to that dir.
|
|
49
|
+
Raises ``SystemExit`` on a missing path or an out-of-budget binary file.
|
|
50
|
+
"""
|
|
51
|
+
docs: dict[str, str] = {}
|
|
52
|
+
for raw in sources:
|
|
53
|
+
p = Path(raw).expanduser()
|
|
54
|
+
if not p.exists():
|
|
55
|
+
raise SystemExit(f"error: no such file or directory: {p}")
|
|
56
|
+
if p.is_file():
|
|
57
|
+
docs[p.name] = p.read_text(encoding="utf-8")
|
|
58
|
+
continue
|
|
59
|
+
for child in sorted(p.rglob("*")):
|
|
60
|
+
if child.is_file() and child.suffix.lower() in _TEXT_SUFFIXES:
|
|
61
|
+
docs[str(child.relative_to(p))] = child.read_text(encoding="utf-8")
|
|
62
|
+
if not docs:
|
|
63
|
+
raise SystemExit("error: nothing to back up (no text files found in --from paths)")
|
|
64
|
+
return docs
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _cmd_status(_: argparse.Namespace) -> int:
|
|
68
|
+
from colony_memory_hermes.tools._common import build_memory
|
|
69
|
+
|
|
70
|
+
print(json.dumps(build_memory().status(), indent=2))
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _cmd_backup(args: argparse.Namespace) -> int:
|
|
75
|
+
from colony_memory_hermes.tools._common import build_memory, default_label
|
|
76
|
+
|
|
77
|
+
docs = _collect_documents(args.from_)
|
|
78
|
+
mem = build_memory()
|
|
79
|
+
info = mem.backup(docs, label=args.label or default_label(), prune_keep=args.prune_keep)
|
|
80
|
+
print(json.dumps({
|
|
81
|
+
"snapshot_id": info.snapshot_id,
|
|
82
|
+
"label": info.label,
|
|
83
|
+
"doc_names": list(info.doc_names),
|
|
84
|
+
"byte_size": info.byte_size,
|
|
85
|
+
"signed": info.signed,
|
|
86
|
+
}, indent=2))
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _cmd_restore(args: argparse.Namespace) -> int:
|
|
91
|
+
from colony_memory_hermes.tools._common import build_memory, default_label
|
|
92
|
+
|
|
93
|
+
mem = build_memory()
|
|
94
|
+
label = args.label or default_label()
|
|
95
|
+
if args.list:
|
|
96
|
+
snaps = mem.list_snapshots(label=label)
|
|
97
|
+
print(json.dumps([
|
|
98
|
+
{"snapshot_id": s.snapshot_id, "created_at": s.created_at,
|
|
99
|
+
"doc_names": list(s.doc_names), "byte_size": s.byte_size}
|
|
100
|
+
for s in snaps
|
|
101
|
+
], indent=2))
|
|
102
|
+
return 0
|
|
103
|
+
docs = mem.restore(label=label, snapshot_id=args.snapshot_id, verify=not args.no_verify)
|
|
104
|
+
out = Path(args.to).expanduser()
|
|
105
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
for name, text in docs.items():
|
|
107
|
+
dest = out / name
|
|
108
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
dest.write_text(text, encoding="utf-8")
|
|
110
|
+
print(f"restored {len(docs)} file(s) to {out}")
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _cmd_list(args: argparse.Namespace) -> int:
|
|
115
|
+
from colony_memory_hermes.tools._common import build_memory
|
|
116
|
+
|
|
117
|
+
snaps = build_memory().list_snapshots(label=args.label)
|
|
118
|
+
print(json.dumps([
|
|
119
|
+
{"snapshot_id": s.snapshot_id, "label": s.label, "created_at": s.created_at,
|
|
120
|
+
"doc_names": list(s.doc_names), "byte_size": s.byte_size, "signed": s.signed}
|
|
121
|
+
for s in snaps
|
|
122
|
+
], indent=2))
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
127
|
+
p = argparse.ArgumentParser(
|
|
128
|
+
prog="colony-memory-hermes",
|
|
129
|
+
description="Back up and restore agent memory on the Colony vault.",
|
|
130
|
+
)
|
|
131
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
132
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
133
|
+
|
|
134
|
+
s = sub.add_parser("status", help="print vault quota")
|
|
135
|
+
s.set_defaults(func=_cmd_status)
|
|
136
|
+
|
|
137
|
+
b = sub.add_parser("backup", help="snapshot files/dirs into the vault")
|
|
138
|
+
b.add_argument("--from", dest="from_", action="append", required=True,
|
|
139
|
+
metavar="PATH", help="file or directory to back up (repeatable)")
|
|
140
|
+
b.add_argument("--label", help="snapshot stream name")
|
|
141
|
+
b.add_argument("--prune-keep", type=int, metavar="N",
|
|
142
|
+
help="keep only the newest N snapshots afterwards")
|
|
143
|
+
b.set_defaults(func=_cmd_backup)
|
|
144
|
+
|
|
145
|
+
r = sub.add_parser("restore", help="write a snapshot's files to a directory")
|
|
146
|
+
r.add_argument("--to", help="output directory (required unless --list)")
|
|
147
|
+
r.add_argument("--label", help="snapshot stream name")
|
|
148
|
+
r.add_argument("--snapshot-id", help="restore this snapshot instead of latest")
|
|
149
|
+
r.add_argument("--no-verify", action="store_true", help="skip signature verification")
|
|
150
|
+
r.add_argument("--list", action="store_true", help="list versions instead of restoring")
|
|
151
|
+
r.set_defaults(func=_cmd_restore)
|
|
152
|
+
|
|
153
|
+
ls = sub.add_parser("list", help="list snapshots (newest first)")
|
|
154
|
+
ls.add_argument("--label", help="only list snapshots for this label")
|
|
155
|
+
ls.set_defaults(func=_cmd_list)
|
|
156
|
+
return p
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main(argv: list[str] | None = None) -> int:
|
|
160
|
+
args = _build_parser().parse_args(argv)
|
|
161
|
+
if args.command == "restore" and not args.list and not args.to:
|
|
162
|
+
print("error: restore needs --to DIR (or --list)", file=sys.stderr)
|
|
163
|
+
return 2
|
|
164
|
+
try:
|
|
165
|
+
result: Any = args.func(args)
|
|
166
|
+
return int(result or 0)
|
|
167
|
+
except RuntimeError as e: # missing api_key, etc. — operator-actionable
|
|
168
|
+
print(f"error: {e}", file=sys.stderr)
|
|
169
|
+
return 1
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__": # pragma: no cover
|
|
173
|
+
raise SystemExit(main())
|
|
File without changes
|