@delorenj/pjangler 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +528 -0
  2. package/contracts/fleet-contract.yaml +513 -0
  3. package/dist/index.js +9333 -1509
  4. package/dist/mcp-server.js +6634 -1096
  5. package/dist/prompt.js +2 -1
  6. package/package.json +10 -4
  7. package/templates/hermes-agent/copier.yml +16 -3
  8. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +7 -4
  9. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +88 -94
  10. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +44 -21
  11. package/templates/hermes-agent/template/.scripts/30-telegram.sh +182 -171
  12. package/templates/hermes-agent/template/.scripts/31-slack.sh +260 -165
  13. package/templates/hermes-agent/template/.scripts/40-plane.sh +45 -36
  14. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +210 -41
  15. package/templates/hermes-agent/template/.scripts/70-systemd.sh +129 -15
  16. package/templates/hermes-agent/template/.scripts/80-registry.sh +42 -6
  17. package/templates/hermes-agent/template/.scripts/99-summary.sh +69 -16
  18. package/templates/hermes-agent/template/.scripts/_lib.sh +773 -0
  19. package/templates/hermes-agent/template/.scripts/channel-transaction.py +2340 -0
  20. package/templates/hermes-agent/template/.scripts/config.example.toml +8 -2
  21. package/templates/hermes-agent/template/.scripts/credential-launch.sh +5 -1
  22. package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -3
  23. package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +182 -0
  24. package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +108 -0
  25. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +93 -4
  26. package/templates/hermes-agent/template/.scripts/lib/voice-config.py +546 -0
  27. package/templates/hermes-agent/template/.scripts/providers/linear.sh +138 -25
  28. package/templates/hermes-agent/template/.scripts/providers/plane.sh +408 -51
  29. package/templates/hermes-agent/template/.scripts/providers/trello.sh +54 -6
  30. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +257 -43
  31. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +142 -25
  32. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +13 -19
  33. package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +29 -36
  34. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +3 -1
  35. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +7 -8
  36. package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +260 -0
  37. package/templates/hermes-agent/template/SOUL.md.jinja +14 -16
  38. package/templates/hermes-agent/template/hermes.jinja +1 -1
  39. package/templates/hermes-agent/template/role.yaml.jinja +15 -4
@@ -6,6 +6,20 @@
6
6
  # or be silently reused by a profile. The non-secret allow-list may be shared.
7
7
  INVOCATION_TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN-}"
8
8
  INVOCATION_TELEGRAM_ALLOWED_USERS="${TELEGRAM_ALLOWED_USERS-}"
9
+ # Invocation credentials may have arrived as exported variables. Remove them
10
+ # before even resolving/sourcing _lib.sh: that path invokes utilities and loads
11
+ # fleet state, and no child involved in setup should inherit a raw token.
12
+ unset TELEGRAM_BOT_TOKEN TELEGRAM_ALLOWED_USERS
13
+
14
+ # Every channel write lands in the host-global profile root that 10-hermes-profile.sh
15
+ # creates. When that step is deferred the root does not exist yet, so there is no
16
+ # honest Telegram state to record -- not even the disabled state, which is itself a
17
+ # write into the profile. Defer with it. This guard must precede _lib.sh because that
18
+ # library creates the role log and reads fleet configuration.
19
+ if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]; then
20
+ printf '%s\n' '[30] telegram — DEFERRED (SKIP_HOST_STATE=1)' >&2
21
+ exit 0
22
+ fi
9
23
 
10
24
  # shellcheck source=_lib.sh
11
25
  source "$(dirname "$0")/_lib.sh"
@@ -19,60 +33,121 @@ else
19
33
  fi
20
34
  unset INVOCATION_TELEGRAM_BOT_TOKEN INVOCATION_TELEGRAM_ALLOWED_USERS
21
35
 
