Skip to content
Open
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
16 changes: 10 additions & 6 deletions src/agents/sandbox/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions tests/sandbox/test_parse_utils.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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="/")