@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
|
@@ -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"
|