AMD1213-C: repair stale array consumer, fail closed on foreign link provenance, validate manifests before mutation, and exercise the fleet MCP preflight call path.
287 lines
7.3 KiB
Bash
Executable File
287 lines
7.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
|
MODE="apply"
|
|
RUNTIME="all"
|
|
STRICT_CHECK=0
|
|
CLAUDE_CONFIG_DIR=""
|
|
|
|
PKG="@modelcontextprotocol/server-sequential-thinking"
|
|
|
|
err() { echo "[mosaic-seq] ERROR: $*" >&2; }
|
|
log() { echo "[mosaic-seq] $*"; }
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--check)
|
|
MODE="check"
|
|
shift
|
|
;;
|
|
--runtime)
|
|
if [[ $# -lt 2 ]]; then
|
|
err "--runtime requires a value: claude|codex|opencode|all"
|
|
exit 2
|
|
fi
|
|
RUNTIME="$2"
|
|
shift 2
|
|
;;
|
|
--strict)
|
|
STRICT_CHECK=1
|
|
shift
|
|
;;
|
|
--claude-config-dir)
|
|
if [[ $# -lt 2 ]]; then
|
|
err "--claude-config-dir requires an absolute seat config directory"
|
|
exit 2
|
|
fi
|
|
CLAUDE_CONFIG_DIR="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
err "Unknown argument: $1"
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
case "$RUNTIME" in
|
|
all|claude|codex|opencode) ;;
|
|
*)
|
|
err "Invalid runtime: $RUNTIME (expected claude|codex|opencode|all)"
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
require_binary() {
|
|
local name="$1"
|
|
if ! command -v "$name" >/dev/null 2>&1; then
|
|
err "Required binary missing: $name"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
check_software() {
|
|
require_binary node
|
|
require_binary npx
|
|
}
|
|
|
|
warm_package() {
|
|
local timeout_sec="${MOSAIC_SEQ_WARM_TIMEOUT_SEC:-15}"
|
|
if command -v timeout >/dev/null 2>&1; then
|
|
timeout "$timeout_sec" npx -y "$PKG" --help >/dev/null 2>&1
|
|
else
|
|
npx -y "$PKG" --help >/dev/null 2>&1
|
|
fi
|
|
}
|
|
|
|
check_claude_config() {
|
|
CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
# Claude reads MCP definitions from .claude.json, not settings.json. The
|
|
# settings.json fallback preserves legacy operator flows until their config is migrated.
|
|
config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
p = Path(config_dir) / ".claude.json" if config_dir else Path.home() / ".claude.json"
|
|
if not p.exists() and not config_dir:
|
|
p = Path.home() / ".claude" / "settings.json"
|
|
# Only explicit fleet seats require a private, non-symlink config. Operator
|
|
# config remains compatible with pre-existing permission conventions.
|
|
if not p.exists() or p.is_symlink() or (config_dir and (p.stat().st_mode & 0o077) != 0):
|
|
raise SystemExit(1)
|
|
try:
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
raise SystemExit(1)
|
|
mcp = data.get("mcpServers")
|
|
if not isinstance(mcp, dict):
|
|
raise SystemExit(1)
|
|
entry = mcp.get("sequential-thinking")
|
|
if not isinstance(entry, dict):
|
|
raise SystemExit(1)
|
|
if entry.get("command") != "npx":
|
|
raise SystemExit(1)
|
|
args = entry.get("args")
|
|
if args != ["-y", "@modelcontextprotocol/server-sequential-thinking"]:
|
|
raise SystemExit(1)
|
|
PY
|
|
}
|
|
|
|
apply_claude_config() {
|
|
CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
# Claude reads MCP definitions from .claude.json for both operator and
|
|
# explicitly isolated fleet config dirs. The checker retains a settings.json
|
|
# fallback only to avoid breaking legacy operator configurations.
|
|
config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
p = Path(config_dir) / ".claude.json" if config_dir else Path.home() / ".claude.json"
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
if p.exists():
|
|
try:
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
data = {}
|
|
else:
|
|
data = {}
|
|
mcp = data.get("mcpServers")
|
|
if not isinstance(mcp, dict):
|
|
mcp = {}
|
|
mcp["sequential-thinking"] = {
|
|
"command": "npx",
|
|
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
|
}
|
|
data["mcpServers"] = mcp
|
|
p.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
PY
|
|
}
|
|
|
|
check_codex_config() {
|
|
local cfg="${CODEX_HOME:-$HOME/.codex}/config.toml"
|
|
[[ -f "$cfg" ]] || return 1
|
|
grep -Eq '^\[mcp_servers\.(sequential-thinking|sequential_thinking)\]' "$cfg" && \
|
|
grep -q '^command = "npx"' "$cfg" && \
|
|
grep -q '@modelcontextprotocol/server-sequential-thinking' "$cfg"
|
|
}
|
|
|
|
apply_codex_config() {
|
|
local cfg="${CODEX_HOME:-$HOME/.codex}/config.toml"
|
|
mkdir -p "$(dirname "$cfg")"
|
|
[[ -f "$cfg" ]] || touch "$cfg"
|
|
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
awk '
|
|
BEGIN { skip = 0 }
|
|
/^\[mcp_servers\.(sequential-thinking|sequential_thinking)\]/ { skip = 1; next }
|
|
skip && /^\[/ { skip = 0 }
|
|
!skip { print }
|
|
' "$cfg" > "$tmp"
|
|
mv "$tmp" "$cfg"
|
|
|
|
{
|
|
echo ""
|
|
echo "[mcp_servers.sequential-thinking]"
|
|
echo "command = \"npx\""
|
|
echo "args = [\"-y\", \"@modelcontextprotocol/server-sequential-thinking\"]"
|
|
} >> "$cfg"
|
|
}
|
|
|
|
check_opencode_config() {
|
|
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-}" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
p = Path(os.environ["XDG_CONFIG_HOME"]) / "opencode" / "config.json" if os.environ.get("XDG_CONFIG_HOME") else Path.home() / ".config" / "opencode" / "config.json"
|
|
if not p.exists():
|
|
raise SystemExit(1)
|
|
try:
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
raise SystemExit(1)
|
|
mcp = data.get("mcp")
|
|
if not isinstance(mcp, dict):
|
|
raise SystemExit(1)
|
|
entry = mcp.get("sequential-thinking")
|
|
if not isinstance(entry, dict):
|
|
raise SystemExit(1)
|
|
if entry.get("type") != "local":
|
|
raise SystemExit(1)
|
|
if entry.get("command") != ["npx", "-y", "@modelcontextprotocol/server-sequential-thinking"]:
|
|
raise SystemExit(1)
|
|
if entry.get("enabled") is not True:
|
|
raise SystemExit(1)
|
|
PY
|
|
}
|
|
|
|
apply_opencode_config() {
|
|
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-}" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
p = Path(os.environ["XDG_CONFIG_HOME"]) / "opencode" / "config.json" if os.environ.get("XDG_CONFIG_HOME") else Path.home() / ".config" / "opencode" / "config.json"
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
if p.exists():
|
|
try:
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
data = {}
|
|
else:
|
|
data = {}
|
|
mcp = data.get("mcp")
|
|
if not isinstance(mcp, dict):
|
|
mcp = {}
|
|
mcp["sequential-thinking"] = {
|
|
"type": "local",
|
|
"command": ["npx", "-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
"enabled": True
|
|
}
|
|
data["mcp"] = mcp
|
|
p.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
PY
|
|
}
|
|
|
|
check_runtime_config() {
|
|
case "$RUNTIME" in
|
|
all)
|
|
check_claude_config
|
|
check_codex_config
|
|
check_opencode_config
|
|
;;
|
|
claude)
|
|
check_claude_config
|
|
;;
|
|
codex)
|
|
check_codex_config
|
|
;;
|
|
opencode)
|
|
check_opencode_config
|
|
;;
|
|
esac
|
|
}
|
|
|
|
apply_runtime_config() {
|
|
case "$RUNTIME" in
|
|
all)
|
|
apply_claude_config
|
|
apply_codex_config
|
|
apply_opencode_config
|
|
;;
|
|
claude)
|
|
apply_claude_config
|
|
;;
|
|
codex)
|
|
apply_codex_config
|
|
;;
|
|
opencode)
|
|
apply_opencode_config
|
|
;;
|
|
esac
|
|
}
|
|
|
|
if [[ "$MODE" == "check" ]]; then
|
|
check_software
|
|
check_runtime_config
|
|
|
|
# Runtime launch checks should be local/fast by default.
|
|
if [[ "$STRICT_CHECK" -eq 1 || "${MOSAIC_SEQ_CHECK_WARM:-0}" == "1" ]]; then
|
|
if ! warm_package; then
|
|
err "sequential-thinking package warm-up failed in strict mode"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
log "sequential-thinking MCP is configured and available (${RUNTIME})"
|
|
exit 0
|
|
fi
|
|
|
|
check_software
|
|
if ! warm_package; then
|
|
err "Unable to warm sequential-thinking package (npx timeout/failure)"
|
|
exit 1
|
|
fi
|
|
apply_runtime_config
|
|
log "sequential-thinking MCP configured (${RUNTIME})"
|