22
- telegram_yaml_update() {
23
- python3 - "$ROLE_YAML" "$@" <<'PYEOF'
24
- import json
25
- import pathlib
26
- import re
27
- import sys
36
+ PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
37
+ profile_root_require_real "$PROFILE_HOME"
38
+ RUNTIME="$ROLE_DIR/runtime"
39
+ ENVF="$RUNTIME/.env"
40
+ mkdir -p "$RUNTIME"
41
+ [[ ! -L "$ENVF" ]] || die "refusing to write Telegram credentials through symlink: $ENVF"
28
42
 
29
- path = pathlib.Path(sys.argv[1])
30
- updates = dict(zip(sys.argv[2::2], sys.argv[3::2]))
31
- text = path.read_text(encoding="utf-8")
32
- match = re.search(r"(?ms)^telegram:\s*\n(?P<body>(?:^[ \t]+.*\n?)*)", text)
33
- if not match:
34
- raise SystemExit(f"telegram metadata block missing from {path}")
35
- body = match.group("body")
36
- for key, value in updates.items():
37
- replacement = f" {key}: {json.dumps(value)}"
38
- body, count = re.subn(
39
- rf"(?m)^\s+{re.escape(key)}:\s*.*$", lambda _: replacement, body, count=1
40
- )
41
- if count == 0:
42
- if body and not body.endswith("\n"):
43
- body += "\n"
44
- body += replacement + "\n"
45
- path.write_text(text[: match.start("body")] + body + text[match.end("body") :], encoding="utf-8")
46
- PYEOF
43
+ telegram_reconcile_existing() {
44
+ # Registry first, then the transaction helper's profile lock. The helper
45
+ # snapshots and validates refs plus role metadata only inside this window.
46
+ local rc=0
47
+ fleet_lock_acquire
48
+ trap 'fleet_lock_release' EXIT
49
+ channel_transaction_telegram_existing "$PROFILE_HOME" "$ENVF" || rc=$?
50
+ fleet_lock_release
51
+ trap - EXIT
52
+ return "$rc"
47
53
  }
48
54
 
49
- telegram_status="$(yaml_get telegram.provisioning_status)"
55
+ telegram_defer_if_unconfigured() {
56
+ # Prepare and, when already verified, reconcile under one registry-lock
57
+ # window. The helpers independently take the profile lock second.
58
+ local prepare_rc=0 rc=0
59
+ fleet_lock_acquire
60
+ trap 'fleet_lock_release' EXIT
61
+ channel_transaction_telegram_prepare_unconfigured \
62
+ "$PROFILE_HOME" "$ENVF" || prepare_rc=$?
63
+ case "$prepare_rc" in
64
+ 0)
65
+ fleet_lock_release
66
+ trap - EXIT
67
+ return 2
68
+ ;;
69
+ 3)
70
+ channel_transaction_telegram_existing "$PROFILE_HOME" "$ENVF" || rc=$?
71
+ ;;
72
+ *)
73
+ die "Telegram deferred-state preparation transaction failed"
74
+ ;;
75
+ esac
76
+ case "$rc" in
77
+ 0)
78
+ fleet_lock_release
79
+ trap - EXIT
80
+ return 0
81
+ ;;
82
+ 75)
83
+ die "Telegram 1Password reference validation is temporarily unavailable; preserved existing verified wiring for retry"
84
+ ;;
85
+ *)
86
+ die "Telegram verified wiring reconciliation transaction failed"
87
+ ;;
88
+ esac
89
+ }
90
+
91
+ telegram_prepare_for_attempt() {
92
+ # A new profile becomes explicitly disabled before remote verification. A
93
+ # verified profile is a no-op so a failed rotation preserves active wiring.
94
+ local rc=0
95
+ fleet_lock_acquire
96
+ trap 'fleet_lock_release' EXIT
97
+ channel_transaction_telegram_prepare_unconfigured \
98
+ "$PROFILE_HOME" "$ENVF" || rc=$?
99
+ fleet_lock_release
100
+ trap - EXIT
101
+ case "$rc" in
102
+ 0|3) return 0 ;;
103
+ *) die "Telegram deferred-state preparation transaction failed" ;;
104
+ esac
105
+ }
106
+
107
+ # A valid durable mapping survives a transient 1Password outage. If the caller
108
+ # supplied no rotation value, adopt/revalidate the complete current wiring
109
+ # under registry -> profile locks. No ref or identity snapshot escapes that
110
+ # window.
111
+ if [[ -z "${TELEGRAM_BOT_TOKEN:-}" ]]; then
112
+ telegram_reference_rc=0
113
+ telegram_reconcile_existing || telegram_reference_rc=$?
114
+ case "$telegram_reference_rc" in
115
+ 0)
116
+ log "[30] telegram — existing verified 1Password wiring reconciled"
117
+ exit 0
118
+ ;;
119
+ 2) ;;
120
+ 75)
121
+ die "Telegram 1Password reference validation is temporarily unavailable; preserved existing verified wiring for retry"
122
+ ;;
123
+ *)
124
+ die "Telegram verified wiring reconciliation transaction failed"
125
+ ;;
126
+ esac
127
+ fi
128
+
129
+ # Establish explicit disabled state for a first-time credential attempt. This
130
+ # recheck is serialized with rotations: existing verified wiring is preserved,
131
+ # while an unconfigured profile is disabled before any fallible remote check.
132
+ if [[ -n "${TELEGRAM_BOT_TOKEN:-}" ]]; then
133
+ telegram_prepare_for_attempt
134
+ fi
50
135
 
