diff --git a/install.sh b/install.sh index 547c897..b023fe3 100755 --- a/install.sh +++ b/install.sh @@ -81,6 +81,251 @@ fi echo "== ensure cto-worker.sh executable ==" chmod +x "$REPO/lib/cto-worker.sh" 2>/dev/null && echo " ✓ lib/cto-worker.sh executable" +echo "" +# ---------------------------------------------------------------------------- +# Wave 7 D6+D4 — disclosure → runtime config wiring (F1-F5) +# Materializes manifest.disclosure (schema v2) into the live Hermes runtime: +# F1 resolve $HERMES_WORKSPACE in inherit_dirs → skills.external_dirs +# F2 compute denylist from disclosure.skills → skills.disabled +# F3 propagate inherit_mcp_toolsets → agent.inherit_mcp_toolsets +# F4 install subrepo pre-push disclosure-drift gate +# F5 (D4) write sovereign vllm model block → model.{default,provider,base_url,…} +# Per-profile config lives at ~/.hermes/profiles/$PROFILE_NAME/config.yaml. +# cto inherit_dirs is empty by design (CONTRACT.md §1, §9) — F1 stays for template +# consistency w/ cmo/ceo workers. Per CONTRACT.md §5: cto-agent itself runs sovereign +# qwen3.6; claudeCode hosted lives only inside sandcastle isolation boundary. +# All steps idempotent + graceful (WARN + skip on missing tooling). +# ---------------------------------------------------------------------------- +echo "== disclosure → runtime config (Wave 7 D6+D4) ==" +HERMES_WORKSPACE="${HERMES_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + +# F1 — resolve $HERMES_WORKSPACE in disclosure.inherit_dirs → skills.external_dirs (global config) +GLOBAL_CFG="$HERMES_HOME/config.yaml" +if [ "$DRY_RUN" -eq 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 (cto is correct-by-design)" + 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_RUN" -eq 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. + 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=$(hermes skills list 2>/dev/null | awk -F'│' 'NR>3 && /builtin/ {gsub(/^ +| +$/, "", $2); print $2}' || 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" </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 + +# F5 (D4) — sovereign vllm model block → per-profile config.yaml +# Per CONTRACT.md §5: cto-agent runs sovereign qwen3.6 (this block). +# claudeCode hosted is constrained INSIDE sandcastle isolation only. +# Matches ceo-planb/curator-planb pattern at http://100.90.54.40:8000/v1. +if [ "$DRY_RUN" -eq 1 ]; then + echo "DRY: F5 write sovereign vllm model block → $PROFILE_CFG" +elif command -v yq >/dev/null 2>&1; then + mkdir -p "$(dirname "$PROFILE_CFG")" + [ -f "$PROFILE_CFG" ] || : > "$PROFILE_CFG" + MODEL_BLOCK=$(cat <<'YAML' +model: + default: qwen3.6-35b-a3b + provider: vllm + api_key: dummy + base_url: http://100.90.54.40:8000/v1 + context_length: 262144 +YAML +) + # yq eval-all merge: existing config wins on non-model keys; model block overwrites. + TMP_CFG=$(mktemp) + echo "$MODEL_BLOCK" | yq eval-all '. as $item ireduce ({}; . * $item)' "$PROFILE_CFG" - > "$TMP_CFG" + mv "$TMP_CFG" "$PROFILE_CFG" + echo " F5 wrote model block (qwen3.6-35b-a3b @ http://100.90.54.40:8000/v1)" +else + echo " WARN: F5 yq not on PATH — skipping model block (cto-agent will fall back to global default)" +fi + +# F4 — install subrepo pre-push hook (disclosure-drift gate) +HOOK_DST="$REPO/.git/hooks/pre-push" +if [ "$DRY_RUN" -eq 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")" # e.g., cmo, ceo, cto +PROFILE_NAME="${PROFILE_DIR_NAME}-planb" +VIOLATIONS=0 +emit() { echo "[pre-push:$PROFILE_DIR_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=$(hermes -p "$PROFILE_NAME" skills list 2>/dev/null | awk 'NR>3 && /enabled|│ *enabled/ {for (i=1; i<=NF; i++) if ($i != "│" && $i != "enabled") {print $i; break}}' | 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: '" + 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 + echo "" echo "== done. canonical install: hermes profile install $REPO ==" echo " verify: hermes -p $PROFILE_NAME skills list | grep cto-agent"