Skip to content

Commit 49447bb

Browse files
committed
test: add adversarial host contracts
1 parent 630d042 commit 49447bb

3 files changed

Lines changed: 140 additions & 9 deletions

File tree

src/agentnet/adapters/specs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616

1717

1818
PINNED_VERSIONS: dict[HarnessKind, str] = {
19-
"claude": "2.1.207",
20-
"codex": "0.144.3",
21-
"pi": "0.80.6",
22-
"antigravity": "1.1.1",
19+
"claude": "2.1.212",
20+
"codex": "0.144.5",
21+
"pi": "0.80.10",
22+
"antigravity": "1.1.3",
2323
}
2424

2525
EXECUTABLE_NAMES: dict[HarnessKind, str] = {

tests/adapters/fake_harness.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919

2020
VERSIONS = {
21-
"claude": "2.1.207 (Claude Code)",
22-
"codex": "codex-cli 0.144.3",
23-
"pi": "0.80.6",
24-
"agy": "1.1.1",
21+
"claude": "2.1.212 (Claude Code)",
22+
"codex": "codex-cli 0.144.5",
23+
"pi": "0.80.10",
24+
"agy": "1.1.3",
2525
}
2626

2727

tests/platform/test_host_support.py

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import subprocess
1010
import sys
1111
import time
12+
from dataclasses import replace
1213
from pathlib import Path
1314

1415
import pytest
@@ -22,7 +23,7 @@
2223
)
2324
from agentnet.bindings.mcp_bootstrap import MCP_BOOTSTRAP_ASSURANCE
2425
from agentnet.bindings.windows_mcp_bootstrap import WindowsMCPBootstrapServer
25-
from agentnet.errors import AuthenticationError
26+
from agentnet.errors import AuthenticationError, GateBlocked
2627
from agentnet.host import host_platform
2728
from agentnet.host_security import measure_process_identity
2829
from agentnet.operations.policy_defaults import OperationsPolicy
@@ -125,6 +126,8 @@ def test_macos_binding_descriptor_is_read_only_pipe() -> None:
125126

126127
reader, writer = BackgroundAdapterRuntime._binding_descriptors()
127128
assert writer is not None
129+
with pytest.raises(OSError):
130+
os.write(reader, b"not-writable")
128131
payload = b'{"safe":true}'
129132
import threading
130133

@@ -220,6 +223,7 @@ def exchange() -> dict[str, object]:
220223
"ok": True,
221224
"result": {"arguments": {}, "method": "agentnet.inbox"},
222225
}
226+
assert await asyncio.to_thread(exchange) == {"error": "replay_rejected"}
223227
finally:
224228
await server.close()
225229

@@ -336,6 +340,109 @@ def test_windows_binding_delivery_and_job_object_are_live() -> None:
336340
guard.close()
337341

338342