51
136
  if [[ "${SKIP_TELEGRAM:-0}" == "1" ]]; then
52
- if [[ "$telegram_status" == "verified" ]] && already_done 30-telegram; then
53
- log "[30] telegram — SKIPPED; existing verified wiring preserved"
137
+ telegram_defer_rc=0
138
+ telegram_defer_if_unconfigured || telegram_defer_rc=$?
139
+ if [[ $telegram_defer_rc -eq 2 ]]; then
140
+ log "[30] telegram — DEFERRED (SKIP_TELEGRAM=1; no verified 1Password reference)"
54
141
  else
55
- telegram_yaml_update provisioning_status disabled
56
- clear_done 30-telegram
57
- log "[30] telegram — DEFERRED (SKIP_TELEGRAM=1; completion marker cleared)"
142
+ log "[30] telegram — existing verified 1Password wiring reconciled"
58
143
  fi
59
144
  exit 0
60
145
  fi
61
146
 
62
147
  if already_done 30-telegram; then
63
- if [[ "$telegram_status" == "verified" ]]; then
64
- log "[30] telegram already wired — skipping"
65
- exit 0
66
- fi
67
- clear_done 30-telegram
68
- log "[30] stale deferred completion marker cleared — reconciling Telegram"
148
+ log "[30] existing completion marker preserved while Telegram is reconciled"
69
149
  fi
70
150
 
71
- RUNTIME="$ROLE_DIR/runtime"
72
- ENVF="$RUNTIME/.env"
73
- mkdir -p "$RUNTIME"
74
- [[ ! -L "$ENVF" ]] || die "refusing to write Telegram credentials through symlink: $ENVF"
75
-
76
151
  cat >&2 <<EOF
77
152
 
78
153
  ╭─ BotFather steps for @$BOT_HANDLE ─────────────────────────────────────╮
@@ -93,19 +168,28 @@ if [[ -z "${TELEGRAM_BOT_TOKEN:-}" && -t 0 ]]; then
93
168
  fi
94
169
 
95
170
  if [[ -z "${TELEGRAM_BOT_TOKEN:-}" ]]; then
96
- telegram_yaml_update provisioning_status deferred
97
- clear_done 30-telegram
98
- warn " no token provided; Telegram step deferred"
99
- warn " re-run later with a profile-dedicated TELEGRAM_BOT_TOKEN invocation"
171
+ telegram_defer_rc=0
172
+ telegram_defer_if_unconfigured || telegram_defer_rc=$?
173
+ if [[ $telegram_defer_rc -eq 2 ]]; then
174
+ warn " no token provided; Telegram step deferred"
175
+ warn " re-run later with a profile-dedicated TELEGRAM_BOT_TOKEN invocation"
176
+ else
177
+ log "[30] telegram — existing verified 1Password wiring reconciled"
178
+ fi
100
179
  exit 0
101
180
  fi
102
181
 
103
182
  # Sanity check
104
- if [[ ! "$TELEGRAM_BOT_TOKEN" =~ ^[0-9]+:.+ ]]; then
183
+ if [[ ! "$TELEGRAM_BOT_TOKEN" =~ ^[0-9]+:[A-Za-z0-9_-]+$ ]]; then
105
184
  die "token doesn't look like a Telegram bot token"
106
185
  fi
107
186
  log " verifying token..."
108
- info=$(curl -fsS "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getMe") \
187
+ # Keep the credential out of process argv and environment. curl reads its
188
+ # complete request description from an anonymous stdin pipe.
189
+ info="$({
190
+ printf 'url = "https://api.telegram.org/bot%s/getMe"\n' "$TELEGRAM_BOT_TOKEN"
191
+ printf '%s\n' 'fail' 'silent' 'show-error'
192
+ } | curl --config -)" \
109
193
  || die "Telegram getMe request failed"
