release: public alpha 0.6.0 baseline

This commit is contained in:
JWeBot Contributors
2026-10-15 03:58:31 +08:00
commit d4242ae07c
238 changed files with 37724 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Install and atomically activate one verified extension release."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [
str(ROOT / "packages" / "jwebot-sdk" / "src"),
str(ROOT / "packages" / "jwebot-kernel" / "src"),
]
from jwebot_kernel import ( # noqa: E402
ActivationError,
ExtensionHostClient,
ReleaseError,
install_release,
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("artifact", type=Path)
parser.add_argument("release", type=Path)
parser.add_argument("--state-root", type=Path, required=True)
parser.add_argument("--signature", type=Path)
parser.add_argument("--allowed-signers", type=Path)
parser.add_argument("--publisher")
parser.add_argument("--allow-unsigned-local", action="store_true")
parser.add_argument("--skip-health-check-local", action="store_true")
args = parser.parse_args()
if args.allow_unsigned_local:
if any(
value is not None
for value in (
args.signature,
args.allowed_signers,
args.publisher,
)
):
parser.error(
"--allow-unsigned-local cannot include signature options"
)
elif args.signature is None or args.allowed_signers is None:
parser.error(
"signed activation requires --signature and --allowed-signers"
)
health_check = None
if not args.allow_unsigned_local and not args.skip_health_check_local:
client = ExtensionHostClient(
allowed_signers=args.allowed_signers,
expected_publisher=args.publisher,
)
health_check = lambda release_dir: client.invoke(
release_dir,
{"op": "health"},
)
try:
destination = install_release(
args.artifact,
args.release,
args.state_root,
signature=args.signature,
allowed_signers=args.allowed_signers,
expected_publisher=args.publisher,
allow_unsigned=args.allow_unsigned_local,
health_check=health_check,
allow_unhealthy_local=args.skip_health_check_local,
)
except (ActivationError, ReleaseError) as error:
parser.error(str(error))
print(
json.dumps(
{
"activated": True,
"release_dir": str(destination),
"current": str(args.state_root / "current"),
},
ensure_ascii=False,
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+90
View File
@@ -0,0 +1,90 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
version=$(cat VERSION)
case "$version" in
alpha\ [0-9]*.[0-9]*.[0-9]*.[0-9][0-9][0-9][0-9])
;;
*)
echo "VERSION must match: alpha X.Y.Z.MMDD" >&2
exit 1
;;
esac
grep -Fq "release = \"$version\"" pyproject.toml
grep -Fq "$version" README.md
grep -Fq "## $version" CHANGELOG.md
for path in \
config/README.md \
config/templates/default/manifest.toml \
config/templates/default/components/bot.toml \
config/templates/default/components/providers.toml \
config/templates/default/components/runtime.toml \
config/templates/default/components/debug.toml \
config/templates/default/components/notification.toml \
config/templates/default/components/deployment.toml \
config/templates/default/context/people.json \
config/templates/default/secrets/runtime.env \
config/templates/default/overrides/runtime.env \
config/templates/default/overrides/root.env \
config/schemas/profile-manifest.schema.json \
config/schemas/private-context.schema.json \
config/schemas/incident.schema.json \
config/schemas/debug-event.schema.json \
docs/configuration-guide.md \
docs/configuration-inventory.md
do
test -f "$path"
done
temporary=$(mktemp -d)
trap 'rm -rf "$temporary"' EXIT HUP INT TERM
PYTHONPATH=packages/jwebot-config/src \
python3 tools/jwebot-config.py validate \
--path config/templates/default \
--config-root "$temporary/config" \
--state-root "$temporary/state" >/dev/null
python3 - config/schemas/*.json <<'PY'
import json
import sys
from pathlib import Path
for value in sys.argv[1:]:
parsed = json.loads(Path(value).read_text(encoding="utf-8"))
if not isinstance(parsed, dict) or "$schema" not in parsed:
raise SystemExit(f"invalid schema: {value}")
PY
if grep -E '^(JWEBOT|WECHAT)_[A-Z0-9_]*(KEY|TOKEN|PASSWORD|SECRET)=.+' \
config/templates/default/secrets/runtime.env >/dev/null
then
echo "Default profile contains a populated secret" >&2
exit 1
fi
for value in \
JWEBOT_CONFIG_ROOT \
JWEBOT_CONFIG_STATE_ROOT \
JWEBOT_BOT_MENTION \
JWEBOT_PRIVATE_CONTEXT_FILE \
JWEBOT_EDGE_DEEPSEEK_API_KEY \
JWEBOT_EDGE_STEAM_API_KEY \
JWEBOT_EDGE_STEAM_ID64 \
JWEBOT_EDGE_STEAM_PROXY_URL \
JWEBOT_DEBUG_INCIDENT_ROOT \
JWEBOT_DEBUG_SESSION_ROOT \
JWEBOT_HEALTH_NOTIFICATION_CONFIG \
JWEBOT_RUNTIME_USER \
JWEBOT_RUNTIME_HOME \
JWEBOT_RUNTIME_PYTHON \
JWEBOT_PRODUCTION_BACKUP_ROOT
do
grep -Fq "$value" docs/configuration-guide.md
done
rm -rf "$temporary"
trap - EXIT HUP INT TERM
+48
View File
@@ -0,0 +1,48 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
service=deploy/systemd/system/jwebot-extension-host@.service
socket=deploy/systemd/system/jwebot-extension-host@.socket
test -f "$service"
test -f "$socket"
for setting in \
'User=jwebot-ext' \
'Group=jwebot-ext' \
'Environment=PYTHONDONTWRITEBYTECODE=1' \
'ConditionPathExists=/etc/jwebot-extension-host/allowed_signers' \
'ExecStart=/opt/jwebot-extension-host/venv/bin/python -I -m jwebot_extension_host --release-dir /var/lib/jwebot-extensions/%i/current --allowed-signers /etc/jwebot-extension-host/allowed_signers' \
'StandardInput=socket' \
'StandardOutput=socket' \
'NoNewPrivileges=true' \
'CapabilityBoundingSet=' \
'PrivateNetwork=true' \
'ProtectSystem=strict' \
'ProtectHome=true' \
'MemoryDenyWriteExecute=true' \
'RestrictAddressFamilies=AF_UNIX' \
'RuntimeMaxSec=12s' \
'MemoryMax=128M' \
'TasksMax=16' \
'LimitCORE=0'
do
grep -Fxq "$setting" "$service"
done
for setting in \
'SocketUser=jwebot' \
'SocketGroup=jwebot' \
'SocketMode=0600' \
'RemoveOnStop=true'
do
grep -Fxq "$setting" "$socket"
done
if [ "${JWEBOT_VERIFY_SYSTEMD:-0}" = 1 ] &&
command -v systemd-analyze >/dev/null 2>&1
then
systemd-analyze verify "$service" "$socket"
fi
+61
View File
@@ -0,0 +1,61 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
./tools/check-config.sh
./tools/check-secrets.py
./tools/check-extension-sandbox.sh
./tools/check-packages.sh
python3 -m py_compile \
jwebot_edge.py \
jwebot_edge_delivery.py \
wechat_local_ocr.py \
wechat_adaptive_recorder.py \
wechat_adaptive_gui_bridge.py \
wechat_health_watch.py \
tools/activate-release.py \
tools/create-plugin.py \
tools/jwebot-config.py \
tools/package-plugin.py \
tools/performance-baseline.py \
tools/rollback-release.py \
tools/check-secrets.py \
tools/sign-release.py \
tools/sync-local-extensions.py \
tools/verify-release.py \
tools/validate-plugin.py
bash -n wechat-gui-bridge
for script in deploy/scripts/*.sh; do
if head -n 1 "$script" | grep -q 'bash'; then
bash -n "$script"
else
sh -n "$script"
fi
done
if command -v rg >/dev/null 2>&1; then
debug_matches=$(
rg -l '192\.168\.31\.15:777|10\.4\.33\.12:777|#region debug-point|\[DEBUG\]' \
*.py README.md docs/configuration-inventory.md \
|| true
)
else
debug_matches=$(
grep -R -l -E \
'192\.168\.31\.15:777|10\.4\.33\.12:777|#region debug-point|\[DEBUG\]' \
-- *.py README.md docs/configuration-inventory.md \
|| true
)
fi
unexpected=$debug_matches
if [ -n "$unexpected" ]; then
echo "Unexpected emergency debug hook references:" >&2
echo "$unexpected" >&2
exit 1
fi
python3 -m unittest discover -s tests -q
+124
View File
@@ -0,0 +1,124 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
release=$(cat VERSION)
version=${release#alpha }
old_ifs=$IFS
IFS=.
set -- $version
IFS=$old_ifs
if [ "$#" -ne 4 ]; then
echo "Cannot derive package version from VERSION" >&2
exit 1
fi
package_version="$1.$2.$3a0+$4"
for path in \
packages/jwebot-config/pyproject.toml \
packages/jwebot-sdk/pyproject.toml \
packages/jwebot-official-extensions/pyproject.toml \
packages/jwebot-kernel/pyproject.toml \
packages/jwebot-extension-host/pyproject.toml
do
grep -Fq "version = \"$package_version\"" "$path"
done
grep -Fq "__version__ = \"$package_version\"" \
packages/jwebot-config/src/jwebot_config/__init__.py
grep -Fq "__version__ = \"$package_version\"" \
packages/jwebot-sdk/src/jwebot_sdk/__init__.py
grep -Fq "__version__ = \"$package_version\"" \
packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py
grep -Fq "__version__ = \"$package_version\"" \
packages/jwebot-kernel/src/jwebot_kernel/__init__.py
grep -Fq "__version__ = \"$package_version\"" \
packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py
for package_name in \
jwebot_config \
jwebot_sdk \
jwebot_kernel \
jwebot_official_extensions
do
grep -Fq "$package_name" deploy/scripts/install-wechat-health-watch.sh
done
for path in \
docs/plugin-contract.md \
docs/capability-catalog.md \
docs/author-guide.md \
docs/api-reference.md \
docs/prompt-for-ai-authors.md \
docs/extension-host.md
do
test -f "$path"
done
bytecode_root=$(mktemp -d)
trap 'rm -rf "$bytecode_root"' EXIT HUP INT TERM
PYTHONPYCACHEPREFIX="$bytecode_root" \
python3 -m compileall -q packages examples/hello-command/src
rm -rf "$bytecode_root"
trap - EXIT HUP INT TERM
./tools/sync-local-extensions.py --check
if command -v rg >/dev/null 2>&1; then
forbidden=$(
rg -n '^(from|import) (os|socket|subprocess)([ .]|$)' \
packages/jwebot-sdk/src || true
)
else
forbidden=$(
find packages/jwebot-sdk/src -type f -name '*.py' \
-exec grep -nHE \
'^(from|import) (os|socket|subprocess)([ .]|$)' {} + || true
)
fi
if [ -n "$forbidden" ]; then
echo "Public SDK must not expose process, socket, or environment access:" >&2
echo "$forbidden" >&2
exit 1
fi
if python3 -c 'import tomllib' >/dev/null 2>&1 ||
python3 -c 'import tomli' >/dev/null 2>&1
then
test -x /usr/bin/ssh-keygen
./tools/validate-plugin.py examples/hello-command/plugin.toml >/dev/null
temporary=$(mktemp -d)
trap 'rm -rf "$temporary"' EXIT HUP INT TERM
ssh-keygen -q -t ed25519 -N '' -f "$temporary/publisher"
printf 'test-publisher %s\n' \
"$(cat "$temporary/publisher.pub")" \
>"$temporary/allowed_signers"
./tools/package-plugin.py examples/hello-command \
--output "$temporary" \
--signing-key "$temporary/publisher" \
--publisher test-publisher >/dev/null
artifact=$(find "$temporary" -maxdepth 1 -type f -name '*.tar.gz')
release=$(find "$temporary" -maxdepth 1 -type f -name '*.release.json')
signature=$(find "$temporary" -maxdepth 1 -type f -name '*.release.sig.json')
./tools/verify-release.py "$artifact" "$release" \
--signature "$signature" \
--allowed-signers "$temporary/allowed_signers" \
--publisher test-publisher >/dev/null
./tools/activate-release.py "$artifact" "$release" \
--signature "$signature" \
--allowed-signers "$temporary/allowed_signers" \
--publisher test-publisher \
--skip-health-check-local \
--state-root "$temporary/state" >/dev/null
test -L "$temporary/state/current"
for manifest in extensions/official/*/plugin.toml extensions/jwebot/*/plugin.toml; do
extension=${manifest%/plugin.toml}
./tools/validate-plugin.py "$manifest" >/dev/null
./tools/package-plugin.py "$extension" \
--output "$temporary/local" >/dev/null
done
rm -rf "$temporary"
trap - EXIT HUP INT TERM
else
echo "notice: skipping TOML file validation; install tomli on Python < 3.11" >&2
fi
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env python3
"""Privacy-safe scanner for tracked files and optional Git object history."""
from __future__ import annotations
import argparse
import hashlib
import ipaddress
import re
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import Iterable
ROOT = Path(__file__).resolve().parents[1]
MAX_BLOB_BYTES = 5_000_000
SENSITIVE_DOTENV = re.compile(
rb"(?im)^(?P<key>(?:JWEBOT|WECHAT)_[A-Z0-9_]*"
rb"(?:KEY|TOKEN|PASSWORD|SECRET))=(?P<value>[^\r\n#]+)$"
)
TOKEN_PATTERNS = (
(
"private_key",
re.compile(rb"-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----"),
),
(
"api_token",
re.compile(
rb"(?i)\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|"
rb"github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|"
rb"AKIA[0-9A-Z]{16})\b"
),
),
(
"bearer_token",
re.compile(rb"(?i)\bBearer\s+([A-Za-z0-9._~+/-]{20,}=*)"),
),
(
"steam_id64",
re.compile(rb"\b7656119[0-9]{10}\b"),
),
(
"email",
re.compile(rb"(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b"),
),
(
"home_path",
re.compile(rb"/(?:Users|home)/[^/\s\"']+"),
),
(
"ipv4",
re.compile(rb"(?<![0-9])(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?![0-9])"),
),
)
PLACEHOLDER = re.compile(
rb"(?i)(?:^$|example|sample|placeholder|change.?me|dummy|fake|test|"
rb"redact|your[_-]|<[^>]+>|\$\{|\{\{|none|null|unset|x{4,}|0{4,})"
)
ALLOWED_EMAIL_DOMAINS = {"users.noreply.github.com", "example.com"}
ALLOWED_HOME_USERS = {"jwebot", "node", "user"}
def run(*args: str) -> bytes:
return subprocess.check_output(args, cwd=ROOT)
def has_git_repository() -> bool:
result = subprocess.run(
["git", "rev-parse", "--is-inside-work-tree"],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
return result.returncode == 0 and result.stdout.strip() == "true"
def digest(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()[:12]
def allowed_ip(raw: bytes) -> bool:
try:
value = ipaddress.ip_address(raw.decode("ascii"))
except ValueError:
return True
if value.is_loopback:
return True
documentation = (
ipaddress.ip_network("192.0.2.0/24"),
ipaddress.ip_network("198.51.100.0/24"),
ipaddress.ip_network("203.0.113.0/24"),
)
return any(value in network for network in documentation)
def findings(data: bytes) -> list[tuple[str, int, str]]:
result: list[tuple[str, int, str]] = []
lines = data.splitlines()
for line_number, line in enumerate(lines, start=1):
for category, pattern in TOKEN_PATTERNS:
for match in pattern.finditer(line):
value = match.group(1) if match.lastindex else match.group(0)
if category == "email":
decoded = value.decode("utf-8", "replace")
if decoded.endswith((".service", ".socket", ".timer")):
continue
domain = decoded.rpartition("@")[2]
if domain.casefold() in ALLOWED_EMAIL_DOMAINS:
continue
elif category == "home_path":
user = value.decode("utf-8", "replace").rsplit("/", 1)[-1]
if user in ALLOWED_HOME_USERS:
continue
elif category == "ipv4" and allowed_ip(value):
continue
result.append((category, line_number, digest(value)))
for match in SENSITIVE_DOTENV.finditer(data):
value = match.group("value").strip().strip(b"'\"")
if value and not PLACEHOLDER.search(value):
line_number = data.count(b"\n", 0, match.start()) + 1
result.append(("populated_secret", line_number, digest(value)))
return result
def tracked_paths() -> list[str]:
if not has_git_repository():
excluded = {".git", ".venv", "__pycache__", ".pytest_cache"}
return sorted(
str(path.relative_to(ROOT))
for path in ROOT.rglob("*")
if path.is_file() and not any(part in excluded for part in path.parts)
)
return [
value.decode("utf-8", "surrogateescape")
for value in run(
"git",
"ls-files",
"--cached",
"--others",
"--exclude-standard",
"-z",
).split(b"\0")
if value
]
def suspicious_path(path: str) -> bool:
value = PurePath(path)
parts = set(value.parts)
if ".dbg" in parts or "dbg" in parts:
return True
if value.name.startswith("homeserver-apikey-"):
return True
if value.suffix in {".pem", ".p12", ".pfx", ".jks", ".keystore"}:
return True
return False
class PurePath:
def __init__(self, value: str) -> None:
self.parts = tuple(part for part in value.replace("\\", "/").split("/") if part)
self.name = self.parts[-1] if self.parts else ""
self.suffix = Path(self.name).suffix.casefold()
def scan_tracked() -> list[tuple[str, str, int, str]]:
result: list[tuple[str, str, int, str]] = []
for path in tracked_paths():
if suspicious_path(path):
result.append(("suspicious_path", path, 0, digest(path.encode())))
candidate = ROOT / path
try:
if candidate.stat().st_size > MAX_BLOB_BYTES:
continue
data = candidate.read_bytes()
except OSError:
continue
if b"\0" in data[:4096]:
continue
for category, line, value_digest in findings(data):
result.append((category, path, line, value_digest))
return result
def reachable_blob_paths() -> dict[str, set[str]]:
values: dict[str, set[str]] = defaultdict(set)
for raw in run("git", "rev-list", "--objects", "--all").splitlines():
sha, separator, path = raw.partition(b" ")
if separator:
values[sha.decode()].add(path.decode("utf-8", "replace"))
return values
def scan_history() -> list[tuple[str, str, int, str]]:
paths = reachable_blob_paths()
result: list[tuple[str, str, int, str]] = []
objects = run(
"git",
"cat-file",
"--batch-all-objects",
"--batch-check=%(objectname) %(objecttype) %(objectsize)",
).decode().splitlines()
for row in objects:
sha, object_type, size = row.split()
if object_type != "blob" or int(size) > MAX_BLOB_BYTES:
continue
data = run("git", "cat-file", "blob", sha)
if b"\0" in data[:4096]:
continue
for category, line, value_digest in findings(data):
for path in sorted(paths.get(sha, {"<unreachable-or-unmapped>"})):
result.append(
(
f"history_{category}",
f"{path}@{sha[:12]}",
line,
value_digest,
)
)
identities = run(
"git",
"log",
"--all",
"--format=%an%x00%ae%x00%cn%x00%ce",
).splitlines()
for row in identities:
fields = row.decode("utf-8", "replace").split("\0")
for email in (fields[1], fields[3]):
domain = email.rpartition("@")[2].casefold()
if domain not in ALLOWED_EMAIL_DOMAINS:
result.append(
(
"history_author_email",
"<commit-metadata>",
0,
digest(email.encode()),
)
)
return result
def unique(
values: Iterable[tuple[str, str, int, str]],
) -> list[tuple[str, str, int, str]]:
return sorted(set(values))
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Scan JWeBot without printing matched secret or PII values"
)
parser.add_argument("--history", action="store_true")
args = parser.parse_args(argv)
result = scan_tracked()
if args.history:
if not has_git_repository():
print(
"check-secrets: --history requires a Git repository",
file=sys.stderr,
)
return 2
result.extend(scan_history())
result = unique(result)
for category, path, line, value_digest in result:
location = f"{path}:{line}" if line else path
print(f"{category} {location} sha256={value_digest}")
if result:
print(f"privacy_scan=failed findings={len(result)}", file=sys.stderr)
return 1
print("privacy_scan=ok findings=0")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""Create a reviewed command-extension skeleton without runtime privileges."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [
str(ROOT / "packages" / "jwebot-sdk" / "src"),
str(ROOT / "packages" / "jwebot-kernel" / "src"),
]
from jwebot_kernel import ManifestError, parse_manifest # noqa: E402
def module_name(plugin_id: str) -> str:
return plugin_id.replace("-", "_")
def manifest_text(
plugin_id: str,
command: str,
author: str,
description: str,
help_entry: str,
) -> str:
module = module_name(plugin_id)
return f"""id = "{plugin_id}"
author = "{author}"
description = {json.dumps(description, ensure_ascii=False)}
help = [{json.dumps(help_entry, ensure_ascii=False)}]
version = "1.0.0"
api_version = "1"
entrypoint = "{module}:plugin"
type = "command"
[capabilities]
commands = ["{command}"]
storage = false
network_profiles = []
service_profiles = []
"""
def module_text(plugin_id: str, command: str) -> str:
return f'''"""Command extension: {plugin_id}."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
def handle(event: Event, arguments: str) -> ExtensionResult:
del event, arguments
return ExtensionResult(reply=ReplyPlan.text("TODO"))
def plugin(context: SingleGroupContext) -> None:
context.commands.register("{command}", handle)
'''
def test_text(plugin_id: str) -> str:
module = module_name(plugin_id)
return f'''import sys
import unittest
from pathlib import Path
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from jwebot_sdk import Event, PlanKind
from {module} import handle
class PluginTests(unittest.TestCase):
def test_command_returns_a_structured_reply(self):
event = Event(
event_id="event-1",
observed_at="2026-01-01T00:00:00+08:00",
sender="tester",
text="@JWeBot test",
group_fingerprint="group-test",
mention_bot=True,
)
result = handle(event, "")
self.assertEqual(result.reply.kind, PlanKind.TEXT)
if __name__ == "__main__":
unittest.main()
'''
def readme_text(
plugin_id: str,
command: str,
author: str,
description: str,
help_entry: str,
) -> str:
return f"""# {plugin_id}
Author: `{author}`
{description}
Command: `{command}`
Help: `{help_entry}`
This extension may return structured JWeBot SDK results only. It must not use
GUI automation, raw storage, environment variables, subprocesses, SSH, Docker,
or unrestricted network access.
"""
def validate_inputs(
plugin_id: str,
command: str,
author: str,
description: str,
help_entry: str,
) -> None:
parse_manifest(
{
"id": plugin_id,
"author": author,
"description": description,
"help": [help_entry],
"version": "1.0.0",
"api_version": "1",
"entrypoint": f"{module_name(plugin_id)}:plugin",
"type": "command",
"capabilities": {
"commands": [command],
"storage": False,
"network_profiles": [],
},
}
)
def create_plugin(
output: Path,
plugin_id: str,
command: str,
author: str,
description: str,
help_entry: str | None = None,
) -> Path:
help_entry = help_entry or command
validate_inputs(
plugin_id,
command,
author,
description,
help_entry,
)
destination = output / plugin_id
if destination.exists():
raise FileExistsError(f"destination already exists: {destination}")
source = destination / "src"
tests = destination / "tests"
source.mkdir(parents=True)
tests.mkdir()
module = module_name(plugin_id)
(destination / "plugin.toml").write_text(
manifest_text(
plugin_id,
command,
author,
description,
help_entry,
),
encoding="utf-8",
)
(source / f"{module}.py").write_text(
module_text(plugin_id, command),
encoding="utf-8",
)
(tests / "test_plugin.py").write_text(
test_text(plugin_id),
encoding="utf-8",
)
(destination / "README.md").write_text(
readme_text(
plugin_id,
command,
author,
description,
help_entry,
),
encoding="utf-8",
)
return destination
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("plugin_id")
parser.add_argument("command")
parser.add_argument("--author", required=True)
parser.add_argument("--description", required=True)
parser.add_argument("--help-entry")
parser.add_argument(
"--output",
type=Path,
default=ROOT / "extensions" / "friends",
)
args = parser.parse_args()
try:
destination = create_plugin(
args.output,
args.plugin_id,
args.command,
args.author,
args.description,
args.help_entry,
)
except (ManifestError, FileExistsError) as error:
parser.error(str(error))
print(destination)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""Repository entrypoint for the JWeBot configuration profile CLI."""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(
0,
str(ROOT / "packages" / "jwebot-config" / "src"),
)
from jwebot_config.__main__ import main # noqa: E402
if __name__ == "__main__":
raise SystemExit(main())
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Build a deterministic, digest-addressed JWeBot extension artifact."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [
str(ROOT / "packages" / "jwebot-sdk" / "src"),
str(ROOT / "packages" / "jwebot-kernel" / "src"),
]
from jwebot_kernel import ( # noqa: E402
ManifestError,
ReleaseError,
SourcePolicyError,
build_release,
sign_release_record,
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("extension", type=Path)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--signing-key", type=Path)
parser.add_argument("--publisher")
args = parser.parse_args()
if (args.signing_key is None) != (args.publisher is None):
parser.error("--signing-key and --publisher must be provided together")
try:
artifact, record_path, record = build_release(
args.extension,
args.output,
)
signature_path = None
if args.signing_key is not None:
signature_path, _ = sign_release_record(
record_path,
args.signing_key,
args.publisher,
)
except (ManifestError, SourcePolicyError, ReleaseError) as error:
parser.error(str(error))
print(
json.dumps(
{
**record.to_mapping(),
"artifact_path": str(artifact),
"release_path": str(record_path),
"signature_path": (
str(signature_path)
if signature_path is not None
else None
),
},
ensure_ascii=False,
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+764
View File
@@ -0,0 +1,764 @@
#!/usr/bin/env python3
"""Measure local JWeBot latency and summarize runtime telemetry."""
from __future__ import annotations
import argparse
import gc
import importlib
import json
import math
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import time
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Iterable, Iterator, Mapping, Sequence
from PIL import Image, ImageDraw
PROJECT_ROOT = Path(__file__).resolve().parents[1]
CONFIG_SRC = PROJECT_ROOT / "packages" / "jwebot-config" / "src"
SDK_SRC = PROJECT_ROOT / "packages" / "jwebot-sdk" / "src"
KERNEL_SRC = PROJECT_ROOT / "packages" / "jwebot-kernel" / "src"
HOST_SRC = PROJECT_ROOT / "packages" / "jwebot-extension-host" / "src"
OFFICIAL_SRC = (
PROJECT_ROOT
/ "packages"
/ "jwebot-official-extensions"
/ "src"
)
sys.path[:0] = [
str(PROJECT_ROOT),
str(CONFIG_SRC),
str(SDK_SRC),
str(OFFICIAL_SRC),
str(KERNEL_SRC),
str(HOST_SRC),
]
os.environ["JWEBOT_CONFIG_ROOT"] = str(
PROJECT_ROOT / ".benchmark-config-does-not-exist"
)
import wechat_local_ocr as ocr # noqa: E402
from jwebot_kernel import ( # noqa: E402
ExtensionHostClient,
build_release,
install_release,
sign_release_record,
)
from jwebot_kernel import edge_runtime # noqa: E402
REPORT_SCHEMA_VERSION = 1
PUBLISHER = "performance-baseline"
def percentile(values: Sequence[float], quantile: float) -> float:
"""Return an interpolated percentile for a non-empty sample."""
if not values:
raise ValueError("percentile requires at least one value")
if not 0 <= quantile <= 100:
raise ValueError("percentile must be between 0 and 100")
ordered = sorted(float(value) for value in values)
position = (len(ordered) - 1) * quantile / 100
lower = math.floor(position)
upper = math.ceil(position)
if lower == upper:
return ordered[lower]
fraction = position - lower
return ordered[lower] + (ordered[upper] - ordered[lower]) * fraction
def summarize_ms(values: Sequence[float]) -> dict[str, float | int]:
if not values:
raise ValueError("latency summary requires at least one value")
return {
"samples": len(values),
"mean_ms": round(sum(values) / len(values), 3),
"min_ms": round(min(values), 3),
"p50_ms": round(percentile(values, 50), 3),
"p95_ms": round(percentile(values, 95), 3),
"max_ms": round(max(values), 3),
}
def measure_case(
name: str,
operation: Callable[[], object],
*,
iterations: int,
warmups: int,
category: str,
) -> dict[str, Any]:
for _ in range(warmups):
operation()
samples: list[float] = []
gc_enabled = gc.isenabled()
gc.disable()
try:
for _ in range(iterations):
started = time.perf_counter_ns()
operation()
samples.append((time.perf_counter_ns() - started) / 1_000_000)
finally:
if gc_enabled:
gc.enable()
return {
"name": name,
"category": category,
"status": "measured",
**summarize_ms(samples),
}
def skipped_case(name: str, category: str, reason: str) -> dict[str, Any]:
return {
"name": name,
"category": category,
"status": "skipped",
"reason": reason[:160],
}
def _synthetic_bubble(width: int = 500, height: int = 180) -> Image.Image:
image = Image.new("RGB", (width, height), ocr.INCOMING_COLOR)
draw = ImageDraw.Draw(image)
for top in (22, 58, 94, 130):
draw.rectangle((24, top, width - 28, top + 12), fill=(38, 38, 38))
return image
def _synthetic_message_view(width: int, height: int) -> Image.Image:
image = Image.new("RGB", (width, height), (245, 245, 245))
draw = ImageDraw.Draw(image)
for index, top in enumerate((60, 180, 310, 455)):
bubble_height = 72 + index * 8
draw.rounded_rectangle(
(64, top, 430, top + bubble_height),
radius=6,
fill=ocr.INCOMING_COLOR,
)
draw.rectangle(
(86, top + 20, 380, top + 31),
fill=(35, 35, 35),
)
return image
def benchmark_ocr(iterations: int, warmups: int) -> list[dict[str, Any]]:
bubble = _synthetic_bubble()
messages = [
ocr.IncomingMessage(
sender=f"sender-{index}",
text=f"message-{index}",
confidence=0.99,
sender_confidence=0.99,
body_confidence=0.99,
y=index * 30,
)
for index in range(18)
]
previous = [message.signature for message in messages[:-1]]
frame_image = Image.new("RGB", (2, 2), "white")
sequence = 0
def buffer_round_trip() -> None:
nonlocal sequence
buffer = ocr.CaptureFrameBuffer(12)
for _ in range(12):
sequence += 1
buffer.offer(
ocr.CapturedFrame(
image=frame_image,
layout_hash=f"layout-{sequence}",
visual_hash=f"visual-{sequence}",
observed_at="2026-10-14T00:00:00+00:00",
captured_monotonic=float(sequence),
sequence=sequence,
latest_synced=True,
)
)
buffer.get_latest()
results = [
measure_case(
"ocr.text_line_regions",
lambda: ocr.text_line_regions(bubble),
iterations=iterations,
warmups=warmups,
category="ocr-preprocess",
),
measure_case(
"ocr.sequence_suffix",
lambda: ocr.new_sequence_suffix(previous, messages[1:]),
iterations=iterations,
warmups=warmups,
category="ocr-sequence",
),
measure_case(
"ocr.capture_buffer_latest",
buffer_round_trip,
iterations=iterations,
warmups=warmups,
category="capture-queue",
),
]
with tempfile.TemporaryDirectory(
prefix="jwebot-performance-ocr-queue-"
) as value:
original_queue = ocr.QUEUE_DIR
ocr.QUEUE_DIR = Path(value)
counter = 0
def durable_spool() -> None:
nonlocal counter
counter += 1
ocr.queue_event(
{
"event_id": f"queue-{counter}",
"observed_at": "2026-10-14T00:00:00+00:00",
"sender": "PerformanceUser",
"text": "@JWeBot /翻硬币",
"mention_bot": True,
"group_fingerprint": "performance-group",
}
)
try:
results.append(
measure_case(
"ocr.local_event.durable_spool",
durable_spool,
iterations=iterations,
warmups=warmups,
category="ocr-persistence",
)
)
finally:
ocr.QUEUE_DIR = original_queue
ocr.QUEUE_WAKE_EVENT.clear()
try:
importlib.import_module("cv2")
except ModuleNotFoundError:
results.extend(
[
skipped_case(
"ocr.frame_fingerprint.full_view",
"ocr-preprocess",
"opencv-python-headless is not installed",
),
skipped_case(
"ocr.frame_fingerprint.roi",
"ocr-preprocess",
"opencv-python-headless is not installed",
),
]
)
return results
message_width = ocr.MESSAGE_BOX[2] - ocr.MESSAGE_BOX[0]
message_height = ocr.MESSAGE_BOX[3] - ocr.MESSAGE_BOX[1]
full_view = _synthetic_message_view(message_width, message_height)
region = _synthetic_message_view(540, 220)
results.extend(
[
measure_case(
"ocr.frame_fingerprint.full_view",
lambda: ocr.incoming_frame_hashes(full_view),
iterations=iterations,
warmups=warmups,
category="ocr-preprocess",
),
measure_case(
"ocr.frame_fingerprint.roi",
lambda: ocr.incoming_frame_hashes(region),
iterations=iterations,
warmups=warmups,
category="ocr-preprocess",
),
]
)
return results
def _edge_config(root: Path) -> edge_runtime.EdgeConfig:
return edge_runtime.EdgeConfig(
socket_path=root / "edge.sock",
database_path=root / "edge.sqlite3",
cloud_host="",
cloud_agent="wechat-responder",
deepseek_key="",
deepseek_base_url="https://api.deepseek.com",
deepseek_model=edge_runtime.EDGE_FLASH_MODEL,
memory_remote_path="/home/node/.openclaw/workspace/MEMORY.md",
delivery_enabled=False,
steam_api_key="",
steam_id64="",
steam_proxy_url=edge_runtime.DEFAULT_STEAM_MIHOMO_PROXY_URL,
extension_mode="in-process",
extension_state_root=root / "extensions",
extension_allowed_signers=root / "allowed_signers",
extension_socket_root=root / "sockets",
telemetry_enabled=False,
)
def _event(identifier: str, text: str) -> dict[str, object]:
return {
"event_id": identifier,
"group_fingerprint": "performance-group",
"sender": "PerformanceUser",
"text": text,
"observed_at": "2026-10-14T00:00:00+00:00",
"mention_bot": True,
"confidence": 0.99,
"sender_confidence": 0.99,
"body_confidence": 0.99,
"source": "performance-baseline",
}
@contextmanager
def _edge_application() -> Iterator[edge_runtime.EdgeApplication]:
with tempfile.TemporaryDirectory(prefix="jwebot-performance-edge-") as value:
application = edge_runtime.EdgeApplication(_edge_config(Path(value)))
try:
yield application
finally:
application.close()
def benchmark_edge(iterations: int, warmups: int) -> list[dict[str, Any]]:
total = iterations + warmups
results: list[dict[str, Any]] = []
with _edge_application() as application:
counter = 0
def local_command() -> None:
nonlocal counter
counter += 1
response = application.accept(
_event(f"local-{counter}", "@JWeBot /翻硬币")
)
if response.get("status") != "reply":
raise RuntimeError("local benchmark did not return a reply")
results.append(
measure_case(
"edge.local_command.in_process",
local_command,
iterations=iterations,
warmups=warmups,
category="edge-ledger",
)
)
with _edge_application() as application:
counter = 0
def prepare_ack() -> None:
nonlocal counter
counter += 1
response = application.accept(
_event(f"ack-prepare-{counter}", "@JWeBot benchmark chat")
)
if response.get("fast_ack") is not True:
raise RuntimeError("chat benchmark did not prepare an ACK")
results.append(
measure_case(
"edge.chat_ack.prepare",
prepare_ack,
iterations=iterations,
warmups=warmups,
category="edge-ledger",
)
)
with _edge_application() as application:
identifiers = [f"ack-confirm-{index}" for index in range(total)]
for identifier in identifiers:
application.accept(
_event(identifier, "@JWeBot benchmark confirmation")
)
iterator = iter(identifiers)
def confirm_ack() -> None:
response = application.confirm_fast_acknowledgement(
next(iterator),
delivered=True,
)
if response.get("acknowledged") is not True:
raise RuntimeError("chat benchmark did not confirm an ACK")
results.append(
measure_case(
"edge.chat_ack.confirm",
confirm_ack,
iterations=iterations,
warmups=warmups,
category="edge-ledger",
)
)
with _edge_application() as application:
identifiers = [f"queue-claim-{index}" for index in range(total)]
for identifier in identifiers:
application.accept(_event(identifier, "@JWeBot queued benchmark"))
application.confirm_fast_acknowledgement(
identifier,
delivered=True,
)
def claim_queue() -> None:
row = application.ledger.claim_direct_chat()
if row is None:
raise RuntimeError("chat benchmark queue is empty")
results.append(
measure_case(
"edge.direct_queue.claim",
claim_queue,
iterations=iterations,
warmups=warmups,
category="edge-ledger",
)
)
return results
def _publisher_key(root: Path) -> tuple[Path, Path]:
executable = shutil.which("ssh-keygen")
if executable is None:
raise RuntimeError("ssh-keygen is unavailable")
private_key = root / "publisher"
completed = subprocess.run(
[
executable,
"-q",
"-t",
"ed25519",
"-N",
"",
"-f",
str(private_key),
],
capture_output=True,
check=False,
)
if completed.returncode != 0:
raise RuntimeError("cannot create the benchmark publisher key")
allowed_signers = root / "allowed_signers"
allowed_signers.write_text(
f"{PUBLISHER} "
+ private_key.with_suffix(".pub").read_text(encoding="utf-8"),
encoding="utf-8",
)
return private_key, allowed_signers
def _host_wrapper(root: Path) -> Path:
paths = [str(SDK_SRC), str(KERNEL_SRC), str(HOST_SRC)]
try:
import tomli
except ModuleNotFoundError:
pass
else:
paths.append(str(Path(tomli.__file__).resolve().parent.parent))
wrapper = root / "host-wrapper.py"
wrapper.write_text(
"\n".join(
(
"import runpy, sys",
f"sys.path[:0] = {paths!r}",
"runpy.run_module('jwebot_extension_host', run_name='__main__')",
)
)
+ "\n",
encoding="utf-8",
)
return wrapper
@contextmanager
def _signed_coin_host() -> Iterator[
tuple[ExtensionHostClient, Path, Mapping[str, object]]
]:
with tempfile.TemporaryDirectory(prefix="jwebot-performance-host-") as value:
root = Path(value)
private_key, allowed_signers = _publisher_key(root)
artifact, record_path, _ = build_release(
PROJECT_ROOT / "extensions" / "alice" / "coin",
root / "dist",
)
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
release_dir = install_release(
artifact,
record_path,
root / "state" / "coin",
signature=signature_path,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
health_check=lambda release: None,
)
client = ExtensionHostClient(
command=(sys.executable, str(_host_wrapper(root))),
timeout_seconds=8,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
plugin_id="coin",
)
request = {
"op": "invoke_command",
"command": "/翻硬币",
"arguments": "",
"event": _event("host-benchmark", "@JWeBot /翻硬币"),
}
yield client, release_dir, request
def benchmark_host(iterations: int, warmups: int) -> list[dict[str, Any]]:
host_iterations = min(iterations, 25)
host_warmups = min(warmups, 3)
try:
with _signed_coin_host() as (client, release_dir, request):
health = measure_case(
"host.signed_process.health",
lambda: client.invoke(release_dir, {"op": "health"}),
iterations=host_iterations,
warmups=host_warmups,
category="extension-host",
)
def invoke_command() -> None:
response = client.invoke(
release_dir,
request,
service_handler=(
lambda profile, operation, payload: {
"text": "benchmark reply"
}
),
)
if response.get("status") != "ok":
raise RuntimeError("signed Host benchmark failed")
command = measure_case(
"host.signed_process.command",
invoke_command,
iterations=host_iterations,
warmups=host_warmups,
category="extension-host",
)
return [health, command]
except Exception as error:
reason = f"{type(error).__name__}: {error}"
return [
skipped_case(
"host.signed_process.health",
"extension-host",
reason,
),
skipped_case(
"host.signed_process.command",
"extension-host",
reason,
),
]
def parse_telemetry_line(line: str) -> dict[str, Any] | None:
text = line.strip()
marker = "telemetry "
if marker in text:
text = text.split(marker, 1)[1].strip()
elif not text.startswith("{"):
return None
try:
value = json.loads(text)
except json.JSONDecodeError:
return None
if not isinstance(value, dict) or not isinstance(value.get("name"), str):
return None
return value
def read_telemetry(paths: Iterable[str]) -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
for value in paths:
if value == "-":
lines = sys.stdin
records.extend(
record
for line in lines
if (record := parse_telemetry_line(line)) is not None
)
continue
with Path(value).open("r", encoding="utf-8", errors="replace") as source:
records.extend(
record
for line in source
if (record := parse_telemetry_line(line)) is not None
)
return records
def summarize_telemetry(records: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
latencies: dict[str, list[float]] = {}
statuses: dict[str, int] = {}
event_hashes: set[str] = set()
for record in records:
name = str(record.get("name", "unknown"))[:80]
status = str(record.get("status", "unknown"))[:24]
statuses[f"{name}:{status}"] = statuses.get(f"{name}:{status}", 0) + 1
event_hash = record.get("event_hash")
if isinstance(event_hash, str) and event_hash:
event_hashes.add(event_hash)
elapsed = record.get("elapsed_ms")
if (
isinstance(elapsed, (int, float))
and not isinstance(elapsed, bool)
and math.isfinite(float(elapsed))
and float(elapsed) >= 0
):
latencies.setdefault(name, []).append(float(elapsed))
stages = [
{"name": name, **summarize_ms(values)}
for name, values in latencies.items()
]
stages.sort(
key=lambda value: (
-float(value["p95_ms"]),
str(value["name"]),
)
)
return {
"records": len(records),
"events": len(event_hashes),
"stages": stages,
"statuses": dict(sorted(statuses.items())),
}
def build_report(
*,
iterations: int,
warmups: int,
include_local: bool,
include_host: bool,
telemetry_paths: Sequence[str],
) -> dict[str, Any]:
benchmarks: list[dict[str, Any]] = []
if include_local:
benchmarks.extend(benchmark_ocr(iterations, warmups))
benchmarks.extend(benchmark_edge(iterations, warmups))
if include_host:
benchmarks.extend(benchmark_host(iterations, warmups))
report: dict[str, Any] = {
"schema_version": REPORT_SCHEMA_VERSION,
"generated_at": datetime.now(timezone.utc).isoformat(
timespec="seconds"
),
"version": (PROJECT_ROOT / "VERSION").read_text(
encoding="utf-8"
).strip(),
"environment": {
"platform": platform.platform(),
"python": platform.python_version(),
"machine": platform.machine(),
},
"parameters": {
"iterations": iterations,
"warmups": warmups,
"host_enabled": include_host,
},
"configured_latency_windows_ms": {
"damage_debounce": round(
ocr.DAMAGE_DEBOUNCE_SECONDS * 1000
),
"damage_max_burst": round(
ocr.DAMAGE_MAX_BURST_SECONDS * 1000
),
"edge_idle_worker_poll": 1000,
"follow_latest_settle": round(
ocr.FOLLOW_LATEST_SETTLE_SECONDS * 1000
),
"ocr_responder_fallback_poll": 1000,
"window_layout_settle": round(
ocr.WINDOW_LAYOUT_SETTLE_SECONDS * 1000
),
},
"benchmarks": benchmarks,
}
if telemetry_paths:
report["telemetry"] = summarize_telemetry(
read_telemetry(telemetry_paths)
)
return report
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--iterations", type=int, default=50)
parser.add_argument("--warmups", type=int, default=5)
parser.add_argument(
"--skip-host",
action="store_true",
help="Skip signed Extension Host process measurements.",
)
parser.add_argument(
"--telemetry",
action="append",
default=[],
metavar="PATH",
help="Read a telemetry log; use '-' for standard input.",
)
parser.add_argument(
"--telemetry-only",
action="store_true",
help="Analyze telemetry without running local microbenchmarks.",
)
parser.add_argument("--output", type=Path)
args = parser.parse_args(argv)
if args.iterations < 1:
parser.error("--iterations must be positive")
if args.warmups < 0:
parser.error("--warmups cannot be negative")
if args.telemetry_only and not args.telemetry:
parser.error("--telemetry-only requires at least one --telemetry path")
report = build_report(
iterations=args.iterations,
warmups=args.warmups,
include_local=not args.telemetry_only,
include_host=not args.skip_host,
telemetry_paths=args.telemetry,
)
payload = json.dumps(
report,
ensure_ascii=False,
indent=2,
sort_keys=True,
)
if args.output is not None:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(payload + "\n", encoding="utf-8")
print(payload)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Atomically switch an extension state root to its previous verified release."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [
str(ROOT / "packages" / "jwebot-sdk" / "src"),
str(ROOT / "packages" / "jwebot-kernel" / "src"),
]
from jwebot_kernel import ( # noqa: E402
ActivationError,
ReleaseError,
rollback_release,
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--state-root", type=Path, required=True)
parser.add_argument("--allowed-signers", type=Path)
parser.add_argument("--publisher")
parser.add_argument("--allow-unsigned-local", action="store_true")
args = parser.parse_args()
if args.allow_unsigned_local:
if args.allowed_signers is not None or args.publisher is not None:
parser.error(
"--allow-unsigned-local cannot include publisher trust options"
)
elif args.allowed_signers is None:
parser.error("signed rollback requires --allowed-signers")
try:
destination = rollback_release(
args.state_root,
allowed_signers=args.allowed_signers,
expected_publisher=args.publisher,
allow_unsigned=args.allow_unsigned_local,
)
except (ActivationError, ReleaseError) as error:
parser.error(str(error))
print(
json.dumps(
{
"rolled_back": True,
"release_dir": str(destination),
"current": str(args.state_root / "current"),
},
ensure_ascii=False,
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Sign one canonical JWeBot release record with an OpenSSH publisher key."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [
str(ROOT / "packages" / "jwebot-sdk" / "src"),
str(ROOT / "packages" / "jwebot-kernel" / "src"),
]
from jwebot_kernel import ReleaseError, sign_release_record # noqa: E402
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("release", type=Path)
parser.add_argument("--artifact", type=Path)
parser.add_argument("--private-key", type=Path, required=True)
parser.add_argument("--publisher", required=True)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
try:
signature_path, signature = sign_release_record(
args.release,
args.private_key,
args.publisher,
artifact=args.artifact,
output=args.output,
)
except ReleaseError as error:
parser.error(str(error))
print(
json.dumps(
{
**signature.to_mapping(),
"signature_path": str(signature_path),
},
ensure_ascii=False,
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env python3
"""Generate or verify all reviewed local extension adapter packages."""
from __future__ import annotations
import argparse
import json
import shutil
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OFFICIAL_SRC = (
ROOT
/ "packages"
/ "jwebot-official-extensions"
/ "src"
)
SDK_SRC = ROOT / "packages" / "jwebot-sdk" / "src"
sys.path[:0] = [str(SDK_SRC), str(OFFICIAL_SRC)]
from jwebot_official_extensions import LOCAL_EXTENSIONS # noqa: E402
LEGACY_EXTENSION_DIRS = (
ROOT / "extensions" / "official" / "core-help",
ROOT / "extensions" / "official" / "group-insights",
ROOT / "extensions" / "official" / "local-operations",
ROOT / "extensions" / "official" / "random-games",
ROOT / "extensions" / "official" / "session-games",
ROOT / "extensions" / "official" / "steam-presence",
)
def module_name(extension_id: str) -> str:
return extension_id.replace("-", "_")
def manifest_text(descriptor) -> str:
command_values = ", ".join(
json.dumps(command, ensure_ascii=False)
for command in descriptor.commands
)
help_values = ", ".join(
json.dumps(entry, ensure_ascii=False)
for entry in descriptor.help_entries
)
extension_id = descriptor.extension_id
return f"""id = "{extension_id}"
author = "{descriptor.author}"
description = {json.dumps(descriptor.description, ensure_ascii=False)}
help = [{help_values}]
version = "1.0.0"
api_version = "1"
entrypoint = "{module_name(extension_id)}:plugin"
type = "command"
[capabilities]
commands = [{command_values}]
storage = false
network_profiles = []
service_profiles = ["{extension_id}"]
"""
def module_text(descriptor) -> str:
extension_id = descriptor.extension_id
return f'''"""Generated reviewed adapter for {extension_id}."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = {extension_id!r}
AUTHOR = {descriptor.author!r}
DESCRIPTION = {descriptor.description!r}
HELP_ENTRIES = {descriptor.help_entries!r}
COMMANDS = {descriptor.commands!r}
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
PROFILE,
operation="execute",
payload={{
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
}},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={{"local_extension": PROFILE, "author": AUTHOR}},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
'''
def readme_text(descriptor) -> str:
command_list = "\n".join(
f"- `{command}`"
for command in descriptor.commands
)
return f"""# {descriptor.extension_id}
Author: `{descriptor.author}`
{descriptor.description}
Help:
{chr(10).join(f"- `{entry}`" for entry in descriptor.help_entries)}
Registered commands:
{command_list}
The adapter has no direct Ledger, GUI, environment, shell, or network access.
It delegates to the reviewed `{descriptor.extension_id}` Kernel service profile
and returns a structured `ReplyPlan`.
"""
def test_text(descriptor) -> str:
module = module_name(descriptor.extension_id)
first_command = descriptor.commands[0]
return f'''import sys
import unittest
from pathlib import Path
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from {module} import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
class PluginTests(unittest.TestCase):
def test_registers_declared_commands_and_author(self):
context = mock.Mock()
plugin(context)
registered = [
call.args[0]
for call in context.commands.register.call_args_list
]
self.assertEqual(AUTHOR, {descriptor.author!r})
self.assertEqual(DESCRIPTION, {descriptor.description!r})
self.assertEqual(HELP_ENTRIES, {descriptor.help_entries!r})
self.assertIn({first_command!r}, registered)
self.assertEqual(len(registered), {len(descriptor.commands)})
if __name__ == "__main__":
unittest.main()
'''
def collection_readme(author: str) -> str:
if author == "official":
description = (
"Official-owned reviewed extensions. Person memory is the only "
"official command plugin in the current inventory."
)
else:
description = (
"Reviewed local extensions maintained by the JWeBot project. Each "
"independently "
"understandable function is packaged separately; dice and dice "
"challenge intentionally remain one complete feature collection."
)
return f"""# {author}
{description}
Every child directory is generated from the authoritative local extension
ownership inventory.
"""
def expected_files() -> dict[Path, str]:
values = {
ROOT / "extensions" / "official" / "README.md": collection_readme(
"official"
),
ROOT / "extensions" / "jwebot" / "README.md": collection_readme(
"jwebot"
),
}
for descriptor in LOCAL_EXTENSIONS:
root = (
ROOT
/ "extensions"
/ descriptor.directory
/ descriptor.extension_id
)
module = module_name(descriptor.extension_id)
values.update(
{
root / "plugin.toml": manifest_text(descriptor),
root / "src" / f"{module}.py": module_text(descriptor),
(
OFFICIAL_SRC
/ "jwebot_official_extensions"
/ "adapters"
/ f"{module}.py"
): module_text(descriptor),
root / "tests" / "test_plugin.py": test_text(descriptor),
root / "README.md": readme_text(descriptor),
}
)
return values
def stale_paths(expected: set[Path]) -> list[Path]:
stale = [
path
for path in LEGACY_EXTENSION_DIRS
if path.exists() and path not in expected
]
adapters = (
OFFICIAL_SRC
/ "jwebot_official_extensions"
/ "adapters"
)
stale.extend(
path
for path in adapters.glob("*.py")
if path.name != "__init__.py" and path not in expected
)
return sorted(stale)
def synchronize(*, check: bool) -> list[Path]:
expected = expected_files()
changed: list[Path] = []
for path, content in expected.items():
current = (
path.read_text(encoding="utf-8")
if path.is_file()
else None
)
if current == content:
continue
changed.append(path)
if not check:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
stale = stale_paths(set(expected))
changed.extend(stale)
if not check:
for path in stale:
if path.is_dir():
shutil.rmtree(path)
else:
path.unlink()
return changed
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
changed = synchronize(check=args.check)
if args.check and changed:
for path in changed:
print(path.relative_to(ROOT), file=sys.stderr)
return 1
for path in changed:
print(path.relative_to(ROOT))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Validate one or more JWeBot plugin manifests."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [
str(ROOT / "packages" / "jwebot-sdk" / "src"),
str(ROOT / "packages" / "jwebot-kernel" / "src"),
]
from jwebot_kernel import ( # noqa: E402
ManifestError,
SourcePolicyError,
load_manifest,
validate_source_tree,
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("manifest", nargs="+", type=Path)
args = parser.parse_args()
results: list[dict[str, object]] = []
for path in args.manifest:
try:
manifest = load_manifest(path)
source_paths = validate_source_tree(path.parent, manifest)
except (ManifestError, SourcePolicyError) as error:
print(
json.dumps(
{"path": str(path), "valid": False, "error": str(error)},
ensure_ascii=False,
sort_keys=True,
)
)
return 1
results.append(
{
"path": str(path),
"valid": True,
"id": manifest.plugin_id,
"author": manifest.author,
"version": manifest.version,
"api_version": manifest.api_version,
"type": manifest.plugin_type.value,
"commands": list(manifest.capabilities.commands),
"network_profiles": list(
manifest.capabilities.network_profiles
),
"service_profiles": list(
manifest.capabilities.service_profiles
),
"source_files": len(source_paths),
}
)
for result in results:
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Verify a JWeBot extension artifact without extracting it."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [
str(ROOT / "packages" / "jwebot-sdk" / "src"),
str(ROOT / "packages" / "jwebot-kernel" / "src"),
]
from jwebot_kernel import ( # noqa: E402
ReleaseError,
load_release_record,
verify_release,
verify_release_signature,
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("artifact", type=Path)
parser.add_argument("release", type=Path)
parser.add_argument("--signature", type=Path)
parser.add_argument("--allowed-signers", type=Path)
parser.add_argument("--publisher")
args = parser.parse_args()
if (args.signature is None) != (args.allowed_signers is None):
parser.error("--signature and --allowed-signers must be provided together")
if args.publisher is not None and args.signature is None:
parser.error("--publisher requires --signature and --allowed-signers")
try:
record = load_release_record(args.release)
verify_release(args.artifact, record)
signature = None
if args.signature is not None:
signature = verify_release_signature(
args.release,
args.signature,
args.allowed_signers,
expected_publisher=args.publisher,
)
except ReleaseError as error:
parser.error(str(error))
print(
json.dumps(
{
"valid": True,
"plugin_id": record.plugin_id,
"author": record.author,
"plugin_version": record.plugin_version,
"source_digest": record.source_digest,
"artifact_sha256": record.artifact_sha256,
"signature_valid": signature is not None,
"publisher": (
signature.publisher if signature is not None else None
),
},
ensure_ascii=False,
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())