feat: add maintained Skill and MCP integration [skip ci]

This commit is contained in:
spiritlhl 2026-10-26 12:06:37 +08:00
parent 977fab66e7
commit 9636fd5dfd
6 changed files with 761 additions and 0 deletions

11
agent/manifest.json Normal file
View File

@ -0,0 +1,11 @@
{
"repository": "oneclickvirt/backtrace",
"skill_name": "oneclickvirt-backtrace",
"mcp": true,
"files": {
"agent/profile.json": "c0051a351c446d4135642cda3af37d302bc83b9c74ddfafc1dc68545e41249d6",
"skills/oneclickvirt-backtrace/SKILL.md": "6627224a6ddd3637291d3cb03793ae29fe798561d38462674baa075940738213",
"skills/oneclickvirt-backtrace/SKILL_ZH.md": "88e3e6d9561f17c53e1b7af0d601c9c7878341a8c4f54256b56da92087c15472",
"agent/server.py": "d01416e6194095d90bf0d49ea29f7122969f0b74cf4d74ed0b1ce78753423a7d"
}
}

39
agent/profile.json Normal file
View File

@ -0,0 +1,39 @@
{
"repository": "oneclickvirt/backtrace",
"branch": "main",
"skill_name": "oneclickvirt-backtrace",
"purpose": "Query RDAP/BGP metadata for an explicit IP; distinguish lookup data from an actual traceroute.",
"notes": [
"Read probe_help from the installed binary before assuming flags are supported. MCP returns native JSON when the program provides JSON, otherwise native text.",
"Keep partial/network errors distinct from confirmed test outcomes. Current release assets and their actual architecture are available through release_assets.",
"The ip-info preset requires a literal target IP. Do not pass a host, proxy, shell expression or a network range."
],
"guide": "https://www.spiritlhl.net/guide/agent_integrations.html",
"releases": true,
"mcp": true,
"binary": "backtrace",
"asset_prefix": "backtrace",
"presets": {
"ip-info": {
"args": [
"-json",
"-timeout",
"12s"
],
"target": "-ip",
"timeout": 20
}
},
"benchmark": false,
"source_files": [
"backtrace_install.sh",
"cmd/main.go",
"go.mod"
],
"purpose_zh": "查询指定 IP 的 RDAP/BGP 信息,区分数据库查询与真实路由探测。",
"notes_zh": [
"先用 probe_help 核对已安装程序的实际帮助。程序支持 JSON 时返回原生 JSON,否则返回原生文本。",
"区分部分结果、网络失败和确定结论;通过 release_assets 查询当前真实产物及架构。",
"ip-info 预设只接受单个真实 IP,不接受域名、代理、Shell 表达式或网段。"
]
}

349
agent/server.py Normal file
View File

