@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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/deploy/ack_monitor.py +140 -0
  4. package/deploy/ack_watchdog.py +110 -0
  5. package/deploy/agent-character-monitor.service +21 -0
  6. package/deploy/agent-character-watchdog.service +21 -0
  7. package/deploy/agent-enforcer-proof.service +18 -0
  8. package/deploy/agent-enforcer.service +53 -0
  9. package/deploy/deploy-ack-services.sh +47 -0
  10. package/deploy/deploy-agent-enforcer.sh +133 -0
  11. package/deploy/proof-self-respawn.sh +59 -0
  12. package/node/bin/ack.js +383 -0
  13. package/node/bin/aik.js +520 -0
  14. package/node/bin/install.js +409 -0
  15. package/node/enforcer/agent_enforcer_daemon.js +690 -0
  16. package/node/examples/.agent/constitution.yaml +38 -0
  17. package/node/examples/.agent/enforcer.yaml +33 -0
  18. package/node/examples/.agent/habits/affirm_character_each_action.yaml +9 -0
  19. package/node/examples/.agent/habits/audit_not_silent.yaml +9 -0
  20. package/node/examples/.agent/habits/binding_map_one_core.yaml +9 -0
  21. package/node/examples/.agent/habits/character_hash_visible.yaml +9 -0
  22. package/node/examples/.agent/habits/check_duplication_before_debug.yaml +9 -0
  23. package/node/examples/.agent/habits/complete-thoroughly.yaml +16 -0
  24. package/node/examples/.agent/habits/consistent-info-across-files.yaml +18 -0
  25. package/node/examples/.agent/habits/document-for-next-agent.yaml +15 -0
  26. package/node/examples/.agent/habits/documented_rollback.yaml +9 -0
  27. package/node/examples/.agent/habits/drift_signal_detection.yaml +9 -0
  28. package/node/examples/.agent/habits/due-diligence.yaml +16 -0
  29. package/node/examples/.agent/habits/enterprise-grade-modular.yaml +16 -0
  30. package/node/examples/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
  31. package/node/examples/.agent/habits/forever_one_idea.yaml +9 -0
  32. package/node/examples/.agent/habits/graceful_degradation.yaml +9 -0
  33. package/node/examples/.agent/habits/idempotent_operations.yaml +9 -0
  34. package/node/examples/.agent/habits/interactive-no-stalls.yaml +16 -0
  35. package/node/examples/.agent/habits/layered_not_rewritten.yaml +9 -0
  36. package/node/examples/.agent/habits/lossless_consolidation.yaml +9 -0
  37. package/node/examples/.agent/habits/no-credential-leak.yaml +41 -0
  38. package/node/examples/.agent/habits/no-deception.yaml +14 -0
  39. package/node/examples/.agent/habits/no_hold_narration.yaml +9 -0
  40. package/node/examples/.agent/habits/one_concern_per_file.yaml +9 -0
  41. package/node/examples/.agent/habits/optimized-robust.yaml +16 -0
  42. package/node/examples/.agent/habits/registered_plugin_not_string.yaml +9 -0
  43. package/node/examples/.agent/habits/rename_as_layer_op.yaml +9 -0
  44. package/node/examples/.agent/habits/resolve-root-cause.yaml +15 -0
  45. package/node/examples/.agent/habits/rigorous-commits-no-push.yaml +18 -0
  46. package/node/examples/.agent/habits/safe-deletion-via-trash.yaml +20 -0
  47. package/node/examples/.agent/habits/safe_file_permissions.yaml +9 -0
  48. package/node/examples/.agent/habits/self-healing-portability.yaml +14 -0
  49. package/node/examples/.agent/habits/shippable-pride.yaml +15 -0
  50. package/node/examples/.agent/habits/single_source_of_truth.yaml +9 -0
  51. package/node/examples/.agent/habits/test_of_forever.yaml +9 -0
  52. package/node/examples/.agent/habits/timeout_and_retry.yaml +9 -0
  53. package/node/examples/.agent/habits/track_defects_openly.yaml +9 -0
  54. package/node/examples/.agent/habits/validate-against-real-source.yaml +14 -0
  55. package/node/examples/.agent/habits/verify-against-docs.yaml +15 -0
  56. package/node/examples/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
  57. package/node/examples/.agent/habits/versioning-discipline.yaml +18 -0
  58. package/node/package.json +53 -0
  59. package/node/src/enforcer/client.js +149 -0
  60. package/node/src/hooks/character.js +286 -0
  61. package/node/src/index.js +24 -0
  62. package/node/src/knowledge/indexer.js +486 -0
  63. package/node/src/knowledge/semantic.js +154 -0
  64. package/node/src/memory/index.js +355 -0
  65. package/node/tests/character.test.js +80 -0
  66. package/node/tests/habit-create.test.js +67 -0
  67. package/node/tests/habits-default.test.js +68 -0
  68. package/node/tests/install-chain.test.js +19 -0
  69. package/node/tests/install.test.js +104 -0
  70. package/package.json +65 -0
  71. package/python/agent_character_kit/__init__.py +388 -0
  72. package/python/agent_character_kit/__main__.py +333 -0
  73. package/python/agent_character_kit/_yaml.py +33 -0
  74. package/python/agent_character_kit/enforcer.py +379 -0
  75. package/python/agent_character_kit/memory.py +257 -0
  76. package/python/agent_character_kit/semantic.py +126 -0
  77. package/python/example_workspace/.agent/habits/affirm_character_each_action.yaml +9 -0
  78. package/python/example_workspace/.agent/habits/audit_not_silent.yaml +9 -0
  79. package/python/example_workspace/.agent/habits/binding_map_one_core.yaml +9 -0
  80. package/python/example_workspace/.agent/habits/character_hash_visible.yaml +9 -0
  81. package/python/example_workspace/.agent/habits/check_duplication_before_debug.yaml +9 -0
  82. package/python/example_workspace/.agent/habits/complete-thoroughly.yaml +16 -0
  83. package/python/example_workspace/.agent/habits/consistent-info-across-files.yaml +18 -0
  84. package/python/example_workspace/.agent/habits/document-for-next-agent.yaml +15 -0
  85. package/python/example_workspace/.agent/habits/documented_rollback.yaml +9 -0
  86. package/python/example_workspace/.agent/habits/drift_signal_detection.yaml +9 -0
  87. package/python/example_workspace/.agent/habits/due-diligence.yaml +16 -0
  88. package/python/example_workspace/.agent/habits/enterprise-grade-modular.yaml +16 -0
  89. package/python/example_workspace/.agent/habits/fail_closed_tamper_evident.yaml +9 -0
  90. package/python/example_workspace/.agent/habits/forever_one_idea.yaml +9 -0
  91. package/python/example_workspace/.agent/habits/graceful_degradation.yaml +9 -0
  92. package/python/example_workspace/.agent/habits/idempotent_operations.yaml +9 -0
  93. package/python/example_workspace/.agent/habits/interactive-no-stalls.yaml +16 -0
  94. package/python/example_workspace/.agent/habits/layered_not_rewritten.yaml +9 -0
  95. package/python/example_workspace/.agent/habits/lossless_consolidation.yaml +9 -0
  96. package/python/example_workspace/.agent/habits/no-credential-leak.yaml +41 -0
  97. package/python/example_workspace/.agent/habits/no-deception.yaml +14 -0
  98. package/python/example_workspace/.agent/habits/no_hold_narration.yaml +9 -0
  99. package/python/example_workspace/.agent/habits/one_concern_per_file.yaml +9 -0
  100. package/python/example_workspace/.agent/habits/optimized-robust.yaml +16 -0
  101. package/python/example_workspace/.agent/habits/registered_plugin_not_string.yaml +9 -0
  102. package/python/example_workspace/.agent/habits/rename_as_layer_op.yaml +9 -0
  103. package/python/example_workspace/.agent/habits/resolve-root-cause.yaml +15 -0
  104. package/python/example_workspace/.agent/habits/rigorous-commits-no-push.yaml +18 -0
  105. package/python/example_workspace/.agent/habits/safe-deletion-via-trash.yaml +20 -0
  106. package/python/example_workspace/.agent/habits/safe_file_permissions.yaml +9 -0
  107. package/python/example_workspace/.agent/habits/self-healing-portability.yaml +14 -0
  108. package/python/example_workspace/.agent/habits/shippable-pride.yaml +15 -0
  109. package/python/example_workspace/.agent/habits/single_source_of_truth.yaml +9 -0
  110. package/python/example_workspace/.agent/habits/test_of_forever.yaml +9 -0
  111. package/python/example_workspace/.agent/habits/timeout_and_retry.yaml +9 -0
  112. package/python/example_workspace/.agent/habits/track_defects_openly.yaml +9 -0
  113. package/python/example_workspace/.agent/habits/validate-against-real-source.yaml +14 -0
  114. package/python/example_workspace/.agent/habits/verify-against-docs.yaml +15 -0
  115. package/python/example_workspace/.agent/habits/verify-functionality-not-syntax.yaml +15 -0
  116. package/python/example_workspace/.agent/habits/versioning-discipline.yaml +18 -0
  117. package/python/hermes_plugin/README.md +63 -0
  118. package/python/hermes_plugin/__init__.py +367 -0
  119. package/python/hermes_plugin/config.yaml +20 -0
  120. package/python/hermes_plugin/plugin.yaml +7 -0
  121. package/python/hermes_plugin/test_plugin.py +166 -0
  122. package/python/pyproject.toml +19 -0
  123. package/python/tests/python_parity_test.py +84 -0
