Q4: confirm personal-scope discriminators (chat_facing, delegates_to=[ceo-planb], sovereign_only=false) Q5: drop google-workspace cred — builtin manages own OAuth via Hermes hub (not credctl vault) Q6: split proton-bridge-imap → proton-bridge-imap-user + proton-bridge-imap-pass (vault exact-match) Q7: rename perplexity-api → perplexity (vault exact-match) Q8: add 3 proton vault entries (account-email, account-password, mailbox-password) Q9: install.sh F6 — MCP allowlist materialization; wires 3 proton MCPs, removes bte (hard-rule leak) Q10: macOS-only externals annotated os_constraint:darwin; install.sh F7 emits INFO on non-Darwin credbridge.sh: drop google-workspace case, rewrite proton-bridge to use 2 vault entries, rename perplexity case Disclosure §7 rewritten with 6 credentials matching vault exact-name policy (DISCLOSURE-SCHEMA §4.5) Disclosure §12 PAUSE table marked all 8 rows RESOLVED (rows 1-7 Wave 8, row 8 Wave 7) Untracked skills/proton-tools/SKILL.md (90 lines, declared in manifest since Wave 4) — committed for clone-ability Verified: hermes -p steev skills list → 6 enabled (matches disclosure.skills declaration) hermes -p steev mcp list → 3 entries (proton-calendar, proton-email, proton-contacts); bte removed F7 on Linux host correctly suppresses macOS-only externals Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
382 lines
16 KiB
Bash
Executable File
382 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# install.sh — wire Steev profile distribution into Hermes.
|
|
# Idempotent. Does NOT set secrets and does NOT enable cron.
|
|
#
|
|
# ./install.sh [--copy] [--dry-run]
|
|
#
|
|
# Default = SYMLINK mode: the repo is canonical, ~/.hermes/steev → this repo.
|
|
# --copy = copy files into ~/.hermes/steev instead.
|
|
set -euo pipefail
|
|
|
|
REPO="$(cd "$(dirname "$0")" && pwd)"
|
|
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
|
|
MODE=symlink; DRY=0
|
|
while [ $# -gt 0 ]; do case "$1" in
|
|
--copy) MODE=copy ;; --dry-run) DRY=1 ;;
|
|
*) echo "unknown arg: $1"; exit 2 ;;
|
|
esac; shift; done
|
|
|
|
CFG="$HERMES_HOME/profiles/steev/config.yaml"
|
|
run() { if [ "$DRY" = 1 ]; then echo "DRY: $*"; else eval "$*"; fi; }
|
|
|
|
echo "== preflight =="
|
|
for c in python3 sqlite3; do command -v "$c" >/dev/null || { echo "MISSING: $c"; exit 1; }; done
|
|
|
|
echo "== link/copy repo → \$HERMES_HOME/steev ($MODE) =="
|
|
if [ "$MODE" = symlink ]; then
|
|
if [ -e "$HERMES_HOME/steev" ] && [ ! -L "$HERMES_HOME/steev" ]; then
|
|
echo "EXISTS (not a symlink): $HERMES_HOME/steev — remove it and re-run"; exit 1
|
|
fi
|
|
run "ln -sfn '$REPO' '$HERMES_HOME/steev'"
|
|
else
|
|
run "mkdir -p '$HERMES_HOME/steev'"
|
|
run "cp -r '$REPO'/skills '$REPO'/schema.sql '$HERMES_HOME/steev/'"
|
|
fi
|
|
|
|
echo "== register skills in steev profile config =="
|
|
SKILL_DIR="$REPO/skills"
|
|
if [ "$DRY" = 0 ]; then
|
|
python3 - "$CFG" "$SKILL_DIR" <<'PY'
|
|
import sys, os, yaml
|
|
cfg, sk = sys.argv[1], sys.argv[2]
|
|
d = yaml.safe_load(open(cfg).read()) if os.path.exists(cfg) else {}
|
|
d = d or {}
|
|
d.setdefault('skills', {}).setdefault('external_dirs', [])
|
|
if sk not in d['skills']['external_dirs']:
|
|
d['skills']['external_dirs'].append(sk)
|
|
open(cfg, 'w').write(yaml.dump(d, sort_keys=False, allow_unicode=True))
|
|
print(" +", sk)
|
|
else:
|
|
print(" already registered:", sk)
|
|
PY
|
|
else
|
|
echo "DRY: register $SKILL_DIR in $CFG"
|
|
fi
|
|
|
|
echo "== steev.db =="
|
|
run "sqlite3 '$HERMES_HOME/steev/steev.db' < '$REPO/schema.sql'"
|
|
|
|
echo ""
|
|
echo "== hermes-native profile install (dispatch-readiness) =="
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "DRY: hermes profile install '$REPO' --yes --force"
|
|
else
|
|
hermes profile install "$REPO" --yes --force 2>&1 | tail -5 || \
|
|
echo " WARN: hermes profile install failed (legacy symlink still works)"
|
|
fi
|
|
|
|
echo ""
|
|
# ----------------------------------------------------------------------------
|
|
# Wave 7 D6 — disclosure → runtime config wiring (F1-F4)
|
|
# Materializes manifest.disclosure (schema v2) into the live Hermes runtime:
|
|
# F1 resolve $HERMES_WORKSPACE in inherit_dirs → skills.external_dirs (global)
|
|
# F2 compute denylist from disclosure.skills → skills.disabled (per-profile)
|
|
# F3 propagate inherit_mcp_toolsets → agent.inherit_mcp_toolsets
|
|
# F4 install subrepo pre-push disclosure-drift gate
|
|
# Steev = personal scope: profile name is `steev` (no -planb suffix per FRAMEWORK §6.1).
|
|
# All steps idempotent + graceful (WARN + skip on missing tooling).
|
|
# ----------------------------------------------------------------------------
|
|
echo "== disclosure → runtime config (Wave 7 D6) =="
|
|
PROFILE_NAME="steev"
|
|
HERMES_WORKSPACE="${HERMES_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
GLOBAL_CFG="$HERMES_HOME/config.yaml"
|
|
PROFILE_CFG="$HOME/.hermes/profiles/$PROFILE_NAME/config.yaml"
|
|
|
|
# F1 — resolve $HERMES_WORKSPACE in disclosure.inherit_dirs → skills.external_dirs (global config)
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "DRY: F1 expand inherit_dirs (HERMES_WORKSPACE=$HERMES_WORKSPACE) → $GLOBAL_CFG"
|
|
elif command -v yq >/dev/null 2>&1; then
|
|
INHERIT_DIRS=$(yq -r '.disclosure.inherit_dirs[]?' "$REPO/manifest.yaml" 2>/dev/null || true)
|
|
if [ -n "$INHERIT_DIRS" ]; then
|
|
while IFS= read -r raw; do
|
|
[ -z "$raw" ] && continue
|
|
resolved="${raw//\$HERMES_WORKSPACE/$HERMES_WORKSPACE}"
|
|
if [ ! -d "$resolved" ]; then
|
|
echo " WARN: F1 inherit_dir not found: $resolved (declared: $raw) — skipping"
|
|
continue
|
|
fi
|
|
python3 - "$GLOBAL_CFG" "$resolved" <<'PY'
|
|
import sys, os, yaml
|
|
cfg, sk = sys.argv[1], sys.argv[2]
|
|
d = yaml.safe_load(open(cfg).read()) if os.path.exists(cfg) else {}
|
|
d = d or {}
|
|
d.setdefault('skills', {}).setdefault('external_dirs', [])
|
|
if sk not in d['skills']['external_dirs']:
|
|
d['skills']['external_dirs'].append(sk)
|
|
open(cfg, 'w').write(yaml.safe_dump(d, sort_keys=False, allow_unicode=True))
|
|
print(f" F1 + {sk}")
|
|
else:
|
|
print(f" F1 = {sk} (already present)")
|
|
PY
|
|
done <<< "$INHERIT_DIRS"
|
|
else
|
|
echo " F1: no disclosure.inherit_dirs declared — skip"
|
|
fi
|
|
else
|
|
echo " WARN: F1 yq not on PATH — skipping inherit_dirs resolution"
|
|
fi
|
|
|
|
# F2 — compute denylist (all builtins NOT in disclosure.skills allowlist) → profile config
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "DRY: F2 compute builtin denylist → $PROFILE_CFG (skills.disabled)"
|
|
elif command -v hermes >/dev/null 2>&1 && command -v yq >/dev/null 2>&1; then
|
|
# Try --json first; fall back to table parse w/ box-draw chars (Wave 5 parser).
|
|
ALL_BUILTINS=$(hermes skills list --json 2>/dev/null | jq -r '.[] | select(.source=="builtin") | .name' 2>/dev/null || true)
|
|
if [ -z "$ALL_BUILTINS" ]; then
|
|
ALL_BUILTINS=$(COLUMNS=200 hermes skills list 2>/dev/null | awk -F'│' 'NR>3 && /builtin/ {name=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", name); gsub(/…$/, "", name); print name}' || true)
|
|
fi
|
|
ALLOWLIST_BUILTIN=$(yq -r '.disclosure.skills[] | select(.source=="builtin") | .id' "$REPO/manifest.yaml" 2>/dev/null | sort -u)
|
|
if [ -z "$ALL_BUILTINS" ]; then
|
|
echo " WARN: F2 could not enumerate live builtins — skipping denylist"
|
|
else
|
|
SORTED_BUILTINS=$(echo "$ALL_BUILTINS" | sort -u)
|
|
DENYLIST=$(comm -23 <(echo "$SORTED_BUILTINS") <(echo "${ALLOWLIST_BUILTIN:-}") | grep -v '^$' || true)
|
|
mkdir -p "$(dirname "$PROFILE_CFG")"
|
|
[ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG"
|
|
python3 - "$PROFILE_CFG" <<PY
|
|
import sys, yaml
|
|
cfg = sys.argv[1]
|
|
denylist = [s for s in """$DENYLIST""".splitlines() if s.strip()]
|
|
d = yaml.safe_load(open(cfg).read()) or {}
|
|
d.setdefault('skills', {})['disabled'] = sorted(set(denylist))
|
|
open(cfg, 'w').write(yaml.safe_dump(d, sort_keys=False, allow_unicode=True))
|
|
print(f" F2 wrote skills.disabled: {len(denylist)} entr{'y' if len(denylist)==1 else 'ies'}")
|
|
PY
|
|
fi
|
|
else
|
|
echo " WARN: F2 hermes/yq missing — skipping denylist"
|
|
fi
|
|
|
|
# F2b — enable builtin allowlist via additive external_dirs
|
|
# Hermes 0.14 uses additive external_dirs model (not pure denylist) — to enable
|
|
# a builtin skill, add its hermes-agent/skills/<category>/<skill> path here.
|
|
HERMES_AGENT_SKILLS="$HERMES_WORKSPACE/hermes-agent/skills"
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "DRY: F2b enable builtin allowlist via additive external_dirs → $PROFILE_CFG"
|
|
elif command -v yq >/dev/null 2>&1; then
|
|
BUILTIN_PATHS=$(yq -r '.disclosure.skills[]? | select(.source=="builtin") | .path' "$REPO/manifest.yaml" 2>/dev/null || true)
|
|
BUILTIN_ENABLED=0
|
|
for p in $BUILTIN_PATHS; do
|
|
full="$HERMES_AGENT_SKILLS/$p"
|
|
if [ -d "$full" ]; then
|
|
if ! yq -r '.skills.external_dirs[]?' "$PROFILE_CFG" 2>/dev/null | grep -qF "$full"; then
|
|
mkdir -p "$(dirname "$PROFILE_CFG")"
|
|
full="$full" yq -i '.skills.external_dirs += [env(full)]' "$PROFILE_CFG" \
|
|
|| echo " WARN: F2b yq write to $PROFILE_CFG failed for $full"
|
|
BUILTIN_ENABLED=$((BUILTIN_ENABLED + 1))
|
|
fi
|
|
else
|
|
echo " ⚠ F2b: builtin path missing — $full (skipped)" >&2
|
|
fi
|
|
done
|
|
[ "$BUILTIN_ENABLED" -gt 0 ] && echo " F2b enabled $BUILTIN_ENABLED builtin allowlist path(s) in external_dirs"
|
|
else
|
|
echo " WARN: F2b yq not on PATH — skipping builtin allowlist"
|
|
fi
|
|
|
|
# F3 — propagate disclosure.inherit_mcp_toolsets to per-profile config.yaml
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "DRY: F3 write agent.inherit_mcp_toolsets → $PROFILE_CFG"
|
|
elif command -v yq >/dev/null 2>&1; then
|
|
INHERIT_MCP=$(yq -r '.disclosure.inherit_mcp_toolsets' "$REPO/manifest.yaml" 2>/dev/null || echo "null")
|
|
if [ "$INHERIT_MCP" = "null" ] || [ -z "$INHERIT_MCP" ]; then
|
|
echo " F3: disclosure.inherit_mcp_toolsets undeclared — skip"
|
|
else
|
|
mkdir -p "$(dirname "$PROFILE_CFG")"
|
|
[ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG"
|
|
python3 - "$PROFILE_CFG" "$INHERIT_MCP" <<'PY'
|
|
import sys, yaml
|
|
cfg, val = sys.argv[1], sys.argv[2]
|
|
b = {"true": True, "false": False}.get(val.lower(), val)
|
|
d = yaml.safe_load(open(cfg).read()) or {}
|
|
d.setdefault('agent', {})['inherit_mcp_toolsets'] = b
|
|
open(cfg, 'w').write(yaml.safe_dump(d, sort_keys=False, allow_unicode=True))
|
|
print(f" F3 wrote agent.inherit_mcp_toolsets: {b}")
|
|
PY
|
|
fi
|
|
else
|
|
echo " WARN: F3 yq not on PATH — skipping inherit_mcp_toolsets"
|
|
fi
|
|
|
|
# F4 — install subrepo pre-push hook (disclosure-drift gate)
|
|
HOOK_DST="$REPO/.git/hooks/pre-push"
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "DRY: F4 install pre-push hook → $HOOK_DST"
|
|
elif [ ! -d "$REPO/.git" ]; then
|
|
echo " WARN: F4 $REPO/.git missing — not a git checkout, skip"
|
|
else
|
|
cat > "$HOOK_DST" <<'HOOK_EOF'
|
|
#!/usr/bin/env bash
|
|
# pre-push.sh — Wave 7 D6 subrepo disclosure-drift gate
|
|
# Schema v2 ref: ../sot/04-STANDARDS/DISCLOSURE-SCHEMA.md
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
PROFILE_DIR_NAME="$(basename "$REPO_ROOT")" # cmo | ceo | cto | steev | curator
|
|
PROFILE_NAME="${PROFILE_DIR_NAME}-planb" # default: org-scoped C-suite naming
|
|
# Personal-scope profiles drop the -planb suffix (FRAMEWORK §6.1).
|
|
[ "$PROFILE_DIR_NAME" = "steev" ] && PROFILE_NAME="steev"
|
|
VIOLATIONS=0
|
|
emit() { echo "[pre-push:$PROFILE_NAME] $*" >&2; }
|
|
fail() { emit "BLOCK: $*"; VIOLATIONS=$((VIOLATIONS + 1)); }
|
|
|
|
while read -r local_ref local_sha remote_ref remote_sha; do
|
|
[ "$local_sha" = "0000000000000000000000000000000000000000" ] && continue
|
|
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
base=$(git merge-base "$local_sha" jp 2>/dev/null || git rev-parse "$local_sha^" 2>/dev/null || echo "$local_sha")
|
|
else
|
|
base="$remote_sha"
|
|
fi
|
|
DELTA=$(git diff --name-only "$base..$local_sha" 2>/dev/null || true)
|
|
[ -z "$DELTA" ] && continue
|
|
|
|
# check 2: manifest governance block
|
|
if echo "$DELTA" | grep -q '^manifest\.yaml$'; then
|
|
emit "check 2: validating governance block…"
|
|
python3 - "$REPO_ROOT/manifest.yaml" <<'PY' || fail "manifest.yaml: governance block invalid"
|
|
import sys, yaml
|
|
data = yaml.safe_load(open(sys.argv[1]).read()) or {}
|
|
gov = data.get("governance")
|
|
if not isinstance(gov, dict):
|
|
print(f"missing governance: block", file=sys.stderr); sys.exit(1)
|
|
req = ["org", "owner", "approval_authority", "vision_refs", "governing_protocols", "standards", "north_star"]
|
|
missing = [k for k in req if k not in gov]
|
|
if missing:
|
|
print(f"governance missing: {', '.join(missing)}", file=sys.stderr); sys.exit(1)
|
|
PY
|
|
fi
|
|
|
|
# check 3: identity doc frontmatter
|
|
CHANGED_IDENTITY=$(echo "$DELTA" | grep -E '^(AGENT|CONTRACT|DISCLOSURE)\.md$' || true)
|
|
if [ -n "$CHANGED_IDENTITY" ]; then
|
|
emit "check 3: validating frontmatter…"
|
|
for f in $CHANGED_IDENTITY; do
|
|
python3 - "$REPO_ROOT/$f" <<'PY' || fail "$f: frontmatter invalid"
|
|
import sys, re, yaml
|
|
content = open(sys.argv[1]).read()
|
|
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
|
if not m: print(f"missing YAML frontmatter", file=sys.stderr); sys.exit(1)
|
|
fm = yaml.safe_load(m.group(1)) or {}
|
|
req = {"name", "tier", "status", "owner", "source", "last_reviewed", "description"}
|
|
missing = req - set(fm.keys())
|
|
if missing: print(f"frontmatter missing: {', '.join(missing)}", file=sys.stderr); sys.exit(1)
|
|
PY
|
|
done
|
|
fi
|
|
|
|
# check 6: disclosure drift (only if manifest changed)
|
|
if echo "$DELTA" | grep -q '^manifest\.yaml$'; then
|
|
emit "check 6: disclosure drift…"
|
|
# 6.a skills drift
|
|
if command -v hermes >/dev/null 2>&1; then
|
|
declared=$(yq -r '.disclosure.skills[].id' "$REPO_ROOT/manifest.yaml" 2>/dev/null | sort -u)
|
|
live=$(COLUMNS=200 hermes -p "$PROFILE_NAME" skills list 2>/dev/null | awk -F'│' 'NF>=6 && $(NF-1) ~ /enabled[[:space:]]*$/ {name=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", name); gsub(/…$/, "", name); if (name ~ /^[a-z]/) print name}' | sort -u || echo "")
|
|
if [ -n "$live" ]; then
|
|
drift=$(diff <(echo "$declared") <(echo "$live") 2>/dev/null || true)
|
|
[ -n "$drift" ] && fail "skills drift: $drift"
|
|
else
|
|
emit " (skip 6.a — hermes CLI live-skill query returned empty; WARN)"
|
|
fi
|
|
else
|
|
emit " (skip 6.a — hermes CLI not on PATH; WARN)"
|
|
fi
|
|
# 6.b/6.c/6.d/6.e simplified: just confirm manifest yq parses + sovereign_only invariant
|
|
if [ "$(yq '.disclosure.sovereign_only' "$REPO_ROOT/manifest.yaml")" = "true" ]; then
|
|
bad=$(yq -r '.disclosure.skills[] | select(.hosted_api != null) | .id' "$REPO_ROOT/manifest.yaml" 2>/dev/null)
|
|
[ -n "$bad" ] && fail "sovereign_only=true but skills declare hosted_api: $bad"
|
|
fi
|
|
fi
|
|
|
|
# bypass marker categorization
|
|
for sha in $(git rev-list "$base..$local_sha"); do
|
|
MSG=$(git log -1 --format=%B "$sha")
|
|
bypass_line=$(echo "$MSG" | grep -E '^enforcement-bypass:' || true)
|
|
if [ -n "$bypass_line" ]; then
|
|
if ! echo "$bypass_line" | grep -qE '^enforcement-bypass: (emergency|upstream-blocker|schema-migration|hermes-bug|third-party-bug) — '; then
|
|
fail "$sha: bypass marker uncategorized — required: 'enforcement-bypass: <CATEGORY> — <line>'"
|
|
fi
|
|
fi
|
|
done
|
|
done
|
|
|
|
if [ "$VIOLATIONS" -gt 0 ]; then
|
|
emit "✗ $VIOLATIONS violation(s) — push blocked"
|
|
exit 1
|
|
fi
|
|
emit "✓ subrepo pre-push gate passed"
|
|
exit 0
|
|
HOOK_EOF
|
|
chmod +x "$HOOK_DST"
|
|
echo " F4 installed: $HOOK_DST"
|
|
fi
|
|
|
|
# F6 — MCP server materialization (Wave 8 Q9)
|
|
# Reads manifest.optional_tools (mcp_<server-name-with-underscores> aliases),
|
|
# maps to runtime MCP server names (hyphenated), copies global config block
|
|
# into per-profile config.yaml. Removes non-declared MCPs (closes bte leak).
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "DRY: F6 materialize MCP allowlist → $PROFILE_CFG"
|
|
elif command -v yq >/dev/null 2>&1 && [ -f "$HERMES_HOME/config.yaml" ]; then
|
|
# Declared MCP set (mcp_proton_calendar → proton-calendar etc).
|
|
DECLARED_MCPS=$(yq -r '.optional_tools[]?' "$REPO/manifest.yaml" 2>/dev/null | sed 's/^mcp_//; s/_/-/g')
|
|
if [ -z "$DECLARED_MCPS" ]; then
|
|
echo " F6: no optional_tools declared — skip"
|
|
else
|
|
mkdir -p "$(dirname "$PROFILE_CFG")"
|
|
[ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG"
|
|
F6_ADDED=0; F6_REMOVED=0; F6_MISSING=0
|
|
# Set the per-profile mcp_servers block from the declared list. Existing
|
|
# entries NOT in declared list are dropped (denylist enforcement).
|
|
GLOBAL_CFG="$HERMES_HOME/config.yaml"
|
|
python3 - "$GLOBAL_CFG" "$PROFILE_CFG" "$DECLARED_MCPS" <<'PY'
|
|
import sys, yaml
|
|
gcfg, pcfg, declared_str = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
declared = [s.strip() for s in declared_str.splitlines() if s.strip()]
|
|
g = yaml.safe_load(open(gcfg).read()) or {}
|
|
p = yaml.safe_load(open(pcfg).read()) or {}
|
|
g_mcps = g.get('mcp_servers', {}) or {}
|
|
new_block = {}
|
|
missing = []
|
|
for name in declared:
|
|
if name in g_mcps:
|
|
new_block[name] = g_mcps[name]
|
|
else:
|
|
missing.append(name)
|
|
prev = set((p.get('mcp_servers') or {}).keys())
|
|
new = set(new_block.keys())
|
|
added = sorted(new - prev)
|
|
removed = sorted(prev - new)
|
|
p['mcp_servers'] = new_block
|
|
open(pcfg, 'w').write(yaml.safe_dump(p, sort_keys=False, allow_unicode=True))
|
|
for n in added: print(f" F6 + {n}")
|
|
for n in removed: print(f" F6 - {n} (denied)")
|
|
for n in missing: print(f" F6 ⚠ {n} (declared but not in global mcp_servers — skipped)")
|
|
print(f" F6 wrote mcp_servers: {len(new_block)} entr{'y' if len(new_block)==1 else 'ies'}")
|
|
PY
|
|
fi
|
|
else
|
|
echo " WARN: F6 yq/global config missing — skipping MCP materialization"
|
|
fi
|
|
|
|
# F7 — macOS-only externals OS-gate (Wave 8 Q10)
|
|
# Reads expected_external_skills entries with os_constraint: darwin and emits
|
|
# an INFO line on non-Darwin hosts. No install action (these are external
|
|
# prereqs, not provisioned by this installer); annotation is the audit record.
|
|
HOST_OS="$(uname -s 2>/dev/null || echo Unknown)"
|
|
if [ "$DRY" = 1 ]; then
|
|
echo "DRY: F7 OS-gate check (host=$HOST_OS)"
|
|
elif command -v yq >/dev/null 2>&1; then
|
|
MACOS_ONLY=$(yq -r '.expected_external_skills[] | select(type == "!!map") | select(.os_constraint == "darwin") | .name' "$REPO/manifest.yaml" 2>/dev/null || true)
|
|
if [ -n "$MACOS_ONLY" ] && [ "$HOST_OS" != "Darwin" ]; then
|
|
echo " F7 INFO: macOS-only externals declared but host=$HOST_OS — unavailable:"
|
|
while IFS= read -r s; do [ -n "$s" ] && echo " - $s"; done <<< "$MACOS_ONLY"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "== done =="
|
|
echo " verify skills: hermes -p steev skills list | grep steev-agent"
|
|
echo " verify mcp servers: hermes -p steev mcp list"
|
|
echo " verify assignee registered: hermes kanban assignees | grep steev"
|
|
echo " start gateway (when ready): hermes profile gateway start steev"
|