@ -0,0 +1,349 @@
#!/usr/bin/env python3
"""OneClickVirt repository MCP adapter. Python 3.9+, stdio, no extra packages."""
import argparse
import ipaddress
import json
import os
from pathlib import Path
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
VERSION = "1.0.0"
PROTOCOLS = ("2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25")
LIMIT = 1024 * 1024
def schema(properties=None, required=()):
return {"type": "object", "properties": properties or {},
"required": list(required), "additionalProperties": False}
def string(description="", **extra):
return dict(type="string", description=description, **extra)
def validate(value, spec, name="arguments"):
expected = spec.get("type")
if expected == "object":
if not isinstance(value, dict):
raise ValueError(name + " must be an object")
props = spec.get("properties", {})
if set(spec.get("required", [])) - value.keys():
raise ValueError(name + " has missing required fields")
if not spec.get("additionalProperties", True) and value.keys() - props.keys():
raise ValueError(name + " has unknown fields")
for key, data in value.items():
if key in props:
validate(data, props[key], key)
elif expected == "string":
maximum = min(512, spec.get("maxLength", 512))
if not isinstance(value, str) or len(value) > maximum:
raise ValueError(name + " must be a string of at most " + str(maximum) + " characters")
if "pattern" in spec and not re.fullmatch(spec["pattern"], value):
raise ValueError(name + " has invalid characters")
elif expected == "integer":
if isinstance(value, bool) or not isinstance(value, int):
raise ValueError(name + " must be an integer")
if value < spec.get("minimum", value) or value > spec.get("maximum", value):
raise ValueError(name + " is outside the supported range")
if "enum" in spec and value not in spec["enum"]:
raise ValueError(name + " is not a supported choice")
def execute(argv, timeout=30, cwd=None, parse_json=True):
"""Bound output, kill process groups on timeout, never invoke a shell."""
if not argv or not shutil.which(argv[0]):
raise ValueError("Required local command is missing: " + (argv[0] if argv else ""))
with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err:
proc = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=out, stderr=err,
start_new_session=os.name != "nt", cwd=cwd)
try:
proc.wait(timeout=timeout)
except subprocess.TimeoutExpired:
if os.name != "nt":
os.killpg(proc.pid, signal.SIGKILL)
else:
subprocess.run(["taskkill", "/PID", str(proc.pid), "/T", "/F"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
if proc.poll() is None:
proc.kill()
proc.wait()
raise ValueError("Command exceeded its time limit; no result is claimed")
out.seek(0)
err.seek(0)
stdout, stderr = out.read(LIMIT + 1), err.read(16385)
if proc.returncode:
raise ValueError("Command failed (exit %s): %s" % (
proc.returncode, stderr[:16384].decode("utf-8", "replace")))
if len(stdout) > LIMIT:
raise ValueError("Command result exceeds 1 MiB; narrow the request")
text = stdout.decode("utf-8", "replace").strip()
if not parse_json:
return text
try:
return json.loads(text)
except ValueError:
return text
def github_json(path):
headers = {"Accept": "application/vnd.github+json", "User-Agent": "oneclickvirt-mcp/" + VERSION}
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
if token:
headers["Authorization"] = "Bearer " + token
class SafeRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, response_headers, newurl):
redirected = super().redirect_request(req, fp, code, msg, response_headers, newurl)
if redirected is not None and urllib.parse.urlsplit(newurl).netloc != "api.github.com":
redirected.remove_header("Authorization")
return redirected
req = urllib.request.Request("https://api.github.com" + path, headers=headers)
try:
with urllib.request.build_opener(SafeRedirect()).open(req, timeout=25) as response:
data = response.read(8 * LIMIT + 1)
except urllib.error.HTTPError as exc:
raise ValueError("GitHub API returned %s; no release or asset is assumed" % exc.code) from None
if len(data) > 8 * LIMIT:
raise ValueError("GitHub response is too large")
return json.loads(data)
class Adapter:
def __init__(self, profile_path):
self.path = Path(profile_path).resolve()
self.profile = json.loads(self.path.read_text(encoding="utf-8"))
self.repo = self.profile["repository"]
if not re.fullmatch(r"oneclickvirt/[A-Za-z0-9_.-]+", self.repo):
raise ValueError("Invalid repository profile")
def binary(self):
return os.environ.get("ONECLICKVIRT_MCP_BINARY") or self.profile.get("binary", "")
def tools(self):
result = []
def add(name, description, spec=None, write=False, external=False):
result.append({"name": name, "description": description, "inputSchema": spec or schema(),
"annotations": {"readOnlyHint": not write, "destructiveHint": write,
"idempotentHint": not write, "openWorldHint": external}})
add("integration_info", "Show repository, supported operations, prerequisites and documentation.")
if self.profile.get("releases"):
add("release_assets", "Resolve a current published release and list its actual assets. No assumed architectures or tags.",
schema({"version": string("Exact release tag, or latest", pattern=r"[A-Za-z0-9_.+/-]{1,128}"),
"filter": string("Optional case-insensitive substring in asset names", maxLength=128)}), external=True)
backend = self.profile.get("runtime")
if backend:
add("list_instances", "List instances on this local runtime using its existing CLI.")
add("instance_status", "Read one instance's state without returning environment variables or cloud-init secrets.",
schema({"id": string(pattern=r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}")}, ["id"]))
if os.environ.get("ONECLICKVIRT_MCP_ALLOW_WRITE") == "1":
add("instance_action", "Start, gracefully stop or restart an explicitly named instance. No delete or reinstall operation.",
schema({"id": string(pattern=r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}"),
"action": string(enum=["start", "stop", "restart"])}, ["id", "action"]), write=True)
if self.profile.get("presets"):
add("probe_help", "Read the installed probe's actual help output.")
add("run_probe", "Run one bounded preset with the existing local binary. Check integration_info for its resource and network effects.",
schema({"preset": string(enum=list(self.profile["presets"])),
"target": string("Only for a target-aware preset", pattern=r"[A-Za-z0-9][A-Za-z0-9.:_-]{0,252}")}, ["preset"]),
write=self.profile.get("benchmark", False), external=True)
if self.profile.get("network"):
add("network_status", "Read Linux IPv6 addresses and routes. Does not change network configuration.")
if self.profile.get("counter"):
add("counter_monthly", "Read an existing counter's monthly statistics without incrementing it.",
schema({"counter": string(pattern=r"[A-Za-z0-9_.-]{1,128}")}, ["counter"]), external=True)
return result
def instances(self):
backend = self.profile["runtime"]
binary = self.binary()
if backend in ("docker", "podman", "containerd"):
raw = execute([binary, "ps", "-a", "--format", "{{json .}}"])
if isinstance(raw, dict):
rows = [raw]
elif not raw:
rows = []
else:
rows = [json.loads(line) for line in raw.splitlines()]
result = []
for row in rows:
item = {key: row.get(key) for key in ("ID", "Names", "Image", "State", "Status")}
if isinstance(item["Names"], list):
item["Names"] = ",".join(item["Names"])
result.append(item)
return result
if backend in ("incus", "lxd"):
rows = execute([binary, "list", "--format=json"])
if not isinstance(rows, list):
raise ValueError("Runtime did not return an instance array")
return [{k: row.get(k) for k in ("name", "status", "type", "location")} for row in rows]
if backend == "pve":
return execute([binary, "get", "/cluster/resources", "--type", "vm", "--output-format", "json"])
if backend == "qemu":
uri = os.environ.get("ONECLICKVIRT_LIBVIRT_URI", "qemu:///system")
if not re.fullmatch(r"qemu:///(system|session)", uri):
raise ValueError("Only local libvirt system/session URIs are supported")
names = execute([binary, "-c", uri, "list", "--all", "--name"], parse_json=False)
return [{"name": name} for name in str(names).splitlines() if name]
if backend == "kubevirt":
namespace = os.environ.get("ONECLICKVIRT_KUBE_NAMESPACE", "default")
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", namespace):
raise ValueError("Invalid Kubernetes namespace")
data = execute([binary, "get", "virtualmachines", "--namespace", namespace, "-o", "json"])
return [{"name": row["metadata"]["name"], "namespace": namespace,
"status": row.get("status", {})} for row in data["items"]]
raise ValueError("Unsupported runtime")
def find_instance(self, identifier):
rows = self.instances()
for row in rows:
names = str(row.get("Names", "")).split(",")
if identifier in names + [str(row.get(k, "")) for k in ("name", "vmid", "ID")]:
return row
raise ValueError("Instance not found on the configured runtime")
def call(self, name, args):
available = {t["name"]: t for t in self.tools()}
if name not in available:
raise ValueError("Unknown or disabled tool")
validate(args, available[name]["inputSchema"])
if name == "integration_info":
return self.profile
if name == "release_assets":
repo = self.profile.get("release_repository", self.repo)
version = args.get("version", "latest")
suffix = "latest" if version == "latest" else "tags/" + urllib.parse.quote(version, safe="")
value = github_json("/repos/" + repo + "/releases/" + suffix)
if value.get("draft") or (version == "latest" and value.get("prerelease")):
raise ValueError("Requested release is not a stable published release")
match = args.get("filter", "").lower()
return {"repository": repo, "version": value["tag_name"], "url": value["html_url"],
"published_at": value["published_at"], "assets": [
{k: a.get(k) for k in ("name", "size", "digest", "browser_download_url")}
for a in value["assets"] if match in a["name"].lower()]}
if name == "list_instances":
return self.instances()
if name == "instance_status":
row = self.find_instance(args["id"])
if self.profile["runtime"] == "qemu":
uri = os.environ.get("ONECLICKVIRT_LIBVIRT_URI", "qemu:///system")
row["state"] = execute([self.binary(), "-c", uri, "domstate", args["id"]])
return row
if name == "instance_action":
row = self.find_instance(args["id"])
backend, action, identifier = self.profile["runtime"], args["action"], args["id"]
binary = self.binary()
if backend in ("docker", "containerd", "podman", "incus", "lxd"):
argv = [binary, action, identifier]
elif backend == "qemu":
argv = [binary, "-c", os.environ.get("ONECLICKVIRT_LIBVIRT_URI", "qemu:///system"),
{"start": "start", "stop": "shutdown", "restart": "reboot"}[action], identifier]
elif backend == "pve":
node = row.get("node", "")
kind = row.get("type", "")
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", node) or kind not in ("qemu", "lxc") or not identifier.isdecimal():
raise ValueError("Invalid PVE resource identity")
endpoint = "/nodes/%s/%s/%s/status/%s" % (node, kind, identifier, {"stop": "shutdown", "restart": "reboot"}.get(action, action))
argv = [binary, "create", endpoint]
else:
namespace = row["namespace"]
argv = ["virtctl", action, identifier, "--namespace", namespace]
return {"id": identifier, "action": action, "output": execute(argv, timeout=90), "accepted": True}
if name == "probe_help":
return execute([self.binary()] + self.profile.get("help_args", ["-h"]), timeout=15)
if name == "run_probe":
preset = self.profile["presets"][args["preset"]]
if self.profile.get("benchmark") and os.environ.get("ONECLICKVIRT_MCP_ALLOW_BENCHMARKS") != "1":
raise ValueError("Benchmark execution is disabled; set ONECLICKVIRT_MCP_ALLOW_BENCHMARKS=1 on an intended test host")
if bool(preset.get("target")) != bool(args.get("target")):
raise ValueError("This preset %s a target" % ("requires" if preset.get("target") else "does not accept"))
argv = [self.binary()] + preset["args"]
if preset.get("target"):
if self.repo == "oneclickvirt/backtrace":
ipaddress.ip_address(args["target"])
argv += [preset["target"], args["target"]]
with tempfile.TemporaryDirectory(prefix="oneclickvirt-probe-") as directory:
argv = [a.replace("{scratch}", directory) for a in argv]
result = execute(argv, timeout=preset.get("timeout", 45), cwd=directory)
return {"preset": args["preset"], "result": result}
if name == "network_status":
return {"addresses": execute(["ip", "-j", "-6", "addr", "show"]),
"routes": execute(["ip", "-j", "-6", "route", "show"])}
if name == "counter_monthly":
base = os.environ.get("ONECLICKVIRT_COUNTER_URL", "https://hits.spiritlhl.net").rstrip("/")
parsed = urllib.parse.urlsplit(base)
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.query or parsed.fragment:
raise ValueError("Counter endpoint must be an HTTPS base URL")
url = base + "/api/monthly?" + urllib.parse.urlencode({"counter": args["counter"]})
request = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "oneclickvirt-mcp/" + VERSION})
with urllib.request.urlopen(request, timeout=20) as response:
content = response.read(LIMIT + 1)
if len(content) > LIMIT:
raise ValueError("Counter response exceeds 1 MiB")
return json.loads(content)
raise ValueError("Unsupported operation")
def serve(adapter, input_stream=sys.stdin, output_stream=sys.stdout):
for line in input_stream:
if len(line) > LIMIT:
error = {"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Message exceeds 1 MiB"}}
output_stream.write(json.dumps(error) + "\n")
output_stream.flush()
continue
try:
message = json.loads(line)
except ValueError:
output_stream.write(json.dumps({"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}}) + "\n")
output_stream.flush()
continue
if not isinstance(message, dict) or message.get("jsonrpc") != "2.0" or not isinstance(message.get("method"), str):
output_stream.write(json.dumps({"jsonrpc": "2.0", "id": message.get("id") if isinstance(message, dict) else None,
"error": {"code": -32600, "message": "Invalid request"}}) + "\n")
output_stream.flush()
continue
if "id" not in message:
continue
reply = {"jsonrpc": "2.0", "id": message["id"]}
method = message["method"]
params = message.get("params", {})
try:
if not isinstance(params, dict):
raise ValueError("params must be an object")
if method == "initialize":
requested = params.get("protocolVersion")
reply["result"] = {"protocolVersion": requested if requested in PROTOCOLS else PROTOCOLS[-1],
"capabilities": {"tools": {}},
"serverInfo": {"name": adapter.profile["skill_name"], "version": VERSION}}
elif method == "ping":
reply["result"] = {}
elif method == "tools/list":
reply["result"] = {"tools": adapter.tools()}
elif method == "tools/call":
try:
value = adapter.call(params.get("name"), params.get("arguments", {}))
reply["result"] = {"content": [{"type": "text", "text": json.dumps(value, ensure_ascii=False)}], "isError": False}
except (ValueError, OSError, KeyError, TypeError, urllib.error.URLError) as exc:
reply["result"] = {"content": [{"type": "text", "text": str(exc)}], "isError": True}
else:
reply["error"] = {"code": -32601, "message": "Method not found"}
except (ValueError, TypeError) as exc:
reply["error"] = {"code": -32602, "message": str(exc)}
output_stream.write(json.dumps(reply, ensure_ascii=False) + "\n")
output_stream.flush()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--profile", type=Path, default=Path(__file__).with_name("profile.json"))
parser.add_argument("--version", action="version", version=VERSION)
args = parser.parse_args()
serve(Adapter(args.profile))
if __name__ == "__main__":
main()

250
scripts/install_agent.py Normal file
View File

@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""Install/update this repository's Skill and optional Python stdio MCP adapter."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import re
import subprocess
import sys
import tempfile
import io
import zipfile
import urllib.parse
import urllib.request
REPOSITORY = "oneclickvirt/backtrace"
DEFAULT_REF = "main"
SKILL_NAME = "oneclickvirt-backtrace"
MAX_FILE = 2 * 1024 * 1024
class SafeRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
redirected = super().redirect_request(req, fp, code, msg, headers, newurl)
if redirected is not None and urllib.parse.urlsplit(req.full_url).netloc != urllib.parse.urlsplit(newurl).netloc:
redirected.remove_header("Authorization")
return redirected
def fetch(url, limit=MAX_FILE):
headers = {"User-Agent": "oneclickvirt-agent-installer/1.0"}
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
if token and urllib.parse.urlsplit(url).netloc == "api.github.com":
headers["Authorization"] = "Bearer " + token
req = urllib.request.Request(url, headers=headers)
with urllib.request.build_opener(SafeRedirect()).open(req, timeout=40) as response:
data = response.read(limit + 1)
if len(data) > limit:
raise ValueError("Integration file exceeds the size limit")
return data
def release_binary(profile, version):
if not profile.get("asset_prefix"):
raise ValueError("This integration does not distribute a standalone binary")
if not re.fullmatch(r"[A-Za-z0-9_.+/-]{1,128}", version):
raise ValueError("Invalid release tag")
repo = profile.get("release_repository", REPOSITORY)
suffix = "latest" if version == "latest" else "tags/" + urllib.parse.quote(version, safe="")
release = json.loads(fetch("https://api.github.com/repos/" + repo + "/releases/" + suffix))
if release.get("draft") or (version == "latest" and release.get("prerelease")):
raise ValueError("Release is not a stable published release")
system = platform.system().lower()
machine = platform.machine().lower()
arch = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64",
"i386": "386", "i686": "386", "armv7l": "arm"}.get(machine, machine)
separator = profile.get("asset_separator", "-")
stem = separator.join([profile["asset_prefix"], system, arch])
candidates = [a for a in release["assets"] if a["name"] in (stem, stem + ".exe", stem + ".zip")]
if len(candidates) != 1:
raise ValueError("Release has no unique binary for " + system + "/" + arch + "; use --binary with an existing build")
asset = candidates[0]
url = asset["browser_download_url"]
parsed = urllib.parse.urlsplit(url)
expected_prefix = "https://github.com/" + repo + "/releases/download/"
if not url.startswith(expected_prefix) or parsed.scheme != "https":
raise ValueError("Unexpected binary download URL")
content = fetch(url, limit=256 * 1024 * 1024)
digest = asset.get("digest") or ""
checksum = hashlib.sha256(content).hexdigest()
if digest and digest != "sha256:" + checksum:
raise ValueError("Published binary digest does not match the download")
if not digest:
checksum_assets = [a for a in release["assets"] if a["name"] in ("checksums.txt", "SHA256SUMS")]
verified = False
for entry in checksum_assets:
checksum_url = entry["browser_download_url"]
if not checksum_url.startswith(expected_prefix):
raise ValueError("Unexpected checksum download URL")
for line in fetch(checksum_url).decode().splitlines():
match = re.fullmatch(r"([a-fA-F0-9]{64})\s+\*?(.+)", line)
if match and match[2] == asset["name"] and match[1].lower() == checksum:
verified = True
if not verified:
raise ValueError("Binary has no matching SHA-256 digest/checksum; existing installations were not changed")
if asset["name"].endswith(".zip"):
with zipfile.ZipFile(io.BytesIO(content)) as archive:
entries = [f for f in archive.infolist() if Path(f.filename).name in (profile["binary"], profile["binary"] + ".exe") and not f.is_dir()]
if len(entries) != 1 or entries[0].file_size > 256 * 1024 * 1024:
raise ValueError("Archive does not contain one expected executable")
content = archive.read(entries[0])
return content, {"version": release["tag_name"], "asset": asset["name"], "archive_sha256": checksum,
"binary_sha256": hashlib.sha256(content).hexdigest()}
def resolve_source(source, ref):
if not re.fullmatch(r"[A-Za-z0-9_.+/-]{1,160}", ref) or ".." in ref:
raise ValueError("Invalid source ref")
if source == "github":
info = json.loads(fetch("https://api.github.com/repos/" + REPOSITORY + "/commits/" + urllib.parse.quote(ref, safe="")))
commit = info["sha"]
base = "https://raw.githubusercontent.com/" + REPOSITORY + "/" + commit + "/"
else:
# Public Git refs require no credentials. Pin the CNB snapshot itself,
# whose commit differs from the history-free GitHub source mirror.
result = subprocess.check_output(["git", "-c", "credential.helper=", "ls-remote", "https://cnb.cool/" + REPOSITORY,
ref if re.fullmatch(r"[a-f0-9]{40}", ref) else "refs/heads/" + ref,
"refs/tags/" + ref, "refs/tags/" + ref + "^{}"], text=True, timeout=45)
rows = [line.split() for line in result.splitlines()]
if re.fullmatch(r"[a-f0-9]{40}", ref):
commit = ref
elif rows:
peeled = [row[0] for row in rows if row[1].endswith("^{}")]
commit = (peeled or [row[0] for row in rows])[0]
else:
raise ValueError("CNB source ref does not exist")
base = "https://cnb.cool/" + REPOSITORY + "/-/git/raw/" + commit + "/"
if not re.fullmatch(r"[a-f0-9]{40}", commit):
raise ValueError("Source did not resolve to an immutable commit")
return commit, lambda path: fetch(base + path)
def load_payload(reader):
manifest = json.loads(reader("agent/manifest.json"))
if manifest.get("repository") != REPOSITORY or manifest.get("skill_name") != SKILL_NAME:
raise ValueError("Manifest does not identify the requested integration")
expected = {"agent/profile.json", "skills/" + SKILL_NAME + "/SKILL.md", "skills/" + SKILL_NAME + "/SKILL_ZH.md"}
if manifest.get("mcp"):
expected.add("agent/server.py")
if set(manifest.get("files", {})) != expected:
raise ValueError("Manifest contains missing or unexpected file paths")
data = {}
for path, digest in manifest["files"].items():
content = reader(path)
if not re.fullmatch(r"[0-9a-f]{64}", digest) or hashlib.sha256(content).hexdigest() != digest:
raise ValueError("SHA-256 verification failed for " + path)
data[path] = content
profile = json.loads(data["agent/profile.json"])
if profile.get("repository") != REPOSITORY or profile.get("skill_name") != SKILL_NAME:
raise ValueError("Profile does not match the requested repository")
return manifest, data
def atomic_write(target, content):
target.parent.mkdir(parents=True, exist_ok=True)
if target.is_symlink():
raise ValueError("Refusing to replace a symlink: " + str(target))
handle, temporary = tempfile.mkstemp(prefix=".oneclickvirt-", dir=target.parent)
try:
with os.fdopen(handle, "wb") as stream:
stream.write(content)
os.replace(temporary, target)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def install(manifest, payload, skill_dir, mcp_dir, record, skill_only=False, binary=None, binary_content=None):
skill_dir = Path(skill_dir).expanduser().absolute()
mcp_dir = Path(mcp_dir).expanduser().absolute()
if skill_dir.is_symlink() or mcp_dir.is_symlink():
raise ValueError("The integration destination itself must not be a symlink")
has_mcp = manifest.get("mcp") and not skill_only
writes = {}
if binary_content is not None:
writes[Path(binary)] = binary_content
for filename in ("SKILL.md", "SKILL_ZH.md"):
writes[skill_dir / filename] = payload["skills/" + SKILL_NAME + "/" + filename]
if has_mcp:
writes[mcp_dir / "server.py"] = payload["agent/server.py"]
writes[mcp_dir / "profile.json"] = payload["agent/profile.json"]
env = {}
if binary:
env["ONECLICKVIRT_MCP_BINARY"] = str(Path(binary).expanduser().resolve())
config = {"mcpServers": {SKILL_NAME: {"command": sys.executable,
"args": [str(mcp_dir / "server.py")], "env": env}}}
writes[skill_dir / "mcp.example.json"] = (json.dumps(config, ensure_ascii=False, indent=2) + "\n").encode()
record = dict(record, repository=REPOSITORY, skill_name=SKILL_NAME, mcp_installed=bool(has_mcp),
files=manifest["files"])
writes[skill_dir / "install.json"] = (json.dumps(record, indent=2) + "\n").encode()
# Verify the entire payload and every destination before replacing anything.
for path in writes:
if path.is_symlink():
raise ValueError("Symlink installation file is not supported: " + str(path))
if path.exists() and not path.is_file():
raise ValueError("Destination is not a file: " + str(path))
for path, content in writes.items():
atomic_write(path, content)
if binary_content is not None and path == Path(binary) and os.name != "nt":
path.chmod(0o755)
return {"skill_dir": str(skill_dir), "mcp_dir": str(mcp_dir) if has_mcp else None,
"source_commit": record["commit"], "mcp_installed": bool(has_mcp)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--client", choices=["claude", "codex", "openclaw"], default="claude")
parser.add_argument("--source", choices=["github", "cnb"], default="github")
parser.add_argument("--ref", default=DEFAULT_REF, help="Source branch, tag or immutable commit")
parser.add_argument("--skill-dir", type=Path)
parser.add_argument("--mcp-dir", type=Path, default=Path.home() / ".local/share/oneclickvirt-mcp" / SKILL_NAME)
parser.add_argument("--skill-only", action="store_true")
parser.add_argument("--binary", help="Absolute path to an already installed runtime/probe binary")
parser.add_argument("--install-binary", action="store_true", help="Install the verified published probe alongside the adapter")
parser.add_argument("--version", default="latest", help="Probe release tag; independent of the integration source --ref")
parser.add_argument("--bin-dir", type=Path, default=Path.home() / ".local/bin")
parser.add_argument("--source-dir", type=Path, help="Install from a verified local source checkout")
args = parser.parse_args()
if args.source_dir:
root = args.source_dir.resolve()
commit = subprocess.check_output(["git", "-C", str(root), "rev-parse", "HEAD"], text=True).strip()
reader = lambda path: (root / path).read_bytes()
else:
commit, reader = resolve_source(args.source, args.ref)
manifest, payload = load_payload(reader)
if args.binary and args.install_binary:
raise ValueError("Choose --binary or --install-binary")
binary_content = None
binary_record = {}
binary = args.binary
if binary:
candidate = Path(binary).expanduser()
if not candidate.is_absolute() or not candidate.is_file() or (os.name != "nt" and not os.access(candidate, os.X_OK)):
raise ValueError("--binary must name an existing executable by its absolute path")
if args.install_binary:
if args.skill_only or not manifest.get("mcp"):
raise ValueError("Binary installation requires an MCP-enabled integration")
profile = json.loads(payload["agent/profile.json"])
binary_content, binary_record = release_binary(profile, args.version)
binary = str(args.bin_dir.expanduser().absolute() / (profile["binary"] + (".exe" if os.name == "nt" else "")))
parents = {"claude": Path.home() / ".claude/skills", "codex": Path.home() / ".agents/skills",
"openclaw": Path.home() / ".openclaw/skills"}
result = install(manifest, payload, args.skill_dir or parents[args.client] / SKILL_NAME,
args.mcp_dir, {"source": args.source, "ref": args.ref, "commit": commit, "binary": binary_record},
args.skill_only, binary, binary_content)
print(json.dumps(result, ensure_ascii=False, indent=2))
if result["mcp_installed"]:
print("Merge mcp.example.json into your client's MCP settings. Existing settings and tokens were not modified.")
else:
print("Installed Skill files. This repository does not install an MCP server in this mode.")
if __name__ == "__main__":
try:
main()
except (ValueError, OSError, subprocess.SubprocessError) as exc:
print("Installation failed: " + str(exc), file=sys.stderr)
sys.exit(1)

View File

@ -0,0 +1,56 @@
---
name: oneclickvirt-backtrace
description: "Query RDAP/BGP metadata for an explicit IP; distinguish lookup data from an actual traceroute."
metadata:
repository: oneclickvirt/backtrace
integration-version: "1.0.0"
---
# backtrace
## Scope
Query RDAP/BGP metadata for an explicit IP; distinguish lookup data from an actual traceroute.
- Read probe_help from the installed binary before assuming flags are supported. MCP returns native JSON when the program provides JSON, otherwise native text.
- Keep partial/network errors distinct from confirmed test outcomes. Current release assets and their actual architecture are available through release_assets.
- The ip-info preset requires a literal target IP. Do not pass a host, proxy, shell expression or a network range.
## Install and update
```bash
curl -fL --retry 3 https://raw.githubusercontent.com/oneclickvirt/backtrace/main/scripts/install_agent.py -o install_agent.py
python3 install_agent.py --client codex
```
The installer is also mirrored at `https://cnb.cool/oneclickvirt/backtrace/-/git/raw/main/scripts/install_agent.py`; add `--source cnb` to install from an immutable CNB snapshot (requires Git).
Use `--client claude` or `--client openclaw` for another client. Download and run again to update; `--ref` pins a source tag/commit. The installer verifies all SHA-256 checksums before replacing its files, preserves extra local files/client settings, and records the source commit in `install.json`.
## MCP
Requires Python 3.9+. The adapter uses existing local commands and their existing permissions; it does not install a virtualization runtime. Merge the installed `mcp.example.json` into the client configuration. For a remote host, run its adapter over noninteractive SSH.
```bash
python3 install_agent.py --client codex --install-binary
```
`--version` selects the probe release tag; `--ref` selects Skill/MCP source independently. The default resolves the latest stable GitHub release once, selects a real asset for this OS/architecture, and verifies SHA-256. Missing assets fail without guessing. Reuse an existing executable with `--binary /absolute/path/to/program`.
Call `integration_info` first for capabilities and prerequisites.
Available probe presets:
- `ip-info`: `backtrace -json -timeout 12s -ip <target>`
`release_assets` returns the actual version, filenames, sizes, digests and download URLs. Pass an exact tag for a pinned release. After a product update, verify help and an actual call rather than treating file presence as a successful test.
## Source and documentation
- [`backtrace_install.sh`](https://github.com/oneclickvirt/backtrace/blob/main/backtrace_install.sh)
- [`cmd/main.go`](https://github.com/oneclickvirt/backtrace/blob/main/cmd/main.go)
- [`go.mod`](https://github.com/oneclickvirt/backtrace/blob/main/go.mod)
- [集成安装说明 / Integration guide](https://www.spiritlhl.net/guide/agent_integrations.html)
Check the relevant source and current documentation before giving commands. Preserve requested version pins, data paths and native host identifiers. Distinguish actual program execution, isolated tests and unverified environments.

View File

@ -0,0 +1,56 @@
---
name: oneclickvirt-backtrace
description: "查询指定 IP 的 RDAP/BGP 信息,区分数据库查询与真实路由探测。"
metadata:
repository: oneclickvirt/backtrace
integration-version: "1.0.0"
---
# backtrace
## 使用范围
查询指定 IP 的 RDAP/BGP 信息,区分数据库查询与真实路由探测。
- 先用 probe_help 核对已安装程序的实际帮助。程序支持 JSON 时返回原生 JSON,否则返回原生文本。
- 区分部分结果、网络失败和确定结论;通过 release_assets 查询当前真实产物及架构。
- ip-info 预设只接受单个真实 IP,不接受域名、代理、Shell 表达式或网段。
## 安装和更新
```bash
curl -fL --retry 3 https://raw.githubusercontent.com/oneclickvirt/backtrace/main/scripts/install_agent.py -o install_agent.py
python3 install_agent.py --client codex
```
可将下载地址改为 `https://cnb.cool/oneclickvirt/backtrace/-/git/raw/main/scripts/install_agent.py`,并使用 `--source cnb` 从 CNB 的固定提交安装;此模式另需 Git。
用 `--client claude` 或 `--client openclaw` 选择其他客户端。重新下载并执行可更新;`--ref` 固定源码标签或提交。安装前校验全部 SHA-256,保留额外本地文件及已有客户端设置,`install.json` 记录来源提交。
## MCP
运行需要 Python 3.9 以上版本;MCP 使用本机已有程序及其权限,不自动安装虚拟化环境。将安装目录内 `mcp.example.json` 合并到客户端配置。远端宿主机应通过无交互 SSH 运行该主机上的适配器。
```bash
python3 install_agent.py --client codex --install-binary
```
`--version` 可指定检测程序的正式 Release 标签,`--ref` 则指定 Skill/MCP 源码版本,两者独立。默认从 GitHub 解析一次最新正式版,选择本机系统/架构的真实产物并校验 SHA-256;没有匹配产物时停止。已有程序可用 `--binary /完整路径/程序` 复用。
先调用 `integration_info` 查看能力和运行要求。
可用检测预设:
- `ip-info`: `backtrace -json -timeout 12s -ip <target>`
`release_assets` 返回实际版本、文件名、大小、摘要及下载地址;需要固定版本时传完整标签。更新产物后应重新安装并验证帮助和实际调用,不能只因文件存在便判定可用。
## 对应源码与说明
- [`backtrace_install.sh`](https://github.com/oneclickvirt/backtrace/blob/main/backtrace_install.sh)
- [`cmd/main.go`](https://github.com/oneclickvirt/backtrace/blob/main/cmd/main.go)
- [`go.mod`](https://github.com/oneclickvirt/backtrace/blob/main/go.mod)
- [集成安装说明 / Integration guide](https://www.spiritlhl.net/guide/agent_integrations.html)
给出操作命令前核对这些源码文件及当前文档,保留现有版本固定、数据路径和宿主机标识。报告测试时区分真实程序执行、隔离测试及未验证的环境。