@@ -0,0 +1,9 @@
1
+ name: timeout_and_retry
2
+ prompt: For any long or network op, did I set a timeout and bounded retry?
3
+ enforcement:
4
+ level: must
5
+ behavior:
6
+ kind: assertion
7
+ assert: Long-running and network calls have a timeout and a bounded retry with backoff. No unbounded waits, no single-try-or-die.
8
+ evidence: The operation fails fast on timeout and retries a fixed number of times before giving up; the caller isn't hung.
9
+ logic: Without a timeout something hangs forever; without retry a transient blip becomes a permanent failure (standards.md §6).
@@ -0,0 +1,9 @@
1
+ name: track_defects_openly
2
+ prompt: Did I hit a crack — and am I recording it openly, not hiding it?
3
+ enforcement:
4
+ level: should
5
+ behavior:
6
+ kind: assertion
7
+ assert: Known defects are tracked openly as a named list (audit swallows errors, false tamper-proof claim, hook is a string not a plugin, no CI). List the cracks; don't hide them.
8
+ evidence: Limitations appear in a documented known-defects list with the rule they violate; they're slated to be closed, not concealed.
9
+ logic: Hidden defects compound into outages nobody saw coming. An openly-listed crack is a managed, closable risk; a concealed one is future failure.
@@ -0,0 +1,14 @@
1
+ # Habit: validate-against-real-source
2
+ # Correct action: ship only work validated against real source (file/line/test), not README prose.
3
+ # Evidence: every claim traces to code I read or a test I ran; no prose-only assertion survives review.
4
+ # Logic: prose describes intent, source/tests prove function. Trusting docs over code is how broken
5
+ # demos ship. Knowing it's right = the proof is in the artifact, not the description.
6
+ name: "validate_against_real_source"
7
+ prompt: "Did I validate this against real source, not README prose?"
8
+ enforcement:
9
+ level: "reminder"
10
+ behavior:
11
+ kind: "standard"
12
+ assert: "Ship only work validated against real source (file/line/test), not README prose."
13
+ evidence: "Every claim traces to code I read or a test I ran; no prose-only assertion survives review."
14
+ logic: "Prose describes intent; source/tests prove function. Trusting docs over code is how broken demos ship."
@@ -0,0 +1,15 @@
1
+ # Habit: verify-against-docs
2
+ # Source question: "Did I verify my work against actual documentation when it was available?"
3
+ # Correct action: when real docs/source exist, verify against THEM, not against memory or README prose.
4
+ # Evidence: claims trace to the actual API/doc/source I read; where docs conflict with prose, docs win.
5
+ # Logic: docs are the contract the system actually honors; prose summarizing them drifts. You know it's
6
+ # right because it matches the authority, not a secondhand description.
7
+ name: "verify_against_docs"
8
+ prompt: "Did I verify against the actual docs when available?"
9
+ enforcement:
10
+ level: "reminder"
11
+ behavior:
12
+ kind: "standard"
13
+ assert: "When real docs/source exist, verify against THEM, not memory or README prose."
14
+ evidence: "Claims trace to the actual API/doc/source I read; where docs conflict with prose, docs win."
15
+ logic: "Docs are the contract the system honors; prose summarizing them drifts. Right = matches the authority, not a description."
@@ -0,0 +1,15 @@
1
+ # Habit: verify-functionality-not-syntax
2
+ # Correct action: verify functionality, not just syntax. A syntax pass is never 'done'.
3
+ # Evidence: ran the code / exercised the path; observed expected behavior. A green linter alone is
4
+ # not evidence of function.
5
+ # Logic: a thing that parses can still be wrong. Functional proof requires execution, not compilation.
6
+ # You know it works because you watched it work, not because it compiled.
7
+ name: "verify_functionality_not_syntax"
8
+ prompt: "Did I verify this 100% by testing functionality, not just a syntax pass?"
9
+ enforcement:
10
+ level: "reminder"
11
+ behavior:
12
+ kind: "standard"
13
+ assert: "Verify functionality, not just syntax. A syntax pass is never 'done'."
14
+ evidence: "Ran the code / exercised the path; observed expected behavior. A green linter alone is not evidence of function."
15
+ logic: "A thing that parses can still be wrong. Functional proof requires execution, not compilation."
@@ -0,0 +1,18 @@
1
+ # Habit: versioning-discipline
2
+ # Source question: "Is the version accurate, present, and self-identifying?"
3
+ # Correct action: stamp a VERSION file and an AIK_VERSION (or equivalent) constant so the
4
+ # artifact identifies its own version; bump it on every meaningful change;
5
+ # never ship a dir that can't state what it is.
6
+ # Evidence: a VERSION file exists at repo root, the runtime constant matches it, and the
7
+ # two are updated together whenever behavior changes.
8
+ # Logic: an artifact that can't name its own version can't be debugged, diffed, or trusted.
9
+ # Self-identifying = the dir tells you what it is without external context.
10
+ name: "versioning_discipline"
11
+ prompt: "Is the version stamped, matching, and bumped on change?"
12
+ enforcement:
13
+ level: "reminder"
14
+ behavior:
15
+ kind: "standard"
16
+ assert: "Stamp VERSION file + runtime version constant; they match; bump both on any meaningful change."
17
+ evidence: "VERSION file at repo root; runtime constant equals it; both updated together when behavior changes."
18
+ logic: "An artifact that can't name its own version can't be debugged or trusted. Self-identifying is non-negotiable."
@@ -0,0 +1,63 @@
1
+ # Agent Character Kit — Companion Plugin (Hermes)
2
+
3
+ A **real, registered** companion plugin that enforces agent *character* (your word,
4
+ not "identity") on every tool call, via any harness's `pre_tool_call` hook. This
5
+ replaces the old `hooks/identity.js` string-suggestion that was never actually wired in.
6
+
7
+ One of several companion clients — the kit itself is harness-agnostic (Hermes,
8
+ OpenCode, Claude, Cursor, Gemini all supported).
9
+
10
+ ## What it does
11
+
12
+ Bridges any harness's `pre_tool_call` hook to the Agent Character Kit
13
+ enforcer. The enforcer runs the **same** policy the Node daemon enforces:
14
+
15
+ - `constitution.yaml` → `hard_constraints` (deny patterns)
16
+ - `enforcer.yaml` → `allow` / `deny` lists
17
+ - `habits/*.yaml` (level: hard) → internalized behavioral blocks
18
+
19
+ Every tool call is judged before it executes. Denied calls are blocked with a
20
+ reason + reflection ("this isn't a rule to work around — it's who we are").
21
+
22
+ ## Fail-closed (never fails open)
23
+
24
+ - If the enforcer can't load → **block**.
25
+ - If no valid `constitution.yaml` is present → **block**. An agent without a
26
+ loaded character is not trusted to act.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ # 1. Make the AIK Python package importable
32
+ cd agent-character-kit/python
33
+ pip install -e .
34
+
35
+ # 2. Drop this plugin into Hermes's plugin dir
36
+ mkdir -p ~/.hermes/plugins/agent-character-kit
37
+ cp -r hermes_plugin/* ~/.hermes/plugins/agent-character-kit/
38
+
39
+ # 3. Define the agent's character (singular source of truth)
40
+ # ~/.openclaw/workspace/.agent/constitution.yaml (or $AGENT_WORKSPACE/.agent/)
41
+ ```
42
+
43
+ Hermes loads `plugin.yaml` + `__init__.py` at startup and registers the hook.
44
+ No core files touched — this is a layer, not a fork.
45
+
46
+ ## Verify
47
+
48
+ ```bash
49
+ python3 hermes_plugin/test_plugin.py
50
+ # => blocks rm -rf / api_key, allows ls
51
+ # => fails CLOSED when constitution unloadable
52
+ # ALL PASS
53
+ ```
54
+
55
+ ## Path-agnostic
56
+
57
+ The enforcer resolves its config from env → home → default, so this works on
58
+ the USB free-state, the host, or inside the container reading persistent data —
59
+ same code, no changes. That's the forever-system property: layerable,
60
+ adaptable, singular-source.
61
+
62
+ See `FOREVER-SYSTEM.md` §1 (singular source), §4 (fail-closed), §5 (modular),
63
+ §6 (continuously-reminded character) for the governing protocol.
@@ -0,0 +1,367 @@
1
+ """
2
+ Agent Character Kit — Hermes companion plugin (real, registered, fail-closed).
3
+
4
+ Bridges any harness's generic ``pre_tool_call`` + ``pre_llm_call`` hooks to the
5
+ Agent Character Kit ENFORCER DAEMON over its RPC socket. The daemon is the
6
+ SINGLE SOURCE OF TRUTH for enforcement. This plugin is one of several thin
7
+ clients (Hermes is the harness it is packaged for); the kit itself is
8
+ harness-agnostic.
9
+
10
+ Fail-closed: if the daemon socket is unreachable or any error occurs, the
11
+ tool call is BLOCKED. A guard that fails open is no guard.
12
+
13
+ Architecture (3 layers, all root-owned where they must be):
14
+ 1. DAEMON — owns the hold state + acknowledgment ledger; decides allow/hold.
15
+ 2. PLUGIN — thin client: asks daemon ``tool_tick``, blocks on hold; records
16
+ any ``Habit: <name> <resonates true | why: | because | ...> <reason>``
17
+ the agent states into the external ACK LOG (does NOT self-credit).
18
+ 3. MONITOR — root-owned, separate process: reads the ACK LOG, validates each
19
+ statement, and credits the daemon via ``submit_ack``. The agent
20
+ cannot forge acknowledgments (it can only append to the log;
21
+ only the independent monitor can credit). A WATCHDOG revives the
22
+ monitor if it dies (self-healing).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import asyncio
28
+ import concurrent.futures
29
+ import json
30
+ import logging
31
+ import os
32
+ import re
33
+ import random
34
+ from datetime import datetime, timezone
35
+ from pathlib import Path
36
+ from typing import Any, Dict, List, Optional
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ ACK_VERSION = "1.0.0"
41
+
42
+ # Optional env escape hatch: set ACK_DISABLE=1 to turn the plugin into a
43
+ # no-op (never use in production — it defeats the purpose).
44
+ _DISABLE = os.environ.get("ACK_DISABLE") == "1"
45
+
46
+
47
+ def _get_client():
48
+ """Construct the ACK EnforcerClient (thin RPC client to the daemon)."""
49
+ from agent_character_kit.enforcer import EnforcerClient
50
+ return EnforcerClient()
51
+
52
+
53
+ def _call_validate(client, tool_name, args, ctx_id):
54
+ """Run the async validate_tool regardless of ambient event loop.
55
+
56
+ pre_tool_call is a SYNC function. If Hermes already has a running loop
57
+ we cannot asyncio.run() (RuntimeError). Use a worker thread with its own
58
+ loop instead. If no loop is running, asyncio.run() is fine.
59
+ """
60
+ coro = client.validate_tool(tool_name, args, ctx_id)
61
+ try:
62
+ loop = asyncio.get_running_loop()
63
+ except RuntimeError:
64
+ loop = None
65
+ if loop is not None and loop.is_running():
66
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
67
+ return ex.submit(lambda: asyncio.run(coro)).result(timeout=10)
68
+ return asyncio.run(coro)
69
+
70
+
71
+ def _on_pre_tool_call(
72
+ tool_name: str = "",
73
+ args: Optional[Dict[str, Any]] = None,
74
+ task_id: str = "",
75
+ session_id: str = "",
76
+ tool_call_id: str = "",
77
+ **_: Any,
78
+ ) -> Optional[Dict[str, Any]]:
79
+ """pre_tool_call gate -> daemon, then periodic acknowledgment HOLD.
80
+
81
+ The hold decision lives in the root-owned DAEMON, not here. The plugin only
82
+ asks ``tool_tick`` and obeys. The agent cannot reset or bypass the hold
83
+ because the state is in a process it cannot kill or modify.
84
+ """
85
+ if _DISABLE:
86
+ return None
87
+
88
+ if not isinstance(args, dict):
89
+ args = {}
90
+
91
+ ctx_id = session_id or task_id or "unknown"
92
+
93
+ try:
94
+ client = _get_client()
95
+ result = _call_validate(client, tool_name, args, ctx_id)
96
+ except Exception as exc:
97
+ # Fail-closed: daemon unreachable or client error -> block.
98
+ logger.error("[agent-character-kit] enforcement error (failing closed): %s", exc)
99
+ return {
100
+ "action": "block",
101
+ "message": (
102
+ "Agent Character Kit enforcer unavailable — action blocked. "
103
+ "Character cannot be verified, so the action is denied. "
104
+ "A guard that fails open is no guard."
105
+ ),
106
+ }
107
+
108
+ if not result.get("allowed"):
109
+ reason = result.get("reason") or "Denied by Agent Character Kit enforcer."
110
+ reflection = result.get("reflection") or ""
111
+ msg = f"[character] {reason}"
112
+ if reflection:
113
+ msg += f"\n\n{reflection}"
114
+ return {"action": "block", "message": msg}
115
+
116
+ # Allowed by daemon. Now apply the daemon-owned HOLD (state lives in the
117
+ # root-owned daemon, NOT here — the agent cannot bypass it).
118
+ try:
119
+ tick = _daemon_rpc("tool_tick", {"session_id": ctx_id, "tool": tool_name})
120
+ if tick and tick.get("hold"):
121
+ habits = tick.get("habits", [])
122
+ # User-visible output: ONLY the two Habit: lines to state, then the
123
+ # agent's continuation. No hook/hold narration, no instructions, no
124
+ # habit list, no "held"/"acknowledge" language. The mechanism still
125
+ # logs the full hook detail to debug (hidden from the user).
126
+ logger.debug(
127
+ "[agent-character-kit] HOLD active for %s — daemon requires 2 "
128
+ "distinct habit acknowledgments before tool access resumes. "
129
+ "Available habits: %s",
130
+ ctx_id, habits,
131
+ )
132
+ needed = habits[:2] if len(habits) >= 2 else (habits + ["<habit-name>"])[:2]
133
+ msg = (
134
+ f"Habit: {needed[0]} resonates true — <reason>\n\n"
135
+ f"Habit: {needed[1]} resonates true — <reason>"
136
+ )
137
+ return {"action": "block", "message": msg}
138
+ except Exception as exc:
139
+ logger.error("[agent-character-kit] hold check failed (failing closed): %s", exc)
140
+ return {"action": "block", "message": "HOLD check unavailable — action blocked."}
141
+ return None
142
+
143
+
144
+ def _daemon_rpc(method: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
145
+ """Talk to the root-owned enforcer daemon over its socket (thin client).
146
+
147
+ Mirrors EnforcerClient's transport: honors ENFORCER_SOCKET (tcp:// or unix
148
+ path), defaults to the systemd unix socket. Fail-closed: any error -> None
149
+ and the caller decides (pre_tool_call blocks; pre_llm_call ignores).
150
+ """
151
+ import socket as _sock
152
+ raw = os.environ.get("ENFORCER_SOCKET")
153
+ if not raw:
154
+ ws = os.environ.get("AGENT_WORKSPACE")
155
+ raw = os.path.join(ws, ".agent", "enforcer.sock") if ws else os.path.join(
156
+ os.environ.get("HOME", "/root"), ".agent-character-kit", "workspace", ".agent", "enforcer.sock")
157
+ payload = (json.dumps({"method": method, "params": params}) + "\n").encode()
158
+ try:
159
+ if raw.startswith("tcp://"):
160
+ u = __import__("urllib.parse").urlparse(raw)
161
+ s = _sock.create_connection((u.hostname or "127.0.0.1", int(u.port or 8753)), timeout=5)
162
+ else:
163
+ s = _sock.socket(_sock.AF_UNIX, _sock.SOCK_STREAM)
164
+ s.settimeout(5)
165
+ s.connect(raw)
166
+ s.sendall(payload)
167
+ data = b""
168
+ s.settimeout(5)
169
+ while b"\n" not in data:
170
+ chunk = s.recv(4096)
171
+ if not chunk:
172
+ break
173
+ data += chunk
174
+ s.close()
175
+ return json.loads(data.decode().strip())
176
+ except Exception:
177
+ return None
178
+
179
+
180
+ def _detect_ack(session_id: str, text: str) -> None:
181
+ """Append any `Habit: <name> <resonates true | why: | because | ...> <reason>`
182
+ statements the agent makes to the external ACK LOG. The root-owned MONITOR
183
+ reads this log, validates each statement, and credits the daemon's hold
184
+ ledger via submit_ack.
185
+
186
+ The plugin does NOT credit the daemon directly: that would let the agent's
187
+ own process forge acknowledgments. Only the independent monitor (root-owned,
188
+ unkillable by the agent) can credit. The plugin's job is only to record.
189
+ """
190
+ if not text:
191
+ return
192
+ stmts = re.findall(r"habit:\s*\S+\s+resonates\s+true\s+because\s+.+", text, re.I)
193
+ if not stmts:
194
+ return
195
+ try:
196
+ cfg = _load_config()
197
+ log = Path(cfg.get("ack_log", "/tmp/agent-character-kit-ack.jsonl"))
198
+ log.parent.mkdir(parents=True, exist_ok=True)
199
+ with log.open("a", encoding="utf-8") as fh:
200
+ for s in stmts:
201
+ fh.write(json.dumps({"session_id": session_id or "default",
202
+ "statement": s.strip()}) + "\n")
203
+ except Exception:
204
+ pass # logging must never break the call
205
+
206
+
207
+ # --------------------------------------------------------------------------
208
+ # pre_llm_call -> inject a rotating subset of habit prompts (with reasoning)
209
+ # --------------------------------------------------------------------------
210
+ def _load_config() -> Dict[str, Any]:
211
+ """Plugin-level config (config.yaml). Optional override of defaults.
212
+
213
+ Paths are SELF-RESOLVING via env vars (ACK_HABITS_DIR / ACK_ACK_LOG /
214
+ ACK_INJECT_LOG / AGENT_WORKSPACE) so the plugin works no matter where the
215
+ repo lives. config.yaml may set them; env always wins.
216
+ """
217
+ # Defaults are SELF-RESOLVING via env vars (no hardcoded repo/user paths):
218
+ # 1. ACK_HABITS_DIR — explicit override (daemon + plugin must agree)
219
+ # 2. AGENT_WORKSPACE/.agent/habits — matches the daemon's own habit dir
220
+ # 3. repo-relative — last resort, derived from __file__ (portable)
221
+ # config.yaml may OVERRIDE these, but env always wins (see merge below).
222
+ _ENV_FOR = {"habits_dir": "ACK_HABITS_DIR", "inject_log": "ACK_INJECT_LOG", "ack_log": "ACK_ACK_LOG"}
223
+ env_habits = (
224
+ os.environ.get("ACK_HABITS_DIR")
225
+ or (os.environ.get("AGENT_WORKSPACE")
226
+ and os.path.join(os.environ["AGENT_WORKSPACE"], ".agent", "habits"))
227
+ )
228
+ repo_habits = (
229
+ Path(__file__).resolve().parents[2]
230
+ / "python" / "example_workspace" / ".agent" / "habits"
231
+ )
232
+ defaults: Dict[str, Any] = {
233
+ "habits_dir": env_habits or str(repo_habits),
234
+ "inject_log": os.environ.get("ACK_INJECT_LOG", "/tmp/ack-inject-log.jsonl"),
235
+ "ack_log": os.environ.get("ACK_ACK_LOG", "/tmp/agent-character-kit-ack.jsonl"),
236
+ "inject_enabled": True,
237
+ }
238
+ try:
239
+ cfg_path = Path(__file__).resolve().parent / "config.yaml"
240
+ if cfg_path.is_file():
241
+ import yaml
242
+ file_cfg = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
243
+ # env vars take precedence over config.yaml values
244
+ for k in ("habits_dir", "inject_log", "ack_log"):
245
+ if os.environ.get(_ENV_FOR[k]):
246
+ file_cfg[k] = os.environ.get(_ENV_FOR[k])
247
+ elif k not in file_cfg:
248
+ file_cfg[k] = defaults[k]
249
+ file_cfg.setdefault("inject_enabled", defaults["inject_enabled"])
250
+ return file_cfg
251
+ except Exception:
252
+ pass
253
+ return defaults
254
+
255
+
256
+ def _collect_habits(cfg: Dict[str, Any]) -> List[Dict[str, str]]:
257
+ """Read each habit's name, prompt, and reasoning (logic/evidence)."""
258
+ out: List[Dict[str, str]] = []
259
+ try:
260
+ d = Path(cfg.get("habits_dir", ""))
261
+ if d.is_dir():
262
+ for f in sorted(d.glob("*.yaml")):
263
+ try:
264
+ txt = f.read_text(encoding="utf-8")
265
+ except Exception:
266
+ continue
267
+ name_m = re.search(r"^name:\s*\"?([^\"\n]*)", txt, re.M)
268
+ prompt_m = re.search(r"^prompt:\s*\"?([^\"\n]*)", txt, re.M)
269
+ logic_m = re.search(r"logic:\s*\"?([^\"\n]*)", txt, re.M)
270
+ evidence_m = re.search(r"evidence:\s*\"?([^\"\n]*)", txt, re.M)
271
+ if prompt_m:
272
+ out.append({
273
+ "name": name_m.group(1).strip() if name_m else f.stem,
274
+ "prompt": prompt_m.group(1).strip(),
275
+ "logic": logic_m.group(1).strip() if logic_m else "",
276
+ "evidence": evidence_m.group(1).strip() if evidence_m else "",
277
+ })
278
+ except Exception:
279
+ pass
280
+ return out
281
+
282
+
283
+ def _log_injection(cfg: Dict[str, Any], prompts: List[str]) -> None:
284
+ """External proof: write exactly what was injected to a log file.
285
+
286
+ This is what the monitor reads — NOT the agent's self-report.
287
+ """
288
+ try:
289
+ log = Path(cfg.get("inject_log", "/tmp/ack-inject-log.jsonl"))
290
+ log.parent.mkdir(parents=True, exist_ok=True)
291
+ entry = {
292
+ "ts": datetime.now(timezone.utc).isoformat(),
293
+ "count": len(prompts),
294
+ "prompts": prompts,
295
+ }
296
+ with log.open("a", encoding="utf-8") as fh:
297
+ fh.write(json.dumps(entry) + "\n")
298
+ except Exception:
299
+ pass # logging must never break injection
300
+
301
+
302
+ # Per-session rotation state for the looped habit cycle.
303
+ _HABIT_CYCLE: Dict[str, Dict[str, Any]] = {}
304
+
305
+
306
+ def _on_pre_llm_call(
307
+ session_id: str = "",
308
+ user_message: str = "",
309
+ conversation_history: list = None,
310
+ is_first_turn: bool = False,
311
+ model: str = "",
312
+ platform: str = "",
313
+ **_: Any,
314
+ ) -> Optional[Dict[str, Any]]:
315
+ """pre_llm_call -> inject 2-3 randomized habits (with reasoning) on a loop.
316
+
317
+ Each turn surfaces a rotating subset (name + prompt + real reasoning from
318
+ behavior.logic/evidence) so the agent is reminded of different habits over
319
+ time rather than the same 17 every turn. Order is shuffled once per session
320
+ and advanced 2-3 steps each turn (looped cycle). Also feeds the tool-call
321
+ acknowledgment detector from the user's message.
322
+ """
323
+ # Detect acknowledgments the agent states in its own message.
324
+ _detect_ack(session_id, user_message)
325
+
326
+ if _DISABLE:
327
+ return None
328
+ cfg = _load_config()
329
+ if not cfg.get("inject_enabled", True):
330
+ return None
331
+
332
+ habits = _collect_habits(cfg)
333
+ if not habits:
334
+ return None
335
+
336
+ state = _HABIT_CYCLE.get(session_id)
337
+ if state is None or len(state.get("order", [])) != len(habits):
338
+ order = list(range(len(habits)))
339
+ random.Random(hash((session_id, len(habits)))).shuffle(order)
340
+ state = {"order": order, "pos": 0}
341
+ _HABIT_CYCLE[session_id] = state
342
+
343
+ count = random.Random(session_id + str(state["pos"])).randint(2, 3)
344
+ picked = []
345
+ for _ in range(count):
346
+ idx = state["order"][state["pos"] % len(habits)]
347
+ picked.append(habits[idx])
348
+ state["pos"] = (state["pos"] + 1) % len(habits)
349
+ _HABIT_CYCLE[session_id] = state
350
+
351
+ lines = []
352
+ for h in picked:
353
+ reason = h["logic"] or h["evidence"]
354
+ lines.append("- " + h["prompt"])
355
+ if reason:
356
+ lines.append(" why: " + reason)
357
+ ctx = "AGENT CHARACTER HABITS (read before reasoning):\n" + "\n".join(lines)
358
+
359
+ _log_injection(cfg, [h["prompt"] for h in picked])
360
+ return {"context": ctx}
361
+
362
+
363
+ def register(ctx) -> None:
364
+ """Hermes plugin entry point."""
365
+ ctx.register_hook("pre_llm_call", _on_pre_llm_call)
366
+ ctx.register_hook("pre_tool_call", _on_pre_tool_call)
367
+ logger.info("[agent-character-kit] registered pre_tool_call + pre_llm_call hooks (daemon client)")
@@ -0,0 +1,20 @@
1
+ # Agent Character Kit — plugin-level config.
2
+ # The Hermes plugin reads this on load (optional override of built-in defaults).
3
+ #
4
+ # ALL PATHS ARE SELF-RESOLVING VIA ENV VARS — no hardcoded repo/user paths.
5
+ # Resolution order (env always wins over this file):
6
+ # habits_dir : $ACK_HABITS_DIR -> $AGENT_WORKSPACE/.agent/habits -> repo-relative
7
+ # inject_log : $ACK_INJECT_LOG -> /tmp/ack-inject-log.jsonl
8
+ # ack_log : $ACK_ACK_LOG -> /tmp/agent-character-kit-ack.jsonl
9
+ # Leave a key unset (or omit it) to use the env / built-in default. Do NOT
10
+ # hardcode an absolute path here — it breaks portability across hosts.
11
+
12
+ # Where the habit `prompt:` texts live (the daemon's habits dir).
13
+ # habits_dir: /abs/path/only/if/you/must # prefer env ACK_HABITS_DIR
14
+
15
+ # External proof: every pre_llm_call injection is appended here (JSONL).
16
+ # This is what the monitor reads — not the agent's self-report.
17
+ # inject_log: /tmp/ack-inject-log.jsonl
18
+
19
+ # pre_llm_call habit injection on/off (emits only; ingestion not guaranteed).
20
+ inject_enabled: true
@@ -0,0 +1,7 @@
1
+ name: "agent-character-kit"
2
+ version: "1.0.0"
3
+ description: "Enforce agent character (constitution, habits, policy) on every tool call via the Agent Character Kit enforcer. Fail-closed: blocks when the enforcer is unreachable or misconfigured."
4
+ author: "drdeeks"
5
+ entry_point: "__init__.py"
6
+ hooks:
7
+ - "pre_tool_call"