343+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows DACL contract")
344+
def test_windows_private_state_rejects_broad_dacl_and_reparse_points(tmp_path: Path) -> None:
345+
import win32security
346+
347+
from agentnet.windows_security import (
348+
ensure_private_directory,
349+
require_private_path,
350+
write_private_file,
351+
)
352+
353+
private_file = (tmp_path / "private" / "secret.bin").absolute()
354+
write_private_file(private_file, b"secret")
355+
descriptor = win32security.GetNamedSecurityInfo(
356+
str(private_file),
357+
win32security.SE_FILE_OBJECT,
358+
win32security.DACL_SECURITY_INFORMATION,
359+
)
360+
dacl = descriptor.GetSecurityDescriptorDacl()
361+
assert dacl is not None
362+
dacl.AddAccessAllowedAceEx(
363+
win32security.ACL_REVISION_DS,
364+
0,
365+
0x00120089,
366+
win32security.ConvertStringSidToSid("S-1-1-0"),
367+
)
368+
win32security.SetNamedSecurityInfo(
369+
str(private_file),
370+
win32security.SE_FILE_OBJECT,
371+
win32security.DACL_SECURITY_INFORMATION
372+
| win32security.PROTECTED_DACL_SECURITY_INFORMATION,
373+
None,
374+
None,
375+
dacl,
376+
None,
377+
)
378+
with pytest.raises(AuthenticationError, match="broad principal"):
379+
require_private_path(private_file, directory=False)
380+
381+
target = (tmp_path / "reparse-target").absolute()
382+
target.mkdir()
383+
link = (tmp_path / "reparse-link").absolute()
384+
os.symlink(target, link, target_is_directory=True)
385+
with pytest.raises(AuthenticationError, match="reparse point"):
386+
ensure_private_directory(link / "child")
387+
388+
389+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows capability theft contract")
390+
def test_windows_binding_delivery_rejects_wrong_exact_process_identity() -> None:
391+
import pywintypes
392+
import win32con
393+
import win32file
394+
import win32pipe
395+
396+
from agentnet.supervisor.windows_binding_delivery import WindowsBindingDelivery
397+
398+
actual = measure_process_identity(os.getpid())
399+
wrong = replace(actual, start_time=str(int(actual.start_time) + 1))
400+
delivery = WindowsBindingDelivery(timeout_seconds=5)
401+
delivery.start()
402+
delivery.publish(b'{"binding":"private"}', expected=wrong)
403+
try:
404+
win32pipe.WaitNamedPipe(delivery.endpoint, 5_000)
405+
handle = win32file.CreateFile(
406+
delivery.endpoint,
407+
win32con.GENERIC_READ,
408+
0,
409+
None,
410+
win32con.OPEN_EXISTING,
411+
0,
412+
None,
413+
)
414+
try:
415+
with pytest.raises(pywintypes.error):
416+
win32file.ReadFile(handle, 4)
417+
finally:
418+
handle.Close()
419+
finally:
420+
delivery.close()
421+
422+
423+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows Job cleanup contract")
424+
def test_windows_job_admission_failure_reaps_suspended_child(monkeypatch) -> None:
425+
from agentnet.adapters.native import _spawn_process_tree
426+
from agentnet.adapters.windows_job import WindowsJobGuard
427+
428+
seen = []
429+
430+
def reject(_self, process) -> None:
431+
seen.append(process)
432+
raise GateBlocked("G05", "synthetic Job admission failure")
433+
434+
monkeypatch.setattr(WindowsJobGuard, "assign_and_resume", reject)
435+
with pytest.raises(GateBlocked, match="synthetic Job admission failure"):
436+
_spawn_process_tree(
437+
(sys.executable, "-c", "raise SystemExit(0)"),
438+
stdout=subprocess.PIPE,
439+
stderr=subprocess.PIPE,
440+
close_fds=True,
441+
)
442+
assert len(seen) == 1
443+
assert seen[0].poll() is not None
444+
445+
339446
def test_operations_policy_names_every_supported_host() -> None:
340447
assert OperationsPolicy().supported_os == ("linux", "macos", "windows")
341448

@@ -387,6 +494,20 @@ def test_live_sqlite_store_creates_reopens_and_persists_replay(tmp_path: Path) -
387494
"SELECT nonce_hash FROM replay_nonces WHERE actor_id=?",
388495
("platform-test-actor",),
389496
) is not None
497+
if sys.platform == "win32":
498+
from agentnet.windows_security import require_private_path
499+
500+
sidecars = tuple(
501+
candidate
502+
for candidate in (
503+
path.with_name(path.name + "-wal"),
504+
path.with_name(path.name + "-shm"),
505+
)
506+
if candidate.exists()
507+
)
508+
assert sidecars
509+
for sidecar in sidecars:
510+
require_private_path(sidecar, directory=False)
390511
finally:
391512
first.close()
392513
second = SQLiteStore(path, cipher)
@@ -404,6 +525,16 @@ def test_live_sqlite_store_creates_reopens_and_persists_replay(tmp_path: Path) -
404525
require_private_path(path, directory=False)
405526

406527

528+
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS SQLite link contract")
529+
def test_macos_sqlite_state_rejects_symlinked_parent(tmp_path: Path) -> None:
530+
target = (tmp_path / "real-state").absolute()
531+
target.mkdir(mode=0o700)
532+
linked = (tmp_path / "linked-state").absolute()
533+
linked.symlink_to(target, target_is_directory=True)
534+
with pytest.raises(GateBlocked, match="owner-only and not a symlink"):
535+
SQLiteStore(linked / "core.sqlite3", LocalEnvelopeCipher(b"s" * 32))
536+
537+
407538
def test_cli_private_state_round_trip_uses_host_security(tmp_path: Path) -> None:
408539
from agentnet.cli import _owner_only_file, _write_owner_only
409540

0 commit comments

Comments
 (0)