720 lines
28 KiB
Bash
Executable File
720 lines
28 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
repo_root="${1:-$(pwd)}"
|
||
output_root="${2:-$repo_root/artifacts}"
|
||
package_version="${LSERP_PACKAGE_VERSION:-0.4.0}"
|
||
if [[ ! "$package_version" =~ ^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}$ ]]; then
|
||
echo 'LSERP_PACKAGE_VERSION must be a three-part numeric version.' >&2
|
||
exit 3
|
||
fi
|
||
dotnet6="${LSERP_DOTNET6:-dotnet}"
|
||
dotnet8="${LSERP_DOTNET8:-dotnet}"
|
||
python_bin="${LSERP_PYTHON:-python3}"
|
||
pwsh_bin="${LSERP_PWSH:-pwsh}"
|
||
npm_bin="${LSERP_NPM:-npm}"
|
||
astrbot_contract_python="${LSERP_ASTRBOT_CONTRACT_PYTHON:-}"
|
||
astrbot_source="${LSERP_ASTRBOT_SOURCE:-}"
|
||
host_certificate_thumbprint="${LSERP_HOST_CERT_THUMBPRINT:-}"
|
||
host_timestamp_url="${LSERP_HOST_TIMESTAMP_URL:-}"
|
||
host_certificate_store="${LSERP_HOST_CERT_STORE:-CurrentUser}"
|
||
host_signtool_path="${LSERP_HOST_SIGNTOOL:-}"
|
||
host_signing_requested=false
|
||
if [[ -z "$astrbot_contract_python" || ! -f "$astrbot_contract_python" || \
|
||
-z "$astrbot_source" || ! -d "$astrbot_source" ]]; then
|
||
echo 'Commercial packaging requires LSERP_ASTRBOT_CONTRACT_PYTHON and LSERP_ASTRBOT_SOURCE for the reviewed AstrBot 4.27.2 checkout.' >&2
|
||
exit 3
|
||
fi
|
||
if [[ -n "$host_certificate_thumbprint" || -n "$host_timestamp_url" || \
|
||
-n "$host_signtool_path" ]]; then
|
||
if [[ ! "$host_certificate_thumbprint" =~ ^[A-Fa-f0-9]{40}$ || \
|
||
! "$host_timestamp_url" =~ ^https:// || \
|
||
( "$host_certificate_store" != CurrentUser && \
|
||
"$host_certificate_store" != LocalMachine ) ]]; then
|
||
echo 'Host signing parameters are incomplete or invalid.' >&2
|
||
exit 3
|
||
fi
|
||
host_signing_requested=true
|
||
fi
|
||
|
||
repo_root="$(cd "$repo_root" && pwd)"
|
||
mkdir -p "$output_root"
|
||
output_root="$(cd "$output_root" && pwd)"
|
||
package_name="Lserp-AgentPet-${package_version}-win-x64"
|
||
final_directory="$output_root/$package_name"
|
||
final_archive="$output_root/$package_name.zip"
|
||
if [[ -e "$final_directory" || -e "$final_archive" ]]; then
|
||
echo "Refusing to overwrite an existing package: $package_name" >&2
|
||
exit 2
|
||
fi
|
||
|
||
temporary_root="$(mktemp -d "${TMPDIR:-/tmp}/lserp-commercial-package.XXXXXX")"
|
||
cleanup() {
|
||
rm -rf -- "$temporary_root"
|
||
}
|
||
trap cleanup EXIT
|
||
logs="$temporary_root/logs"
|
||
stage="$temporary_root/$package_name"
|
||
mkdir -p "$logs" "$stage/Host" "$stage/AstrBotPlugin" \
|
||
"$stage/Deployment/SqlServer" "$stage/Deployment/customer-profiles" \
|
||
"$stage/PythonWheels" "$stage/Contracts"
|
||
|
||
plugin_root="$repo_root/插件库/astrbot_plugin_lserp"
|
||
python_test_venv="$temporary_root/python-test-venv"
|
||
python_test_wheels="$temporary_root/python-test-wheels"
|
||
mkdir -p "$python_test_wheels"
|
||
"$python_bin" -m pip download \
|
||
--only-binary=:all: \
|
||
--require-hashes \
|
||
--dest "$python_test_wheels" \
|
||
--requirement "$plugin_root/requirements.txt"
|
||
"$python_bin" -m pip download \
|
||
--only-binary=:all: \
|
||
--platform win_amd64 \
|
||
--require-hashes \
|
||
--dest "$stage/PythonWheels" \
|
||
--requirement "$plugin_root/requirements.txt"
|
||
"$python_bin" -m venv "$python_test_venv"
|
||
if [[ -f "$python_test_venv/bin/python" ]]; then
|
||
test_python="$python_test_venv/bin/python"
|
||
elif [[ -f "$python_test_venv/Scripts/python.exe" ]]; then
|
||
test_python="$python_test_venv/Scripts/python.exe"
|
||
else
|
||
echo 'Isolated Python test runtime was not created.' >&2
|
||
exit 3
|
||
fi
|
||
"$test_python" -m pip install \
|
||
--no-index \
|
||
--require-hashes \
|
||
--find-links "$python_test_wheels" \
|
||
--requirement "$plugin_root/requirements.txt"
|
||
"$test_python" -m pip check
|
||
|
||
astrbot_contract_evidence="$logs/astrbot-runtime-contract.json"
|
||
"$astrbot_contract_python" "$plugin_root/verify_astrbot_contract.py" \
|
||
--astrbot-source "$astrbot_source" \
|
||
--output "$astrbot_contract_evidence" \
|
||
2>&1 | tee "$logs/astrbot-runtime-contract.log"
|
||
if [[ ! -s "$astrbot_contract_evidence" ]]; then
|
||
echo 'Actual AstrBot runtime contract evidence was not created.' >&2
|
||
exit 4
|
||
fi
|
||
|
||
"$dotnet6" run --project "$repo_root/插件库/Lskj.CommandKernel.Tests/Lskj.CommandKernel.Tests.csproj" -c Release 2>&1 | tee "$logs/kernel.log"
|
||
"$dotnet8" build \
|
||
"$repo_root/插件库/Lskj.LegacyApiCompatibility.Tests/Lskj.LegacyApiCompatibility.Tests.csproj" \
|
||
-c Release 2>&1 | tee "$logs/legacy-api.log"
|
||
"$dotnet8" run \
|
||
--project "$repo_root/插件库/Lskj.SqlContract.Tests/Lskj.SqlContract.Tests.csproj" \
|
||
-c Release -- "$repo_root" 2>&1 | tee "$logs/sql-contract.log"
|
||
"$dotnet8" run --project "$repo_root/插件库/Lskj.AgentPet.Host.Tests/Lskj.AgentPet.Host.Tests.csproj" -c Release 2>&1 | tee "$logs/host.log"
|
||
(cd "$repo_root/插件库/Lskj.AgentPet" && "$npm_bin" test) 2>&1 | tee "$logs/node.log"
|
||
(cd "$plugin_root" && "$test_python" -m unittest discover -s tests -v) 2>&1 | tee "$logs/python.log"
|
||
if grep -Eq 'skipped=[1-9][0-9]*' "$logs/python.log"; then
|
||
echo 'Commercial AstrBot verification must not skip worker tests.' >&2
|
||
exit 4
|
||
fi
|
||
"$pwsh_bin" -NoLogo -NoProfile -File \
|
||
"$repo_root/插件库/Lskj.AgentBridge/Deployment/CommercialPackage/Test-DeploymentContracts.ps1" \
|
||
-RepoRoot "$repo_root" 2>&1 | tee "$logs/deployment.log"
|
||
host_publish="$temporary_root/host-publish"
|
||
"$dotnet8" publish \
|
||
"$repo_root/插件库/Lskj.AgentPet.Host/Lskj.AgentPet.Host.csproj" \
|
||
-c Release \
|
||
-p:PublishProfile=WinX64 \
|
||
--output "$host_publish" 2>&1 | tee "$logs/publish.log"
|
||
|
||
if [[ ! -s "$host_publish/Lskj.AgentPet.Host.exe" ]]; then
|
||
echo 'Windows host publish output is missing.' >&2
|
||
exit 3
|
||
fi
|
||
bridge_cli_publish="$temporary_root/bridge-cli-publish"
|
||
"$dotnet8" publish \
|
||
"$repo_root/插件库/Lskj.BridgeCli/Lskj.BridgeCli.csproj" \
|
||
-c Release \
|
||
-r win-x64 \
|
||
--self-contained true \
|
||
-p:Version="$package_version" \
|
||
-p:PublishSingleFile=true \
|
||
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||
-p:EnableCompressionInSingleFile=true \
|
||
-p:PublishTrimmed=false \
|
||
-p:PublishReadyToRun=false \
|
||
-p:DebugType=None \
|
||
-p:DebugSymbols=false \
|
||
--output "$bridge_cli_publish" 2>&1 | tee "$logs/bridge-cli-publish.log"
|
||
if [[ ! -s "$bridge_cli_publish/lserp-agent-cli.exe" ]] || \
|
||
find "$bridge_cli_publish" -mindepth 1 -maxdepth 1 \
|
||
! -name 'lserp-agent-cli.exe' | grep -q .; then
|
||
echo 'Windows bridge-only CLI must be one self-contained executable.' >&2
|
||
exit 3
|
||
fi
|
||
if [[ -e "$host_publish/lserp-agent-cli.exe" ]]; then
|
||
echo 'Host publish unexpectedly contains the bridge CLI name.' >&2
|
||
exit 3
|
||
fi
|
||
cp "$bridge_cli_publish/lserp-agent-cli.exe" "$host_publish/"
|
||
if [[ ! -s "$host_publish/lserp-agent-cli.exe" ]]; then
|
||
echo 'Merged commercial bridge-only CLI is missing.' >&2
|
||
exit 3
|
||
fi
|
||
host_authenticode_signed=false
|
||
if [[ "$host_signing_requested" == true ]]; then
|
||
sign_arguments=(
|
||
-NoLogo -NoProfile -File
|
||
"$repo_root/插件库/Lskj.AgentBridge/Deployment/Sign-LserpAgentPetHost.ps1"
|
||
-HostDirectory "$host_publish"
|
||
-CertificateThumbprint "$host_certificate_thumbprint"
|
||
-CertificateStoreLocation "$host_certificate_store"
|
||
-TimestampUrl "$host_timestamp_url"
|
||
)
|
||
if [[ -n "$host_signtool_path" ]]; then
|
||
sign_arguments+=( -SignToolPath "$host_signtool_path" )
|
||
fi
|
||
"$pwsh_bin" "${sign_arguments[@]}" 2>&1 | tee "$logs/host-signing.log"
|
||
host_authenticode_signed=true
|
||
fi
|
||
if [[ -e "$host_publish/Web" ]] || \
|
||
find "$host_publish" -maxdepth 1 -type f \( \
|
||
-name 'index.html' -o -name 'pet.css' -o -name 'pet-runtime.js' \
|
||
-o -name 'bridge-client.js' -o -name 'pet-shell.js' \) | grep -q .; then
|
||
echo 'Commercial host publish must not contain mutable external Web assets.' >&2
|
||
exit 3
|
||
fi
|
||
cp -R "$host_publish/." "$stage/Host/"
|
||
|
||
astrbot_plugin_files=(
|
||
README.md
|
||
__init__.py
|
||
_conf_schema.json
|
||
astrbot-contract.json
|
||
astrbot_contract.py
|
||
attachment_extract.py
|
||
attachment_provenance.py
|
||
attachment_sandbox.py
|
||
attachment_worker.py
|
||
bridge_protocol.py
|
||
main.py
|
||
metadata.yaml
|
||
pdf_render_sandbox.py
|
||
pdf_render_worker.py
|
||
pdf_vision.py
|
||
plan_chain.py
|
||
prompt.py
|
||
purchase_tabular_binding.py
|
||
purchase_vision_binding.py
|
||
requirements.txt
|
||
session_auth.py
|
||
tools.py
|
||
verify_astrbot_contract.py
|
||
verify_minimax_vlm_contract.py
|
||
vision.py
|
||
)
|
||
for relative in "${astrbot_plugin_files[@]}"; do
|
||
source="$plugin_root/$relative"
|
||
if [[ ! -f "$source" || -L "$source" ]]; then
|
||
echo "Required AstrBot plugin source is missing or linked: $relative" >&2
|
||
exit 3
|
||
fi
|
||
cp "$source" "$stage/AstrBotPlugin/$relative"
|
||
done
|
||
|
||
deployment_root="$repo_root/插件库/Lskj.AgentBridge/Deployment"
|
||
cp "$deployment_root/CUSTOMER_ACCEPTANCE.md" "$stage/Deployment/"
|
||
cp "$deployment_root/WRITE_ACCEPTANCE.md" "$stage/Deployment/"
|
||
cp "$deployment_root/DYNAMIC_MODULE_WRITE_ACCEPTANCE.md" "$stage/Deployment/"
|
||
cp "$deployment_root/FIELD_VALIDATION_RUNBOOK.md" "$stage/Deployment/"
|
||
cp "$deployment_root/THIRD_PARTY_COMPLIANCE.md" "$stage/Deployment/"
|
||
cp "$deployment_root/guga-upstream-audit.v1.json" "$stage/Deployment/"
|
||
cp "$deployment_root/New-WorkflowAcceptanceEvidence.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/New-DynamicModuleWriteAcceptance.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/dynamic-module-write-modules.example.json" "$stage/Deployment/"
|
||
cp "$deployment_root/New-DynamicModuleUpdateAcceptance.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/dynamic-module-update-modules.example.json" "$stage/Deployment/"
|
||
cp "$deployment_root/New-WorkflowWriteCasesTemplate.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/New-WorkflowWriteIntegrationEvidence.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/Invoke-WorkflowWriteCaseCapture.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/Invoke-LserpFieldReadOnlyValidation.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/field-readonly-validation.example.json" "$stage/Deployment/"
|
||
cp "$deployment_root/Invoke-LserpReadOnlySessionPreflight.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/Invoke-LserpSelectOnlyCatalogSnapshot.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/Invoke-LserpSelectOnlyProfilePreflight.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/New-WorkflowUatAuthorization.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/New-WorkflowWriteUatCampaign.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/Test-WorkflowWriteUatCampaign.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/workflow-write-uat-case-catalog.v1.json" "$stage/Deployment/"
|
||
cp "$deployment_root/New-CustomerAcceptanceBundle.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/Build-LegacyErpAcceptance.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/Sign-LserpAgentPetHost.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/CommercialPackage/Test-DeploymentContracts.ps1" "$stage/Deployment/"
|
||
cp "$deployment_root/business-adapters.example.json" "$stage/Deployment/"
|
||
cp "$deployment_root/command-rollout.example.json" "$stage/Deployment/"
|
||
cp "$deployment_root/SqlServer/"*.sql "$stage/Deployment/SqlServer/"
|
||
cp "$deployment_root/customer-profiles/"*.json "$stage/Deployment/customer-profiles/"
|
||
cp "$deployment_root/customer-profiles/"*.sql "$stage/Deployment/customer-profiles/"
|
||
cp "$deployment_root/customer-profiles/README.md" "$stage/Deployment/customer-profiles/"
|
||
cp "$deployment_root/CommercialPackage/README.md" "$stage/README.md"
|
||
cp "$deployment_root/CommercialPackage/Start-LserpAgentPet.ps1" "$stage/Start-LserpAgentPet.ps1"
|
||
cp "$deployment_root/CommercialPackage/Verify-LserpCommercialPackage.ps1" "$stage/Verify-LserpCommercialPackage.ps1"
|
||
cp "$repo_root/插件库/Contracts/erp-agent-wire-contract-v1.json" "$stage/Contracts/"
|
||
|
||
# The package version is also the version reported by the bundled bridge CLI.
|
||
# Keep customer-facing runbooks and handoff templates bound to that exact
|
||
# version; a source checkout may be used to produce more than one package
|
||
# version, so these documents must not carry a stale release number.
|
||
"$python_bin" - "$stage" "$package_version" <<'PY'
|
||
import json
|
||
import pathlib
|
||
import sys
|
||
|
||
stage = pathlib.Path(sys.argv[1])
|
||
package_version = sys.argv[2]
|
||
marker = "@LSERP_PACKAGE_VERSION@"
|
||
relative_paths = [
|
||
"README.md",
|
||
"Deployment/CUSTOMER_ACCEPTANCE.md",
|
||
"Deployment/WRITE_ACCEPTANCE.md",
|
||
"Deployment/field-readonly-validation.example.json",
|
||
]
|
||
for relative in relative_paths:
|
||
path = stage / relative
|
||
if not path.is_file():
|
||
raise SystemExit("package version binding source is missing: " + relative)
|
||
text = path.read_text(encoding="utf-8")
|
||
if marker not in text:
|
||
raise SystemExit("package version marker is missing: " + relative)
|
||
rendered = text.replace(marker, package_version)
|
||
if marker in rendered:
|
||
raise SystemExit("package version marker was not fully rendered: " + relative)
|
||
path.write_text(rendered, encoding="utf-8")
|
||
|
||
template = json.loads(
|
||
(stage / "Deployment/field-readonly-validation.example.json")
|
||
.read_text(encoding="utf-8")
|
||
)
|
||
expected_cli_version = template.get("cli", {}).get("version")
|
||
expected_cli_path = template.get("cli", {}).get("path")
|
||
if expected_cli_version != package_version or package_version not in expected_cli_path:
|
||
raise SystemExit("rendered field validation template is not package-version bound")
|
||
PY
|
||
|
||
git_commit="unknown"
|
||
worktree_dirty=true
|
||
if git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||
git_commit="$(git -C "$repo_root" rev-parse HEAD)"
|
||
if [[ -z "$(git -C "$repo_root" status --porcelain)" ]]; then
|
||
worktree_dirty=false
|
||
fi
|
||
fi
|
||
export LSERP_STAGE="$stage"
|
||
export LSERP_LOGS="$logs"
|
||
export LSERP_PACKAGE_VERSION_VALUE="$package_version"
|
||
export LSERP_GIT_COMMIT_VALUE="$git_commit"
|
||
export LSERP_WORKTREE_DIRTY_VALUE="$worktree_dirty"
|
||
export LSERP_HOST_AUTHENTICODE_SIGNED_VALUE="$host_authenticode_signed"
|
||
export LSERP_HOST_CERT_THUMBPRINT_VALUE="$host_certificate_thumbprint"
|
||
export LSERP_ASTRBOT_CONTRACT_EVIDENCE="$astrbot_contract_evidence"
|
||
"$python_bin" - <<'PY'
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import pathlib
|
||
import re
|
||
import sys
|
||
from datetime import datetime, timezone
|
||
|
||
stage = pathlib.Path(os.environ["LSERP_STAGE"])
|
||
logs = pathlib.Path(os.environ["LSERP_LOGS"])
|
||
|
||
def parse_pair(name: str) -> tuple[int, int]:
|
||
text = (logs / name).read_text(encoding="utf-8", errors="replace")
|
||
matches = re.findall(r"passed=(\d+) failed=(\d+)", text)
|
||
if not matches:
|
||
raise SystemExit(f"missing test summary in {name}")
|
||
return tuple(map(int, matches[-1]))
|
||
|
||
kernel = parse_pair("kernel.log")
|
||
host = parse_pair("host.log")
|
||
deployment = parse_pair("deployment.log")
|
||
sql_contract = parse_pair("sql-contract.log")
|
||
node_text = (logs / "node.log").read_text(encoding="utf-8", errors="replace")
|
||
python_text = (logs / "python.log").read_text(encoding="utf-8", errors="replace")
|
||
sql_contract_text = (logs / "sql-contract.log").read_text(
|
||
encoding="utf-8", errors="replace"
|
||
)
|
||
node_match = re.findall(
|
||
r"(?m)^\s*(?:#|ℹ)\s+tests\s+(\d+)\s*$",
|
||
node_text,
|
||
)
|
||
python_match = re.findall(r"Ran (\d+) tests", python_text)
|
||
if not node_match or not python_match:
|
||
raise SystemExit("missing Node or Python test summary")
|
||
python_skipped_match = re.findall(r"skipped=(\d+)", python_text)
|
||
python_skipped = int(python_skipped_match[-1]) if python_skipped_match else 0
|
||
if python_skipped != 0:
|
||
raise SystemExit("commercial AstrBot verification contains skipped tests")
|
||
|
||
astrbot_contract = json.loads(
|
||
pathlib.Path(os.environ["LSERP_ASTRBOT_CONTRACT_EVIDENCE"]).read_text(
|
||
encoding="utf-8"
|
||
)
|
||
)
|
||
expected_astrbot_contract_keys = {
|
||
"schemaVersion",
|
||
"passed",
|
||
"repository",
|
||
"sourceTag",
|
||
"sourceCommit",
|
||
"runtimeVersion",
|
||
"versionSpecifier",
|
||
"pluginVersion",
|
||
"licenseExpression",
|
||
"licenseSha256",
|
||
"eulaSha256",
|
||
"criticalSourceFilesVerified",
|
||
"registeredTools",
|
||
}
|
||
if (
|
||
not isinstance(astrbot_contract, dict)
|
||
or set(astrbot_contract) != expected_astrbot_contract_keys
|
||
or astrbot_contract["schemaVersion"] != "1.1"
|
||
or astrbot_contract["passed"] is not True
|
||
or astrbot_contract["repository"]
|
||
!= "https://github.com/AstrBotDevs/AstrBot.git"
|
||
or astrbot_contract["sourceTag"] != "v4.27.2"
|
||
or astrbot_contract["sourceCommit"]
|
||
!= "ad4fbfa90ca0c4ac2b30b3250e34dbf8fe7babbf"
|
||
or astrbot_contract["runtimeVersion"] != "4.27.2"
|
||
or astrbot_contract["versionSpecifier"] != "==4.27.2"
|
||
or astrbot_contract["pluginVersion"] != "0.4.0"
|
||
or astrbot_contract["licenseExpression"] != "AGPL-3.0-or-later"
|
||
or astrbot_contract["licenseSha256"]
|
||
!= "ccf7d08f932af3e813848881731113afbb7c80d0fd6d958e8d319002bf344d02"
|
||
or astrbot_contract["eulaSha256"]
|
||
!= "c332de7781e87c67d6d3beda463fa04705075a6bae9e52a252f7c639f6defd80"
|
||
or astrbot_contract["criticalSourceFilesVerified"] != 20
|
||
or astrbot_contract["registeredTools"]
|
||
!= ["erp_get_context", "erp_get_capabilities", "erp_plan_command"]
|
||
):
|
||
raise SystemExit("actual AstrBot runtime contract evidence is invalid")
|
||
|
||
guga_audit_path = stage / "Deployment" / "guga-upstream-audit.v1.json"
|
||
guga_audit_bytes = guga_audit_path.read_bytes()
|
||
guga_audit = json.loads(guga_audit_bytes.decode("utf-8"))
|
||
asset = guga_audit.get("asset") if isinstance(guga_audit, dict) else None
|
||
installer = guga_audit.get("installer") if isinstance(guga_audit, dict) else None
|
||
service_source = (
|
||
guga_audit.get("serviceSource") if isinstance(guga_audit, dict) else None
|
||
)
|
||
commercial_decision = (
|
||
guga_audit.get("commercialDecision")
|
||
if isinstance(guga_audit, dict)
|
||
else None
|
||
)
|
||
if (
|
||
not isinstance(guga_audit, dict)
|
||
or set(guga_audit) != {
|
||
"schemaVersion",
|
||
"auditedAtUtc",
|
||
"asset",
|
||
"installer",
|
||
"serviceSource",
|
||
"commercialDecision",
|
||
}
|
||
or guga_audit["schemaVersion"] != "1.0"
|
||
or guga_audit["auditedAtUtc"] != "2026-08-13T19:31:24+00:00"
|
||
or not isinstance(asset, dict)
|
||
or set(asset)
|
||
!= {
|
||
"id",
|
||
"displayName",
|
||
"ownerHandle",
|
||
"ownerName",
|
||
"uploadedAtUtc",
|
||
"shareUrl",
|
||
"shareDataUrl",
|
||
"downloadUrl",
|
||
"packageSha256",
|
||
"packageSizeBytes",
|
||
"manifestSha256",
|
||
"spriteSha256",
|
||
"spriteSizeBytes",
|
||
"atlasSize",
|
||
"licenseMetadataPresent",
|
||
"licenseFilePresent",
|
||
}
|
||
or asset["id"] != "guga"
|
||
or asset["ownerHandle"] != "circus"
|
||
or asset["shareUrl"] != "https://codex-pets.net/share/guga"
|
||
or asset["packageSha256"]
|
||
!= "3ebd971ba59a0c988a6be0924669b4c5db9234bcc5d17d506e34eba332e6021f"
|
||
or asset["packageSizeBytes"] != 1946012
|
||
or asset["manifestSha256"]
|
||
!= "f9f715811c26ca610764a7698e28f2e182882f097f4c60a3f00a79dd7530bd20"
|
||
or asset["spriteSha256"]
|
||
!= "1b61ea2af98717b9ebe55beb4c6b820b89e9c42d4fdfeca21cf63ed3ad4e38da"
|
||
or asset["spriteSizeBytes"] != 1945586
|
||
or asset["atlasSize"] != "1536x1872"
|
||
or asset["licenseMetadataPresent"] is not False
|
||
or asset["licenseFilePresent"] is not False
|
||
or not isinstance(installer, dict)
|
||
or set(installer)
|
||
!= {
|
||
"packageName",
|
||
"version",
|
||
"registryUrl",
|
||
"tarballUrl",
|
||
"tarballSha1",
|
||
"tarballSha256",
|
||
"npmIntegrity",
|
||
"declaredLicense",
|
||
"repositoryDeclared",
|
||
"defaultApiBase",
|
||
"installRoot",
|
||
"writtenFiles",
|
||
"assetDigestVerification",
|
||
"assetSignatureVerification",
|
||
"assetLicenseVerification",
|
||
}
|
||
or installer["packageName"] != "codex-pets"
|
||
or installer["version"] != "0.3.0"
|
||
or installer["tarballSha1"] != "82e41349ae63eb9e63099f2e06a56468182e2c90"
|
||
or installer["tarballSha256"]
|
||
!= "9ec8bf1ea09e6d8fdc17b33a594a178a9b20bd3dc6decbb22973758394c9c1c7"
|
||
or installer["npmIntegrity"]
|
||
!= "sha512-b7PjV0phEK7jn0rnyXzh3LMIsAdqSUf75mCdSxZEyIuFScCxwOeUZoDxnWj97rfg4ihk6XLaKvg6fgWD+CWcAQ=="
|
||
or installer["declaredLicense"] != "MIT"
|
||
or installer["repositoryDeclared"] is not False
|
||
or installer["installRoot"] != "$CODEX_HOME/pets/{pet-id}"
|
||
or installer["writtenFiles"] != ["pet.json", "spritesheet.webp"]
|
||
or installer["assetDigestVerification"] is not False
|
||
or installer["assetSignatureVerification"] is not False
|
||
or installer["assetLicenseVerification"] is not False
|
||
or not isinstance(service_source, dict)
|
||
or set(service_source)
|
||
!= {
|
||
"repository",
|
||
"commit",
|
||
"softwareLicense",
|
||
"licenseSha256",
|
||
"termsSourceSha256",
|
||
"termsEffectiveDate",
|
||
"uploadTermsScope",
|
||
}
|
||
or service_source["repository"]
|
||
!= "https://github.com/portons/codex-pet-share"
|
||
or service_source["commit"]
|
||
!= "22725091da2787e8e525c9289cb7826a34be4950"
|
||
or service_source["softwareLicense"] != "MIT"
|
||
or service_source["licenseSha256"]
|
||
!= "13e779572adacb503b7e7a0c676571fcd86114a73f6aa000412c24a9a06a97d3"
|
||
or service_source["termsSourceSha256"]
|
||
!= "70ad12414864566b8cd469a7d2ca39fe60050686cecacc126ff1aca587f790bb"
|
||
or service_source["uploadTermsScope"] != "public-sharing-through-service"
|
||
or not isinstance(commercial_decision, dict)
|
||
or set(commercial_decision)
|
||
!= {"status", "code", "reason", "requiredEvidence"}
|
||
or commercial_decision["status"] != "external-license-required"
|
||
or commercial_decision["code"] != "guga_commercial_license_missing"
|
||
or commercial_decision["requiredEvidence"]
|
||
!= [
|
||
"rights-holder-identity",
|
||
"commercial-product-use",
|
||
"customer-deployment-and-copying",
|
||
"product-display",
|
||
"territory-and-term",
|
||
"asset-sha256-binding",
|
||
"authorized-legal-review",
|
||
]
|
||
):
|
||
raise SystemExit("guga upstream supply-chain audit is invalid")
|
||
guga_audit_sha256 = hashlib.sha256(guga_audit_bytes).hexdigest()
|
||
|
||
banned_guga_hashes = {
|
||
asset["packageSha256"],
|
||
asset["manifestSha256"],
|
||
asset["spriteSha256"],
|
||
}
|
||
for path in stage.rglob("*"):
|
||
if not path.is_file():
|
||
continue
|
||
lower_name = path.name.lower()
|
||
if lower_name == "spritesheet.webp" or lower_name.endswith(".codex-pet.zip"):
|
||
raise SystemExit(
|
||
f"external guga asset must not be bundled: {path.relative_to(stage)}"
|
||
)
|
||
if path.stat().st_size <= 20 * 1024 * 1024:
|
||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||
if digest in banned_guga_hashes:
|
||
raise SystemExit(
|
||
f"external guga asset bytes must not be bundled: {path.relative_to(stage)}"
|
||
)
|
||
|
||
minimum_pass_counts = {
|
||
"commandKernel": 284,
|
||
"desktopHost": 50,
|
||
"petWebUi": 24,
|
||
"astrbotPlugin": 96,
|
||
"deploymentContracts": 76,
|
||
"sqlServerCompatibility100": 16,
|
||
}
|
||
actual_pass_counts = {
|
||
"commandKernel": kernel[0],
|
||
"desktopHost": host[0],
|
||
"petWebUi": int(node_match[-1]),
|
||
"astrbotPlugin": int(python_match[-1]),
|
||
"deploymentContracts": deployment[0],
|
||
"sqlServerCompatibility100": sql_contract[0],
|
||
}
|
||
if kernel[1] != 0 or host[1] != 0 or deployment[1] != 0 or sql_contract[1] != 0:
|
||
raise SystemExit("commercial verification contains failed tests")
|
||
if (
|
||
"parserPackage=Microsoft.SqlServer.TransactSql.ScriptDom "
|
||
"parserVersion=180.59.2 dialect=TSql100"
|
||
) not in sql_contract_text:
|
||
raise SystemExit("commercial SQL Server compatibility parser metadata is invalid")
|
||
for suite, minimum in minimum_pass_counts.items():
|
||
if actual_pass_counts[suite] < minimum:
|
||
raise SystemExit(
|
||
f"commercial verification test baseline regressed: {suite}"
|
||
)
|
||
|
||
report = {
|
||
"schemaVersion": "1.1",
|
||
"packageVersion": os.environ["LSERP_PACKAGE_VERSION_VALUE"],
|
||
"generatedAtUtc": datetime.now(timezone.utc).isoformat(),
|
||
"sourceCommit": os.environ["LSERP_GIT_COMMIT_VALUE"],
|
||
"sourceWorktreeDirty": os.environ["LSERP_WORKTREE_DIRTY_VALUE"] == "true",
|
||
"deliveryTopology": {
|
||
"desktopBundleContainsLserpCli": False,
|
||
"desktopBundleContainsBridgeCli": True,
|
||
"bridgeCliPath": "Host/lserp-agent-cli.exe",
|
||
"bridgeCliDatabaseDirectAccess": False,
|
||
"bridgeCliPublishMode": "win_x64_single_file_self_contained",
|
||
"desktopBundleContainsLegacyErp": False,
|
||
"legacyArtifactMode": "separate_signed_windows_build",
|
||
"legacyBuildTool": "Deployment/Build-LegacyErpAcceptance.ps1",
|
||
"runtimeRequiresLegacyArtifact": True,
|
||
},
|
||
"automatedVerification": {
|
||
"commandKernel": {"passed": actual_pass_counts["commandKernel"], "failed": kernel[1]},
|
||
"desktopHost": {"passed": actual_pass_counts["desktopHost"], "failed": host[1]},
|
||
"petWebUi": {"passed": actual_pass_counts["petWebUi"], "failed": 0},
|
||
"astrbotPlugin": {
|
||
"passed": actual_pass_counts["astrbotPlugin"],
|
||
"failed": 0,
|
||
"skipped": python_skipped,
|
||
},
|
||
"astrbotRuntimeContract": astrbot_contract,
|
||
"miniMaxVision": {
|
||
"mode": "direct_https_vlm",
|
||
"allowedRegions": ["global", "cn"],
|
||
"bundledCli": False,
|
||
},
|
||
"gugaSupplyChainAudit": {
|
||
"passed": True,
|
||
"auditFile": "Deployment/guga-upstream-audit.v1.json",
|
||
"auditSha256": guga_audit_sha256,
|
||
"assetId": "guga",
|
||
"installerPackage": "codex-pets",
|
||
"installerVersion": "0.3.0",
|
||
"installerTarballSha256": installer["tarballSha256"],
|
||
"observedSpriteSha256": asset["spriteSha256"],
|
||
"upstreamCommercialLicensePresent": False,
|
||
"assetBundled": False,
|
||
},
|
||
"deploymentContracts": {
|
||
"passed": actual_pass_counts["deploymentContracts"],
|
||
"failed": deployment[1],
|
||
},
|
||
"sqlServerCompatibility100": {
|
||
"passed": actual_pass_counts["sqlServerCompatibility100"],
|
||
"failed": sql_contract[1],
|
||
"parserPackage": "Microsoft.SqlServer.TransactSql.ScriptDom",
|
||
"parserVersion": "180.59.2",
|
||
"dialect": "TSql100",
|
||
},
|
||
"legacyNet40ApiCompile": True,
|
||
"winX64SelfContainedPublish": True,
|
||
"bridgeCliWinX64SelfContainedPublish": True,
|
||
"hostAuthenticode": {
|
||
"signed": os.environ["LSERP_HOST_AUTHENTICODE_SIGNED_VALUE"] == "true",
|
||
"certificateThumbprint": (
|
||
os.environ["LSERP_HOST_CERT_THUMBPRINT_VALUE"].upper() or None
|
||
),
|
||
},
|
||
},
|
||
"releaseReadiness": False,
|
||
"remainingHardGates": [
|
||
"clean reviewed source commit",
|
||
"valid Windows Authenticode signature and signed installer",
|
||
"written commercial license for the external guga artwork",
|
||
"approved AstrBot AGPL-3.0-or-later and EULA compliance plan",
|
||
"approved MiniMax API service terms, data processing, deployment region and billing plan",
|
||
"customer Windows/.NET Framework 4/DevExpress 15.2 integration",
|
||
"customer SQL Server transactional write, rollback, idempotency and audit evidence",
|
||
"signed workflow acceptance manifests bound to final business-adapters.json",
|
||
],
|
||
}
|
||
(stage / "BUILD-VERIFICATION.json").write_text(
|
||
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||
)
|
||
|
||
secret_patterns = [
|
||
re.compile("sk" + r"-cp-[A-Za-z0-9_-]{20,}"),
|
||
re.compile(r"(?i)(?:api[_-]?key|password)\s*[=:]\s*['\"][A-Za-z0-9_-]{16,}['\"]"),
|
||
]
|
||
for path in stage.rglob("*"):
|
||
if not path.is_file() or path.stat().st_size > 2 * 1024 * 1024:
|
||
continue
|
||
if path.suffix.lower() not in {".json", ".yaml", ".yml", ".md", ".ps1", ".py", ".js", ".sql", ".txt"}:
|
||
continue
|
||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||
if any(pattern.search(text) for pattern in secret_patterns):
|
||
raise SystemExit(f"possible secret in staged text file: {path.relative_to(stage)}")
|
||
|
||
files = []
|
||
for path in sorted(stage.rglob("*"), key=lambda item: item.as_posix()):
|
||
if not path.is_file() or path.name == "SHA256SUMS.json":
|
||
continue
|
||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||
files.append({
|
||
"path": path.relative_to(stage).as_posix(),
|
||
"sizeBytes": path.stat().st_size,
|
||
"sha256": digest,
|
||
})
|
||
manifest = {
|
||
"schemaVersion": "1.0",
|
||
"packageVersion": os.environ["LSERP_PACKAGE_VERSION_VALUE"],
|
||
"generatedAtUtc": datetime.now(timezone.utc).isoformat(),
|
||
"files": files,
|
||
}
|
||
(stage / "SHA256SUMS.json").write_text(
|
||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||
)
|
||
PY
|
||
|
||
archive_base="$temporary_root/$package_name"
|
||
"$python_bin" - "$stage" "$archive_base" <<'PY'
|
||
import pathlib
|
||
import shutil
|
||
import sys
|
||
|
||
stage = pathlib.Path(sys.argv[1])
|
||
archive_base = pathlib.Path(sys.argv[2])
|
||
shutil.make_archive(str(archive_base), "zip", root_dir=stage.parent, base_dir=stage.name)
|
||
PY
|
||
|
||
mv "$stage" "$final_directory"
|
||
mv "$archive_base.zip" "$final_archive"
|
||
"$python_bin" - "$final_archive" <<'PY'
|
||
import hashlib
|
||
import pathlib
|
||
import sys
|
||
|
||
path = pathlib.Path(sys.argv[1])
|
||
print(f"package={path}")
|
||
print(f"sizeBytes={path.stat().st_size}")
|
||
print(f"sha256={hashlib.sha256(path.read_bytes()).hexdigest()}")
|
||
PY
|