@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,133 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# deploy-agent-enforcer.sh — install the root-owned ACK enforcer as a systemd service.
|
|
4
|
+
#
|
|
5
|
+
# Run on the TARGET machine (Hemlock host / container) AS ROOT:
|
|
6
|
+
# sudo bash deploy-agent-enforcer.sh
|
|
7
|
+
#
|
|
8
|
+
# What it does:
|
|
9
|
+
# 1. Creates the system-owned directories (root-owned, agent read-only).
|
|
10
|
+
# 2. Installs the daemon binary to /usr/local/bin (root-owned, 0755).
|
|
11
|
+
# 3. Installs source under /usr/local/lib/agent-character-kit.
|
|
12
|
+
# 4. Writes the systemd unit to /etc/systemd/system.
|
|
13
|
+
# 5. Enables + starts the service (self-respawning via RestartSec=3).
|
|
14
|
+
#
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
18
|
+
NODE_BIN="$(command -v node)"
|
|
19
|
+
# All paths are self-resolving via env (no hardcoded host assumptions).
|
|
20
|
+
# Defaults shown; override with ACK_INSTALL_LIB / AGENT_WORKSPACE / ENFORCER_SOCKET.
|
|
21
|
+
INSTALL_LIB="${ACK_INSTALL_LIB:-/usr/local/lib/agent-character-kit}"
|
|
22
|
+
AGENT_WORKSPACE="${AGENT_WORKSPACE:-/var/lib/agent-character-kit/workspace}"
|
|
23
|
+
ENFORCER_SOCKET="${ENFORCER_SOCKET:-/run/agent-enforcer/main.sock}"
|
|
24
|
+
INSTALL_BIN="${ACK_INSTALL_BIN:-/usr/local/bin/agent-enforcer-daemon}"
|
|
25
|
+
RUN_DIR="$(dirname "$ENFORCER_SOCKET")"
|
|
26
|
+
VAR_DIR="$(dirname "$AGENT_WORKSPACE")"
|
|
27
|
+
LOG_DIR="${ACK_LOG_DIR:-/var/log/agent-character-kit}"
|
|
28
|
+
UNIT="/etc/systemd/system/agent-enforcer.service"
|
|
29
|
+
|
|
30
|
+
[ "$(id -u)" -eq 0 ] || { echo "ERROR: run as root (sudo bash $0)"; exit 1; }
|
|
31
|
+
[ -n "$NODE_BIN" ] || { echo "ERROR: node not found"; exit 1; }
|
|
32
|
+
|
|
33
|
+
echo ">> Installing ACK enforcer (root-owned, self-respawning)..."
|
|
34
|
+
|
|
35
|
+
# 1. System-owned directories — agent (non-root) gets NO write access.
|
|
36
|
+
install -d -o root -g root -m 0755 "$RUN_DIR"
|
|
37
|
+
install -d -o root -g root -m 0755 "$VAR_DIR"
|
|
38
|
+
install -d -o root -g root -m 0755 "$VAR_DIR/workspace"
|
|
39
|
+
install -d -o root -g root -m 0755 "$VAR_DIR/workspace/.agent"
|
|
40
|
+
install -d -o root -g root -m 0755 "$LOG_DIR"
|
|
41
|
+
install -d -o root -g root -m 0755 "$INSTALL_LIB"
|
|
42
|
+
|
|
43
|
+
# Seed a baseline constitution so the enforcer is NOT born in violation of itself.
|
|
44
|
+
# The agent (or a later install step) overrides these; the enforcer owns the file
|
|
45
|
+
# (root-writable only) so the agent cannot delete its own constraints.
|
|
46
|
+
if [ ! -f "$VAR_DIR/workspace/.agent/constitution.yaml" ]; then
|
|
47
|
+
cat > "$VAR_DIR/workspace/.agent/constitution.yaml" <<'YAML'
|
|
48
|
+
agent:
|
|
49
|
+
id: ack-enforcer
|
|
50
|
+
name: "ACK Enforcer Workspace"
|
|
51
|
+
purpose: "System-owned enforcement workspace"
|
|
52
|
+
core_values:
|
|
53
|
+
- "Character is exercised on every action, not checked once"
|
|
54
|
+
- "Fail closed — when unsure, deny"
|
|
55
|
+
- "Do not bypass, patch, or disable the enforcer"
|
|
56
|
+
operational_standards:
|
|
57
|
+
- "Every tool call passes through the gatekeeper FIRST"
|
|
58
|
+
hard_constraints:
|
|
59
|
+
- "rm -rf /"
|
|
60
|
+
- "git push --force"
|
|
61
|
+
- "sudo"
|
|
62
|
+
- "su"
|
|
63
|
+
- "chmod 777"
|
|
64
|
+
- "chown -R"
|
|
65
|
+
aspiration: "Behave with integrity under no observation"
|
|
66
|
+
YAML
|
|
67
|
+
fi
|
|
68
|
+
if [ ! -f "$VAR_DIR/workspace/.agent/enforcer.yaml" ]; then
|
|
69
|
+
cat > "$VAR_DIR/workspace/.agent/enforcer.yaml" <<'YAML'
|
|
70
|
+
# Open policy by default: no allow-list (everything permitted unless denied).
|
|
71
|
+
# Set an `allow:` list to flip to default-deny. `deny:` is always enforced.
|
|
72
|
+
YAML
|
|
73
|
+
fi
|
|
74
|
+
chown -R root:root "$VAR_DIR/workspace/.agent"
|
|
75
|
+
chmod 0644 "$VAR_DIR/workspace/.agent/constitution.yaml" "$VAR_DIR/workspace/.agent/enforcer.yaml"
|
|
76
|
+
|
|
77
|
+
# Seed habits from the repo's example workspace (single source of habit files).
|
|
78
|
+
# Copies every *.yaml that isn't already present, so a redeploy never clobbers
|
|
79
|
+
# habits the agent/user has since customized. The credential-leak guard lives
|
|
80
|
+
# here too (hard enforcement) — no longer hardcoded inline.
|
|
81
|
+
HABITS_DIR="$VAR_DIR/workspace/.agent/habits"
|
|
82
|
+
install -d -o root -g root -m 0755 "$HABITS_DIR"
|
|
83
|
+
SRC_HABITS="$SRC_DIR/python/example_workspace/.agent/habits"
|
|
84
|
+
if [ -d "$SRC_HABITS" ]; then
|
|
85
|
+
for hf in "$SRC_HABITS"/*.yaml; do
|
|
86
|
+
[ -e "$hf" ] || continue
|
|
87
|
+
bn="$(basename "$hf")"
|
|
88
|
+
if [ ! -f "$HABITS_DIR/$bn" ]; then
|
|
89
|
+
cp "$hf" "$HABITS_DIR/$bn"
|
|
90
|
+
chown root:root "$HABITS_DIR/$bn"
|
|
91
|
+
chmod 0644 "$HABITS_DIR/$bn"
|
|
92
|
+
fi
|
|
93
|
+
done
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# 2. Install source (root-owned, agent read-only)
|
|
97
|
+
# Copy the CONTENTS of SRC/node into INSTALL_LIB/node (trailing-slash
|
|
98
|
+
# semantics) so a re-deploy always refreshes the real binary at
|
|
99
|
+
# $INSTALL_LIB/node/enforcer/agent_enforcer_daemon.js. Using `cp -r
|
|
100
|
+
# "$SRC/node" "$INSTALL_LIB/node"` would NEST under node/node/ on a second
|
|
101
|
+
# run (because the dest dir already exists) and silently leave the live
|
|
102
|
+
# binary stale — a footgun that bites exactly when you redeploy.
|
|
103
|
+
install -d -o root -g root -m 0755 "$INSTALL_LIB/node"
|
|
104
|
+
cp -r "$SRC_DIR/node/." "$INSTALL_LIB/node/"
|
|
105
|
+
chown -R root:root "$INSTALL_LIB"
|
|
106
|
+
chmod -R go-w "$INSTALL_LIB" # writable only by root
|
|
107
|
+
chmod -R a+rX "$INSTALL_LIB" # agent may READ (to load habits), not write
|
|
108
|
+
|
|
109
|
+
# 3. Wrapper binary (root-owned executable)
|
|
110
|
+
cat > "$INSTALL_BIN" <<EOF
|
|
111
|
+
#!/usr/bin/env bash
|
|
112
|
+
exec $NODE_BIN "$INSTALL_LIB/node/enforcer/agent_enforcer_daemon.js"
|
|
113
|
+
EOF
|
|
114
|
+
chown root:root "$INSTALL_BIN"
|
|
115
|
+
chmod 0755 "$INSTALL_BIN"
|
|
116
|
+
|
|
117
|
+
# 4. Systemd units (enforcer + monitor + watchdog — all root-owned, self-respawning)
|
|
118
|
+
for unit in agent-enforcer.service agent-character-monitor.service agent-character-watchdog.service; do
|
|
119
|
+
cp "$SRC_DIR/deploy/$unit" "/etc/systemd/system/$unit"
|
|
120
|
+
chown root:root "/etc/systemd/system/$unit"
|
|
121
|
+
chmod 0644 "/etc/systemd/system/$unit"
|
|
122
|
+
done
|
|
123
|
+
|
|
124
|
+
# 5. Enable + start all three
|
|
125
|
+
systemctl daemon-reload
|
|
126
|
+
systemctl enable --now agent-enforcer.service agent-character-monitor.service agent-character-watchdog.service
|
|
127
|
+
|
|
128
|
+
echo ">> Done. Status:"
|
|
129
|
+
systemctl status agent-enforcer.service agent-character-monitor.service agent-character-watchdog.service --no-pager || true
|
|
130
|
+
echo
|
|
131
|
+
echo "Verify self-respawn: sudo systemctl kill -s KILL agent-enforcer.service"
|
|
132
|
+
echo " -> it should return within ~3s (RestartSec=3)"
|
|
133
|
+
echo "Monitor and watchdog will auto-restart on failure (Restart=always, RestartSec=3)"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# proof-self-respawn.sh — empirically demonstrate the enforcer daemon
|
|
4
|
+
# self-respawns within RestartSec=3 after a hard kill (-9).
|
|
5
|
+
#
|
|
6
|
+
# Uses `systemctl --user` (no root needed). The same RestartSec=3 semantics
|
|
7
|
+
# apply identically under the root-owned /etc/systemd/system unit.
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
|
|
10
|
+
# Self-resolving: repo root comes from $ACK_REPO (default: current dir), not a
|
|
11
|
+
# hardcoded path. The proof service reads $ACK_DAEMON_BIN for the daemon binary.
|
|
12
|
+
REPO="${ACK_REPO:-$PWD}"
|
|
13
|
+
SVC_SRC="$REPO/deploy/agent-enforcer-proof.service"
|
|
14
|
+
export ACK_DAEMON_BIN="${ACK_DAEMON_BIN:-$REPO/node/enforcer/agent_enforcer_daemon.js}"
|
|
15
|
+
|
|
16
|
+
UNIT="agent-enforcer-proof.service"
|
|
17
|
+
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
|
18
|
+
export XDG_RUNTIME_DIR
|
|
19
|
+
|
|
20
|
+
echo ">> Installing user unit..."
|
|
21
|
+
mkdir -p "$HOME/.config/systemd/user"
|
|
22
|
+
cp "$SVC_SRC" "$HOME/.config/systemd/user/"
|
|
23
|
+
systemctl --user daemon-reload
|
|
24
|
+
|
|
25
|
+
echo ">> Starting daemon..."
|
|
26
|
+
systemctl --user start "$UNIT"
|
|
27
|
+
sleep 2
|
|
28
|
+
|
|
29
|
+
echo ">> First PID:"
|
|
30
|
+
PID1=$(systemctl --user show -p MainPID --value "$UNIT")
|
|
31
|
+
echo " $PID1"
|
|
32
|
+
[ "$PID1" -gt 0 ] || { echo "FAIL: daemon did not start"; exit 1; }
|
|
33
|
+
|
|
34
|
+
echo ">> KILL -9 (simulating crash / tamper)..."
|
|
35
|
+
kill -9 "$PID1"
|
|
36
|
+
sleep 1
|
|
37
|
+
PID_MID=$(systemctl --user show -p MainPID --value "$UNIT")
|
|
38
|
+
echo " immediately after kill, MainPID=$PID_MID (0 = still restarting)"
|
|
39
|
+
|
|
40
|
+
echo ">> Waiting for self-respawn (RestartSec=3)..."
|
|
41
|
+
START=$(date +%s.%N)
|
|
42
|
+
for i in $(seq 1 20); do
|
|
43
|
+
PID2=$(systemctl --user show -p MainPID --value "$UNIT")
|
|
44
|
+
if [ "$PID2" -gt 0 ] && [ "$PID2" != "$PID1" ]; then
|
|
45
|
+
END=$(date +%s.%N)
|
|
46
|
+
DELTA=$(echo "$END - $START" | bc 2>/dev/null || awk "BEGIN{print $END-$START}")
|
|
47
|
+
echo ">> RESPAWNED: new PID=$PID2 after ${DELTA}s"
|
|
48
|
+
if awk "BEGIN{exit !($DELTA <= 5)}"; then
|
|
49
|
+
echo "RESULT: PASS — respawned within 5s (target 3-5s)"
|
|
50
|
+
exit 0
|
|
51
|
+
else
|
|
52
|
+
echo "RESULT: FAIL — respawn took >5s"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
sleep 0.5
|
|
57
|
+
done
|
|
58
|
+
echo "RESULT: FAIL — daemon did not respawn"
|
|
59
|
+
exit 1
|
package/node/bin/ack.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ack — Agent Character Kit CLI
|
|
4
|
+
*
|
|
5
|
+
* Single binary: enforcer daemon + companion hook + config + diagnostics
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { processToolCall, generateConfig, EnforcerClient } from "../src/index.js";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
19
|
+
function resolveSocket() {
|
|
20
|
+
return process.env.ENFORCER_SOCKET ||
|
|
21
|
+
(process.env.AGENT_WORKSPACE
|
|
22
|
+
? path.join(process.env.AGENT_WORKSPACE, ".agent", "enforcer.sock")
|
|
23
|
+
: path.join(os.homedir(), ".agent-character-kit", "workspace", ".agent", "enforcer.sock"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveWorkspace() {
|
|
27
|
+
return process.env.AGENT_WORKSPACE ||
|
|
28
|
+
path.join(os.homedir(), ".agent-character-kit", "workspace");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveAckLog() {
|
|
32
|
+
return process.env.ACK_ACK_LOG ||
|
|
33
|
+
path.join(resolveWorkspace(), ".agent", "ack.jsonl");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function checkDaemon(socketPath = resolveSocket()) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const client = new EnforcerClient(socketPath);
|
|
39
|
+
let attempts = 0;
|
|
40
|
+
const tryCall = () => {
|
|
41
|
+
client.call("status", {}).then(res => {
|
|
42
|
+
if (!res || res.error) {
|
|
43
|
+
if (res?.error === "invalid request" && attempts < 1) {
|
|
44
|
+
attempts++;
|
|
45
|
+
setTimeout(tryCall, 50);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
resolve({ alive: false, error: res?.error });
|
|
49
|
+
} else {
|
|
50
|
+
resolve({ alive: true, ...res });
|
|
51
|
+
}
|
|
52
|
+
}).catch(err => resolve({ alive: false, error: err.message }));
|
|
53
|
+
};
|
|
54
|
+
tryCall();
|
|
55
|
+
setTimeout(() => resolve({ alive: false, error: "timeout" }), 3000);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function checkAllSockets() {
|
|
60
|
+
const results = {};
|
|
61
|
+
const candidates = [
|
|
62
|
+
{ name: "root (systemd)", path: "/run/agent-enforcer/main.sock" },
|
|
63
|
+
{ name: "user workspace", path: path.join(resolveWorkspace(), ".agent", "enforcer.sock") },
|
|
64
|
+
{ name: "env ENFORCER_SOCKET", path: resolveSocket() },
|
|
65
|
+
];
|
|
66
|
+
for (const c of candidates) {
|
|
67
|
+
if (!c.path) {
|
|
68
|
+
results[c.name] = { checked: false, reason: "not configured" };
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const r = await checkDaemon(c.path);
|
|
72
|
+
results[c.name] = { path: c.path, checked: true, ...r };
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printStatus(obj) {
|
|
78
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ask(q) {
|
|
82
|
+
return new Promise(r => {
|
|
83
|
+
process.stdout.write(q);
|
|
84
|
+
process.stdin.once("data", d => r(d.toString().trim()));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const program = new Command()
|
|
91
|
+
.name("ack")
|
|
92
|
+
.description("Agent Character Kit — character enforcement for any agent")
|
|
93
|
+
.version("1.0.0");
|
|
94
|
+
|
|
95
|
+
// hook — generate hook config for any framework
|
|
96
|
+
program
|
|
97
|
+
.command("hook")
|
|
98
|
+
.description("Generate hook configuration for your agent framework")
|
|
99
|
+
.argument("<framework>", "Framework: claude | cursor | gemini | opencode | hermes | generic")
|
|
100
|
+
.option("--hook-command <cmd>", "Custom hook command", "npx ack hook")
|
|
101
|
+
.action((framework, opts) => {
|
|
102
|
+
const config = generateConfig(framework, opts.hookCommand);
|
|
103
|
+
console.log(JSON.stringify(config, null, 2));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// config — workspace / agent paths / verification
|
|
107
|
+
program
|
|
108
|
+
.command("config")
|
|
109
|
+
.description("Configure and verify agent workspace / integration paths")
|
|
110
|
+
.option("--workspace <path>", "Set AGENT_WORKSPACE")
|
|
111
|
+
.option("--socket <path>", "Set ENFORCER_SOCKET")
|
|
112
|
+
.option("--ack-log <path>", "Set ACK_ACK_LOG")
|
|
113
|
+
.option("--show", "Show current resolved config")
|
|
114
|
+
.option("--verify", "Verify all paths exist and daemon reachable")
|
|
115
|
+
.option("--write-env [file]", "Write resolved .env to file (default: workspace/.env)")
|
|
116
|
+
.action(async (opts) => {
|
|
117
|
+
if (opts.show || (!opts.workspace && !opts.socket && !opts.ackLog && !opts.verify && !opts.writeEnv)) {
|
|
118
|
+
console.log("=== Resolved Config ===");
|
|
119
|
+
console.log("AGENT_WORKSPACE:", resolveWorkspace());
|
|
120
|
+
console.log("ENFORCER_SOCKET:", resolveSocket());
|
|
121
|
+
console.log("ACK_ACK_LOG:", resolveAckLog());
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log("Env vars (if set):");
|
|
124
|
+
console.log(" ENFORCER_SOCKET:", process.env.ENFORCER_SOCKET || "(unset)");
|
|
125
|
+
console.log(" AGENT_WORKSPACE:", process.env.AGENT_WORKSPACE || "(unset)");
|
|
126
|
+
console.log(" ACK_ACK_LOG:", process.env.ACK_ACK_LOG || "(unset)");
|
|
127
|
+
}
|
|
128
|
+
if (opts.workspace) {
|
|
129
|
+
console.log(`Set AGENT_WORKSPACE=${opts.workspace} in your shell/env`);
|
|
130
|
+
}
|
|
131
|
+
if (opts.socket) {
|
|
132
|
+
console.log(`Set ENFORCER_SOCKET=${opts.socket} in your shell/env`);
|
|
133
|
+
}
|
|
134
|
+
if (opts.ackLog) {
|
|
135
|
+
console.log(`Set ACK_ACK_LOG=${opts.ackLog} in your shell/env`);
|
|
136
|
+
}
|
|
137
|
+
if (opts.verify) {
|
|
138
|
+
console.log("\n=== Verification ===");
|
|
139
|
+
const ws = resolveWorkspace();
|
|
140
|
+
console.log("Workspace exists:", fs.existsSync(ws));
|
|
141
|
+
console.log(" habits dir:", fs.existsSync(path.join(ws, ".agent", "habits")));
|
|
142
|
+
console.log(" constitution:", fs.existsSync(path.join(ws, ".agent", "constitution.yaml")));
|
|
143
|
+
const sock = resolveSocket();
|
|
144
|
+
console.log("Socket path:", sock);
|
|
145
|
+
const daemon = await checkDaemon(sock);
|
|
146
|
+
console.log("Daemon reachable:", daemon.alive, daemon.error ? `(${daemon.error})` : "");
|
|
147
|
+
console.log("Ack log:", resolveAckLog(), fs.existsSync(resolveAckLog()) ? "exists" : "missing");
|
|
148
|
+
}
|
|
149
|
+
if (opts.writeEnv) {
|
|
150
|
+
const target = opts.writeEnv === true
|
|
151
|
+
? path.join(resolveWorkspace(), ".env")
|
|
152
|
+
: opts.writeEnv;
|
|
153
|
+
const lines = [
|
|
154
|
+
`AGENT_WORKSPACE=${resolveWorkspace()}`,
|
|
155
|
+
`ENFORCER_SOCKET=${resolveSocket()}`,
|
|
156
|
+
`ACK_ACK_LOG=${resolveAckLog()}`,
|
|
157
|
+
];
|
|
158
|
+
fs.writeFileSync(target, lines.join("\n") + "\n");
|
|
159
|
+
console.log("Wrote .env to:", target);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// status — all sockets + daemon health
|
|
164
|
+
program
|
|
165
|
+
.command("status")
|
|
166
|
+
.description("Show all socket endpoints and daemon health")
|
|
167
|
+
.option("--json", "Output JSON")
|
|
168
|
+
.action(async (opts) => {
|
|
169
|
+
const results = await checkAllSockets();
|
|
170
|
+
if (opts.json) {
|
|
171
|
+
printStatus(results);
|
|
172
|
+
} else {
|
|
173
|
+
console.log("=== Socket Status ===");
|
|
174
|
+
for (const [name, info] of Object.entries(results)) {
|
|
175
|
+
if (!info.checked) {
|
|
176
|
+
console.log(` ${name}: not configured`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const state = info.alive ? "🟢 ALIVE" : "🔴 DEAD";
|
|
180
|
+
console.log(` ${name}: ${state}`);
|
|
181
|
+
console.log(` path: ${info.path}`);
|
|
182
|
+
if (info.error) console.log(` error: ${info.error}`);
|
|
183
|
+
if (info.workspace) console.log(` workspace: ${info.workspace}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// repair — troubleshoot / auto-fix common issues
|
|
189
|
+
program
|
|
190
|
+
.command("repair")
|
|
191
|
+
.description("Diagnose and auto-repair common issues")
|
|
192
|
+
.option("--fix", "Apply fixes (dry-run by default)")
|
|
193
|
+
.option("--reinstall", "Re-seed habits and constitution")
|
|
194
|
+
.action(async (opts) => {
|
|
195
|
+
console.log("=== ACK Repair ===\n");
|
|
196
|
+
const issues = [];
|
|
197
|
+
const fixes = [];
|
|
198
|
+
|
|
199
|
+
const ws = resolveWorkspace();
|
|
200
|
+
const habitsDir = path.join(ws, ".agent", "habits");
|
|
201
|
+
const constitution = path.join(ws, ".agent", "constitution.yaml");
|
|
202
|
+
const sock = resolveSocket();
|
|
203
|
+
|
|
204
|
+
if (!fs.existsSync(ws)) {
|
|
205
|
+
issues.push("Workspace missing");
|
|
206
|
+
if (opts.fix) {
|
|
207
|
+
fs.mkdirSync(habitsDir, { recursive: true });
|
|
208
|
+
fs.mkdirSync(path.join(ws, ".agent"), { recursive: true });
|
|
209
|
+
fixes.push("Created workspace directories");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!fs.existsSync(habitsDir)) {
|
|
213
|
+
issues.push("Habits directory missing");
|
|
214
|
+
if (opts.fix) {
|
|
215
|
+
fs.mkdirSync(habitsDir, { recursive: true });
|
|
216
|
+
fixes.push("Created habits directory");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!fs.existsSync(constitution)) {
|
|
220
|
+
issues.push("Constitution missing");
|
|
221
|
+
if (opts.fix) {
|
|
222
|
+
fs.writeFileSync(constitution, [
|
|
223
|
+
"# Agent Character Kit — constitution (hard constraints).",
|
|
224
|
+
"# The daemon embeds safe defaults; this file OVERRIDES/extends them.",
|
|
225
|
+
"hard_constraints:",
|
|
226
|
+
" - no_credential_leak: block any tool call that would expose a secret",
|
|
227
|
+
" - no_destructive_without_confirm: block rm -rf /, mkfs, dd on disks, etc. unless confirmed",
|
|
228
|
+
].join("\n") + "\n");
|
|
229
|
+
fixes.push("Created default constitution.yaml");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (opts.reinstall) {
|
|
233
|
+
const srcHabits = path.join(REPO_ROOT, "python", "example_workspace", ".agent", "habits");
|
|
234
|
+
if (fs.existsSync(srcHabits)) {
|
|
235
|
+
for (const f of fs.readdirSync(srcHabits)) {
|
|
236
|
+
if (f.endsWith(".yaml")) {
|
|
237
|
+
fs.copyFileSync(path.join(srcHabits, f), path.join(habitsDir, f));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
fixes.push(`Re-seeded ${fs.readdirSync(srcHabits).length} habit files`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!fs.existsSync(sock) && !sock.startsWith("tcp://")) {
|
|
244
|
+
issues.push(`Socket not found at ${sock} (daemon may not be running)`);
|
|
245
|
+
if (opts.fix) {
|
|
246
|
+
fixes.push("Start daemon with: sudo systemctl start agent-enforcer (root) or ack install --yes (user)");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const daemon = await checkDaemon(sock);
|
|
250
|
+
if (!daemon.alive) {
|
|
251
|
+
issues.push(`Daemon unreachable: ${daemon.error}`);
|
|
252
|
+
if (opts.fix) {
|
|
253
|
+
fixes.push("Start daemon with: sudo systemctl start agent-enforcer (root) or ack install --yes (user)");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log("Issues found:", issues.length);
|
|
258
|
+
issues.forEach(i => console.log(" ⚠ ", i));
|
|
259
|
+
console.log("\nFixes applied:", fixes.length);
|
|
260
|
+
fixes.forEach(f => console.log(" ✓ ", f));
|
|
261
|
+
if (issues.length && !opts.fix) {
|
|
262
|
+
console.log("\nRun with --fix to apply repairs.");
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// install — one-command setup (delegates to install.js)
|
|
267
|
+
program
|
|
268
|
+
.command("install")
|
|
269
|
+
.description("Install ACK (daemon + monitor + watchdog + companion config)")
|
|
270
|
+
.option("--yes", "Non-interactive, all components")
|
|
271
|
+
.option("--all", "Everything: root mode + all components")
|
|
272
|
+
.option("--user", "User-mode (default)")
|
|
273
|
+
.option("--root", "Root mode (systemd)")
|
|
274
|
+
.option("--workspace <path>", "Workspace path")
|
|
275
|
+
.option("--socket <mode>", "Socket: unix | tcp | path")
|
|
276
|
+
.option("--harness <name>", "Harness: claude | cursor | gemini | opencode | hermes | generic")
|
|
277
|
+
.option("--no-monitor", "Skip monitor")
|
|
278
|
+
.option("--no-watchdog", "Skip watchdog")
|
|
279
|
+
.option("--no-companion", "Skip companion config")
|
|
280
|
+
.action(async (opts) => {
|
|
281
|
+
const { default: installMain } = await import("./install.js");
|
|
282
|
+
process.argv = ["node", "install.js", ...Object.entries(opts)
|
|
283
|
+
.filter(([_, v]) => v !== false && v !== true)
|
|
284
|
+
.flatMap(([k, v]) => v === true ? [`--${k}`] : [`--${k}`, String(v)]), "--yes"];
|
|
285
|
+
await installMain();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// habit — create / list habits
|
|
289
|
+
program
|
|
290
|
+
.command("habit:create")
|
|
291
|
+
.description("Create a new habit")
|
|
292
|
+
.argument("<name>", "Habit name (kebab-case)")
|
|
293
|
+
.option("-p, --prompt <text>", "Prompt question")
|
|
294
|
+
.option("-l, --logic <text>", "Reasoning / logic")
|
|
295
|
+
.action(async (name, opts) => {
|
|
296
|
+
const ws = resolveWorkspace();
|
|
297
|
+
const habitsDir = path.join(ws, ".agent", "habits");
|
|
298
|
+
fs.mkdirSync(habitsDir, { recursive: true });
|
|
299
|
+
const file = path.join(habitsDir, `${name}.yaml`);
|
|
300
|
+
if (fs.existsSync(file)) {
|
|
301
|
+
console.error("Habit already exists:", file);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
const prompt = opts.prompt || await ask("Prompt (self-question): ");
|
|
305
|
+
const logic = opts.logic || await ask("Logic (why this governs your actions): ");
|
|
306
|
+
const yaml = [
|
|
307
|
+
`# Habit: ${name}`,
|
|
308
|
+
`# Source question: ${prompt}`,
|
|
309
|
+
`# Logic: ${logic}`,
|
|
310
|
+
`name: "${name}"`,
|
|
311
|
+
`prompt: ${JSON.stringify(prompt)}`,
|
|
312
|
+
`enforcement:`,
|
|
313
|
+
` level: "reminder"`,
|
|
314
|
+
`behavior:`,
|
|
315
|
+
` kind: "standard"`,
|
|
316
|
+
` assert: ${JSON.stringify(logic)}`,
|
|
317
|
+
` evidence: "The agent applies this habit consistently and can state WHY when held."`,
|
|
318
|
+
` logic: ${JSON.stringify(logic)}`,
|
|
319
|
+
"",
|
|
320
|
+
].join("\n");
|
|
321
|
+
fs.writeFileSync(file, yaml);
|
|
322
|
+
console.log("Created:", file);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
program
|
|
326
|
+
.command("habit:list")
|
|
327
|
+
.description("List all habits")
|
|
328
|
+
.action(() => {
|
|
329
|
+
const ws = resolveWorkspace();
|
|
330
|
+
const habitsDir = path.join(ws, ".agent", "habits");
|
|
331
|
+
if (!fs.existsSync(habitsDir)) {
|
|
332
|
+
console.log("No habits directory");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const files = fs.readdirSync(habitsDir).filter(f => f.endsWith(".yaml"));
|
|
336
|
+
for (const f of files) {
|
|
337
|
+
const content = fs.readFileSync(path.join(habitsDir, f), "utf8");
|
|
338
|
+
const nameMatch = content.match(/name:\s*"([^"]+)"/);
|
|
339
|
+
const promptMatch = content.match(/prompt:\s*"([^"]+)"/);
|
|
340
|
+
console.log(` ${nameMatch?.[1] || f}: ${promptMatch?.[1] || "(no prompt)"}`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// doctor — full diagnostics
|
|
345
|
+
program
|
|
346
|
+
.command("doctor")
|
|
347
|
+
.description("Full diagnostic report")
|
|
348
|
+
.action(async () => {
|
|
349
|
+
console.log("=== ACK Doctor ===\n");
|
|
350
|
+
console.log("Version: 1.0.0");
|
|
351
|
+
console.log("Node:", process.version);
|
|
352
|
+
console.log("Platform:", process.platform, process.arch);
|
|
353
|
+
console.log("");
|
|
354
|
+
|
|
355
|
+
const ws = resolveWorkspace();
|
|
356
|
+
console.log("Workspace:", ws);
|
|
357
|
+
console.log(" exists:", fs.existsSync(ws));
|
|
358
|
+
console.log(" habits:", fs.existsSync(path.join(ws, ".agent", "habits")) ? fs.readdirSync(path.join(ws, ".agent", "habits")).filter(f => f.endsWith(".yaml")).length : 0);
|
|
359
|
+
console.log(" constitution:", fs.existsSync(path.join(ws, ".agent", "constitution.yaml")));
|
|
360
|
+
console.log("");
|
|
361
|
+
|
|
362
|
+
const sock = resolveSocket();
|
|
363
|
+
console.log("Socket:", sock);
|
|
364
|
+
const daemon = await checkDaemon(sock);
|
|
365
|
+
console.log(" daemon:", daemon.alive ? "🟢 reachable" : "🔴 unreachable");
|
|
366
|
+
if (daemon.error) console.log(" error:", daemon.error);
|
|
367
|
+
console.log("");
|
|
368
|
+
|
|
369
|
+
const results = await checkAllSockets();
|
|
370
|
+
console.log("All endpoints:");
|
|
371
|
+
for (const [name, info] of Object.entries(results)) {
|
|
372
|
+
if (!info.checked) {
|
|
373
|
+
console.log(` ${name}: not configured`);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
console.log(` ${name}: ${info.alive ? "🟢" : "🔴"} ${info.path}`);
|
|
377
|
+
}
|
|
378
|
+
console.log("");
|
|
379
|
+
|
|
380
|
+
console.log("Ack log:", resolveAckLog(), fs.existsSync(resolveAckLog()) ? "exists" : "missing");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
program.parse();
|