diff --git a/src/agents/sandbox/types.py b/src/agents/sandbox/types.py index f9549a5e33..d19df2fd77 100644 --- a/src/agents/sandbox/types.py +++ b/src/agents/sandbox/types.py @@ -65,7 +65,7 @@ def from_str(cls, perms: str) -> "Permissions": if perms[0] not in {"d", "-"}: raise ValueError(f"invalid permissions type: {perms!r}") - def parse_triplet(triplet: str) -> int: + def parse_triplet(triplet: str, *, special_exec_chars: tuple[str, str]) -> int: if len(triplet) != 3: raise ValueError(f"invalid permissions triplet: {triplet!r}") mask = 0 @@ -77,15 +77,19 @@ def parse_triplet(triplet: str) -> int: mask |= FileMode.WRITE elif triplet[1] != "-": raise ValueError(f"invalid write flag: {triplet!r}") - if triplet[2] == "x": + + exec_flag = triplet[2] + exec_with_special, special_without_exec = special_exec_chars + + if exec_flag in {"x", exec_with_special}: mask |= FileMode.EXEC - elif triplet[2] != "-": + elif exec_flag not in {"-", special_without_exec}: raise ValueError(f"invalid exec flag: {triplet!r}") return int(mask) - owner = parse_triplet(perms[1:4]) - group = parse_triplet(perms[4:7]) - other = parse_triplet(perms[7:10]) + owner = parse_triplet(perms[1:4], special_exec_chars=("s", "S")) + group = parse_triplet(perms[4:7], special_exec_chars=("s", "S")) + other = parse_triplet(perms[7:10], special_exec_chars=("t", "T")) return cls( owner=owner, group=group, diff --git a/tests/sandbox/test_parse_utils.py b/tests/sandbox/test_parse_utils.py index 35e53e49e9..136b56e090 100644 --- a/tests/sandbox/test_parse_utils.py +++ b/tests/sandbox/test_parse_utils.py @@ -1,4 +1,7 @@ +import pytest + from agents.sandbox.files import EntryKind +from agents.sandbox.types import FileMode from agents.sandbox.util.parse_utils import parse_ls_la @@ -34,3 +37,47 @@ def test_parse_ls_la_keeps_arrow_in_regular_file_names() -> None: assert len(entries) == 1 assert entries[0].path == "/workspace/docs/notes -> final.txt" assert entries[0].kind == EntryKind.FILE + + +def test_parse_ls_la_accepts_special_permission_bits() -> None: + output = ( + "drwxrwxrwt 2 root root 4096 Jan 1 00:00 tmp\n" + "-rwsr-sr-t 1 root root 123 Jan 1 00:00 setuid-tool\n" + "-rwSr-Sr-T 1 root root 456 Jan 1 00:00 special-no-exec\n" + ) + + entries = parse_ls_la(output, base="/") + + assert [entry.path for entry in entries] == [ + "/tmp", + "/setuid-tool", + "/special-no-exec", + ] + assert entries[0].permissions.directory is True + assert entries[0].permissions.other & FileMode.EXEC + assert entries[1].permissions.owner & FileMode.EXEC + assert entries[1].permissions.group & FileMode.EXEC + assert entries[1].permissions.other & FileMode.EXEC + assert not (entries[2].permissions.owner & FileMode.EXEC) + assert not (entries[2].permissions.group & FileMode.EXEC) + assert not (entries[2].permissions.other & FileMode.EXEC) + + +@pytest.mark.parametrize( + "permissions", + [ + "-rwTr--r--", + "-rwxrwTr--", + "-rwxrwxr-S", + "-rwtr--r--", + "-rwxrwtr--", + "-rwxrwxr-s", + ], +) +def test_parse_ls_la_rejects_special_permission_bits_in_wrong_position( + permissions: str, +) -> None: + output = f"{permissions} 1 root root 123 Jan 1 00:00 invalid\n" + + with pytest.raises(ValueError, match="invalid exec flag"): + parse_ls_la(output, base="/")