99import subprocess
1010import sys
1111import time
12+ from dataclasses import replace
1213from pathlib import Path
1314
1415import pytest
2223)
2324from agentnet .bindings .mcp_bootstrap import MCP_BOOTSTRAP_ASSURANCE
2425from agentnet .bindings .windows_mcp_bootstrap import WindowsMCPBootstrapServer
25- from agentnet .errors import AuthenticationError
26+ from agentnet .errors import AuthenticationError , GateBlocked
2627from agentnet .host import host_platform
2728from agentnet .host_security import measure_process_identity
2829from 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+
339446def 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+
407538def 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