@delorenj/pjangler 1.4.6 → 1.5.2
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/README.md +41 -596
- package/dist/assets/project-notebook-skill/SHA256SUMS +5 -5
- package/dist/assets/project-notebook-skill/export-manifest.json +5 -5
- package/dist/assets/project-notebook-skill/hooks/session-end.sh +20 -1
- package/dist/assets/project-notebook-skill/hooks/session-start.sh +20 -1
- package/dist/assets/project-notebook-skill/references/configuration.md +2 -0
- package/dist/assets/project-notebook-skill/scripts/project-hooks.py +33 -3
- package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +37 -0
- package/dist/index.js +9385 -18379
- package/dist/mcp-server.js +9432 -17852
- package/dist/notebook/journal-service-main.js +5682 -0
- package/dist/project-registry-service.js +748 -0
- package/dist/prompt.js +2 -1
- package/package.json +15 -10
- package/scripts/install-project-registry.mjs +34 -0
- package/scripts/migrate-project-registry.mjs +117 -0
- package/src/project/registryClient.ts +62 -0
- package/templates/commonproject/copier.yml +16 -19
- package/templates/commonproject/template/.agents/hooks/README.md +10 -4
- package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +12 -2
- package/templates/commonproject/template/.agents/hooks/hooks.master.json +10 -10
- package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +13 -0
- package/templates/commonproject/template/.agents/hooks/sync.py +66 -17
- package/templates/commonproject/template/.agents/skills.json +0 -1
- package/templates/commonproject/template/.github/workflows/code-review.yml.jinja +7 -1
- package/templates/commonproject/template/.project.json.jinja +1 -1
- package/templates/commonproject/template/mise.toml.jinja +11 -21
- package/contracts/fleet-contract.yaml +0 -584
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +0 -587
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +0 -2183
- package/templates/hermes-agent/copier.yml +0 -186
- package/templates/hermes-agent/template/.gitignore.jinja +0 -11
- package/templates/hermes-agent/template/.runtime-scaffold/.gitattributes +0 -10
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +0 -44
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +0 -56
- package/templates/hermes-agent/template/.runtime-scaffold/decisions/.gitkeep +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/logs/.gitkeep +0 -1
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +0 -50
- package/templates/hermes-agent/template/.runtime-scaffold/memories/USER.md +0 -51
- package/templates/hermes-agent/template/.scripts/00-banner.sh +0 -21
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -60
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -42
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +0 -222
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -199
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +0 -363
- package/templates/hermes-agent/template/.scripts/31-slack.sh +0 -457
- package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -60
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +0 -373
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +0 -18
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +0 -354
- package/templates/hermes-agent/template/.scripts/80-registry.sh +0 -185
- package/templates/hermes-agent/template/.scripts/99-summary.sh +0 -87
- package/templates/hermes-agent/template/.scripts/_lib.sh +0 -1138
- package/templates/hermes-agent/template/.scripts/channel-transaction.py +0 -2340
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +0 -82
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -70
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +0 -85
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +0 -283
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +0 -202
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +0 -761
- package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +0 -182
- package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +0 -108
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +0 -199
- package/templates/hermes-agent/template/.scripts/lib/voice-config.py +0 -546
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +0 -126
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +0 -137
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -329
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +0 -671
- package/templates/hermes-agent/template/.scripts/providers/trello.sh +0 -241
- package/templates/hermes-agent/template/.scripts/reporter-watchdog.py +0 -242
- package/templates/hermes-agent/template/.scripts/secret-scan.py +0 -134
- package/templates/hermes-agent/template/.scripts/sentinel/bin/emit-event.py +0 -106
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +0 -339
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +0 -179
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +0 -162
- package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +0 -42
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +0 -77
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +0 -116
- package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +0 -260
- package/templates/hermes-agent/template/SOUL.md.jinja +0 -197
- package/templates/hermes-agent/template/hermes.jinja +0 -109
- package/templates/hermes-agent/template/momo.jinja +0 -177
- package/templates/hermes-agent/template/role.yaml.jinja +0 -112
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Opt-in, profile-local Slack Socket Mode provisioning.
|
|
3
|
-
#
|
|
4
|
-
# Slack credentials are invocation-only inputs. Capture them before _lib.sh
|
|
5
|
-
# sources fleet.env so an accidentally shared fleet token can never provision a
|
|
6
|
-
# profile. The non-secret allowed-user policy may still come from fleet.env.
|
|
7
|
-
INVOCATION_SLACK_BOT_TOKEN="${SLACK_BOT_TOKEN-}"
|
|
8
|
-
INVOCATION_SLACK_APP_TOKEN="${SLACK_APP_TOKEN-}"
|
|
9
|
-
INVOCATION_SLACK_ALLOWED_USERS="${SLACK_ALLOWED_USERS-}"
|
|
10
|
-
INVOCATION_ENABLE_SLACK="${ENABLE_SLACK-${WIRE_SLACK-}}"
|
|
11
|
-
# Copy invocation-only inputs, then immediately clear their exported form.
|
|
12
|
-
# Resolving/sourcing _lib.sh executes PATH children; none may inherit channel
|
|
13
|
-
# credentials (or accidentally treat the invocation policy as fleet state).
|
|
14
|
-
unset SLACK_BOT_TOKEN SLACK_APP_TOKEN SLACK_ALLOWED_USERS ENABLE_SLACK WIRE_SLACK
|
|
15
|
-
|
|
16
|
-
# Every channel write lands in the host-global profile root that 10-hermes-profile.sh
|
|
17
|
-
# creates. When that step is deferred the root does not exist yet, so there is no
|
|
18
|
-
# honest Slack state to record -- not even the disabled state, which is itself a
|
|
19
|
-
# write into the profile. Defer with it. This guard must precede _lib.sh because that
|
|
20
|
-
# library creates the role log and reads fleet configuration.
|
|
21
|
-
if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]; then
|
|
22
|
-
printf '%s\n' '[31] slack — DEFERRED (SKIP_HOST_STATE=1)' >&2
|
|
23
|
-
exit 0
|
|
24
|
-
fi
|
|
25
|
-
|
|
26
|
-
# shellcheck source=_lib.sh
|
|
27
|
-
source "$(dirname "$0")/_lib.sh"
|
|
28
|
-
load_role_env
|
|
29
|
-
|
|
30
|
-
SLACK_BOT_TOKEN="$INVOCATION_SLACK_BOT_TOKEN"
|
|
31
|
-
SLACK_APP_TOKEN="$INVOCATION_SLACK_APP_TOKEN"
|
|
32
|
-
if [[ -n "$INVOCATION_SLACK_ALLOWED_USERS" ]]; then
|
|
33
|
-
SLACK_ALLOWED_USERS="$INVOCATION_SLACK_ALLOWED_USERS"
|
|
34
|
-
else
|
|
35
|
-
SLACK_ALLOWED_USERS="${SLACK_ALLOWED_USERS-}"
|
|
36
|
-
fi
|
|
37
|
-
ENABLE_SLACK="$INVOCATION_ENABLE_SLACK"
|
|
38
|
-
unset INVOCATION_SLACK_BOT_TOKEN INVOCATION_SLACK_APP_TOKEN
|
|
39
|
-
|
|
40
|
-
PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
|
|
41
|
-
profile_root_require_real "$PROFILE_HOME"
|
|
42
|
-
RUNTIME="$ROLE_DIR/runtime"
|
|
43
|
-
ENVF="$RUNTIME/.env"
|
|
44
|
-
mkdir -p "$RUNTIME"
|
|
45
|
-
[[ ! -L "$ENVF" ]] || die "refusing to write Slack credentials through symlink: $ENVF"
|
|
46
|
-
|
|
47
|
-
slack_reconcile_existing() {
|
|
48
|
-
# Registry first, then the transaction helper's profile lock. The helper
|
|
49
|
-
# snapshots and validates both refs plus role metadata inside this window.
|
|
50
|
-
local rc=0
|
|
51
|
-
fleet_lock_acquire
|
|
52
|
-
trap 'fleet_lock_release' EXIT
|
|
53
|
-
channel_transaction_slack_existing "$PROFILE_HOME" "$ENVF" || rc=$?
|
|
54
|
-
fleet_lock_release
|
|
55
|
-
trap - EXIT
|
|
56
|
-
return "$rc"
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
slack_defer_if_unconfigured() {
|
|
60
|
-
# Prepare and, when already verified, reconcile under one registry-lock
|
|
61
|
-
# window. The helpers independently take the profile lock second.
|
|
62
|
-
local prepare_rc=0 rc=0
|
|
63
|
-
fleet_lock_acquire
|
|
64
|
-
trap 'fleet_lock_release' EXIT
|
|
65
|
-
channel_transaction_slack_prepare_unconfigured \
|
|
66
|
-
"$PROFILE_HOME" "$ENVF" || prepare_rc=$?
|
|
67
|
-
case "$prepare_rc" in
|
|
68
|
-
0)
|
|
69
|
-
fleet_lock_release
|
|
70
|
-
trap - EXIT
|
|
71
|
-
return 2
|
|
72
|
-
;;
|
|
73
|
-
3)
|
|
74
|
-
channel_transaction_slack_existing "$PROFILE_HOME" "$ENVF" || rc=$?
|
|
75
|
-
;;
|
|
76
|
-
*)
|
|
77
|
-
die "Slack deferred-state preparation transaction failed"
|
|
78
|
-
;;
|
|
79
|
-
esac
|
|
80
|
-
case "$rc" in
|
|
81
|
-
0)
|
|
82
|
-
fleet_lock_release
|
|
83
|
-
trap - EXIT
|
|
84
|
-
return 0
|
|
85
|
-
;;
|
|
86
|
-
75)
|
|
87
|
-
die "Slack 1Password reference validation is temporarily unavailable; preserved existing verified wiring for retry"
|
|
88
|
-
;;
|
|
89
|
-
*)
|
|
90
|
-
die "Slack verified wiring reconciliation transaction failed"
|
|
91
|
-
;;
|
|
92
|
-
esac
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
slack_prepare_for_attempt() {
|
|
96
|
-
# A new profile becomes explicitly disabled before remote verification. A
|
|
97
|
-
# verified profile is a no-op so a failed rotation preserves active wiring.
|
|
98
|
-
local rc=0
|
|
99
|
-
fleet_lock_acquire
|
|
100
|
-
trap 'fleet_lock_release' EXIT
|
|
101
|
-
channel_transaction_slack_prepare_unconfigured \
|
|
102
|
-
"$PROFILE_HOME" "$ENVF" || rc=$?
|
|
103
|
-
fleet_lock_release
|
|
104
|
-
trap - EXIT
|
|
105
|
-
case "$rc" in
|
|
106
|
-
0|3) return 0 ;;
|
|
107
|
-
*) die "Slack deferred-state preparation transaction failed" ;;
|
|
108
|
-
esac
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
truthy() {
|
|
112
|
-
case "${1,,}" in
|
|
113
|
-
1|true|yes|on) return 0 ;;
|
|
114
|
-
*) return 1 ;;
|
|
115
|
-
esac
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
# Preserve and reconcile durable wiring across transient 1Password outages.
|
|
119
|
-
# An explicit token is a rotation request. With no tokens, the complete current
|
|
120
|
-
# ref/identity snapshot is taken only after registry -> profile locks are held.
|
|
121
|
-
if [[ -z "${SLACK_BOT_TOKEN:-}" && -z "${SLACK_APP_TOKEN:-}" ]]; then
|
|
122
|
-
slack_reference_rc=0
|
|
123
|
-
slack_reconcile_existing || slack_reference_rc=$?
|
|
124
|
-
case "$slack_reference_rc" in
|
|
125
|
-
0)
|
|
126
|
-
log "[31] slack — existing verified 1Password wiring reconciled"
|
|
127
|
-
exit 0
|
|
128
|
-
;;
|
|
129
|
-
2) ;;
|
|
130
|
-
75)
|
|
131
|
-
die "Slack 1Password reference validation is temporarily unavailable; preserved existing verified wiring for retry"
|
|
132
|
-
;;
|
|
133
|
-
*)
|
|
134
|
-
die "Slack verified wiring reconciliation transaction failed"
|
|
135
|
-
;;
|
|
136
|
-
esac
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
# Establish explicit disabled state for a first-time credential attempt. The
|
|
140
|
-
# registry -> profile lock recheck preserves any already-verified wiring and
|
|
141
|
-
# prevents this preparation from racing a concurrent rotation.
|
|
142
|
-
if [[ -n "${SLACK_BOT_TOKEN:-}" || -n "${SLACK_APP_TOKEN:-}" ]]; then
|
|
143
|
-
slack_prepare_for_attempt
|
|
144
|
-
fi
|
|
145
|
-
|
|
146
|
-
if [[ "${SKIP_SLACK:-0}" == "1" ]]; then
|
|
147
|
-
slack_defer_rc=0
|
|
148
|
-
slack_defer_if_unconfigured || slack_defer_rc=$?
|
|
149
|
-
if [[ $slack_defer_rc -eq 2 ]]; then
|
|
150
|
-
log "[31] slack — DEFERRED (SKIP_SLACK=1; no verified 1Password reference pair)"
|
|
151
|
-
else
|
|
152
|
-
log "[31] slack — existing verified 1Password wiring reconciled"
|
|
153
|
-
fi
|
|
154
|
-
exit 0
|
|
155
|
-
fi
|
|
156
|
-
|
|
157
|
-
if already_done 31-slack; then
|
|
158
|
-
log "[31] existing completion marker preserved while Slack is reconciled"
|
|
159
|
-
fi
|
|
160
|
-
|
|
161
|
-
have_bot=0
|
|
162
|
-
have_app=0
|
|
163
|
-
[[ -n "$SLACK_BOT_TOKEN" ]] && have_bot=1
|
|
164
|
-
[[ -n "$SLACK_APP_TOKEN" ]] && have_app=1
|
|
165
|
-
|
|
166
|
-
if ! truthy "${ENABLE_SLACK:-0}" && (( ! have_bot && ! have_app )); then
|
|
167
|
-
slack_defer_rc=0
|
|
168
|
-
slack_defer_if_unconfigured || slack_defer_rc=$?
|
|
169
|
-
if [[ $slack_defer_rc -eq 2 ]]; then
|
|
170
|
-
log "[31] slack — deferred (opt in with ENABLE_SLACK=1 or supply both Slack tokens)"
|
|
171
|
-
else
|
|
172
|
-
log "[31] slack — existing verified 1Password wiring reconciled"
|
|
173
|
-
fi
|
|
174
|
-
exit 0
|
|
175
|
-
fi
|
|
176
|
-
|
|
177
|
-
if (( have_bot != have_app )) && ! truthy "${ENABLE_SLACK:-0}"; then
|
|
178
|
-
die "Slack provisioning requires a dedicated SLACK_BOT_TOKEN and SLACK_APP_TOKEN pair"
|
|
179
|
-
fi
|
|
180
|
-
|
|
181
|
-
if truthy "${ENABLE_SLACK:-0}" && [[ -t 0 ]]; then
|
|
182
|
-
if [[ -z "$SLACK_BOT_TOKEN" ]]; then
|
|
183
|
-
read -r -s -p "Slack Bot User OAuth Token (xoxb-...): " SLACK_BOT_TOKEN
|
|
184
|
-
echo >&2
|
|
185
|
-
fi
|
|
186
|
-
if [[ -z "$SLACK_APP_TOKEN" ]]; then
|
|
187
|
-
read -r -s -p "Slack App-Level Socket Mode Token (xapp-...): " SLACK_APP_TOKEN
|
|
188
|
-
echo >&2
|
|
189
|
-
fi
|
|
190
|
-
fi
|
|
191
|
-
|
|
192
|
-
[[ -n "$SLACK_BOT_TOKEN" && -n "$SLACK_APP_TOKEN" ]] \
|
|
193
|
-
|| die "Slack provisioning requires both SLACK_BOT_TOKEN and SLACK_APP_TOKEN"
|
|
194
|
-
[[ "$SLACK_BOT_TOKEN" =~ ^xoxb-[A-Za-z0-9-]+$ ]] || die "SLACK_BOT_TOKEN must be a Bot User OAuth token (xoxb-...)"
|
|
195
|
-
[[ "$SLACK_APP_TOKEN" =~ ^xapp-[A-Za-z0-9-]+$ ]] || die "SLACK_APP_TOKEN must be an App-Level Socket Mode token (xapp-...)"
|
|
196
|
-
|
|
197
|
-
SLACK_ALLOWED_USERS="${SLACK_ALLOWED_USERS//[[:space:]]/}"
|
|
198
|
-
if [[ -n "$SLACK_ALLOWED_USERS" && "$SLACK_ALLOWED_USERS" != "*" \
|
|
199
|
-
&& ! "$SLACK_ALLOWED_USERS" =~ ^[UW][A-Z0-9]+(,[UW][A-Z0-9]+)*$ ]]; then
|
|
200
|
-
die "SLACK_ALLOWED_USERS must be '*' or comma-separated Slack member IDs"
|
|
201
|
-
fi
|
|
202
|
-
|
|
203
|
-
log "[31] verifying Slack bot identity via auth.test"
|
|
204
|
-
auth_response="$({
|
|
205
|
-
printf '%s\n' 'url = "https://slack.com/api/auth.test"'
|
|
206
|
-
printf 'header = "Authorization: Bearer %s"\n' "$SLACK_BOT_TOKEN"
|
|
207
|
-
printf '%s\n' 'header = "Content-Type: application/x-www-form-urlencoded"'
|
|
208
|
-
printf '%s\n' 'request = "POST"' 'fail' 'silent' 'show-error' \
|
|
209
|
-
'connect-timeout = 10' 'max-time = 20' 'max-filesize = 1048576'
|
|
210
|
-
} | curl --config -)" \
|
|
211
|
-
|| die "Slack auth.test request failed"
|
|
212
|
-
|
|
213
|
-
identity=$(printf '%s' "$auth_response" | python3 -c '
|
|
214
|
-
import json, sys
|
|
215
|
-
try:
|
|
216
|
-
data = json.load(sys.stdin)
|
|
217
|
-
except Exception:
|
|
218
|
-
raise SystemExit("Slack auth.test returned an invalid response")
|
|
219
|
-
if not data.get("ok"):
|
|
220
|
-
error = str(data.get("error") or "unknown_error")
|
|
221
|
-
safe = "".join(c for c in error if c.isalnum() or c in "_-.")[:80]
|
|
222
|
-
raise SystemExit("Slack auth.test rejected the bot token ({})".format(safe or "unknown_error"))
|
|
223
|
-
values = [data.get(k, "") for k in ("team_id", "team", "user_id", "bot_id", "user")]
|
|
224
|
-
if not values[0] or not values[2] or not values[3]:
|
|
225
|
-
raise SystemExit("Slack auth.test response omitted required identity fields")
|
|
226
|
-
print("\t".join(str(v).replace("\t", " ").replace("\n", " ") for v in values))
|
|
227
|
-
') || die "Slack bot identity verification failed"
|
|
228
|
-
IFS=$'\t' read -r slack_team_id slack_team_name slack_bot_user_id slack_bot_id slack_bot_username <<< "$identity"
|
|
229
|
-
|
|
230
|
-
# auth.test proves the bot installation/workspace, while bots.info provides the
|
|
231
|
-
# authoritative Slack app identity for that bot. Correlation requires the
|
|
232
|
-
# app's users:read scope; missing scope is a hard, truthful deferred outcome.
|
|
233
|
-
log "[31] resolving Slack bot app identity via bots.info"
|
|
234
|
-
bot_info_response="$({
|
|
235
|
-
printf '%s\n' 'url = "https://slack.com/api/bots.info"'
|
|
236
|
-
printf 'header = "Authorization: Bearer %s"\n' "$SLACK_BOT_TOKEN"
|
|
237
|
-
printf '%s\n' 'header = "Content-Type: application/x-www-form-urlencoded"'
|
|
238
|
-
printf 'data = "bot=%s"\n' "$slack_bot_id"
|
|
239
|
-
printf '%s\n' 'request = "POST"' 'fail' 'silent' 'show-error' \
|
|
240
|
-
'connect-timeout = 10' 'max-time = 20' 'max-filesize = 1048576'
|
|
241
|
-
} | curl --config -)" \
|
|
242
|
-
|| die "Slack bots.info request failed; Slack remains deferred"
|
|
243
|
-
slack_bot_app_id="$(printf '%s' "$bot_info_response" | python3 -c '
|
|
244
|
-
import json, re, sys
|
|
245
|
-
try:
|
|
246
|
-
data = json.load(sys.stdin)
|
|
247
|
-
except Exception:
|
|
248
|
-
raise SystemExit("Slack bots.info returned an invalid response")
|
|
249
|
-
if not isinstance(data, dict) or not data.get("ok"):
|
|
250
|
-
error = str(data.get("error") if isinstance(data, dict) else "unknown_error")
|
|
251
|
-
if error == "missing_scope":
|
|
252
|
-
raise SystemExit(
|
|
253
|
-
"Slack bots.info requires users:read; add that bot scope, reinstall the app, and rerun"
|
|
254
|
-
)
|
|
255
|
-
safe = "".join(c for c in error if c.isalnum() or c in "_-." )[:80]
|
|
256
|
-
raise SystemExit("Slack bots.info rejected identity lookup ({})".format(safe or "unknown_error"))
|
|
257
|
-
bot = data.get("bot")
|
|
258
|
-
app_id = bot.get("app_id") if isinstance(bot, dict) else None
|
|
259
|
-
if not isinstance(app_id, str) or not re.fullmatch(r"[A-Z][A-Z0-9]+", app_id):
|
|
260
|
-
raise SystemExit("Slack bots.info response omitted a valid bot.app_id")
|
|
261
|
-
print(app_id)
|
|
262
|
-
')" || die "Slack bot app identity verification failed; Slack remains deferred"
|
|
263
|
-
|
|
264
|
-
# Socket Mode requires a separately authenticated app-level token. A valid bot
|
|
265
|
-
# token says nothing about the xapp credential, so prove that credential can
|
|
266
|
-
# open a connection before claiming ownership or persisting either secret. The
|
|
267
|
-
# token and Slack's returned WebSocket URL stay on anonymous pipes: neither is
|
|
268
|
-
# exposed through curl argv, child environments, logs, or durable files.
|
|
269
|
-
log "[31] verifying Slack Socket Mode app token via apps.connections.open"
|
|
270
|
-
slack_socket_app_id="$({
|
|
271
|
-
printf '%s\n' 'url = "https://slack.com/api/apps.connections.open"'
|
|
272
|
-
printf 'header = "Authorization: Bearer %s"\n' "$SLACK_APP_TOKEN"
|
|
273
|
-
printf '%s\n' 'header = "Content-Type: application/x-www-form-urlencoded"'
|
|
274
|
-
printf '%s\n' 'request = "POST"' 'fail' 'silent' 'show-error' \
|
|
275
|
-
'connect-timeout = 10' 'max-time = 20' 'max-filesize = 1048576'
|
|
276
|
-
} | curl --config - | python3 -c '
|
|
277
|
-
import json, re, sys
|
|
278
|
-
from urllib.parse import parse_qs, urlparse
|
|
279
|
-
try:
|
|
280
|
-
data = json.load(sys.stdin)
|
|
281
|
-
except Exception:
|
|
282
|
-
raise SystemExit("Slack apps.connections.open returned an invalid response")
|
|
283
|
-
if not isinstance(data, dict) or not data.get("ok"):
|
|
284
|
-
error = str(data.get("error") if isinstance(data, dict) else "unknown_error")
|
|
285
|
-
safe = "".join(c for c in error if c.isalnum() or c in "_-." )[:80]
|
|
286
|
-
raise SystemExit("Slack apps.connections.open rejected the app token ({})".format(safe or "unknown_error"))
|
|
287
|
-
url = data.get("url")
|
|
288
|
-
if not isinstance(url, str):
|
|
289
|
-
raise SystemExit("Slack apps.connections.open response omitted a Socket Mode URL")
|
|
290
|
-
parsed = urlparse(url)
|
|
291
|
-
if parsed.scheme != "wss" or not parsed.netloc:
|
|
292
|
-
raise SystemExit("Slack apps.connections.open response omitted a valid Socket Mode URL")
|
|
293
|
-
app_ids = parse_qs(parsed.query, keep_blank_values=True).get("app_id", [])
|
|
294
|
-
if len(app_ids) != 1 or not re.fullmatch(r"[A-Z][A-Z0-9]+", app_ids[0]):
|
|
295
|
-
raise SystemExit("Slack Socket Mode URL omitted a valid app_id identity")
|
|
296
|
-
print(app_ids[0])
|
|
297
|
-
')" || die "Slack Socket Mode app-token verification failed; Slack remains deferred"
|
|
298
|
-
|
|
299
|
-
[[ "$slack_bot_app_id" == "$slack_socket_app_id" ]] \
|
|
300
|
-
|| die "Slack bot and Socket Mode tokens belong to different apps; Slack remains deferred"
|
|
301
|
-
|
|
302
|
-
# Reject credential reuse, token rotation onto an identity owned by another
|
|
303
|
-
# agent, and credentials parked in shared env files. The scan, durable identity
|
|
304
|
-
# claim, and profile credential write share one fleet-wide flock.
|
|
305
|
-
fleet_lock_acquire
|
|
306
|
-
trap 'fleet_lock_release' EXIT
|
|
307
|
-
python3 /dev/fd/3 "$REGISTRY_FILE" "$FLEET_ENV" "$ENVF" "$AGENT_ID" \
|
|
308
|
-
"$slack_team_id" "$slack_bot_user_id" "$slack_bot_id" \
|
|
309
|
-
"$ROLE_DIR" "$PROFILE_NAME" "$slack_team_name" "$slack_bot_username" \
|
|
310
|
-
3<<'PYEOF' <<<"${SLACK_BOT_TOKEN}"$'\n'"${SLACK_APP_TOKEN}"
|
|
311
|
-
import os
|
|
312
|
-
import pathlib
|
|
313
|
-
import re
|
|
314
|
-
import sys
|
|
315
|
-
try:
|
|
316
|
-
import yaml # type: ignore
|
|
317
|
-
except ImportError:
|
|
318
|
-
raise SystemExit("PyYAML is required for Slack fleet claims")
|
|
319
|
-
|
|
320
|
-
(
|
|
321
|
-
registry_path,
|
|
322
|
-
fleet_path,
|
|
323
|
-
target_path,
|
|
324
|
-
agent_id,
|
|
325
|
-
team_id,
|
|
326
|
-
user_id,
|
|
327
|
-
bot_id,
|
|
328
|
-
role_dir,
|
|
329
|
-
profile_name,
|
|
330
|
-
team_name,
|
|
331
|
-
bot_username,
|
|
332
|
-
) = sys.argv[1:]
|
|
333
|
-
credential_lines = sys.stdin.read().splitlines()
|
|
334
|
-
if len(credential_lines) != 2 or not all(credential_lines):
|
|
335
|
-
raise SystemExit("Slack ownership scan received an invalid credential pair")
|
|
336
|
-
bot_token, app_token = credential_lines
|
|
337
|
-
for metadata_path in (pathlib.Path("/proc/self/cmdline"), pathlib.Path("/proc/self/environ")):
|
|
338
|
-
if metadata_path.is_file():
|
|
339
|
-
metadata = metadata_path.read_bytes()
|
|
340
|
-
if any(secret.encode() in metadata for secret in (bot_token, app_token)):
|
|
341
|
-
raise SystemExit("Slack ownership scan detected credential exposure in process metadata")
|
|
342
|
-
target = pathlib.Path(target_path).resolve(strict=False)
|
|
343
|
-
|
|
344
|
-
def env_values(path):
|
|
345
|
-
try:
|
|
346
|
-
text = path.read_text(encoding="utf-8")
|
|
347
|
-
except (OSError, UnicodeError):
|
|
348
|
-
return {}
|
|
349
|
-
values = {}
|
|
350
|
-
for key in ("SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"):
|
|
351
|
-
match = re.search(rf"(?m)^\s*(?:export\s+)?{key}\s*=\s*(.*)$", text)
|
|
352
|
-
if match:
|
|
353
|
-
value = match.group(1).strip()
|
|
354
|
-
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
355
|
-
value = value[1:-1]
|
|
356
|
-
values[key] = value
|
|
357
|
-
return values
|
|
358
|
-
|
|
359
|
-
owners = [("shared fleet environment", pathlib.Path(fleet_path))]
|
|
360
|
-
registry = pathlib.Path(registry_path)
|
|
361
|
-
if registry.is_symlink():
|
|
362
|
-
raise SystemExit(f"refusing to update registry symlink: {registry}")
|
|
363
|
-
data = {"schema_version": 1, "agents": {}}
|
|
364
|
-
if registry.is_file():
|
|
365
|
-
try:
|
|
366
|
-
data = yaml.safe_load(registry.read_text(encoding="utf-8")) or data
|
|
367
|
-
except Exception as exc:
|
|
368
|
-
raise SystemExit(f"cannot safely inspect Slack ownership registry: {type(exc).__name__}")
|
|
369
|
-
if not isinstance(data, dict) or not isinstance(data.get("agents", {}), dict):
|
|
370
|
-
raise SystemExit("cannot safely inspect Slack ownership registry: invalid agents mapping")
|
|
371
|
-
agents = data.setdefault("agents", {})
|
|
372
|
-
for other_id, entry in agents.items():
|
|
373
|
-
if other_id == agent_id or not isinstance(entry, dict):
|
|
374
|
-
continue
|
|
375
|
-
slack = entry.get("slack") or {}
|
|
376
|
-
if isinstance(slack, dict):
|
|
377
|
-
same_user = user_id and slack.get("bot_user_id") == user_id
|
|
378
|
-
same_bot = bot_id and slack.get("bot_id") == bot_id
|
|
379
|
-
same_team_user = team_id and same_user and slack.get("team_id") == team_id
|
|
380
|
-
if same_bot or same_team_user:
|
|
381
|
-
raise SystemExit(f"Slack bot identity is already assigned to agent {other_id}")
|
|
382
|
-
other_role_dir = entry.get("role_dir")
|
|
383
|
-
if other_role_dir:
|
|
384
|
-
owners.append((f"agent {other_id}", pathlib.Path(str(other_role_dir)) / "runtime" / ".env"))
|
|
385
|
-
|
|
386
|
-
home_value = os.environ.get("HOME", "")
|
|
387
|
-
if home_value:
|
|
388
|
-
home = pathlib.Path(home_value)
|
|
389
|
-
owners.append(("shared Hermes root", home / ".hermes" / ".env"))
|
|
390
|
-
profiles = home / ".hermes" / "profiles"
|
|
391
|
-
if profiles.is_dir():
|
|
392
|
-
for profile in profiles.iterdir():
|
|
393
|
-
owners.append((f"profile {profile.name}", profile / ".env"))
|
|
394
|
-
|
|
395
|
-
seen = set()
|
|
396
|
-
for owner, path in owners:
|
|
397
|
-
resolved = path.resolve(strict=False)
|
|
398
|
-
if resolved == target or resolved in seen:
|
|
399
|
-
continue
|
|
400
|
-
seen.add(resolved)
|
|
401
|
-
values = env_values(path)
|
|
402
|
-
if values.get("SLACK_BOT_TOKEN") == bot_token:
|
|
403
|
-
raise SystemExit(f"Slack bot token is already assigned to {owner}")
|
|
404
|
-
if values.get("SLACK_APP_TOKEN") == app_token:
|
|
405
|
-
raise SystemExit(f"Slack app token is already assigned to {owner}")
|
|
406
|
-
|
|
407
|
-
# Read-only by design. The channel transaction repeats the identity conflict
|
|
408
|
-
# check under this same lock immediately before its recoverable registry write.
|
|
409
|
-
PYEOF
|
|
410
|
-
|
|
411
|
-
# Stage both credentials in one new 1Password item, verify both fields, then
|
|
412
|
-
# switch both profile refs with one atomic delta update. A failed rotation
|
|
413
|
-
# leaves the previously active pair untouched.
|
|
414
|
-
ONEPASSWORD_ITEM_PREFIX="${HERMES_ONEPASSWORD_ITEM_PREFIX:-$(config_get fleet.onepassword_item_prefix 'hermes-agent')}"
|
|
415
|
-
slack_stage="$(stage_onepassword_secret_pair \
|
|
416
|
-
"${ONEPASSWORD_ITEM_PREFIX}-${AGENT_ID}-slack-credentials" \
|
|
417
|
-
slack_bot_token "$SLACK_BOT_TOKEN" \
|
|
418
|
-
slack_app_token "$SLACK_APP_TOKEN")" \
|
|
419
|
-
|| die "Slack credential pair could not be staged and verified in 1Password"
|
|
420
|
-
if [[ "$slack_stage" != *$'\n'*$'\n'* ]]; then
|
|
421
|
-
die "Slack credential-pair storage returned an invalid reference set"
|
|
422
|
-
fi
|
|
423
|
-
slack_staged_item_id="${slack_stage%%$'\n'*}"
|
|
424
|
-
slack_pair_references="${slack_stage#*$'\n'}"
|
|
425
|
-
slack_bot_reference="${slack_pair_references%%$'\n'*}"
|
|
426
|
-
slack_app_reference="${slack_pair_references#*$'\n'}"
|
|
427
|
-
[[ "$slack_app_reference" != *$'\n'* ]] \
|
|
428
|
-
|| die "Slack credential-pair storage returned too many references"
|
|
429
|
-
unset SLACK_BOT_TOKEN SLACK_APP_TOKEN
|
|
430
|
-
|
|
431
|
-
slack_transaction_exit() {
|
|
432
|
-
local rc=$?
|
|
433
|
-
trap - EXIT
|
|
434
|
-
if [[ -n "${slack_staged_item_id:-}" ]]; then
|
|
435
|
-
delete_staged_onepassword_item "$slack_staged_item_id" >/dev/null 2>&1 \
|
|
436
|
-
|| warn " staged Slack item cleanup failed; archive manually by immutable item id"
|
|
437
|
-
fi
|
|
438
|
-
fleet_lock_release
|
|
439
|
-
exit "$rc"
|
|
440
|
-
}
|
|
441
|
-
trap slack_transaction_exit EXIT
|
|
442
|
-
|
|
443
|
-
channel_transaction_slack \
|
|
444
|
-
"$PROFILE_HOME" "$ENVF" "$slack_bot_reference" "$slack_app_reference" \
|
|
445
|
-
"$slack_team_id" "$slack_team_name" "$slack_bot_user_id" \
|
|
446
|
-
"$slack_bot_id" "$slack_bot_username" "$SLACK_ALLOWED_USERS" \
|
|
447
|
-
|| die "Slack local credential transaction failed"
|
|
448
|
-
|
|
449
|
-
slack_staged_item_id=""
|
|
450
|
-
|
|
451
|
-
fleet_lock_release
|
|
452
|
-
trap - EXIT
|
|
453
|
-
|
|
454
|
-
if [[ -z "$SLACK_ALLOWED_USERS" ]]; then
|
|
455
|
-
warn " Slack is wired but denies all inbound users until SLACK_ALLOWED_USERS is set"
|
|
456
|
-
fi
|
|
457
|
-
log " verified Slack bot $slack_bot_username in $slack_team_name (profile-local credentials)"
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Bind this agent to its Plane board — WITHOUT inventing a board identifier.
|
|
3
|
-
#
|
|
4
|
-
# The identifier is Plane's to assign. This step never mints one. It resolves
|
|
5
|
-
# the board through providers/plane.sh and persists ONLY the identifier Plane
|
|
6
|
-
# reports back; if Plane reports none, it dies loudly rather than writing a
|
|
7
|
-
# guess into role.yaml.
|
|
8
|
-
#
|
|
9
|
-
# Resolution order:
|
|
10
|
-
# 1. The repo-root .project.json binding (the SOT) — read back live.
|
|
11
|
-
# 2. Otherwise a repo-named board lookup, falling back to creation.
|
|
12
|
-
#
|
|
13
|
-
# Proposing an identifier for a BRAND-NEW board is legitimate, but it may only
|
|
14
|
-
# come from explicit configuration (PLANE_IDENTIFIER, or an already-recorded
|
|
15
|
-
# role.yaml plane.identifier). Full new-board provisioning with the
|
|
16
|
-
# .project.json transaction lives in 42-ticket-provider.sh.
|
|
17
|
-
# shellcheck source=_lib.sh
|
|
18
|
-
source "$(dirname "$0")/_lib.sh"
|
|
19
|
-
load_role_env
|
|
20
|
-
|
|
21
|
-
already_done 40-plane && { log "[40] plane already set up — skipping"; exit 0; }
|
|
22
|
-
[[ "${SKIP_PLANE:-0}" == "1" ]] && { log "[40] plane — SKIPPED"; mark_done 40-plane; exit 0; }
|
|
23
|
-
|
|
24
|
-
[[ -n "$PLANE_API_KEY" ]] || { warn "[40] PLANE_API_KEY not set; skipping. set PLANE_33GOD_API_KEY and re-run ./.scripts/40-plane.sh"; exit 0; }
|
|
25
|
-
|
|
26
|
-
PLANE_ADAPTER="$(dirname "$0")/providers/plane.sh"
|
|
27
|
-
[[ -x "$PLANE_ADAPTER" ]] || die "[40] missing Plane adapter: $PLANE_ADAPTER"
|
|
28
|
-
|
|
29
|
-
# The board belongs to the REPO, not to this role. display_name carries the
|
|
30
|
-
# role suffix ("Holocene PM") and must never become the board name — that is
|
|
31
|
-
# how role-suffixed duplicate boards get created. Matches 42-ticket-provider.sh.
|
|
32
|
-
NAME="$(printf '%s' "$REPO" | tr '_-' ' ' | python3 -c 'import sys; print(" ".join(w[:1].upper()+w[1:] for w in sys.stdin.read().split()))')"
|
|
33
|
-
|
|
34
|
-
# Identifier PROPOSAL, used only in a create request and never persisted as
|
|
35
|
-
# though it were confirmed. Explicit configuration only — never derived from
|
|
36
|
-
# the repo or role strings.
|
|
37
|
-
PROPOSED_IDENT="${PLANE_IDENTIFIER:-$(yaml_get plane.identifier)}"
|
|
38
|
-
|
|
39
|
-
# 1. Prefer the recorded .project.json binding and read it back live.
|
|
40
|
-
OUT="$("$PLANE_ADAPTER" resolve 2>/dev/null || true)"
|
|
41
|
-
if [[ -n "$OUT" ]]; then
|
|
42
|
-
log "[40] resolving plane board from the .project.json binding"
|
|
43
|
-
else
|
|
44
|
-
log "[40] no bound board — resolving/creating repo board '$NAME'${PROPOSED_IDENT:+ (proposed identifier $PROPOSED_IDENT)}"
|
|
45
|
-
OUT="$("$PLANE_ADAPTER" create_board "$NAME" "$PROPOSED_IDENT" "Hermes agent board for $AGENT_ID")" \
|
|
46
|
-
|| die "[40] plane board resolution failed for '$NAME'"
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
PROJECT_ID="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("board_id","") or "")')"
|
|
50
|
-
LIVE_IDENTIFIER="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("identifier","") or "")')"
|
|
51
|
-
|
|
52
|
-
[[ -n "$PROJECT_ID" ]] || die "[40] plane returned no board id: $OUT"
|
|
53
|
-
[[ -n "$LIVE_IDENTIFIER" ]] \
|
|
54
|
-
|| die "[40] plane returned no authoritative identifier — refusing to persist a guess: $OUT"
|
|
55
|
-
|
|
56
|
-
log " plane board id=$PROJECT_ID identifier=$LIVE_IDENTIFIER (authoritative, read back from Plane)"
|
|
57
|
-
|
|
58
|
-
yaml_set plane.identifier "$LIVE_IDENTIFIER"
|
|
59
|
-
echo "$PROJECT_ID" > "$ROLE_DIR/.scripts/.plane-project-id"
|
|
60
|
-
mark_done 40-plane
|