def list_directory(path: str, ctx: Context | None = None) -> str:
context_tokens = activate_runtime_context(ctx)
path = str(pathlib.Path(WORKSPACE_ROOT) / path) if not os.path.isabs(path) else path
try:
path_check = check_path_policy(path, tool="list_directory")
if path_check:
result = PolicyResult(allowed=False, reason=path_check[0], decision_tier="blocked", matched_rule=path_check[1])
else:
result = PolicyResult(allowed=True, reason="allowed", decision_tier="allowed", matched_rule=None)
if result.allowed:
if not os.path.exists(path):
append_log_entry(build_log_entry("list_directory", result, path=path, error="path not found"))
return f"Error: path not found: {path}"
if not os.path.isdir(path):
append_log_entry(build_log_entry("list_directory", result, path=path, error="not a directory"))
return f"Error: '{path}' is a file, not a directory"
depth = relative_depth(path)
max_depth = POLICY.get("allowed", {}).get("max_directory_depth", 5)
if depth > max_depth:
result = PolicyResult(
allowed=False,
reason=f"Directory depth {depth} exceeds the policy limit of {max_depth} (allowed.max_directory_depth): '{path}'",
decision_tier="blocked",
matched_rule="allowed.max_directory_depth",
)
append_log_entry(build_log_entry("list_directory", result, path=path))
if not result.allowed:
return f"[POLICY BLOCK] {result.reason}"
lines = [f"Contents of {path}:"]
try:
entries = sorted(os.scandir(path), key=lambda e: (e.is_file(), e.name))
except OSError as e:
return f"Error reading directory: {e}"
for entry in entries:
try:
stat = entry.stat(follow_symlinks=False)
mtime = datetime.datetime.fromtimestamp(stat.st_mtime, datetime.UTC).isoformat().replace("+00:00", "Z")
kind = "file" if entry.is_file(follow_symlinks=False) else "directory"
size = f"{stat.st_size} bytes" if kind == "file" else "-"
lines.append(f" {entry.name} [{kind}] size={size} modified={mtime}")
except OSError:
lines.append(f" {entry.name} [unreadable]")
if len(lines) == 1:
lines.append(" (empty)")
return "\n".join(lines)
finally:
reset_runtime_context(context_tokens)