From 5330664cd694edcc54c175eb22b0bc8a1d169143 Mon Sep 17 00:00:00 2001 From: RoyLin Date: Wed, 29 Jul 2026 09:58:19 +0800 Subject: [PATCH] fix(update): bridge legacy SRT archive checks --- .github/workflows/release.yml | 23 +++++- CHANGELOG.md | 7 ++ Cargo.toml | 1 + release-compat/README.md | 12 +++ .../@anthropic-ai/sandbox-runtime/dist/cli.js | 8 ++ .../sandbox-runtime/package.json | 9 +++ .../support/managed-srt/package-lock.json | 22 ++++++ .../support/managed-srt/package.json | 9 +++ tests/legacy_self_update_bridge.rs | 74 +++++++++++++++++++ 9 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 release-compat/README.md create mode 100755 release-compat/support/managed-srt/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js create mode 100644 release-compat/support/managed-srt/node_modules/@anthropic-ai/sandbox-runtime/package.json create mode 100644 release-compat/support/managed-srt/package-lock.json create mode 100644 release-compat/support/managed-srt/package.json create mode 100644 tests/legacy_self_update_bridge.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31dc0cf..7e0a757 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -551,7 +551,7 @@ jobs: bin: a3s target: ${{ matrix.target }} archive: a3s-$tag-$target - include: web,${{ matrix.helper }} + include: web,release-compat,${{ matrix.helper }} tar: all zip: windows checksum: sha256 @@ -597,6 +597,27 @@ jobs: exit 1 fi done + bridge_root='release-compat/support/managed-srt' + bridge_lock="${bridge_root}/package-lock.json" + bridge_cli="${bridge_root}/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js" + tar_entries="$(tar -tzf "${base}.tar.gz")" + for bridge_path in "$bridge_lock" "$bridge_cli"; do + bridge_count="$(grep -Ec "(^|/)${bridge_path}$" <<<"$tar_entries" || true)" + if [[ "$bridge_count" -ne 1 ]]; then + echo "::error::Release archive must contain exactly one legacy self-update bridge path ${bridge_path}; found ${bridge_count}" + exit 1 + fi + done + if [[ "$RELEASE_TARGET" == "x86_64-pc-windows-msvc" ]]; then + zip_entries="$(unzip -Z1 "${base}.zip")" + for bridge_path in "$bridge_lock" "$bridge_cli"; do + bridge_count="$(grep -Ec "(^|/)${bridge_path}$" <<<"$zip_entries" || true)" + if [[ "$bridge_count" -ne 1 ]]; then + echo "::error::Windows release archive must contain exactly one legacy self-update bridge path ${bridge_path}; found ${bridge_count}" + exit 1 + fi + done + fi gh release upload "$tag" "${assets[@]}" --clobber crate: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f681e7..abed4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Restored one-command self-updates from A3S 0.9.9 through 0.10.10 by adding a + minimal, fail-closed retirement marker to release archives for their legacy + managed-SRT payload check. The Anthropic SRT runtime remains removed and the + marker is never discovered or executed by current A3S versions. + ## [0.10.14] - 2026-07-29 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index fa2ecfd..09a3a9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/A3S-Lab/Cli" homepage = "https://github.com/A3S-Lab/a3s" keywords = ["a3s", "cli", "agent", "tui", "coding"] categories = ["command-line-utilities"] +exclude = ["release-compat/**", "tests/legacy_self_update_bridge.rs"] [[bin]] name = "a3s" diff --git a/release-compat/README.md b/release-compat/README.md new file mode 100644 index 0000000..ceab096 --- /dev/null +++ b/release-compat/README.md @@ -0,0 +1,12 @@ +# Legacy self-update bridge + +This directory is included only in release archives. + +A3S 0.9.9 through 0.10.10 required every self-update archive to contain a +`support/managed-srt` tree before they would install the new binary. A3S 0.10.11 +removed the Anthropic SRT runtime, so omitting that tree made those older +clients unable to update themselves. + +The nested package is an inert, fail-closed compatibility marker. Current A3S +versions do not discover or execute it, and it must never acquire sandbox +runtime dependencies or implementation code. diff --git a/release-compat/support/managed-srt/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js b/release-compat/support/managed-srt/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js new file mode 100755 index 0000000..99ed099 --- /dev/null +++ b/release-compat/support/managed-srt/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +"use strict"; + +process.stderr.write( + "Anthropic SRT was removed from A3S. This inert file exists only so legacy A3S self-updaters can install the current release.\n", +); +process.exitCode = 1; diff --git a/release-compat/support/managed-srt/node_modules/@anthropic-ai/sandbox-runtime/package.json b/release-compat/support/managed-srt/node_modules/@anthropic-ai/sandbox-runtime/package.json new file mode 100644 index 0000000..5ba3ade --- /dev/null +++ b/release-compat/support/managed-srt/node_modules/@anthropic-ai/sandbox-runtime/package.json @@ -0,0 +1,9 @@ +{ + "name": "@anthropic-ai/sandbox-runtime", + "version": "0.0.0-a3s-retired", + "private": true, + "description": "Inert compatibility marker; Anthropic SRT is not bundled", + "bin": { + "sandbox-runtime": "dist/cli.js" + } +} diff --git a/release-compat/support/managed-srt/package-lock.json b/release-compat/support/managed-srt/package-lock.json new file mode 100644 index 0000000..fe6fdff --- /dev/null +++ b/release-compat/support/managed-srt/package-lock.json @@ -0,0 +1,22 @@ +{ + "name": "a3s-managed-srt-retirement-bridge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "a3s-managed-srt-retirement-bridge", + "version": "1.0.0", + "dependencies": { + "@anthropic-ai/sandbox-runtime": "0.0.0-a3s-retired" + } + }, + "node_modules/@anthropic-ai/sandbox-runtime": { + "name": "@anthropic-ai/sandbox-runtime", + "version": "0.0.0-a3s-retired", + "bin": { + "sandbox-runtime": "dist/cli.js" + } + } + } +} diff --git a/release-compat/support/managed-srt/package.json b/release-compat/support/managed-srt/package.json new file mode 100644 index 0000000..c6e72e6 --- /dev/null +++ b/release-compat/support/managed-srt/package.json @@ -0,0 +1,9 @@ +{ + "name": "a3s-managed-srt-retirement-bridge", + "version": "1.0.0", + "private": true, + "description": "Inert release marker for legacy A3S self-updaters", + "dependencies": { + "@anthropic-ai/sandbox-runtime": "0.0.0-a3s-retired" + } +} diff --git a/tests/legacy_self_update_bridge.rs b/tests/legacy_self_update_bridge.rs new file mode 100644 index 0000000..0e02d4e --- /dev/null +++ b/tests/legacy_self_update_bridge.rs @@ -0,0 +1,74 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::Value; + +const RETIRED_PACKAGE_VERSION: &str = "0.0.0-a3s-retired"; + +fn repository_path(relative: impl AsRef) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join(relative) +} + +#[test] +fn bridge_matches_the_legacy_updater_contract_and_fails_closed() { + let root = repository_path("release-compat/support/managed-srt"); + let package_path = root.join("node_modules/@anthropic-ai/sandbox-runtime/package.json"); + let package: Value = serde_json::from_slice(&fs::read(&package_path).unwrap()).unwrap(); + assert_eq!( + package.get("name").and_then(Value::as_str), + Some("@anthropic-ai/sandbox-runtime") + ); + assert_eq!( + package.get("version").and_then(Value::as_str), + Some(RETIRED_PACKAGE_VERSION) + ); + + let lock_path = root.join("package-lock.json"); + let lock: Value = serde_json::from_slice(&fs::read(&lock_path).unwrap()).unwrap(); + assert_eq!(lock.get("lockfileVersion").and_then(Value::as_u64), Some(3)); + assert_eq!( + lock.pointer("/packages/node_modules~1@anthropic-ai~1sandbox-runtime/version") + .and_then(Value::as_str), + Some(RETIRED_PACKAGE_VERSION) + ); + assert_eq!( + lock.pointer("/packages//dependencies/@anthropic-ai~1sandbox-runtime") + .and_then(Value::as_str), + Some(RETIRED_PACKAGE_VERSION) + ); + + let cli_path = root.join("node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js"); + let cli = fs::read_to_string(&cli_path).unwrap(); + assert!(cli.contains("Anthropic SRT was removed from A3S")); + assert!(cli.contains("process.exitCode = 1")); + for forbidden in ["child_process", "require(", "import ", "eval("] { + assert!( + !cli.contains(forbidden), + "bridge must not contain {forbidden}" + ); + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_ne!( + fs::metadata(cli_path).unwrap().permissions().mode() & 0o111, + 0 + ); + } +} + +#[test] +fn release_workflow_packages_and_verifies_the_bridge() { + let workflow = fs::read_to_string(repository_path(".github/workflows/release.yml")).unwrap(); + assert!(workflow.contains("include: web,release-compat,${{ matrix.helper }}")); + assert!(workflow.contains("bridge_root='release-compat/support/managed-srt'")); + assert!(workflow.contains("bridge_lock=\"${bridge_root}/package-lock.json\"")); + assert!(workflow.contains( + "bridge_cli=\"${bridge_root}/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js\"" + )); + + let manifest = fs::read_to_string(repository_path("Cargo.toml")).unwrap(); + assert!(manifest + .contains("exclude = [\"release-compat/**\", \"tests/legacy_self_update_bridge.rs\"]")); +}