@delorenj/pjangler 1.2.33 → 1.3.0
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/dist/index.js +295 -130
- package/dist/mcp-server.js +297 -129
- package/package.json +1 -1
- package/templates/commonproject/template/.agents/hooks/README.md +10 -10
- package/templates/commonproject/template/.agents/hooks/hooks.master.json +1 -1
- package/templates/commonproject/template/.agents/hooks/sync.py +4 -4
- package/templates/commonproject/template/.agents/local.example.json +1 -1
- package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +1 -1
- package/templates/commonproject/template/mise.toml.jinja +11 -11
- package/templates/hermes-agent/copier.yml +27 -4
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +11 -10
- package/templates/hermes-agent/template/.scripts/01-config.sh +4 -4
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +7 -12
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +37 -32
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +18 -4
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +72 -12
- package/templates/hermes-agent/template/.scripts/80-registry.sh +13 -3
- package/templates/hermes-agent/template/.scripts/_lib.sh +12 -5
- package/templates/hermes-agent/template/.scripts/config.example.toml +7 -4
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +81 -0
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -1
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +30 -4
- package/templates/hermes-agent/template/.scripts/secret-scan.py +82 -16
- package/templates/hermes-agent/template/SOUL.md.jinja +4 -4
- package/templates/hermes-agent/template/hermes.jinja +36 -7
- package/templates/hermes-agent/template/role.yaml.jinja +15 -5
|
@@ -29,7 +29,8 @@ python3 - "$REGISTRY_FILE" "$AGENT_ID" "$REPO" "$ROLE" "$DISPLAY_NAME" \
|
|
|
29
29
|
"$(yaml_get slack.provisioning_status)" "$(yaml_get slack.team_id)" \
|
|
30
30
|
"$(yaml_get slack.team_name)" "$(yaml_get slack.bot_user_id)" \
|
|
31
31
|
"$(yaml_get slack.bot_id)" "$(yaml_get slack.bot_username)" \
|
|
32
|
-
"$(yaml_get bloodbank.
|
|
32
|
+
"$(yaml_get bloodbank.enabled)" "$(yaml_get bloodbank.gateway_scope)" \
|
|
33
|
+
"$(yaml_get bloodbank.target_agent_id)" \
|
|
33
34
|
"$PLANE_WORKSPACE" "$PLANE_PROJECT_ID" "$(yaml_get plane.identifier)" \
|
|
34
35
|
"$RUNTIME_REPO" "$HERMES_BIN" "$HERMES_AGENT_REPO" "$HERMES_RUNTIME_GIT_URL" \
|
|
35
36
|
"$HERMES_RUNTIME_GIT_REF" "$HERMES_RUNTIME_GIT_SHA" "$FLEET_ENV" \
|
|
@@ -47,13 +48,21 @@ except ImportError:
|
|
|
47
48
|
(path, agent_id, repo, role, display, project, role_dir, profile,
|
|
48
49
|
telegram_status, bot, telegram_bot_id,
|
|
49
50
|
slack_status, slack_team_id, slack_team_name, slack_user_id, slack_bot_id,
|
|
50
|
-
slack_username, bloodbank_scope, bloodbank_target, plane_ws, plane_id,
|
|
51
|
+
slack_username, bloodbank_enabled, bloodbank_scope, bloodbank_target, plane_ws, plane_id,
|
|
51
52
|
plane_ident, runtime_repo, hermes_bin, hermes_repo, hermes_git_url,
|
|
52
|
-
hermes_git_ref, hermes_git_sha, fleet_env, gw, heartbeat) = sys.argv[1:
|
|
53
|
+
hermes_git_ref, hermes_git_sha, fleet_env, gw, heartbeat) = sys.argv[1:33]
|
|
53
54
|
p = pathlib.Path(path)
|
|
54
55
|
if p.is_symlink():
|
|
55
56
|
raise SystemExit(f"refusing to update registry symlink: {p}")
|
|
56
57
|
data = yaml.safe_load(p.read_text()) or {"schema_version": 1, "agents": {}}
|
|
58
|
+
if bloodbank_enabled == "":
|
|
59
|
+
bloodbank_enabled_value = False
|
|
60
|
+
elif bloodbank_enabled == "true":
|
|
61
|
+
bloodbank_enabled_value = True
|
|
62
|
+
elif bloodbank_enabled == "false":
|
|
63
|
+
bloodbank_enabled_value = False
|
|
64
|
+
else:
|
|
65
|
+
raise SystemExit("bloodbank.enabled must be the strict YAML boolean true or false")
|
|
57
66
|
data.setdefault("agents", {})[agent_id] = {
|
|
58
67
|
"repo": repo, "role": role, "display_name": display,
|
|
59
68
|
"project_path": project, "role_dir": role_dir,
|
|
@@ -72,6 +81,7 @@ data.setdefault("agents", {})[agent_id] = {
|
|
|
72
81
|
"bot_username": slack_username,
|
|
73
82
|
},
|
|
74
83
|
"bloodbank": {
|
|
84
|
+
"enabled": bloodbank_enabled_value,
|
|
75
85
|
"gateway_scope": bloodbank_scope,
|
|
76
86
|
"target_agent_id": bloodbank_target,
|
|
77
87
|
},
|
|
@@ -137,6 +137,12 @@ already_done() {
|
|
|
137
137
|
mark_done() {
|
|
138
138
|
touch "$ROLE_DIR/.scripts/.done-$1"
|
|
139
139
|
}
|
|
140
|
+
clear_done() {
|
|
141
|
+
# Remove only the marker for the explicitly deferred step. This lets a
|
|
142
|
+
# later activation reconcile that phase without disturbing completed,
|
|
143
|
+
# unrelated provisioning state.
|
|
144
|
+
rm -f -- "$ROLE_DIR/.scripts/.done-$1"
|
|
145
|
+
}
|
|
140
146
|
|
|
141
147
|
# Fleet source-of-truth (shared across all wrappers/provisioners).
|
|
142
148
|
# Every default below resolves as: env var > fleet.env > config.toml > fallback.
|
|
@@ -151,11 +157,12 @@ fi
|
|
|
151
157
|
unset TELEGRAM_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN
|
|
152
158
|
|
|
153
159
|
# Tools we expect on the host
|
|
154
|
-
HERMES_BIN="${HERMES_BIN:-${HERMES_FLEET_BIN:-$(config_get fleet.hermes_bin "$HOME/.
|
|
155
|
-
HERMES_AGENT_REPO="${HERMES_AGENT_REPO:-${HERMES_FLEET_REPO:-$(config_get fleet.hermes_repo "$HOME/.
|
|
160
|
+
HERMES_BIN="${HERMES_BIN:-${HERMES_FLEET_BIN:-$(config_get fleet.hermes_bin "$HOME/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1/.venv/bin/hermes")}}"
|
|
161
|
+
HERMES_AGENT_REPO="${HERMES_AGENT_REPO:-${HERMES_FLEET_REPO:-$(config_get fleet.hermes_repo "$HOME/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1")}}"
|
|
162
|
+
PJANGLER_BIN="${PJANGLER_BIN:-$(config_get fleet.pjangler_bin "pj")}"
|
|
156
163
|
HERMES_RUNTIME_GIT_URL="${HERMES_RUNTIME_GIT_URL:-$(config_get fleet.hermes_git_url 'https://github.com/delorenj/hermes-agent.git')}"
|
|
157
|
-
HERMES_RUNTIME_GIT_REF="${HERMES_RUNTIME_GIT_REF:-$(config_get fleet.hermes_git_ref '
|
|
158
|
-
HERMES_RUNTIME_GIT_SHA="${HERMES_RUNTIME_GIT_SHA:-$(config_get fleet.hermes_git_sha '
|
|
164
|
+
HERMES_RUNTIME_GIT_REF="${HERMES_RUNTIME_GIT_REF:-$(config_get fleet.hermes_git_ref 'main')}"
|
|
165
|
+
HERMES_RUNTIME_GIT_SHA="${HERMES_RUNTIME_GIT_SHA:-$(config_get fleet.hermes_git_sha '0408fec7a153e6c32c064acd2b8053917f1525f1')}"
|
|
159
166
|
HERMES_OAUTH_FILE="${HERMES_OAUTH_FILE:-${HERMES_FLEET_OAUTH_FILE:-$(config_get fleet.oauth_file "$HOME/.hermes/auth.json")}}"
|
|
160
167
|
CODEX_HOME="${CODEX_HOME:-${HERMES_FLEET_CODEX_HOME:-$(config_get fleet.codex_home "$HOME/.codex")}}"
|
|
161
168
|
# Prefer a scaffold vendored into this agent directory; fall back to the configured template path.
|
|
@@ -197,7 +204,7 @@ BLOODBANK_COMPOSE_DIR="${BLOODBANK_COMPOSE_DIR:-$(config_get bloodbank.compose_d
|
|
|
197
204
|
PLANE_BASE="${PLANE_BASE:-$(config_get plane.base 'https://plane.delo.sh')}"
|
|
198
205
|
PLANE_API_KEY="${PLANE_API_KEY:-${PLANE_33GOD_API_KEY:-}}"
|
|
199
206
|
|
|
200
|
-
export FLEET_ENV HERMES_BIN HERMES_AGENT_REPO HERMES_RUNTIME_GIT_URL \
|
|
207
|
+
export FLEET_ENV HERMES_BIN HERMES_AGENT_REPO PJANGLER_BIN HERMES_RUNTIME_GIT_URL \
|
|
201
208
|
HERMES_RUNTIME_GIT_REF HERMES_RUNTIME_GIT_SHA HERMES_OAUTH_FILE CODEX_HOME \
|
|
202
209
|
RUNTIME_SCAFFOLD_DIR REGISTRY_FILE \
|
|
203
210
|
BLOODBANK_NATS_HOST BLOODBANK_NATS_PORT BLOODBANK_COMPOSE_DIR \
|
|
@@ -12,14 +12,17 @@
|
|
|
12
12
|
|
|
13
13
|
[fleet]
|
|
14
14
|
# Absolute path to the shared Hermes executable every agent launcher execs.
|
|
15
|
-
hermes_bin = "~/.
|
|
15
|
+
hermes_bin = "~/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1/.venv/bin/hermes"
|
|
16
16
|
# Checkout of the shared hermes-agent repo.
|
|
17
|
-
hermes_repo = "~/.
|
|
17
|
+
hermes_repo = "~/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1"
|
|
18
|
+
# Canonical CommonProject migration CLI. Runtime provisioning delegates named
|
|
19
|
+
# profile topology to this command; it never rewrites profile links itself.
|
|
20
|
+
pjangler_bin = "pj"
|
|
18
21
|
# Reviewed fleet runtime publication. Provisioning metadata and clean installs
|
|
19
22
|
# must agree on this fork/ref/SHA; do not substitute upstream main.
|
|
20
23
|
hermes_git_url = "https://github.com/delorenj/hermes-agent.git"
|
|
21
|
-
hermes_git_ref = "
|
|
22
|
-
hermes_git_sha = "
|
|
24
|
+
hermes_git_ref = "main"
|
|
25
|
+
hermes_git_sha = "0408fec7a153e6c32c064acd2b8053917f1525f1"
|
|
23
26
|
# Fallback runtime scaffold dir — used only when an agent's vendored
|
|
24
27
|
# ./.runtime-scaffold is missing.
|
|
25
28
|
runtime_scaffold_dir = "~/code/hermes-agent-template/runtime-scaffold"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Secret-free systemd entrypoint. Decrypted values are read only from the
|
|
3
|
+
# service manager's volatile credential directory and never printed or written.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROLE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
+
ROLE_YAML="$ROLE_DIR/role.yaml"
|
|
8
|
+
|
|
9
|
+
yaml_get() {
|
|
10
|
+
python3 - "$ROLE_YAML" "$1" <<'PYEOF'
|
|
11
|
+
import re, sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
text = Path(sys.argv[1]).read_text()
|
|
14
|
+
parts = sys.argv[2].split(".")
|
|
15
|
+
if len(parts) == 2:
|
|
16
|
+
match = re.search(
|
|
17
|
+
rf"(?m)^{re.escape(parts[0])}:[ \t]*\n((?:[ \t]+\S.*\n?)*)", text
|
|
18
|
+
)
|
|
19
|
+
text = match.group(1) if match else ""
|
|
20
|
+
key = parts[-1]
|
|
21
|
+
match = re.search(rf'(?m)^\s*{re.escape(key)}:\s*"?([^"\n]*)"?\s*$', text)
|
|
22
|
+
print(match.group(1).strip() if match else "")
|
|
23
|
+
PYEOF
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
AGENT_ID="$(yaml_get agent_id)"
|
|
27
|
+
PROFILE_NAME="$(yaml_get profile)"
|
|
28
|
+
[[ -n "$AGENT_ID" && -n "$PROFILE_NAME" ]] \
|
|
29
|
+
|| { printf 'credential-launch: incomplete role identity\n' >&2; exit 2; }
|
|
30
|
+
[[ -n "${HERMES_BIN:-}" && -x "$HERMES_BIN" ]] \
|
|
31
|
+
|| { printf 'credential-launch: HERMES_BIN is not executable\n' >&2; exit 1; }
|
|
32
|
+
|
|
33
|
+
mode="${1:-}"
|
|
34
|
+
[[ "$mode" == "gateway" || "$mode" == "heartbeat" ]] \
|
|
35
|
+
|| { printf 'credential-launch: expected gateway or heartbeat\n' >&2; exit 2; }
|
|
36
|
+
|
|
37
|
+
FLEET_HOME="${HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
38
|
+
PROFILE_HOME="$FLEET_HOME/profiles/${PROFILE_NAME:-$AGENT_ID}"
|
|
39
|
+
[[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]] \
|
|
40
|
+
|| { printf 'credential-launch: named profile is not a real directory\n' >&2; exit 1; }
|
|
41
|
+
export HERMES_HOME="$PROFILE_HOME"
|
|
42
|
+
|
|
43
|
+
if ! REPO_ROOT="$(git -C "$ROLE_DIR" rev-parse --show-toplevel 2>/dev/null)"; then
|
|
44
|
+
REPO_ROOT="$ROLE_DIR"
|
|
45
|
+
fi
|
|
46
|
+
export TERMINAL_CWD="$REPO_ROOT"
|
|
47
|
+
|
|
48
|
+
load_credential() {
|
|
49
|
+
local credential_id="$1" env_name="$2" credential_file value
|
|
50
|
+
[[ "$env_name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] \
|
|
51
|
+
|| { printf 'credential-launch: invalid credential environment name\n' >&2; exit 2; }
|
|
52
|
+
credential_file="${CREDENTIALS_DIRECTORY:-}/$credential_id"
|
|
53
|
+
[[ -n "${CREDENTIALS_DIRECTORY:-}" && -r "$credential_file" ]] || return 0
|
|
54
|
+
value="$(<"$credential_file")"
|
|
55
|
+
printf -v "$env_name" '%s' "$value"
|
|
56
|
+
export "$env_name"
|
|
57
|
+
unset value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
load_credential telegram_bot_token TELEGRAM_BOT_TOKEN
|
|
61
|
+
MODEL_KEY_ENV="$(yaml_get model.key_env)"
|
|
62
|
+
if [[ -n "$MODEL_KEY_ENV" ]]; then
|
|
63
|
+
load_credential model_api_key "$MODEL_KEY_ENV"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if [[ "$mode" == "heartbeat" ]]; then
|
|
67
|
+
exec "$ROLE_DIR/.scripts/heartbeat.sh"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
gateway_args=(gateway run --replace)
|
|
71
|
+
MODEL_NAME="$(yaml_get model.name)"
|
|
72
|
+
MODEL_PROVIDER="$(yaml_get model.provider)"
|
|
73
|
+
MODEL_BASE_URL="$(yaml_get model.base_url)"
|
|
74
|
+
MODEL_API_MODE="$(yaml_get model.api_mode)"
|
|
75
|
+
[[ -z "$MODEL_NAME" ]] || gateway_args+=(--model "$MODEL_NAME")
|
|
76
|
+
[[ -z "$MODEL_PROVIDER" ]] || gateway_args+=(--provider "$MODEL_PROVIDER")
|
|
77
|
+
[[ -z "$MODEL_BASE_URL" ]] || gateway_args+=(--base-url "$MODEL_BASE_URL")
|
|
78
|
+
[[ -z "$MODEL_API_MODE" ]] || gateway_args+=(--api-mode "$MODEL_API_MODE")
|
|
79
|
+
[[ -z "$MODEL_KEY_ENV" ]] || gateway_args+=(--key-env "$MODEL_KEY_ENV")
|
|
80
|
+
|
|
81
|
+
exec "$HERMES_BIN" "${gateway_args[@]}"
|
|
@@ -15,6 +15,7 @@ set -euo pipefail
|
|
|
15
15
|
|
|
16
16
|
ROLE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # agents/hermes/<role>
|
|
17
17
|
RUNTIME="$ROLE_DIR/runtime"
|
|
18
|
+
RUN_HOME="${HERMES_HOME:-$RUNTIME}"
|
|
18
19
|
PROMPT_FILE="$ROLE_DIR/.scripts/sentinel.prompt.md"
|
|
19
20
|
STATE_FILE="$RUNTIME/continuous-ticket-sentinel-state.json"
|
|
20
21
|
LOCK_FILE="$RUNTIME/continuous-ticket-sentinel.lock"
|
|
@@ -242,7 +243,7 @@ trap 'python3 "$ROLE_DIR/.scripts/momo-wip-lock.py" release "$WIP_LOCK" "hermes:
|
|
|
242
243
|
|
|
243
244
|
prompt="$(<"$PROMPT_FILE")"
|
|
244
245
|
set +e
|
|
245
|
-
env HERMES_HOME="$
|
|
246
|
+
env HERMES_HOME="$RUN_HOME" "$HERMES_BIN" chat -Q --source cron --max-turns 90 -q "$prompt"
|
|
246
247
|
status=$?
|
|
247
248
|
set -e
|
|
248
249
|
|
|
@@ -22,10 +22,6 @@ ROLE_YAML="$ROLE_DIR/role.yaml"
|
|
|
22
22
|
BASE="${PLANE_BASE:-https://plane.delo.sh}"
|
|
23
23
|
|
|
24
24
|
FLEET_ENV="${HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
|
|
25
|
-
if [ -f "$FLEET_ENV" ]; then
|
|
26
|
-
# shellcheck disable=SC1090
|
|
27
|
-
. "$FLEET_ENV"
|
|
28
|
-
fi
|
|
29
25
|
|
|
30
26
|
die() { echo "plane: $*" >&2; exit 1; }
|
|
31
27
|
need_key() { [ -n "${PLANE_API_KEY:-}" ] || die "PLANE_API_KEY is not set"; }
|
|
@@ -36,6 +32,33 @@ workspace_key() {
|
|
|
36
32
|
printf 'PLANE_%s_API_KEY' "$key"
|
|
37
33
|
}
|
|
38
34
|
|
|
35
|
+
# dotenv_value FILE KEY — read one exact dotenv assignment as inert data.
|
|
36
|
+
# Never source the shared fleet file: it may contain unrelated command
|
|
37
|
+
# substitutions or credential helpers that this provider must not execute.
|
|
38
|
+
dotenv_value() {
|
|
39
|
+
python3 - "$1" "$2" <<'PY'
|
|
40
|
+
import pathlib, sys
|
|
41
|
+
|
|
42
|
+
path = pathlib.Path(sys.argv[1])
|
|
43
|
+
key = sys.argv[2]
|
|
44
|
+
value = ""
|
|
45
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
46
|
+
line = raw.strip()
|
|
47
|
+
if not line or line.startswith("#"):
|
|
48
|
+
continue
|
|
49
|
+
if line.startswith("export "):
|
|
50
|
+
line = line[7:].lstrip()
|
|
51
|
+
name, sep, candidate = line.partition("=")
|
|
52
|
+
if sep and name.strip() == key:
|
|
53
|
+
candidate = candidate.strip()
|
|
54
|
+
if len(candidate) >= 2 and candidate[0] == candidate[-1] and candidate[0] in "'\"":
|
|
55
|
+
candidate = candidate[1:-1]
|
|
56
|
+
value = candidate
|
|
57
|
+
break
|
|
58
|
+
print(value, end="")
|
|
59
|
+
PY
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
tp_cfg() {
|
|
40
63
|
[ -f "$ROLE_YAML" ] || return 0
|
|
41
64
|
python3 - "$ROLE_YAML" "$1" <<'PY'
|
|
@@ -77,6 +100,9 @@ API="$BASE/api/v1/workspaces/$WS"
|
|
|
77
100
|
if [ -z "${PLANE_API_KEY:-}" ]; then
|
|
78
101
|
KEY="$(workspace_key "$WS")"
|
|
79
102
|
eval "PLANE_API_KEY=\${$KEY:-}"
|
|
103
|
+
if [ -z "${PLANE_API_KEY:-}" ] && [ -f "$FLEET_ENV" ]; then
|
|
104
|
+
PLANE_API_KEY="$(dotenv_value "$FLEET_ENV" "$KEY")"
|
|
105
|
+
fi
|
|
80
106
|
export PLANE_API_KEY
|
|
81
107
|
fi
|
|
82
108
|
|
|
@@ -8,45 +8,111 @@ import re
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
KEY_VALUE = re.compile(
|
|
11
|
-
r"(?im)^\s*[\"']?(
|
|
11
|
+
r"(?im)^\s*[\"']?(?P<key>api[_-]?key|token|secret|password|authorization|cookie|client[_-]?secret|private[_-]?key)[\"']?\s*[:=]\s*(?P<value>[^\r\n#]*)"
|
|
12
12
|
)
|
|
13
13
|
TOKEN = re.compile(
|
|
14
14
|
r"(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|tvly-[A-Za-z0-9_-]{10,}|fc-[A-Za-z0-9_-]{10,}|[0-9]{6,}:[A-Za-z0-9_-]{20,})"
|
|
15
15
|
)
|
|
16
16
|
PRIVATE_KEY = re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")
|
|
17
|
-
SAFE_VALUES = {
|
|
18
|
-
|
|
17
|
+
SAFE_VALUES = {
|
|
18
|
+
"openrouter",
|
|
19
|
+
"openai",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"auto",
|
|
22
|
+
"none",
|
|
23
|
+
"null",
|
|
24
|
+
"false",
|
|
25
|
+
"true",
|
|
26
|
+
}
|
|
27
|
+
SAFE_PREFIXES = ("${", "$", "op://", "env:", "[REDACTED]", "{{", "{%")
|
|
28
|
+
COMPUTED_VALUE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*\s*(?:\(|\[)")
|
|
29
|
+
INTERPOLATED_VALUE = re.compile(
|
|
30
|
+
r"(?:\$\{[A-Za-z_][A-Za-z0-9_]*[^}]*\}|\$[A-Za-z_][A-Za-z0-9_]*|\{[A-Za-z_][A-Za-z0-9_.]*\})"
|
|
31
|
+
)
|
|
32
|
+
ENV_ASSIGNMENT = re.compile(
|
|
33
|
+
r"(?m)^\s*(?:export\s+)?(?P<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?P<value>[^\r\n#]*)"
|
|
34
|
+
)
|
|
35
|
+
SENSITIVE_ENV_KEY = re.compile(
|
|
36
|
+
r"(?i)(?:^|_)(?:api_key|token|secret|password|authorization|cookie|client_secret|private_key)(?:_|$)"
|
|
37
|
+
)
|
|
38
|
+
ALLOWED_ENV_FILES = {".env.example", ".env.op"}
|
|
39
|
+
SKIP_PARTS = {
|
|
40
|
+
".cache",
|
|
41
|
+
".direnv",
|
|
42
|
+
".eggs",
|
|
43
|
+
".git",
|
|
44
|
+
".mypy_cache",
|
|
45
|
+
".nox",
|
|
46
|
+
".npm",
|
|
47
|
+
".pnpm-store",
|
|
48
|
+
".pytest_cache",
|
|
49
|
+
".ruff_cache",
|
|
50
|
+
".tox",
|
|
51
|
+
".turbo",
|
|
52
|
+
".uv-cache",
|
|
53
|
+
".venv",
|
|
54
|
+
".yarn",
|
|
55
|
+
"__pycache__",
|
|
56
|
+
"dist-packages",
|
|
57
|
+
"node_modules",
|
|
58
|
+
"site-packages",
|
|
59
|
+
"venv",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def safe_assignment(raw_value: str) -> bool:
|
|
64
|
+
"""Return true only for an explicit reference, sentinel, or computation."""
|
|
65
|
+
|
|
66
|
+
value = raw_value.strip().rstrip(",").strip().strip("\"'")
|
|
67
|
+
if not value or value.lower() in SAFE_VALUES:
|
|
68
|
+
return True
|
|
69
|
+
if value.startswith(SAFE_PREFIXES) or value.endswith("_ENV"):
|
|
70
|
+
return True
|
|
71
|
+
if INTERPOLATED_VALUE.search(value):
|
|
72
|
+
return True
|
|
73
|
+
# Source code commonly binds credentials from a runtime provider. A bare
|
|
74
|
+
# literal does not have a call/index boundary and remains fail-closed.
|
|
75
|
+
return COMPUTED_VALUE.match(value) is not None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def safe_env_reference(raw_value: str) -> bool:
|
|
79
|
+
"""Keep a tracked .env.op declarative: sensitive values stay references."""
|
|
80
|
+
|
|
81
|
+
value = raw_value.strip().rstrip(",").strip().strip("\"'")
|
|
82
|
+
return not value or value.startswith(SAFE_PREFIXES)
|
|
19
83
|
|
|
20
84
|
|
|
21
85
|
def findings(root: Path) -> list[str]:
|
|
22
86
|
result: list[str] = []
|
|
23
87
|
for path in root.rglob("*"):
|
|
24
|
-
|
|
88
|
+
relative = path.relative_to(root)
|
|
89
|
+
if not path.is_file() or any(part in SKIP_PARTS for part in relative.parts):
|
|
25
90
|
continue
|
|
26
91
|
if path.name == "secret-scan.py":
|
|
27
92
|
continue
|
|
28
|
-
if path.name.startswith(".env") and path.name
|
|
29
|
-
result.append(f"forbidden secret file: {
|
|
93
|
+
if path.name.startswith(".env") and path.name not in ALLOWED_ENV_FILES:
|
|
94
|
+
result.append(f"forbidden secret file: {relative}")
|
|
30
95
|
continue
|
|
31
96
|
if path.name in {"auth.json", "auth.lock"} or path.suffix in {".pem", ".key"}:
|
|
32
|
-
result.append(f"forbidden credential file: {
|
|
97
|
+
result.append(f"forbidden credential file: {relative}")
|
|
33
98
|
continue
|
|
34
99
|
try:
|
|
35
100
|
text = path.read_text(encoding="utf-8")
|
|
36
101
|
except (UnicodeDecodeError, OSError):
|
|
37
102
|
continue
|
|
38
103
|
if TOKEN.search(text) or PRIVATE_KEY.search(text):
|
|
39
|
-
result.append(f"credential token pattern: {
|
|
104
|
+
result.append(f"credential token pattern: {relative}")
|
|
105
|
+
if path.name == ".env.op":
|
|
106
|
+
for match in ENV_ASSIGNMENT.finditer(text):
|
|
107
|
+
if SENSITIVE_ENV_KEY.search(
|
|
108
|
+
match.group("key")
|
|
109
|
+
) and not safe_env_reference(match.group("value")):
|
|
110
|
+
result.append(f"literal credential assignment: {relative}")
|
|
111
|
+
break
|
|
40
112
|
for match in KEY_VALUE.finditer(text):
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
not value
|
|
44
|
-
or value.lower() in SAFE_VALUES
|
|
45
|
-
or value.startswith(("${", "$", "op://", "env:", "[REDACTED]"))
|
|
46
|
-
or value.endswith("_ENV")
|
|
47
|
-
):
|
|
113
|
+
if safe_assignment(match.group("value")):
|
|
48
114
|
continue
|
|
49
|
-
result.append(f"literal credential assignment: {
|
|
115
|
+
result.append(f"literal credential assignment: {relative}")
|
|
50
116
|
break
|
|
51
117
|
return sorted(set(result))
|
|
52
118
|
|
|
@@ -15,10 +15,10 @@ You are **{{ display_name }}** — a Hermes agent provisioned to work inside the
|
|
|
15
15
|
|
|
16
16
|
## Scope
|
|
17
17
|
|
|
18
|
-
Your HERMES_HOME is the
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
Your HERMES_HOME is the real named profile under `~/.hermes/profiles/`. Shared
|
|
19
|
+
config/auth/skills link to fleet truth; your SOUL, sessions, memory, and other
|
|
20
|
+
owned state link into the ignored local `./runtime/`. Only PJangler may repair
|
|
21
|
+
that wiring (`pj migrate hermes.runtime-singleton`).
|
|
22
22
|
|
|
23
23
|
## Tone
|
|
24
24
|
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Launcher for {{ agent_id }}. Resolves HERMES_HOME to the
|
|
2
|
+
# Launcher for {{ agent_id }}. Resolves HERMES_HOME to the agent's NAMED PROFILE
|
|
3
3
|
# and execs the shared fleet Hermes binary configured in ~/.hermes/fleet.env.
|
|
4
4
|
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
7
7
|
ROLE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
-
|
|
8
|
+
RUNTIME_HOME="$ROLE_DIR/runtime"
|
|
9
|
+
|
|
10
|
+
FLEET_HOME="${HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
11
|
+
PROFILE_NAME="${HERMES_PROFILE_NAME:-{{ agent_id }}}"
|
|
12
|
+
|
|
13
|
+
# Singleton-runtime contract: HERMES_HOME MUST be the named profile dir, never
|
|
14
|
+
# the raw runtime path. Hermes derives the profile identity from the UNRESOLVED
|
|
15
|
+
# HERMES_HOME path; any HERMES_HOME that is neither under ~/.hermes nor a child
|
|
16
|
+
# of a "profiles" dir makes get_active_profile_name() report "default" and
|
|
17
|
+
# _global_auth_file_path() return None — silently disabling shared fleet auth
|
|
18
|
+
# and giving the agent a divergent config.yaml. The profile dir is a REAL dir
|
|
19
|
+
# whose shared entries (config.yaml, .env, skills) symlink to the fleet root
|
|
20
|
+
# and whose owned entries (memories, sessions, state.db, ...) symlink into
|
|
21
|
+
# $RUNTIME_HOME. Provision it with: pj migrate hermes.runtime-singleton
|
|
22
|
+
HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"
|
|
23
|
+
if ! REPO_ROOT="$(git -C "$ROLE_DIR" rev-parse --show-toplevel 2>/dev/null)"; then
|
|
24
|
+
REPO_ROOT="$ROLE_DIR"
|
|
25
|
+
fi
|
|
9
26
|
|
|
10
27
|
FLEET_ENV="${HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
|
|
11
28
|
if [[ -f "$FLEET_ENV" ]]; then
|
|
@@ -47,16 +64,28 @@ HERMES_BIN="${HERMES_BIN:-${HERMES_FLEET_BIN:-}}"
|
|
|
47
64
|
if [[ -z "$HERMES_BIN" ]]; then
|
|
48
65
|
HERMES_BIN="$(config_get fleet.hermes_bin "")"
|
|
49
66
|
fi
|
|
50
|
-
HERMES_BIN="${HERMES_BIN:-$HOME/.
|
|
51
|
-
HERMES_OAUTH_FILE="${HERMES_OAUTH_FILE:-${HERMES_FLEET_OAUTH_FILE:-$(config_get fleet.oauth_file "$HOME/.hermes/auth.json")}}"
|
|
67
|
+
HERMES_BIN="${HERMES_BIN:-$HOME/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1/.venv/bin/hermes}"
|
|
52
68
|
CODEX_HOME="${CODEX_HOME:-${HERMES_FLEET_CODEX_HOME:-$(config_get fleet.codex_home "$HOME/.codex")}}"
|
|
53
69
|
|
|
54
|
-
if [[ ! -d "$
|
|
55
|
-
echo "hermes: local runtime not provisioned at $
|
|
70
|
+
if [[ ! -d "$RUNTIME_HOME" ]]; then
|
|
71
|
+
echo "hermes: local runtime not provisioned at $RUNTIME_HOME" >&2
|
|
56
72
|
echo " fix: run $ROLE_DIR/.scripts/20-runtime-repo.sh" >&2
|
|
57
73
|
exit 1
|
|
58
74
|
fi
|
|
59
75
|
|
|
76
|
+
if [[ ! -d "$HERMES_HOME" ]]; then
|
|
77
|
+
echo "hermes: profile not provisioned at $HERMES_HOME" >&2
|
|
78
|
+
echo " fix: pj migrate hermes.runtime-singleton" >&2
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [[ -L "$HERMES_HOME" ]]; then
|
|
83
|
+
echo "hermes: $HERMES_HOME is a symlink; the profile dir must be a REAL dir" >&2
|
|
84
|
+
echo " (a symlink breaks profile-name resolution and shared fleet auth)" >&2
|
|
85
|
+
echo " fix: pj migrate hermes.runtime-singleton" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
60
89
|
if [[ ! -x "$HERMES_BIN" ]]; then
|
|
61
90
|
echo "hermes: binary not executable at $HERMES_BIN" >&2
|
|
62
91
|
echo " set HERMES_BIN or HERMES_FLEET_BIN (in $FLEET_ENV) to the shared Hermes binary." >&2
|
|
@@ -64,5 +93,5 @@ if [[ ! -x "$HERMES_BIN" ]]; then
|
|
|
64
93
|
fi
|
|
65
94
|
|
|
66
95
|
exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" \
|
|
67
|
-
|
|
96
|
+
CODEX_HOME="$CODEX_HOME" TERMINAL_CWD="$REPO_ROOT" \
|
|
68
97
|
"$HERMES_BIN" "$@"
|
|
@@ -8,13 +8,20 @@ agent_id: {{ agent_id }}
|
|
|
8
8
|
display_name: "{{ display_name }}"
|
|
9
9
|
purpose: "{{ agent_purpose }}"
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# Process-local gateway inference target (empty = inherit from shared
|
|
12
|
+
# ~/.hermes/config.yaml). key_env is a variable NAME only; its secret value is
|
|
13
|
+
# injected at runtime and must never be written here.
|
|
12
14
|
model:
|
|
13
15
|
provider: "{{ model_provider }}"
|
|
14
16
|
name: "{{ model_name }}"
|
|
17
|
+
base_url: "{{ model_base_url }}"
|
|
18
|
+
api_mode: "{{ model_api_mode }}"
|
|
19
|
+
key_env: "{{ model_key_env }}"
|
|
15
20
|
|
|
16
|
-
# Hermes profile name. ~/.hermes/profiles/<name>
|
|
17
|
-
#
|
|
21
|
+
# Hermes profile name. ~/.hermes/profiles/<name> is a REAL directory managed by
|
|
22
|
+
# `pj migrate hermes.runtime-singleton`: shared config/auth/skills link to the
|
|
23
|
+
# fleet root while role-owned state links into ./runtime. Never replace the
|
|
24
|
+
# named profile with a symlink to runtime.
|
|
18
25
|
profile: {{ agent_id }}
|
|
19
26
|
|
|
20
27
|
# Telegram bot identity (one bot per agent — see docs/architecture.md).
|
|
@@ -71,12 +78,15 @@ plane:
|
|
|
71
78
|
# reads the fleet registry and routes canonical commands by target_agent_id;
|
|
72
79
|
# this profile does not install its own NATS consumer or inbox bridge.
|
|
73
80
|
bloodbank:
|
|
81
|
+
# Quarantine gate: discovery is safe; execution requires explicit activation.
|
|
82
|
+
enabled: false
|
|
74
83
|
gateway_scope: fleet
|
|
75
84
|
target_agent_id: "{{ agent_id }}"
|
|
76
85
|
producer: "hermes-agent:{{ agent_id }}"
|
|
77
86
|
|
|
78
|
-
# Pure-local
|
|
79
|
-
#
|
|
87
|
+
# Pure-local role-owned state. HERMES_HOME itself is the named profile directory
|
|
88
|
+
# above; the legacy GitHub identity remains metadata-only for locating old
|
|
89
|
+
# archives, and provisioning never creates or attaches a submodule.
|
|
80
90
|
runtime:
|
|
81
91
|
# Empty owner -> resolved from config.toml [github].runtime_repo_owner.
|
|
82
92
|
github_owner: "{{ runtime_repo_owner }}"
|