From 87893626b7c0392c13753a29f8f6547410725b2e Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Fri, 17 Jul 2026 22:13:49 +0500 Subject: [PATCH 1/2] fix: install ZCode from the per-artifact CDN subpath The official CDN moved each artifact under a per-platform directory segment between the version and the filename: `.../releases///`. The bootstrap URL builder still produced `.../releases//`, so every download 404'd (Aliyun NoSuchKey). Declare the exact segment per artifact as `cdn_subpath` in build/version.json rather than deriving it from the key: macOS uses the platform-arch key, but the two Linux formats share one `linux-` directory, so no single derivation is correct. bootstrap.sh validates the segment as a single lowercase path component and inserts it into the URL; the public-contract validator pins the exact expected segment per artifact so a wrong path fails closed at validation time instead of at download. All six artifacts were confirmed by HTTPS HEAD to return 200 with a content-length equal to the pinned size, so only the path was wrong -- the digests and identities are unchanged. Claude-Session: https://claude.ai/code/session_01Y29c9sJRhChvxDcUjab5Xo --- build/release-evidence.json | 6 +++--- build/version.json | 8 +++++++- cli-tools/scripts/bootstrap.sh | 12 +++++++++--- cli-tools/validate_public_contracts.py | 13 +++++++++++++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/build/release-evidence.json b/build/release-evidence.json index a246427..ff8a114 100644 --- a/build/release-evidence.json +++ b/build/release-evidence.json @@ -6,7 +6,7 @@ }, "harness": { "repository": "NDDev-it-com/nddev-harnesses", - "commit": "489648ecab29e000683ab02c4d33fec00af05a98" + "commit": "a598102aff438239c8efcd2c790cd31356278267" }, "adapter": { "id": "zcode", @@ -30,8 +30,8 @@ } ], "lanes": [], - "generated_at_utc": "2026-07-17T15:23:49Z", - "expires_at_utc": "2027-01-13T15:23:49Z", + "generated_at_utc": "2026-07-17T17:12:37Z", + "expires_at_utc": "2027-01-13T17:12:37Z", "promotion": { "decision": "pending", "waivers": [] diff --git a/build/version.json b/build/version.json index 51f2e0f..158eb2f 100644 --- a/build/version.json +++ b/build/version.json @@ -7,6 +7,7 @@ "zcode_download_artifacts": { "macos-arm64": { "filename": "ZCode-3.3.6-mac-arm64.dmg", + "cdn_subpath": "macos-arm64", "size_bytes": 161814493, "sha512": "a469d3d9645efd92987727e39c1c04d7061d8ae38b105c1675896c1d0cbb5c193c1f9441d4abaf3f02b8df244cc6f66f05744b0830ee9edbb35ad1ebd7e9613b", "team_id": "8A5X4JJ39T", @@ -16,6 +17,7 @@ }, "macos-x64": { "filename": "ZCode-3.3.6-mac-x64.dmg", + "cdn_subpath": "macos-x64", "size_bytes": 170109451, "sha512": "39b99df6c3efb1e41454c07d103d29273e45798f87579691bc6526b4da9a60da5f17252cc7e53ffa663a22ce703a6e180312abe747b0e27e86ce1e19c60a77dc", "team_id": "8A5X4JJ39T", @@ -25,11 +27,13 @@ }, "linux-x64-appimage": { "filename": "ZCode-3.3.6-linux-x64.AppImage", + "cdn_subpath": "linux-x64", "size_bytes": 154199981, "sha512": "bf4e6c283b93aae3a474b06f872098e22fa4385183cbc7f4d13ec7d73119821c9c46362c06fa26fe6d9ef21f577c7981b7c2875f25b3e9ae042038a41853d549" }, "linux-x64-deb": { "filename": "ZCode-3.3.6-linux-x64.deb", + "cdn_subpath": "linux-x64", "size_bytes": 114251860, "sha512": "3704334817fe439151dd55b00044147802c747d8ef0e3453ecf24b550e222cec93c4f9bb2edf243106d05c8ecc27fc040d0d042b71410632505652620b82a0b3", "package_name": "zcode", @@ -38,11 +42,13 @@ }, "linux-arm64-appimage": { "filename": "ZCode-3.3.6-linux-arm64.AppImage", + "cdn_subpath": "linux-arm64", "size_bytes": 154132032, "sha512": "44c6979a141556a35ee890bbfde8f4bc0c979983dbb0499b51240334edfb02703a74f7657a6c9415c1ed5eae07a3c22be40ce4895d801ee81636e5a5f08d3ee9" }, "linux-arm64-deb": { "filename": "ZCode-3.3.6-linux-arm64.deb", + "cdn_subpath": "linux-arm64", "size_bytes": 108462636, "sha512": "b5da4d16210e17597f02d508a1ed583b3a5e6b1ce169bb2b8a75f8885626311b86f8d8638199869235ea870b1e91b63ef851c063a86b086806de6be9640d0589", "package_name": "zcode", @@ -57,5 +63,5 @@ "wrapper_target": "${HOME}/.local/bin/zcode" }, "schema": 2, - "_comment": "build_version is the semantic version of this nddev-zcode-app build. zcode_app_version and zcode_cli_version pin the exact ZCode desktop app and CLI versions this build was verified against. Every downloadable artifact is pinned by filename, byte size, and SHA-512; platform-native identity metadata is verified before installation where available. The CLI is the zcode.cjs inside the verified app bundle, launched via node through a wrapper script." + "_comment": "build_version is the semantic version of this nddev-zcode-app build. zcode_app_version and zcode_cli_version pin the exact ZCode desktop app and CLI versions this build was verified against. Every downloadable artifact is pinned by filename, byte size, and SHA-512; platform-native identity metadata is verified before installation where available. cdn_subpath is the exact per-artifact directory segment the official CDN serves the file under, between the version and the filename (macOS uses the platform-arch key; the two Linux formats share one linux- directory), so it is declared per artifact rather than derived from the key. The CLI is the zcode.cjs inside the verified app bundle, launched via node through a wrapper script." } diff --git a/cli-tools/scripts/bootstrap.sh b/cli-tools/scripts/bootstrap.sh index 1c949d7..189ab1e 100755 --- a/cli-tools/scripts/bootstrap.sh +++ b/cli-tools/scripts/bootstrap.sh @@ -191,6 +191,12 @@ if not isinstance(filename, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._+- raise SystemExit(f"invalid artifact filename: {key}") if "/" in filename or "\\" in filename or any(ord(char) < 32 for char in filename): raise SystemExit(f"artifact filename must be a basename: {key}") +cdn_subpath = artifact.get("cdn_subpath") +# A single lowercase path segment (e.g. macos-arm64, linux-x64). It sits between +# the version and the filename in the artifact URL, so it must not smuggle in +# extra path structure or traversal. +if not isinstance(cdn_subpath, str) or not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", cdn_subpath): + raise SystemExit(f"artifact cdn_subpath must be a lowercase path segment: {key}") if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-fA-F]{128}", digest): raise SystemExit(f"artifact sha512 must be 128 hexadecimal characters: {key}") if not isinstance(size, int) or isinstance(size, bool) or not 1 <= size <= 2 * 1024**3: @@ -232,7 +238,7 @@ if launcher != expected_launcher: raise SystemExit("zcode_cli_launcher must match the verified launcher contract exactly") fields = ( - base.rstrip("/"), filename, digest.lower(), str(size), team_id, bundle_id, + base.rstrip("/"), cdn_subpath, filename, digest.lower(), str(size), team_id, bundle_id, bundle_version, package_name, package_arch, package_version, launcher["linux_deb_entry"], ) @@ -241,8 +247,8 @@ if any("|" in field or "\n" in field or "\r" in field for field in fields): print("|".join(fields)) PY )" || exit 1 -IFS='|' read -r CDN_BASE artifact expected_sha512 expected_size TEAM_ID BUNDLE_ID BUNDLE_VERSION PACKAGE_NAME PACKAGE_ARCH PACKAGE_VERSION DEB_CLI_ENTRY <<< "$artifact_record" -url="${CDN_BASE}/${APP_VERSION}/${artifact}" +IFS='|' read -r CDN_BASE CDN_SUBPATH artifact expected_sha512 expected_size TEAM_ID BUNDLE_ID BUNDLE_VERSION PACKAGE_NAME PACKAGE_ARCH PACKAGE_VERSION DEB_CLI_ENTRY <<< "$artifact_record" +url="${CDN_BASE}/${APP_VERSION}/${CDN_SUBPATH}/${artifact}" python3 -I - "$url" <<'PY' import sys diff --git a/cli-tools/validate_public_contracts.py b/cli-tools/validate_public_contracts.py index 28baf68..604d524 100644 --- a/cli-tools/validate_public_contracts.py +++ b/cli-tools/validate_public_contracts.py @@ -102,6 +102,19 @@ def check_artifacts(version: dict, errors: list[str]) -> None: digest = str(entry.get("sha512", "")) if _SHA512.fullmatch(digest) is None: errors.append(f"{context}: sha512 must be 128 hex characters") + # The CDN serves each file under a per-artifact directory segment between + # the version and the filename. macOS uses the platform-arch key; the two + # Linux formats share one linux- directory. Pin the exact expected + # segment so a wrong path (the 404 this field exists to prevent) fails + # closed instead of only surfacing at download time. + expected_subpath = key[: -len("-appimage")] if key.endswith("-appimage") else key + expected_subpath = ( + expected_subpath[: -len("-deb")] + if expected_subpath.endswith("-deb") + else expected_subpath + ) + if entry.get("cdn_subpath") != expected_subpath: + errors.append(f"{context}: cdn_subpath must be {expected_subpath!r}") if key.startswith("macos"): if entry.get("app_version") != app: errors.append(f"{context}: app_version must equal zcode_app_version") From a70586f22c2c3fdb49584498c6e16761ce56f7fd Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Fri, 17 Jul 2026 22:49:50 +0500 Subject: [PATCH 2/2] fix: give the CLI version probe enough time for a cold start The bootstrap postcondition probes the freshly installed CLI immediately, so its 9 MB bundle is read cold from disk. Measured cold start is 2-13 s (warm: under 1 s), but the probe timeout was 3 s, so it killed the CLI mid-start and reported "unknown", aborting an otherwise-complete install with atomic rollback. Raise the default probe timeout to 30 s, with an NDDEV_ZCODE_PROBE_TIMEOUT_SECONDS override (clamped 1-120) for a slower host or a fast enforcement test. A real isolated `bootstrap --apply` with the official 3.3.6 artifact now completes: "ZCode app 3.3.6 and CLI 0.15.2 verified exactly". The isolation, output-size, and process-group kill guarantees are unchanged. Claude-Session: https://claude.ai/code/session_01Y29c9sJRhChvxDcUjab5Xo --- build/release-evidence.json | 6 +++--- cli-tools/scripts/lib/version.sh | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/build/release-evidence.json b/build/release-evidence.json index ff8a114..ddf6c3d 100644 --- a/build/release-evidence.json +++ b/build/release-evidence.json @@ -2,7 +2,7 @@ "schema_version": 2, "module": { "repository": "NDDev-it-com/nddev-zcode-app", - "setup_digest": "sha256:4ad6390371be5047622942d28039bd572b3e5345239f0013b535f6ac367e1f7e" + "setup_digest": "sha256:ae37209353cd6a10f43312c24941975d845587558619ade8f37ba55e535bfac5" }, "harness": { "repository": "NDDev-it-com/nddev-harnesses", @@ -30,8 +30,8 @@ } ], "lanes": [], - "generated_at_utc": "2026-07-17T17:12:37Z", - "expires_at_utc": "2027-01-13T17:12:37Z", + "generated_at_utc": "2026-07-17T18:03:49Z", + "expires_at_utc": "2027-01-13T18:03:49Z", "promotion": { "decision": "pending", "waivers": [] diff --git a/cli-tools/scripts/lib/version.sh b/cli-tools/scripts/lib/version.sh index 1b84414..5ce4fb7 100644 --- a/cli-tools/scripts/lib/version.sh +++ b/cli-tools/scripts/lib/version.sh @@ -148,7 +148,21 @@ import tempfile command = sys.argv[1:] limit = 64 * 1024 -timeout = 3 +# The postcondition probes the CLI immediately after install, so its 9 MB +# bundle is read cold from disk: measured cold start is 2-13 s (warm: <1 s), +# and a 3 s bound killed it mid-start, returning "unknown". Default to 30 s of +# headroom; allow an override for a slower host or a fast enforcement test, +# clamped so it can neither hang unbounded nor drop below a usable floor. +DEFAULT_TIMEOUT = 30 +timeout = DEFAULT_TIMEOUT +_override = os.environ.get("NDDEV_ZCODE_PROBE_TIMEOUT_SECONDS") +if _override: + try: + _parsed = int(_override) + except ValueError: + _parsed = 0 + if 1 <= _parsed <= 120: + timeout = _parsed def constrain_output(): resource.setrlimit(resource.RLIMIT_FSIZE, (limit, limit))