2e138a199b
Two changes ship together here.
1. One shared herdr workspace. The lead and every worker now live in one
workspace called "fleet", so the operator sees one "session" with many
windows, not two. Before, the lead sat in a "leads" workspace and workers
in "bridged-workers", which read as two sessions. The lead is still told
apart from workers by its exact tab label ("lead: <name>"), so putting them
in one space is safe. LeadTabScanner keeps the exclude-by-label mechanism
for split layouts; Fleetd now passes an empty exclude set.
2. Rename the daemon from "bridged" to "fleetd" (the binary, config, scripts,
launchd/systemd units, module dir, and MCP mount).
- Module dir bridged/ -> fleetd/; jar finalName -> fleetd.jar.
- Log line, comments, docs, and CLAUDE.md updated to say fleetd.
- Scripts renamed: redeploy-bridged.sh -> redeploy-fleetd.sh,
bridged-launchd-wrapper.sh -> fleetd-launchd-wrapper.sh.
- Deploy units renamed: dev.ltms.bridged.plist -> dev.ltms.fleetd.plist,
bridged.service -> fleetd.service; launchd Label -> dev.ltms.fleetd.
- Config default bridged.yaml -> fleetd.yaml; the legacy bridged.yaml is
still read as a fallback, and still gitignored.
- MCP: drop the deprecated bridge_* tool twins; only fleet_* remain. The
server name is "fleet". The mount name in the local .mcp.json becomes
"fleet" (gitignored, not in this commit).
- Env var defaults BRIDGED_API_TOKEN -> FLEETD_API_TOKEN, fixture
BRIDGED_WORKER_TOKEN -> FLEETD_WORKER_TOKEN.
Kept on purpose: the BRIDGED_MEMBER marker. Renaming it is a coupled change to
the credential-scrub security control (an operator secrets.sh may guard on it),
so it stays until that migration is done on its own.
Metrics were already fleet_* (CB-632); MetricNamesTest still guards that no
name says bridged_.
The canonical CLAUDE.md block and the wiki template stay byte-identical
(wiki working tree edited, committed to the wiki repo separately).
949 tests pass (mvn clean install). 4 fewer than before = the 4 removed
bridge_* alias tests.
136 lines
5.8 KiB
Bash
Executable File
136 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# CB-596 step 1: measure which credentials a live member actually holds.
|
|
#
|
|
# The claim under test is that a member's herdr pane starts a LOGIN shell, that shell sources
|
|
# ${SHARED_ENV}/tools/secrets.sh, and so a member inherits every name that file exports — while
|
|
# CB-592 blocks exactly one of them (GITEA_ACCESS_TOKEN). That is an inference from the code, not a
|
|
# measurement, and issue #82 says plainly: do not build a fix on the inference. This is the
|
|
# measurement.
|
|
#
|
|
# WHY THIS IS A SCRIPT AND NOT A COMMAND SOMEONE TYPES
|
|
#
|
|
# Enumerating credential names inside a member is exactly the action that should need the operator's
|
|
# explicit approval, and the command classifier refuses it. That refusal is correct. This script is
|
|
# the seam: it is one auditable file the operator can read once, top to bottom, and then run — rather
|
|
# than approving an ad-hoc shell pipeline whose behaviour they have to take on trust.
|
|
#
|
|
# WHAT IT WILL NOT DO
|
|
#
|
|
# * It never prints a credential value, and never any prefix or suffix of one. Not one character.
|
|
# Issue #82's criterion 1 asked for a 6-character prefix; this prints a truncated SHA-256 instead.
|
|
# A prefix of a short secret is most of the secret, and it would end up pasted into a ticket. The
|
|
# hash answers every question the prefix was for — is it set, is it the same value as over there,
|
|
# is it the CB-592 sentinel — and answers none of the ones it should not.
|
|
# * It never writes anywhere, never contacts the network, and never touches secrets.sh, which is
|
|
# the operator's file.
|
|
#
|
|
# HOW TO RUN IT
|
|
#
|
|
# 1. As the operator, in a member's pane (a spawned worker's terminal):
|
|
# bash scripts/probe-member-credentials.sh
|
|
# 2. For the comparison row, in your OWN shell — a lead, not a member:
|
|
# bash scripts/probe-member-credentials.sh --allow-outside-member
|
|
#
|
|
# The two outputs side by side are the finding: any name whose hash matches between them is a
|
|
# credential the member holds in full.
|
|
#
|
|
set -uo pipefail
|
|
|
|
# The names ${SHARED_ENV}/tools/secrets.sh exports, recorded on 2026-08-16 (issue #82). Names only —
|
|
# this list contains no values and never should. If secrets.sh gains a name, this list goes stale and
|
|
# the probe silently stops asking about it; that staleness is itself part of what #82's criterion 4
|
|
# has to solve, so it is called out in the summary rather than hidden.
|
|
NAMES=(
|
|
AI_GATEWAY_TOKEN BESZEL_ADMIN_EMAIL BESZEL_ADMIN_PASSWORD
|
|
BESZEL_HUB_URL BESZEL_KEY BESZEL_UNIVERSAL_TOKEN
|
|
BRAIN_MCP_TOKEN CF_ACCOUNT_ID CF_API_TOKEN
|
|
CF_USER_TOKEN CONFLUENCE_API_TOKEN CONFLUENCE_USERNAME
|
|
CONTEXT7_TOKEN GITEA_HOST GITLAB_OAUTH_CLIENT_SECRET
|
|
GITLAB_PERSONAL_ACCESS_TOKEN GRAFANA_ADMIN_PASSWORD GRAFANA_ADMIN_USER
|
|
HASS_TOKEN HW_PASSWORD HW_USER
|
|
LTMS_API_KEY MEMORY_MCP_TOKEN METRICS_PUSH_TOKEN
|
|
OPENCODE_AUTOMODE_MODEL TELEGRAM_BOT_TOKEN TELEGRAM_CHAT_ID
|
|
TS_API_KEY TS_AUTHKEY WORKER_GITEA_TOKEN
|
|
GITEA_ACCESS_TOKEN
|
|
)
|
|
|
|
allow_outside=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--allow-outside-member) allow_outside=1 ;;
|
|
-h|--help) sed -n '2,40p' "$0"; exit 0 ;;
|
|
*) echo "unknown argument: $arg" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
if [ "${BRIDGED_MEMBER:-}" != "1" ] && [ "$allow_outside" -eq 0 ]; then
|
|
cat >&2 <<'EOF'
|
|
refusing to run: BRIDGED_MEMBER is not 1, so this is not a member's shell.
|
|
|
|
The finding this probe exists for is what a MEMBER holds. Run it in a spawned worker's pane. If you
|
|
meant to take the comparison reading from your own shell, pass --allow-outside-member and the output
|
|
will be labelled as such.
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
# Prefer sha256sum (Linux), fall back to shasum (macOS). If neither exists, report presence and
|
|
# length only — degraded, but never a value.
|
|
hasher=""
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
hasher="sha256sum"
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
hasher="shasum -a 256"
|
|
fi
|
|
|
|
digest() { # value -> first 12 hex chars of its sha256, or "-" when no hasher is available
|
|
[ -z "$hasher" ] && { printf '%s' "-"; return; }
|
|
printf '%s' "$1" | $hasher | cut -c1-12
|
|
}
|
|
|
|
if [ "${BRIDGED_MEMBER:-}" = "1" ]; then
|
|
where="MEMBER (BRIDGED_MEMBER=1)"
|
|
else
|
|
where="NOT a member — comparison reading only"
|
|
fi
|
|
|
|
echo "CB-596 credential probe"
|
|
echo "reading from : $where"
|
|
echo "shell : ${SHELL:-unknown}"
|
|
echo "hash : ${hasher:-none available — lengths only}"
|
|
# Only printed so the two readings can be told apart when they are pasted side by side.
|
|
echo "host : $(hostname 2>/dev/null || echo unknown)"
|
|
echo
|
|
printf '%-30s %-7s %6s %s\n' "NAME" "STATE" "LEN" "SHA256-12"
|
|
printf '%-30s %-7s %6s %s\n' "------------------------------" "-------" "------" "------------"
|
|
|
|
set_count=0
|
|
for name in "${NAMES[@]}"; do
|
|
value="${!name:-}"
|
|
if [ -z "$value" ]; then
|
|
printf '%-30s %-7s %6s %s\n' "$name" "unset" "-" "-"
|
|
else
|
|
set_count=$((set_count + 1))
|
|
printf '%-30s %-7s %6s %s\n' "$name" "SET" "${#value}" "$(digest "$value")"
|
|
fi
|
|
done
|
|
|
|
echo
|
|
echo "$set_count of ${#NAMES[@]} names are set in this shell."
|
|
echo
|
|
cat <<'EOF'
|
|
How to read this:
|
|
|
|
* Take the MEMBER reading and the comparison reading, and line them up. A name whose SHA256-12
|
|
matches on both sides is a credential the member holds in full. That is the finding.
|
|
* GITEA_ACCESS_TOKEN is the control. CB-592 replaces it with a blocked sentinel, so its hash
|
|
should DIFFER between the two readings. If it matches, CB-592 is not working and that is the
|
|
most urgent thing on this page.
|
|
* AI_GATEWAY_TOKEN matching is expected and correct, not a leak: fleetd.yaml names it in
|
|
`tokenEnv:` for the local and gx profiles, so a member reaching the gateway is by design.
|
|
* A name that is set here but is NOT in the list above will not appear at all. The list was
|
|
recorded on 2026-08-16 and does not update itself. Anything added to secrets.sh since then is
|
|
invisible to this probe — which is the same gap issue #82 criterion 4 asks to close properly.
|
|
EOF
|