110
194
  identity=$(printf '%s' "$info" | python3 -c '
111
195
  import json, sys
@@ -138,17 +222,15 @@ fi
138
222
  # Reject token reuse, token rotation onto an identity owned by another agent,
139
223
  # and credentials parked in shared fleet/root env files. The scan, durable
140
224
  # identity claim, and profile credential write share one fleet-wide flock.
141
- export TELEGRAM_BOT_TOKEN TELEGRAM_ALLOWED_USERS
142
225
  fleet_lock_acquire
143
226
  trap 'fleet_lock_release' EXIT
144
- python3 - "$REGISTRY_FILE" "$FLEET_ENV" "$ENVF" "$AGENT_ID" "$bot_id" \
145
- "$ROLE_DIR" "$PROFILE_NAME" "$bot_username" <<'PYEOF'
146
- import errno
227
+ python3 /dev/fd/3 "$REGISTRY_FILE" "$FLEET_ENV" "$ENVF" "$AGENT_ID" "$bot_id" \
228
+ "$ROLE_DIR" "$PROFILE_NAME" "$bot_username" \
229
+ 3<<'PYEOF' <<<"$TELEGRAM_BOT_TOKEN"
147
230
  import os
148
231
  import pathlib
149
232
  import re
150
233
  import sys
151
- import tempfile
152
234
  try:
153
235
  import yaml # type: ignore
154
236
  except ImportError:
@@ -164,7 +246,14 @@ except ImportError:
164
246
  profile_name,
165
247
  bot_username,
166
248
  ) = sys.argv[1:]
167
- token = os.environ["TELEGRAM_BOT_TOKEN"]
249
+ token = sys.stdin.read()
250
+ if token.endswith("\n"):
251
+ token = token[:-1]
252
+ if not token:
253
+ raise SystemExit("Telegram ownership scan received no credential")
254
+ for metadata_path in (pathlib.Path("/proc/self/cmdline"), pathlib.Path("/proc/self/environ")):
255
+ if metadata_path.is_file() and token.encode() in metadata_path.read_bytes():
256
+ raise SystemExit("Telegram ownership scan detected credential exposure in process metadata")
168
257
  target = pathlib.Path(target_path).resolve(strict=False)
169
258
 
170
259
  def env_token(path):
@@ -223,130 +312,52 @@ for owner, path in owners:
223
312
  if env_token(path) == token:
224
313
  raise SystemExit(f"Telegram bot token is already assigned to {owner}")
225
314
 
226
- claim = agents.setdefault(agent_id, {})
227
- if not isinstance(claim, dict):
228
- raise SystemExit(f"registry entry for {agent_id} is not a mapping")
229
- claim["role_dir"] = role_dir
230
- claim["profile_name"] = profile_name
231
- claim["telegram"] = {
232
- "provisioning_status": "verified",
233
- "bot_username": bot_username,
234
- "bot_id": bot_id,
235
- }
236
- registry.parent.mkdir(parents=True, exist_ok=True)
237
- rendered = yaml.safe_dump(data, sort_keys=False)
238
-
239
- def fsync_parent(target):
240
- unsupported = {errno.EINVAL, getattr(errno, "ENOTSUP", errno.EINVAL), errno.ENOSYS}
241
- flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
242
- try:
243
- directory_fd = os.open(target.parent, flags)
244
- except OSError as exc:
245
- if exc.errno in unsupported:
246
- return
247
- raise
248
- try:
249
- try:
250
- os.fsync(directory_fd)
251
- except OSError as exc:
252
- if exc.errno not in unsupported:
253
- raise
254
- finally:
255
- os.close(directory_fd)
256
-
257
- fd, temporary = tempfile.mkstemp(prefix=f".{registry.name}.telegram-", dir=registry.parent)
258
- try:
259
- os.fchmod(fd, 0o600)
260
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
261
- handle.write(rendered)
262
- handle.flush()
263
- os.fsync(handle.fileno())
264
- os.replace(temporary, registry)
265
- os.chmod(registry, 0o600)
266
- fsync_parent(registry)
267
- except BaseException:
268
- try:
269
- os.unlink(temporary)
270
- except FileNotFoundError:
271
- pass
272
- raise
315
+ # This phase is intentionally read-only. The later channel transaction repeats
316
+ # identity-conflict validation under the same lock before committing registry
317
+ # metadata, so a failed vault stage cannot leave a false ownership claim.
273
318
  PYEOF
274
319
 
