Skip to content

Commit 0553c98

Browse files
committed
fix: make Bridge CI checks platform-safe
1 parent 4098db5 commit 0553c98

3 files changed

Lines changed: 67 additions & 11 deletions

File tree

tests/test_build_sealed_bridge_runtimes.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ def sha(path: Path) -> str:
3030
return hashlib.sha256(path.read_bytes()).hexdigest()
3131

3232

33+
def host_system_tool_paths() -> dict[str, Path]:
34+
def usable(path: Path) -> bool:
35+
try:
36+
metadata = path.lstat()
37+
except OSError:
38+
return False
39+
return (
40+
stat.S_ISREG(metadata.st_mode)
41+
and metadata.st_uid == 0
42+
and not stat.S_IMODE(metadata.st_mode) & 0o022
43+
and Path(os.path.realpath(path)) == path
44+
and os.access(path, os.X_OK)
45+
)
46+
47+
fallback = next(
48+
path
49+
for path in map(
50+
Path,
51+
("/usr/bin/true", "/bin/true", "/usr/bin/env", "/bin/echo"),
52+
)
53+
if usable(path)
54+
)
55+
return {
56+
name: path if usable(path) else fallback
57+
for name, path in builder.SYSTEM_TOOL_PATHS.items()
58+
}
59+
60+
3361
class ManifestFixture:
3462
def __init__(self, temporary: Path) -> None:
3563
self.root = temporary
@@ -152,10 +180,15 @@ class SealedRuntimeBuilderTests(unittest.TestCase):
152180
def setUp(self) -> None:
153181
self.temporary = tempfile.TemporaryDirectory(prefix="sealed-builder-test-")
154182
self.root = Path(os.path.realpath(self.temporary.name))
183+
self.system_tools_patch = mock.patch.object(
184+
builder, "SYSTEM_TOOL_PATHS", host_system_tool_paths()
185+
)
186+
self.system_tools_patch.start()
155187
self.fixture = ManifestFixture(self.root)
156188

157189
def tearDown(self) -> None:
158190
builder._unseal(self.root)
191+
self.system_tools_patch.stop()
159192
self.temporary.cleanup()
160193

161194
def test_manifest_accepts_exact_four_role_schema(self) -> None:

tools/agent-fleet/src/agent_fleet/recovery.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,10 @@ def _verified_binary(self) -> Path:
9999
raise ValueError("macOS Keychain control is unsafe")
100100
return self.binary
101101

102-
def _execution_context(self) -> tuple[Path, str, Path, dict[str, str]]:
102+
def _execution_context(
103+
self, account: str
104+
) -> tuple[Path, str, Path, dict[str, str]]:
103105
binary = self._verified_binary()
104-
account = _keychain_account()
105106
home = current_user_home()
106107
environment = {
107108
"HOME": str(home),
@@ -115,6 +116,15 @@ def _execution_context(self) -> tuple[Path, str, Path, dict[str, str]]:
115116
}
116117
return binary, account, home, environment
117118

119+
@staticmethod
120+
def _validate_scope(service: str, account: str, expected_account: str) -> None:
121+
if (
122+
CLAUDE_KEYCHAIN_SERVICE.fullmatch(service) is None
123+
or not account
124+
or account != expected_account
125+
):
126+
raise ValueError("refusing an unscoped Claude Keychain operation")
127+
118128
@staticmethod
119129
def _prefix(
120130
binary: Path,
@@ -123,16 +133,15 @@ def _prefix(
123133
account: str,
124134
expected_account: str,
125135
) -> list[str]:
126-
if (
127-
CLAUDE_KEYCHAIN_SERVICE.fullmatch(service) is None
128-
or not account
129-
or account != expected_account
130-
):
131-
raise ValueError("refusing an unscoped Claude Keychain operation")
136+
SecurityKeychain._validate_scope(service, account, expected_account)
132137
return [str(binary), operation, "-s", service, "-a", account]
133138

134139
def exists(self, service: str, account: str) -> bool:
135-
binary, expected_account, home, environment = self._execution_context()
140+
expected_account = _keychain_account()
141+
self._validate_scope(service, account, expected_account)
142+
binary, expected_account, home, environment = self._execution_context(
143+
expected_account
144+
)
136145
result = subprocess.run(
137146
self._prefix(
138147
binary,
@@ -155,7 +164,12 @@ def exists(self, service: str, account: str) -> bool:
155164
def copy(self, source: str, destination: str, account: str) -> None:
156165
# `security -w` writes the secret only to the pipe. The destination
157166
# command receives it through stdin because -w is its final option.
158-
binary, expected_account, home, environment = self._execution_context()
167+
expected_account = _keychain_account()
168+
self._validate_scope(source, account, expected_account)
169+
self._validate_scope(destination, account, expected_account)
170+
binary, expected_account, home, environment = self._execution_context(
171+
expected_account
172+
)
159173
reader = subprocess.Popen(
160174
[
161175
*self._prefix(
@@ -205,7 +219,11 @@ def copy(self, source: str, destination: str, account: str) -> None:
205219
raise ValueError("scoped Claude Keychain copy failed")
206220

207221
def delete(self, service: str, account: str, *, missing_ok: bool = False) -> None:
208-
binary, expected_account, home, environment = self._execution_context()
222+
expected_account = _keychain_account()
223+
self._validate_scope(service, account, expected_account)
224+
binary, expected_account, home, environment = self._execution_context(
225+
expected_account
226+
)
209227
result = subprocess.run(
210228
self._prefix(
211229
binary,

tools/agent-fleet/tests/test_profile_recovery.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4297,6 +4297,11 @@ def test_security_keychain_rejects_default_or_non_derived_service(
42974297
service: str,
42984298
) -> None:
42994299
monkeypatch.setattr(recovery, "_keychain_account", lambda: "fixture-user")
4300+
monkeypatch.setattr(
4301+
SecurityKeychain,
4302+
"_verified_binary",
4303+
lambda self: pytest.fail("binary verification ran before scope validation"),
4304+
)
43004305
with pytest.raises(ValueError, match="unscoped Claude Keychain operation"):
43014306
SecurityKeychain().exists(service, "fixture-user")
43024307

0 commit comments

Comments
 (0)