Files
pydantic--pydantic-ai/.github/scripts/pydantic_ai_gh_aw_shim/glob.py
T
wehub-resource-sync 9201ef759e
CI / lint (push) Waiting to run
CI / mypy (push) Waiting to run
CI / docs (push) Waiting to run
CI / test on 3.10 (standard) (push) Waiting to run
CI / test on 3.11 (standard) (push) Waiting to run
CI / test on 3.12 (standard) (push) Waiting to run
CI / test on 3.10 (all-extras) (push) Waiting to run
CI / test on 3.11 (all-extras) (push) Waiting to run
CI / test on 3.12 (all-extras) (push) Waiting to run
CI / test on 3.13 (all-extras) (push) Waiting to run
CI / test on 3.14 (pydantic-evals) (push) Waiting to run
Harness Compat / harness compat (push) Waiting to run
CI / test on 3.13 (standard) (push) Waiting to run
CI / test on 3.14 (standard) (push) Waiting to run
CI / test on 3.14 (all-extras) (push) Waiting to run
CI / test on 3.10 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.11 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.12 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.13 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.14 (pydantic-ai-slim) (push) Waiting to run
CI / test on 3.10 (pydantic-evals) (push) Waiting to run
CI / test on 3.11 (pydantic-evals) (push) Waiting to run
CI / test on 3.12 (pydantic-evals) (push) Waiting to run
CI / test on 3.13 (pydantic-evals) (push) Waiting to run
CI / test on 3.10 (lowest-versions) (push) Waiting to run
CI / test on 3.11 (lowest-versions) (push) Waiting to run
CI / test on 3.12 (lowest-versions) (push) Waiting to run
CI / test on 3.13 (lowest-versions) (push) Waiting to run
CI / test on 3.14 (lowest-versions) (push) Waiting to run
CI / test examples on 3.11 (push) Waiting to run
CI / test examples on 3.12 (push) Waiting to run
CI / test examples on 3.13 (push) Waiting to run
CI / test examples on 3.14 (push) Waiting to run
CI / coverage (push) Blocked by required conditions
CI / check (push) Blocked by required conditions
CI / deploy-docs (push) Blocked by required conditions
CI / deploy-docs-preview (push) Blocked by required conditions
CI / build release artifacts (push) Blocked by required conditions
CI / publish to PyPI (push) Blocked by required conditions
CI / Send tweet (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:27:52 +08:00

52 lines
2.6 KiB
Python

"""Claude's `Glob` tool -- list workspace paths matching a glob pattern.
Containment comes from a pydantic-ai-harness `FileSystemToolset.file_info`
preflight on the search `path` (the same check `Grep`/`LS` use) plus a
per-match resolved-path check, but the glob itself is hand-rolled with the stdlib
rather than delegated to `FileSystemToolset.find_files`: the harness walker hides
every dot-prefixed path, which would make `.github/**` -- where gh-aw's own
workflows live -- unmatchable. The Claude `Glob` signature is preserved, matches
are returned relative to the workspace root, and the directory-scoped AGENTS.md /
CLAUDE.md context blocks are still prepended.
"""
import glob as globlib
import os
import pathlib
from pydantic_ai.exceptions import ModelRetry
from ._backends import filesystem
from .shared import attach_context, clip, resolve, workspace
async def glob_search(pattern: str, path: str = '.') -> str:
"""Return workspace paths matching a glob `pattern` (supports `**`)."""
# Claude's `Glob` takes a relative pattern plus a separate `path`; an absolute
# pattern would be joined as-is and could escape the search root, so reject it.
if os.path.isabs(pattern):
return 'error: glob pattern must be relative to the search path'
try:
# `file_info` contains the search `path` (a clear error if it escapes); the
# per-match resolve() + is_relative_to() below then contains the matches,
# dropping anything a `..` pattern or an in-workspace symlink points to
# outside the root (a purely lexical `relative_to` would not catch those).
await filesystem().file_info(path)
base = resolve(path)
ws = pathlib.Path(workspace())
root = ws.resolve()
matches: list[str] = []
for match in globlib.glob(str(base / pattern), recursive=True):
# Resolve only to *decide* containment (collapse `..`, follow symlinks);
# the returned path is the matched name relative to the workspace, so a
# symlink that matched (e.g. `CLAUDE.md` -> `AGENTS.md`) is reported as
# itself rather than silently rewritten to its target.
if pathlib.Path(match).resolve().is_relative_to(root):
matches.append(os.path.relpath(match, ws))
except (ModelRetry, OSError, ValueError) as exc:
# ModelRetry: the containment preflight rejected `path`. OSError (e.g.
# `ENAMETOOLONG`) / ValueError: not recoverable in the harness, so catch
# them here rather than let them abort the whole run.
return f'error: {exc}'
return clip(attach_context(path) + ('\n'.join(sorted(set(matches))) or 'No matches found.'))