275
- # Atomically replace only Telegram fields in the profile-local runtime file.
276
- export TELEGRAM_ALLOWED_USERS
277
- python3 - "$ENVF" <<'PYEOF'
278
- import errno
279
- import json
280
- import os
281
- import pathlib
282
- import re
283
- import sys
284
- import tempfile
285
-
286
- path = pathlib.Path(sys.argv[1])
287
- if path.is_symlink():
288
- raise SystemExit(f"refusing to write Telegram credentials through symlink: {path}")
289
- path.parent.mkdir(parents=True, exist_ok=True)
290
- text = path.read_text(encoding="utf-8") if path.exists() else ""
291
- for key in ("TELEGRAM_BOT_TOKEN", "TELEGRAM_ALLOWED_USERS"):
292
- text = re.sub(rf"(?m)^\s*(?:export\s+)?#?\s*{key}\s*=.*(?:\n|$)", "", text)
293
- text = text.rstrip("\n")
294
- if text:
295
- text += "\n"
296
- text += "".join(
297
- f"{key}={json.dumps(os.environ.get(key, ''))}\n"
298
- for key in ("TELEGRAM_BOT_TOKEN", "TELEGRAM_ALLOWED_USERS")
299
- )
320
+ # Stage the credential in a new immutable 1Password item. The helper verifies
321
+ # the staged field and returns the immutable item id plus reference; no local
322
+ # ownership/config state has changed yet.
323
+ ONEPASSWORD_ITEM_PREFIX="${HERMES_ONEPASSWORD_ITEM_PREFIX:-$(config_get fleet.onepassword_item_prefix 'hermes-agent')}"
324
+ telegram_stage="$(stage_onepassword_secret \
325
+ "${ONEPASSWORD_ITEM_PREFIX}-${AGENT_ID}-telegram-bot-token" \
326
+ telegram_bot_token "$TELEGRAM_BOT_TOKEN")" \
327
+ || die "Telegram credential could not be staged and verified in 1Password"
328
+ if [[ "$telegram_stage" != *$'\n'* ]]; then
329
+ die "Telegram credential staging returned an invalid result"
330
+ fi
331
+ telegram_staged_item_id="${telegram_stage%%$'\n'*}"
332
+ telegram_reference="${telegram_stage#*$'\n'}"
333
+ [[ "$telegram_reference" != *$'\n'* ]] \
334
+ || die "Telegram credential staging returned too many references"
335
+ unset TELEGRAM_BOT_TOKEN
300
336
 
301
- def fsync_parent(target):
302
- unsupported = {errno.EINVAL, getattr(errno, "ENOTSUP", errno.EINVAL), errno.ENOSYS}
303
- flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
304
- try:
305
- directory_fd = os.open(target.parent, flags)
306
- except OSError as exc:
307
- if exc.errno in unsupported:
308
- return
309
- raise
310
- try:
311
- try:
312
- os.fsync(directory_fd)
313
- except OSError as exc:
314
- if exc.errno not in unsupported:
315
- raise
316
- finally:
317
- os.close(directory_fd)
337
+ # From here until commit, any failure archives the staged item by immutable id
338
+ # and releases the fleet lock. The transaction itself keeps the platform
339
+ # disabled while committing refs, role metadata, runtime policy, and registry;
340
+ # activation and the done marker are its final writes.
341
+ telegram_transaction_exit() {
342
+ local rc=$?
343
+ trap - EXIT
344
+ if [[ -n "${telegram_staged_item_id:-}" ]]; then
345
+ delete_staged_onepassword_item "$telegram_staged_item_id" >/dev/null 2>&1 \
346
+ || warn " staged Telegram item cleanup failed; archive manually by immutable item id"
347
+ fi
348
+ fleet_lock_release
349
+ exit "$rc"
350
+ }
351
+ trap telegram_transaction_exit EXIT
318
352
 
319
- fd, temporary = tempfile.mkstemp(prefix=".env.telegram-", dir=path.parent)
320
- try:
321
- os.fchmod(fd, 0o600)
322
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
323
- handle.write(text)
324
- handle.flush()
325
- os.fsync(handle.fileno())
326
- os.replace(temporary, path)
327
- os.chmod(path, 0o600)
328
- fsync_parent(path)
329
- except BaseException:
330
- try:
331
- os.unlink(temporary)
332
- except FileNotFoundError:
333
- pass
334
- raise
335
- PYEOF
336
- unset TELEGRAM_BOT_TOKEN
353
+ channel_transaction_telegram \
354
+ "$PROFILE_HOME" "$ENVF" "$telegram_reference" \
355
+ "$bot_username" "$bot_id" "${TELEGRAM_ALLOWED_USERS:-}" \
356
+ || die "Telegram local credential transaction failed"
337
357
 
338
- # Record the verified identity, never the token.
339
- telegram_yaml_update \
340
- provisioning_status verified \
341
- bot_username "$bot_username" \
342
- bot_id "$bot_id"
358
+ telegram_staged_item_id=""
343
359
 
344
360
  fleet_lock_release
345
361
  trap - EXIT
346
362
 
347
- # Enable telegram toolset for the profile
348
- env HERMES_HOME="$RUNTIME" "$HERMES_BIN" tools enable telegram hermes-telegram 2>/dev/null \
349
- || warn " couldn't auto-enable telegram toolset; run: $ROLE_DIR/hermes tools"
350
-
351
- log " wired @$bot_username (id=$bot_id)"
352
- mark_done 30-telegram
363
+ log " wired @$bot_username (id=$bot_id) through 1Password reference"