Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions release-compat/README.md
Original file line number Diff line number Diff line change
@@ -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.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions release-compat/support/managed-srt/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions release-compat/support/managed-srt/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
74 changes: 74 additions & 0 deletions tests/legacy_self_update_bridge.rs
Original file line number Diff line number Diff line change
@@ -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<Path>) -> 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\"]"));
}
Loading