release: public alpha 0.6.0 baseline
This commit is contained in:
Executable
+92
@@ -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())
|
||||
Executable
+90
@@ -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
|
||||
Executable
+48
@@ -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
|
||||
Executable
+61
@@ -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
|
||||
Executable
+124
@@ -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
|
||||
Executable
+278
@@ -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())
|
||||
Executable
+233
@@ -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())
|
||||
Executable
+20
@@ -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())
|
||||
Executable
+70
@@ -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())
|
||||
Executable
+764
@@ -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())
|
||||
Executable
+63
@@ -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())
|
||||
Executable
+53
@@ -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())
|
||||
Executable
+286
@@ -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())
|
||||
Executable
+69
@@ -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())
|
||||
Executable
+73
@@ -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())
|
||||
Reference in New Issue
Block a user