mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-14 16:08:41 +00:00
* fix(subagents): make acceptance checks portable on Windows * fix(subagents): reject drive-root path escapes * fix(subagents): preserve drive-root containment * fix(subagents): use Windows path casing rules * fix(subagents): reject drive-relative cd paths * fix(subagents): reject shell-dependent cd targets * fix(subagents): reject shell-dependent runner paths * fix(subagents): harden cross-shell acceptance checks * fix(subagents): reject ambiguous shell tokenization * fix(subagents): reject tokenizer segment drift * fix(subagents): reject ambiguous PowerShell syntax * fix(subagents): include all PowerShell quote delimiters * fix(subagents): fail closed on cross-family paths * fix(subagents): reject ambiguous Windows aliases * fix(subagents): reject PSDrive alias exclusions * fix(subagents): reject PSDrive-relative aliases --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
1827 lines
91 KiB
Python
1827 lines
91 KiB
Python
"""Deterministic acceptance-criteria checks (RFC #4651 PR4).
|
||
|
||
Layer 2 of the verification stack: the lead attaches ``acceptance_criteria``
|
||
to a ``task`` delegation (PR3 wired the parameter and the prompt contract);
|
||
this module checks the decidable criteria *in code* once the subagent
|
||
completes, so a self-report can never silently pass an objectively checkable
|
||
requirement.
|
||
|
||
Leaf families:
|
||
|
||
- ``file:<path> exists`` / ``file:<path> non-empty`` — read through
|
||
``read_current_file_content`` (the ``ReadBeforeWriteMiddleware``
|
||
precedent), **scoped to the shared thread workspace**: the path must
|
||
resolve under the thread's ``workspace_path``/``outputs_path`` (virtual
|
||
``/mnt/user-data/...`` prefixes and workspace-relative spellings are
|
||
normalized first). The read itself uses the sandbox-native **virtual**
|
||
form — the local read path validator accepts ``/mnt/user-data/...``
|
||
paths, not host paths. Paths outside the shared domain return
|
||
``checked=False`` (UNVERIFIED) rather than assuming cross-sandbox
|
||
reachability — if a future isolated-sandbox provider breaks sharing,
|
||
leaves degrade to UNVERIFIED instead of misjudging. Reads are
|
||
byte-bounded: the size is established first (``os.stat`` on the local
|
||
host path; a metadata-only ``stat``/``realpath`` probe in a fresh
|
||
``env -i`` shell on remote providers — absolute-path utilities a
|
||
poisoned persistent session cannot steer, no content opens so a FIFO
|
||
cannot block, regular-file type required, containment canonicalized
|
||
against the canonical mount root, matching what the provider's own
|
||
read path resolves); above ``_FILE_CONTENT_READ_CAP_BYTES``
|
||
the leaf answers from the size alone, at or below it the full read
|
||
runs, and when the size cannot be established the leaf degrades to
|
||
UNVERIFIED — never an unbounded read.
|
||
- ``file_written:<path>`` — typed claim binding: existence + read-back
|
||
through the same workspace-scoped read.
|
||
- ``tests_passed:<command>`` — typed claim binding: the criterion must
|
||
anchor to a *specific recorded execution* — a matching bash execution
|
||
(harvested by the executor from the same stamped ``ToolMessage``s the
|
||
receipt layer reads) with ``status=success`` and a test-summary shape in
|
||
its output tail — not merely to some successful call. Matching accounts
|
||
for shell command structure (operator-separated segments, executables,
|
||
arguments), so an unrelated command that merely mentions the criterion
|
||
string (e.g. inside an ``echo`` argument or a comment) cannot anchor the
|
||
leaf. Full parent-side re-execution stays deferred to the read-only
|
||
verifier (RFC §6).
|
||
- Anything else is undecidable in code: ``checked=False``, rendered
|
||
``UNVERIFIED``, never silently passed.
|
||
|
||
Vocabulary layering: the leaf booleans are ``checked``/``holds`` — never
|
||
``satisfied``/``verified``/``passed``. Strong-positive words stay exclusive
|
||
to the runtime hard gate so the model never conflates deterministic
|
||
execution evidence with task acceptance.
|
||
|
||
All functions are pure (sandbox IO only through the injected reader/prober);
|
||
the async caller offloads the whole check with ``asyncio.to_thread``.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import ntpath
|
||
import os
|
||
import posixpath
|
||
import re
|
||
import shlex
|
||
import stat
|
||
import unicodedata
|
||
from collections.abc import Callable, Mapping
|
||
from typing import Any, TypedDict
|
||
|
||
from deerflow.config.paths import VIRTUAL_PATH_PREFIX
|
||
from deerflow.subagents.report_contract import MAX_ACCEPTANCE_CRITERIA, normalize_acceptance_criteria
|
||
|
||
CHECK_SOURCE = "acceptance_checklist"
|
||
CHECK_REQUIREMENT = "delegation_acceptance_criteria"
|
||
|
||
#: Anti-automation-bias: model-visible verdict text always states its boundary
|
||
#: (same fixed line the citation layer renders).
|
||
_LIMITATION = "execution evidence only, does not validate claim correctness"
|
||
|
||
#: Bounds for untrusted evidence text folded into leaf details.
|
||
_DETAIL_MAX_CHARS = 160
|
||
|
||
#: Remote providers (E2B/OpenSandbox/BoxLite/Tenki/AIO) return an
|
||
#: ``"Error: ..."`` string from ``read_file`` instead of raising — the same
|
||
#: prefix convention ``tool_result_meta`` uses to classify tool errors. A
|
||
#: returned error string is NOT file content: treating it as such would
|
||
#: report a missing file as existing (and non-empty, and read-back-ok).
|
||
_PROVIDER_ERROR_PREFIX = "Error:"
|
||
|
||
_FILE_LEAF_RE = re.compile(r"^file:(?P<path>.+?)\s+(?P<mode>exists|non-empty)$", re.IGNORECASE)
|
||
_FILE_WRITTEN_RE = re.compile(r"^file_written:(?P<path>.+)$", re.IGNORECASE)
|
||
_TESTS_PASSED_RE = re.compile(r"^tests_passed:(?P<command>.+)$", re.IGNORECASE)
|
||
|
||
#: Byte budget for the content read behind ``file:`` leaves — the same scale
|
||
#: at which ``read_file_output_max_chars`` caps the read tool's output. A
|
||
#: larger deliverable is proven by the size probe alone instead of
|
||
#: loading ~2× its size (decoded text plus the utf-8 re-encode used for the
|
||
#: byte count) onto the worker thread, once per ``file:`` criterion.
|
||
_FILE_CONTENT_READ_CAP_BYTES = 50_000
|
||
|
||
#: Test-runner summary shapes recognized in a recorded bash output tail.
|
||
#: Pass shapes require an explicit success summary; fail shapes require an
|
||
#: explicit failure or error record. An output carrying neither is not evidence either
|
||
#: way (UNVERIFIED), and fail shapes win over pass shapes when both appear.
|
||
_TEST_PASS_SHAPE_RE = re.compile(
|
||
r"\b[1-9]\d*\s+passed\b" # pytest / jest: "5 passed" (zero is not a pass)
|
||
r"|^OK$" # unittest: bare OK line
|
||
r"|test result: ok" # cargo test
|
||
r"|^ok\s+\S" # go test: "ok \tpkg/path"
|
||
r"|\bBUILD SUCCESS(?:FUL)?\b" # maven / gradle
|
||
r"|\ball tests passed\b",
|
||
re.IGNORECASE | re.MULTILINE,
|
||
)
|
||
|
||
#: Zero-test evidence vetoes the pass shapes the count-bearing alternatives
|
||
#: cannot see: "0 passed", go's no-test markers, unittest "Ran 0 tests".
|
||
_TEST_ZERO_SHAPE_RE = re.compile(r"\b0\s+passed\b|\[no test files\]|\[no tests to run\]|\bRan 0 tests\b", re.IGNORECASE)
|
||
|
||
_TEST_FAIL_SHAPE_RE = re.compile(
|
||
r"\b[1-9]\d*\s+failed\b" # pytest / jest: "1 failed"
|
||
r"|\b[1-9]\d*\s+errors?\b" # pytest: "1 error" — an errored collection means part of the selection never ran
|
||
r"|^FAILED\b" # unittest summary line
|
||
r"|^ERROR\s+\S" # pytest short summary: "ERROR tests/unit/test_auth.py"
|
||
r"|test result: FAILED" # cargo test
|
||
r"|^FAIL\s+\S" # go test: "FAIL\tpkg/path"
|
||
r"|\bBUILD FAILURE\b", # maven / gradle
|
||
re.IGNORECASE | re.MULTILINE,
|
||
)
|
||
|
||
|
||
class AcceptanceLeaf(TypedDict):
|
||
criterion: str # original criterion text (bounded)
|
||
family: str # file_exists | file_non_empty | file_written | tests_passed | undecidable
|
||
checked: bool # a deterministic check ran
|
||
holds: bool # checked AND the condition holds; always False when unchecked
|
||
detail: str # short evidence note (bounded)
|
||
|
||
|
||
class AcceptanceVerdict(TypedDict):
|
||
source: str
|
||
requirement: str
|
||
leaves: list[AcceptanceLeaf]
|
||
unchecked: list[str] # criteria with no deterministic check (PR5 judge input)
|
||
all_hold: bool # every leaf checked and holds
|
||
|
||
|
||
def parse_file_criterion(criterion: str) -> tuple[str, str] | None:
|
||
"""Classify normalized file criteria for both sandbox admission and checks.
|
||
|
||
Keep admission on the same Unicode-aware patterns as the checker so no
|
||
recognized spelling can fall through to an unowned lazy acquisition.
|
||
"""
|
||
file_match = _FILE_LEAF_RE.match(criterion)
|
||
if file_match is not None:
|
||
family = "file_exists" if file_match.group("mode").lower() == "exists" else "file_non_empty"
|
||
return family, file_match.group("path")
|
||
written_match = _FILE_WRITTEN_RE.match(criterion)
|
||
if written_match is not None:
|
||
return "file_written", written_match.group("path")
|
||
return None
|
||
|
||
|
||
def _bound_detail(text: str) -> str:
|
||
cleaned = " ".join(text.split())
|
||
if len(cleaned) <= _DETAIL_MAX_CHARS:
|
||
return cleaned
|
||
return f"{cleaned[: _DETAIL_MAX_CHARS - 3]}..."
|
||
|
||
|
||
def _resolve_scoped_path(path: str, thread_data: Mapping[str, Any] | None, *, resolve_symlinks: bool = False) -> str | None:
|
||
"""Resolve a criterion path to its sandbox-native virtual form, else ``None``.
|
||
|
||
Virtual ``/mnt/user-data/...`` prefixes map to the thread's host paths
|
||
(``replace_virtual_path``); relative spellings resolve against
|
||
``workspace_path``. The normalized host result must sit under
|
||
``workspace_path`` or ``outputs_path`` — everything else is outside the
|
||
shared domain and the caller marks the leaf UNVERIFIED. The returned
|
||
path is converted back to the virtual form because the sandbox read
|
||
path (local validation and provider mount tables alike) resolves
|
||
virtual paths, not host paths.
|
||
"""
|
||
if not thread_data:
|
||
return None
|
||
roots = [("workspace", thread_data.get("workspace_path")), ("outputs", thread_data.get("outputs_path"))]
|
||
roots = [(kind, root) for kind, root in roots if isinstance(root, str) and root.strip()]
|
||
if not roots:
|
||
return None
|
||
workspace = thread_data.get("workspace_path")
|
||
candidate = path.strip()
|
||
if not candidate:
|
||
return None
|
||
# Lazy import: sandbox.tools pulls the provider stack, and this package is
|
||
# imported in cycles with deerflow.tools (same pattern as report_contract).
|
||
from deerflow.sandbox.tools import replace_virtual_path
|
||
|
||
candidate = replace_virtual_path(candidate, thread_data) # type: ignore[arg-type]
|
||
if not os.path.isabs(candidate):
|
||
if not isinstance(workspace, str) or not workspace.strip():
|
||
return None
|
||
candidate = os.path.join(workspace, candidate)
|
||
normalized = os.path.normpath(candidate)
|
||
for kind, root in roots:
|
||
root_normalized = os.path.normpath(root)
|
||
if normalized == root_normalized or normalized.startswith(root_normalized + os.sep):
|
||
if resolve_symlinks:
|
||
# The lexical check is not enough on the local sandbox: a
|
||
# symlink inside the workspace can point outside the scoped
|
||
# roots (e.g. into uploads), and the later read would follow
|
||
# it. Canonicalize both sides before accepting the scope.
|
||
canonical = os.path.realpath(normalized)
|
||
canonical_root = os.path.realpath(root_normalized)
|
||
if canonical != canonical_root and not canonical.startswith(canonical_root + os.sep):
|
||
return None
|
||
relative = normalized[len(root_normalized) :].lstrip(os.sep).replace(os.sep, "/")
|
||
return f"{VIRTUAL_PATH_PREFIX}/{kind}" + (f"/{relative}" if relative else "")
|
||
return None
|
||
|
||
|
||
#: Remote size-probe script (POSIX sh, positional params: ``$1`` path,
|
||
#: ``$2`` mount root). Answers a bare byte count for a regular file, or one
|
||
#: of ``NOFILE`` / ``UNREADABLE`` / ``NONREGULAR`` / ``ESCAPED``. Everything
|
||
#: runs from a fresh ``env -i`` shell with absolute-path utilities, so a
|
||
#: completed subagent's shell state cannot steer it; ``stat`` never opens
|
||
#: content, so a FIFO cannot block. Containment is canonicalized: the file's
|
||
#: ``realpath`` must stay under the mount root's ``realpath``. The canonical
|
||
#: root — not the literal spelling — is the reference because e2b and Tenki
|
||
#: realize ``/mnt/user-data`` as a symlink to the home dir by default; a
|
||
#: canonical root is also exactly what the provider's own read path
|
||
#: resolves, so the probe and the later read-back stay consistent. A
|
||
#: leaf-level symlink is rejected outright by the non-dereferencing
|
||
#: ``stat -c %F``; an intermediate dir-link escape under a sane root still
|
||
#: lands outside the canonical root (ESCAPED).
|
||
_SIZE_PROBE_INNER_SCRIPT = (
|
||
'[ -e "$1" ] || { echo NOFILE; exit 0; }; '
|
||
't=$(/usr/bin/stat -c %F -- "$1") || { echo UNREADABLE; exit 0; }; '
|
||
'[ "$t" = "regular file" ] || { echo NONREGULAR; exit 0; }; '
|
||
'r=$(/usr/bin/realpath -- "$2") || { echo UNREADABLE; exit 0; }; '
|
||
'p=$(/usr/bin/realpath -- "$1") || { echo UNREADABLE; exit 0; }; '
|
||
'case $p in "$r"/*) /usr/bin/stat -c %s -- "$p" ;; *) echo ESCAPED ;; esac'
|
||
)
|
||
|
||
|
||
def _probe_file_size(runtime: Any, resolved: str, thread_data: Mapping[str, Any] | None) -> int | None:
|
||
"""Bounded byte size of the resolved file, else ``None`` when it cannot be
|
||
established without reading content.
|
||
|
||
Local sandbox: a direct ``os.stat`` of the validated host path — the same
|
||
filesystem access the read itself would perform, no shell, so the
|
||
host-bash kill switch (a shell-execution policy) does not apply and the
|
||
supported host-bash-disabled configuration keeps working. Remote
|
||
providers: a metadata-only probe in a fresh ``env -i`` shell — utilities
|
||
by absolute path (a poisoned persistent session's functions, aliases,
|
||
exported functions, ``PATH``, ``IFS``, or locale cannot steer it; the
|
||
marker env also routes AIO off its persistent shell onto a fresh
|
||
per-call session), no content opens (``stat`` only, so a FIFO cannot
|
||
block the parent for the provider's idle timeout), a regular-file type
|
||
requirement, and canonicalized containment: the file's ``realpath``
|
||
must stay under the mount root's ``realpath`` — the reference is
|
||
canonical because e2b and Tenki realize ``/mnt/user-data`` as a symlink
|
||
to the home dir by default, and a canonical root is exactly what the
|
||
provider's own read path resolves, so probe and read-back stay
|
||
consistent. A final-component symlink is rejected outright
|
||
(``stat -c %F`` without dereference); an intermediate dir-link escape
|
||
under a sane root lands outside the canonical root (``ESCAPED``).
|
||
Outcomes are rendered in the probe's own words (``NOFILE`` /
|
||
``UNREADABLE`` / ``NONREGULAR`` / ``ESCAPED``), never parsed from
|
||
provider error text. Only a bare integer is a size; every other outcome
|
||
is ``None`` and the caller degrades to UNVERIFIED rather than
|
||
performing an unbounded read. Residual: a root-privileged subagent
|
||
(replacing the container's own binaries, or remounting the storage
|
||
root) is only answerable by provider-side metadata APIs, which the
|
||
sandbox contract does not expose.
|
||
|
||
Non-regular local entries (directories, fifos) and mount-mapped virtual
|
||
paths the parent cannot resolve to a host path yield ``None``. The size
|
||
is a point-in-time probe: the subagent has completed, so a grow-between-
|
||
probe-and-read race is accepted (same tradeoff as ``download_file``).
|
||
|
||
Raises ``FileNotFoundError`` when the file is provably absent — the same
|
||
contract ``content_reader`` has.
|
||
"""
|
||
try:
|
||
from deerflow.sandbox.tools import _resolve_local_read_path, ensure_sandbox_initialized, is_local_sandbox
|
||
|
||
if is_local_sandbox(runtime):
|
||
host_path = _resolve_local_read_path(resolved, thread_data)
|
||
if host_path == resolved:
|
||
# A mount-mapped virtual path: only the provider's mount
|
||
# table resolves it, and the parent cannot stat that.
|
||
return None
|
||
stat_result = os.stat(host_path)
|
||
return stat_result.st_size if stat.S_ISREG(stat_result.st_mode) else None
|
||
sandbox = ensure_sandbox_initialized(runtime)
|
||
root = "/".join(resolved.split("/")[:4]) # the /mnt/user-data/{workspace|outputs} mount root
|
||
output = sandbox.execute_command(
|
||
f"/usr/bin/env -i /bin/sh -c {shlex.quote(_SIZE_PROBE_INNER_SCRIPT)} probe {shlex.quote(resolved)} {shlex.quote(root)}",
|
||
env={"_DEERFLOW_SIZE_PROBE": "1"},
|
||
)
|
||
except FileNotFoundError:
|
||
raise
|
||
except Exception:
|
||
# Best-effort optimization over provider-specific failure modes; the
|
||
# caller degrades to UNVERIFIED. Runs only inside the offloaded
|
||
# checklist call, so this broad catch cannot mask a BlockingError on
|
||
# the event loop (same precedent as _safe_load_agent_config).
|
||
return None
|
||
text = str(output or "").strip()
|
||
if text == "NOFILE":
|
||
raise FileNotFoundError(resolved)
|
||
return int(text) if text.isdigit() else None
|
||
|
||
|
||
#: Remote read-probe script (POSIX sh, positional params: ``$1`` path,
|
||
#: ``$2`` mount root). Answers ``READABLE`` / ``UNREADABLE`` (or one of the
|
||
#: shared ``NOFILE`` / ``NONREGULAR`` / ``ESCAPED`` rejections). The same
|
||
#: fresh-shell, absolute-path, non-dereferencing, canonicalized-containment
|
||
#: discipline as ``_SIZE_PROBE_INNER_SCRIPT``; only then one bounded open
|
||
#: (``head -c 1``) proves the file can be opened for reading — ``stat``
|
||
#: alone cannot: a mode-000 deliverable stats fine while any open raises
|
||
#: EACCES, and metadata must not stand in for ``file_written``'s read-back
|
||
#: claim. The regular-file gate runs first, so no FIFO or device is ever
|
||
#: opened.
|
||
_READ_PROBE_INNER_SCRIPT = (
|
||
'[ -e "$1" ] || { echo NOFILE; exit 0; }; '
|
||
't=$(/usr/bin/stat -c %F -- "$1") || { echo UNREADABLE; exit 0; }; '
|
||
'[ "$t" = "regular file" ] || { echo NONREGULAR; exit 0; }; '
|
||
'r=$(/usr/bin/realpath -- "$2") || { echo UNREADABLE; exit 0; }; '
|
||
'p=$(/usr/bin/realpath -- "$1") || { echo UNREADABLE; exit 0; }; '
|
||
'case $p in "$r"/*) ;; *) echo ESCAPED; exit 0 ;; esac; '
|
||
'/usr/bin/head -c 1 -- "$p" >/dev/null 2>&1 && echo READABLE || echo UNREADABLE'
|
||
)
|
||
|
||
|
||
def _probe_file_readable(runtime: Any, resolved: str, thread_data: Mapping[str, Any] | None) -> bool | None:
|
||
"""Whether the resolved file can be opened for reading — one bounded byte —
|
||
else ``None`` when the probe itself cannot answer.
|
||
|
||
Backs the ``file_written`` read-back claim above the content read cap:
|
||
metadata (``stat``) proves existence and size, not readability. Local
|
||
sandbox: a direct one-byte ``open`` of the validated host path — the
|
||
same filesystem access the read itself would perform, no shell.
|
||
Remote providers: one bounded open in a fresh ``env -i`` shell with
|
||
absolute-path utilities (same discipline as the size probe; the marker
|
||
env also routes AIO off its persistent shell), after the
|
||
non-dereferencing regular-file gate — a FIFO is rejected before any
|
||
open, so nothing can block — and canonicalized containment.
|
||
``False`` means the open provably failed (e.g. EACCES); any
|
||
probe-level failure is ``None`` and the caller degrades to UNVERIFIED.
|
||
"""
|
||
try:
|
||
from deerflow.sandbox.tools import _resolve_local_read_path, ensure_sandbox_initialized, is_local_sandbox
|
||
|
||
if is_local_sandbox(runtime):
|
||
host_path = _resolve_local_read_path(resolved, thread_data)
|
||
if host_path == resolved:
|
||
# A mount-mapped virtual path: only the provider's mount
|
||
# table resolves it, and the parent cannot open that.
|
||
return None
|
||
try:
|
||
with open(host_path, "rb") as handle:
|
||
handle.read(1)
|
||
return True
|
||
except OSError:
|
||
return False
|
||
sandbox = ensure_sandbox_initialized(runtime)
|
||
root = "/".join(resolved.split("/")[:4]) # the /mnt/user-data/{workspace|outputs} mount root
|
||
output = sandbox.execute_command(
|
||
f"/usr/bin/env -i /bin/sh -c {shlex.quote(_READ_PROBE_INNER_SCRIPT)} probe {shlex.quote(resolved)} {shlex.quote(root)}",
|
||
env={"_DEERFLOW_SIZE_PROBE": "1"},
|
||
)
|
||
except Exception:
|
||
# Same failure-isolation precedent as _probe_file_size: best-effort
|
||
# over provider-specific failure modes; the caller degrades.
|
||
return None
|
||
text = str(output or "").strip()
|
||
if text == "READABLE":
|
||
return True
|
||
if text == "UNREADABLE":
|
||
return False
|
||
return None
|
||
|
||
|
||
def _check_file_leaf(
|
||
family: str,
|
||
path: str,
|
||
*,
|
||
runtime: Any,
|
||
thread_data: Mapping[str, Any] | None,
|
||
content_reader: Callable[[Any, str], str],
|
||
size_prober: Callable[[Any, str, Mapping[str, Any] | None], int | None],
|
||
readable_prober: Callable[[Any, str, Mapping[str, Any] | None], bool | None],
|
||
) -> AcceptanceLeaf:
|
||
criterion_path = path.strip()
|
||
# Lazy imports: the sandbox helpers pull the provider stack, and this
|
||
# package is imported in cycles with deerflow.tools (same pattern as
|
||
# report_contract).
|
||
from deerflow.sandbox.exceptions import SandboxError, SandboxFileNotFoundError
|
||
from deerflow.sandbox.tools import is_local_sandbox
|
||
|
||
# Symlink escapes are a local-sandbox concern (host-visible links); remote
|
||
# providers resolve paths inside the sandbox where the parent cannot
|
||
# canonicalize, so the check stays lexical there.
|
||
resolved = _resolve_scoped_path(criterion_path, thread_data, resolve_symlinks=is_local_sandbox(runtime))
|
||
base: AcceptanceLeaf = {"criterion": "", "family": family, "checked": False, "holds": False, "detail": ""}
|
||
if resolved is None:
|
||
base["detail"] = "path is outside the shared thread workspace" if thread_data else "shared thread workspace unavailable"
|
||
return base
|
||
try:
|
||
probed_size = size_prober(runtime, resolved, thread_data)
|
||
except (FileNotFoundError, SandboxFileNotFoundError):
|
||
base["checked"] = True
|
||
base["detail"] = "file does not exist"
|
||
return base
|
||
if probed_size is None:
|
||
# The size could not be established by a bounded probe (unreadable or
|
||
# non-regular file, mount-mapped path, probe unavailable). Reading the
|
||
# content anyway could materialize an unbounded deliverable on the
|
||
# worker — degrade to UNVERIFIED instead.
|
||
base["detail"] = "file size could not be established by a bounded probe; content not read"
|
||
return base
|
||
if probed_size > _FILE_CONTENT_READ_CAP_BYTES:
|
||
# Large deliverable: the size probe proved the file exists, is
|
||
# regular, and is non-empty (size > cap > 0) — answering the
|
||
# existence/non-empty leaves without loading content (and its utf-8
|
||
# re-encode) onto the worker. Metadata is NOT read-back, though: a
|
||
# mode-000 file stats fine while any open raises EACCES, so
|
||
# ``file_written`` additionally requires a bounded one-byte open
|
||
# probe; without its proof the leaf stays UNVERIFIED.
|
||
base["checked"] = True
|
||
base["holds"] = True
|
||
if family == "file_non_empty":
|
||
base["detail"] = f"{probed_size} bytes (size probe; content not loaded)"
|
||
elif family == "file_written":
|
||
readable = readable_prober(runtime, resolved, thread_data)
|
||
if readable is not True:
|
||
base["checked"] = False
|
||
base["holds"] = False
|
||
base["detail"] = "file cannot be opened for reading (bounded open probe failed)" if readable is False else "readability could not be established by a bounded probe; content not read"
|
||
return base
|
||
base["detail"] = f"read probe ok, {probed_size} bytes (content above the read cap not loaded)"
|
||
else: # file_exists
|
||
base["detail"] = f"exists, {probed_size} bytes (size probe; content not loaded)"
|
||
return base
|
||
try:
|
||
content = content_reader(runtime, resolved) # resolved is the virtual read path
|
||
except (FileNotFoundError, SandboxFileNotFoundError):
|
||
base["checked"] = True
|
||
base["detail"] = "file does not exist"
|
||
return base
|
||
except UnicodeDecodeError:
|
||
# A binary deliverable (PDF, image, spreadsheet): undecodable bytes
|
||
# prove the file exists and is non-empty — a valid outcome for every
|
||
# file leaf, not an error.
|
||
base["checked"] = True
|
||
base["holds"] = True
|
||
base["detail"] = "binary file (undecodable as text)"
|
||
return base
|
||
except (OSError, SandboxError) as exc:
|
||
base["detail"] = _bound_detail(f"read failed: {exc}")
|
||
return base
|
||
if not is_local_sandbox(runtime) and content.startswith(_PROVIDER_ERROR_PREFIX):
|
||
# A missing/inaccessible file on a REMOTE provider comes back as an
|
||
# error string, not an exception — the check ran and the file cannot
|
||
# be confirmed, so the leaf deterministically does not hold. The
|
||
# local sandbox raises instead, so an ``Error:``-prefixed string from
|
||
# it is genuine file content and must not be classified as a failure.
|
||
base["checked"] = True
|
||
base["detail"] = _bound_detail(f"read returned an error: {content}")
|
||
return base
|
||
byte_count = len(content.encode("utf-8"))
|
||
base["checked"] = True
|
||
if family == "file_non_empty":
|
||
base["holds"] = byte_count > 0
|
||
base["detail"] = f"{byte_count} bytes" if byte_count > 0 else "file is empty"
|
||
elif family == "file_written":
|
||
# Existence + read-back: the persisted bytes are retrievable.
|
||
base["holds"] = True
|
||
base["detail"] = f"read-back ok, {byte_count} bytes"
|
||
else: # file_exists
|
||
base["holds"] = True
|
||
base["detail"] = f"exists, {byte_count} bytes"
|
||
return base
|
||
|
||
|
||
_SHELL_OPERATORS = ";&|"
|
||
#: Leading ``VAR=value`` assignments are environment setup, not the executable.
|
||
_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
||
_WINDOWS_DRIVE_QUALIFIED_RE = re.compile(r"^[A-Za-z]:")
|
||
_WINDOWS_DRIVE_ABSOLUTE_RE = re.compile(r"^[A-Za-z]:/")
|
||
_WINDOWS_UNC_ABSOLUTE_RE = re.compile(r"^//[^/]+/[^/]+(?:/|$)")
|
||
#: A generated 8.3 component has at most six legal stem characters before a
|
||
#: one-digit ``~N`` tail and may retain an extension of at most three legal
|
||
#: characters. Lexical normalization cannot prove that it differs from the
|
||
#: corresponding long name on a volume where short-name generation is active.
|
||
_WINDOWS_SHORT_NAME_COMPONENT_RE = re.compile(r"^[A-Za-z0-9$%_'@~`!(){}^#&-]{1,6}~[1-9](?:\.[A-Za-z0-9$%_'@~`!(){}^#&-]{1,3})?$")
|
||
#: PowerShell paths may name a provider/PSDrive before ``:`` (for example,
|
||
#: ``FileSystem::C:/tmp`` or ``External:/tmp``). Those forms are absolute in
|
||
#: PowerShell but look relative to POSIX path normalization.
|
||
_POWERSHELL_DRIVE_QUALIFIED_RE = re.compile(r"^[^/\\:]+:")
|
||
#: A provider-qualified filesystem path carries either a drive designator or
|
||
#: a UNC root after PowerShell's ``Provider::`` prefix. Keep that delimiter
|
||
#: distinct from pytest's later ``::nodeid`` separator.
|
||
_POWERSHELL_PROVIDER_QUALIFIED_RE = re.compile(r"^[^/\\:]+::(?P<path>(?:[^/\\:]+:|//))")
|
||
#: Unlike drive-letter paths, a named PSDrive may have a multi-character name.
|
||
#: A separator after the colon distinguishes root-anchored from drive-relative
|
||
#: spellings whose resolution depends on that PSDrive's remembered location.
|
||
_POWERSHELL_DRIVE_ABSOLUTE_RE = re.compile(r"^[^/\\:]+:/")
|
||
#: ``cmd.exe`` expands paired-percent environment references before running
|
||
#: the command. Without shell provenance, a token such as ``%TEMP%`` cannot
|
||
#: be treated as the literal relative path seen by the POSIX parser.
|
||
_CMD_ENV_EXPANSION_RE = re.compile(r"%[^%\r\n]+%")
|
||
_CMD_DELAYED_ENV_EXPANSION_RE = re.compile(r"![^!\r\n]+!")
|
||
#: Shells supported by ``LocalSandbox`` do not agree on Bash brace expansion.
|
||
#: The evidence currently records no shell kind, so matching must reject it
|
||
#: before a POSIX parser can turn the expression into one harmless-looking
|
||
#: token.
|
||
_BASH_BRACE_EXPANSION_RE = re.compile(r"\{[^{}\r\n]*(?:,|\.\.)[^{}\r\n]*\}")
|
||
#: PowerShell treats seven typographic single and double quotes as string
|
||
#: delimiters, while POSIX ``shlex`` retains them as ordinary token characters.
|
||
_POWERSHELL_QUOTE_DELIMITERS = frozenset("‘’‚‛“”„")
|
||
|
||
|
||
def _carries_summary_shape(text: str) -> bool:
|
||
"""Whether *text* carries any recognized test-summary shape (pass, fail,
|
||
or zero-test veto). The pass shapes match as substrings, so
|
||
subagent-chosen strings — a ``cd`` argument, a ``CDPATH`` value — must
|
||
not be allowed to lend the recorded tail a summary shape."""
|
||
return bool(_TEST_PASS_SHAPE_RE.search(text) or _TEST_FAIL_SHAPE_RE.search(text) or _TEST_ZERO_SHAPE_RE.search(text))
|
||
|
||
|
||
def _normalize_cd_scope_path(path: str) -> tuple[str, bool] | None:
|
||
"""Normalize a ``cd`` path and retain Windows-absolute provenance.
|
||
|
||
``posixpath.normpath`` drops the slash from a bare Windows drive root and
|
||
collapses traversal above that root into a relative-looking path. Handle
|
||
drive-qualified absolute and UNC paths component by component so those
|
||
spellings remain absolute, and fail closed when ``..`` would cross the
|
||
drive or share root. Drive-relative forms such as ``C:tmp`` also fail
|
||
closed because their resolution depends on the process's remembered
|
||
directory for that drive.
|
||
"""
|
||
slash_path = path.replace("\\", "/")
|
||
if slash_path.startswith(("//?/", "//./")):
|
||
# Win32 device namespaces can alias ordinary drive/UNC paths, but the
|
||
# lexical checker has no filesystem provenance to resolve them.
|
||
return None
|
||
if _POWERSHELL_DRIVE_QUALIFIED_RE.match(slash_path) and not _WINDOWS_DRIVE_QUALIFIED_RE.match(slash_path):
|
||
return None
|
||
if _WINDOWS_DRIVE_QUALIFIED_RE.match(slash_path) and not _WINDOWS_DRIVE_ABSOLUTE_RE.match(slash_path):
|
||
return None
|
||
if _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path):
|
||
unc_parts = slash_path[2:].split("/")
|
||
share_root = unc_parts[:2]
|
||
parts: list[str] = []
|
||
for part in unc_parts[2:]:
|
||
if not part or part == ".":
|
||
continue
|
||
if part == "..":
|
||
if not parts:
|
||
return None
|
||
parts.pop()
|
||
else:
|
||
parts.append(part)
|
||
normalized = "//" + "/".join((*share_root, *parts))
|
||
return normalized, True
|
||
if not _WINDOWS_DRIVE_ABSOLUTE_RE.match(slash_path):
|
||
return posixpath.normpath(slash_path), False
|
||
|
||
parts: list[str] = []
|
||
for part in slash_path[3:].split("/"):
|
||
if not part or part == ".":
|
||
continue
|
||
if part == "..":
|
||
if not parts:
|
||
return None
|
||
parts.pop()
|
||
else:
|
||
parts.append(part)
|
||
normalized = slash_path[:2] + "/" + "/".join(parts)
|
||
return normalized, True
|
||
|
||
|
||
def _cd_target_in_scope(target: str, thread_data: Mapping[str, Any] | None) -> bool:
|
||
"""Whether a preceding ``cd`` target provably keeps the criterion's
|
||
relative path-like targets resolving inside the thread's data roots.
|
||
|
||
A relative target with no ``..`` component descends from the current
|
||
directory without escaping it; an absolute target must sit under the
|
||
thread's workspace/outputs/uploads paths or the virtual data prefix
|
||
(``/mnt/user-data/...``) — the roots a subagent is expected to work in,
|
||
so ``cd /mnt/user-data/workspace && …`` and the local auto-prefix stay
|
||
verifiable. ``cd /tmp/fake && pytest tests/`` (a directory the subagent
|
||
fully controls, outside any data root), ``cd ../out`` (walks out),
|
||
``~`` spellings (the subagent's home), and ``-`` (prints OLDPWD) are
|
||
all out. A symlink INSIDE an allowed root pointing out is a
|
||
filesystem-layer concern this text matcher cannot see — see the Known
|
||
boundaries note in subagents/AGENTS.md.
|
||
"""
|
||
if not target or target == "-" or target.startswith("~"):
|
||
return False
|
||
if ".." in target.replace("\\", "/").split("/"):
|
||
# Lexical cleanup is not proof of containment: the OS follows a
|
||
# directory symlink before resolving the following ``..`` component.
|
||
return False
|
||
# After shell-dependent spellings have failed closed, keep deterministic
|
||
# POSIX lexical semantics even when this checker itself runs on Windows.
|
||
normalized_target = _normalize_cd_scope_path(target)
|
||
if normalized_target is None:
|
||
return False
|
||
normalized, is_windows_absolute = normalized_target
|
||
if _thread_uses_windows_paths(thread_data) and normalized.startswith("/") and not is_windows_absolute:
|
||
# ``/mnt/...`` is absolute under Bash but drive-rooted under
|
||
# PowerShell. Without the executing shell, a Windows workspace cannot
|
||
# prove that the virtual POSIX spelling stayed inside its data root.
|
||
return False
|
||
if normalized.startswith("/") or is_windows_absolute:
|
||
roots = [VIRTUAL_PATH_PREFIX]
|
||
for key in ("workspace_path", "outputs_path", "uploads_path"):
|
||
value = (thread_data or {}).get(key)
|
||
if isinstance(value, str) and value:
|
||
roots.append(value)
|
||
for root in roots:
|
||
normalized_root_result = _normalize_cd_scope_path(root)
|
||
if normalized_root_result is None:
|
||
continue
|
||
normalized_root, root_is_windows_absolute = normalized_root_result
|
||
use_windows_comparison = is_windows_absolute and root_is_windows_absolute
|
||
candidate_for_comparison = ntpath.normcase(normalized) if use_windows_comparison else normalized
|
||
root_for_comparison = ntpath.normcase(normalized_root) if use_windows_comparison else normalized_root
|
||
separator = "\\" if use_windows_comparison else "/"
|
||
root_prefix = root_for_comparison if root_for_comparison.endswith(separator) else root_for_comparison + separator
|
||
if candidate_for_comparison == root_for_comparison or candidate_for_comparison.startswith(root_prefix):
|
||
return True
|
||
return False
|
||
return ".." not in normalized.split("/")
|
||
|
||
|
||
def _is_silent_segment(tokens: list[str], thread_data: Mapping[str, Any] | None = None) -> bool:
|
||
"""Whether a preceding segment is provably output-free, by invocation
|
||
form — not by executable name alone: ``pushd``/``popd`` print
|
||
the directory stack, ``umask``/``ulimit`` print on several forms, and
|
||
``source``/``.`` execute arbitrary file content — a ``*/bin/activate``
|
||
path shape says nothing about what the script emits (the subagent
|
||
controls the filesystem and can craft one), so sourced prefixes are
|
||
never provably silent. ``export``/``unset`` are never provably silent
|
||
either: an invalid identifier makes bash print ``export: <arg>: not a
|
||
valid identifier`` — subagent-chosen text that can itself carry a
|
||
summary shape (``export 'all tests passed'; make test``) — and valid
|
||
argument forms mutate shell state (see ``_segment_pollutes_state``).
|
||
|
||
``cd`` is silent only with exactly one argument that is literal and
|
||
shape-free: with CDPATH set it prints the resolved destination —
|
||
subagent-chosen text — so a shaped (``mkdir 'all tests passed'``) or
|
||
runtime-expanded (``cd $D``, ``cd all*``) argument could lend the tail
|
||
a summary shape. The target must also stay in scope
|
||
(``_cd_target_in_scope``): the criterion's relative path-like targets
|
||
resolve in whatever directory the wrapper sets, and an out-of-scope
|
||
``cd /tmp/fake`` would let a subagent-crafted directory certify them.
|
||
Bare ``cd`` (goes HOME) and ``cd old new`` (substitution) are likewise
|
||
unprovable. Behavior-changing assignments that feed the print
|
||
(``CDPATH=``) are state pollution and degrade the match upstream; the
|
||
everyday shape-free ``cd dir &&`` wrapper stays silent (bash_tool
|
||
auto-prefixes it for every local command).
|
||
"""
|
||
stripped = _strip_env_assignments(tokens)
|
||
if not stripped:
|
||
return True # pure VAR=value assignments
|
||
executable = os.path.basename(stripped[0])
|
||
args = stripped[1:]
|
||
if executable == "cd":
|
||
if len(args) != 1:
|
||
return False
|
||
target = args[0]
|
||
if _carries_summary_shape(target) or _EXPANSION_CHAR_RE.search(target) or _GLOB_CHAR_RE.search(target):
|
||
return False
|
||
return _cd_target_in_scope(target, thread_data)
|
||
# pushd/popd (print the stack), umask/ulimit (print forms), export/unset
|
||
# (see the docstring), source/. and every other executable are not
|
||
# provably silent.
|
||
return False
|
||
|
||
|
||
#: Runner options whose value *excludes* a target instead of running it
|
||
#: (pytest ``--ignore``/``--deselect`` and the generic skip/exclude family).
|
||
#: A criterion matching such a value would affirm tests that were explicitly
|
||
#: deselected, so negated tokens are ineligible as match evidence.
|
||
_NEGATING_OPTION_TOKENS = frozenset({"--ignore", "--ignore-glob", "--deselect", "--exclude", "--exclude-glob", "--skip", "--skip-file"})
|
||
|
||
|
||
def _negated_positions(tokens: list[str]) -> tuple[set[int], set[int]]:
|
||
"""Split *tokens* positions into (negating option tokens, their values).
|
||
|
||
Both are accounted-for shell structure rather than free extras: the
|
||
option names a known exclusion mechanism and the value names what did
|
||
NOT run, so neither is eligible as match evidence and neither is
|
||
classified as a behavior-changing *extra* flag.
|
||
"""
|
||
options: set[int] = set()
|
||
values: set[int] = set()
|
||
for index, token in enumerate(tokens):
|
||
if token in _NEGATING_OPTION_TOKENS:
|
||
options.add(index)
|
||
if index + 1 < len(tokens):
|
||
values.add(index + 1)
|
||
elif any(token.startswith(f"{option}=") for option in _NEGATING_OPTION_TOKENS):
|
||
options.add(index)
|
||
values.add(index)
|
||
return options, values
|
||
|
||
|
||
def _negated_value(token: str) -> str:
|
||
"""The exclusion target a negated token names: the bare value as-is, or
|
||
the part after ``=`` for the glued form (``--deselect=tests/x.py``)."""
|
||
for option in _NEGATING_OPTION_TOKENS:
|
||
if token.startswith(f"{option}="):
|
||
return token.split("=", 1)[1]
|
||
return token
|
||
|
||
|
||
def _thread_uses_windows_paths(thread_data: Mapping[str, Any] | None) -> bool:
|
||
"""Whether the thread roots establish Windows filesystem semantics."""
|
||
for key in ("workspace_path", "outputs_path", "uploads_path"):
|
||
value = (thread_data or {}).get(key)
|
||
if not isinstance(value, str):
|
||
continue
|
||
slash_path = value.replace("\\", "/")
|
||
if _WINDOWS_DRIVE_ABSOLUTE_RE.match(slash_path) or _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path):
|
||
return True
|
||
return False
|
||
|
||
|
||
def _selection_path_parts(value: str) -> tuple[str, str | None]:
|
||
"""Split a runner selection into its filesystem path and pytest nodeid."""
|
||
provider_match = _POWERSHELL_PROVIDER_QUALIFIED_RE.match(value)
|
||
nodeid_start = provider_match.end() if provider_match is not None else 0
|
||
marker_index = value.find("::", nodeid_start)
|
||
if marker_index < 0:
|
||
return value, None
|
||
return value[:marker_index], value[marker_index + 2 :]
|
||
|
||
|
||
def _without_powershell_provider(path: str) -> str:
|
||
"""Return the rooted portion of a provider-qualified filesystem path."""
|
||
provider_match = _POWERSHELL_PROVIDER_QUALIFIED_RE.match(path)
|
||
if provider_match is None:
|
||
return path
|
||
return path[provider_match.start("path") :]
|
||
|
||
|
||
def _has_parent_path_component(value: str) -> bool:
|
||
path, _nodeid = _selection_path_parts(value)
|
||
return ".." in path.replace("\\", "/").split("/")
|
||
|
||
|
||
def _uses_windows_selection_semantics(value: str, *, windows_path_context: bool) -> bool:
|
||
path, _nodeid = _selection_path_parts(value)
|
||
slash_path = _without_powershell_provider(path).replace("\\", "/")
|
||
return bool(windows_path_context or _POWERSHELL_DRIVE_QUALIFIED_RE.match(slash_path) or _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path))
|
||
|
||
|
||
def _selection_path_kind(value: str) -> str:
|
||
path, _nodeid = _selection_path_parts(value)
|
||
slash_path = _without_powershell_provider(path).replace("\\", "/")
|
||
if _WINDOWS_DRIVE_ABSOLUTE_RE.match(slash_path):
|
||
return "windows_drive_absolute"
|
||
if _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path):
|
||
return "windows_unc_absolute"
|
||
if slash_path.startswith("/"):
|
||
return "posix_absolute"
|
||
if _POWERSHELL_DRIVE_QUALIFIED_RE.match(slash_path) and not _WINDOWS_DRIVE_QUALIFIED_RE.match(slash_path):
|
||
if _POWERSHELL_DRIVE_ABSOLUTE_RE.match(slash_path):
|
||
return "powershell_drive_absolute"
|
||
return "powershell_drive_relative"
|
||
return "relative"
|
||
|
||
|
||
def _windows_volume_identifier(value: str) -> tuple[str, str] | None:
|
||
"""Return a comparable drive, PSDrive, or UNC-root identifier."""
|
||
path, _nodeid = _selection_path_parts(value)
|
||
slash_path = _without_powershell_provider(path).replace("\\", "/")
|
||
if _WINDOWS_DRIVE_QUALIFIED_RE.match(slash_path):
|
||
return "drive", ntpath.normcase(slash_path[:2])
|
||
if _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path):
|
||
server, share, *_rest = slash_path[2:].split("/")
|
||
return "unc", ntpath.normcase(f"//{server}/{share}")
|
||
if _POWERSHELL_DRIVE_QUALIFIED_RE.match(slash_path):
|
||
drive, _separator, _rest = slash_path.partition(":")
|
||
return "psdrive", ntpath.normcase(drive)
|
||
return None
|
||
|
||
|
||
def _has_ambiguous_windows_component(value: str, *, windows_path_context: bool) -> bool:
|
||
"""Whether ordinary Win32 cleanup may alias a textual path component.
|
||
|
||
Windows APIs normally discard trailing spaces and periods from path
|
||
components, while extended-length paths can preserve them. Generated 8.3
|
||
short names can also identify a longer component without any lexical
|
||
relationship. Without shell and filesystem provenance, these spellings
|
||
must make an overlap decision fail closed.
|
||
"""
|
||
if not _uses_windows_selection_semantics(value, windows_path_context=windows_path_context):
|
||
return False
|
||
path, _nodeid = _selection_path_parts(value)
|
||
path = _without_powershell_provider(path)
|
||
return any(component not in {"", ".", ".."} and (component.endswith((" ", ".")) or _WINDOWS_SHORT_NAME_COMPONENT_RE.fullmatch(component)) for component in path.replace("\\", "/").split("/"))
|
||
|
||
|
||
def _normalize_selection_path(value: str, *, windows_path_context: bool) -> tuple[str, str | None, bool]:
|
||
"""Normalize safe lexical aliases without erasing pytest nodeid case.
|
||
|
||
Parent traversal is rejected by the caller because resolving ``..``
|
||
lexically can cross symlinks. ``.`` and duplicate separators are safe to
|
||
collapse. Drive-qualified and UNC paths always use ``ntpath`` semantics;
|
||
when the thread roots establish a Windows workspace, every runner path
|
||
does, including drive-rooted ``/tests`` and mapped virtual paths.
|
||
"""
|
||
path, nodeid = _selection_path_parts(value)
|
||
slash_path = _without_powershell_provider(path).replace("\\", "/")
|
||
is_windows_path = _uses_windows_selection_semantics(value, windows_path_context=windows_path_context)
|
||
if is_windows_path:
|
||
normalized = ntpath.normcase(ntpath.normpath(slash_path))
|
||
else:
|
||
normalized = posixpath.normpath(slash_path)
|
||
return normalized, nodeid, is_windows_path
|
||
|
||
|
||
def _nodeids_overlap(a: str | None, b: str | None) -> bool:
|
||
if a is None or b is None:
|
||
return True
|
||
return a == b or a.startswith(b + "::") or b.startswith(a + "::")
|
||
|
||
|
||
def _negation_overlaps(criterion_token: str, negated_value: str, *, windows_path_context: bool = False) -> bool:
|
||
"""Whether a negated value overlaps a matched criterion target: equal, or
|
||
one nested under the other at a path boundary (``tests`` vs
|
||
``tests/unit/test_auth.py``) or a pytest nodeid boundary (``tests/x.py``
|
||
vs ``tests/x.py::test_y``). Overlap means part of the criterion's
|
||
selection never ran, so the passing summary may not cover it; unrelated
|
||
exclusions (``--ignore tests/slow`` against ``pytest tests/unit``) do not
|
||
overlap and keep matching. Safe lexical aliases are normalized, while
|
||
mixed path families fail closed because evidence does not carry enough
|
||
filesystem provenance to prove that their spellings are distinct."""
|
||
a_kind = _selection_path_kind(criterion_token)
|
||
b_kind = _selection_path_kind(negated_value)
|
||
if a_kind != b_kind:
|
||
# Relative selections resolve against an unrecorded cwd, and Windows
|
||
# can resolve POSIX-rooted spellings against the current drive or map
|
||
# a drive onto a UNC share. Cross-family spellings can therefore alias
|
||
# even when their lexical prefixes differ.
|
||
return True
|
||
a_volume = _windows_volume_identifier(criterion_token)
|
||
b_volume = _windows_volume_identifier(negated_value)
|
||
if a_volume is not None and b_volume is not None and a_volume != b_volume:
|
||
# Distinct drive letters can alias through SUBST or mapped drives, and
|
||
# distinct UNC roots can alias through DFS, DNS, or share mappings.
|
||
# The execution evidence records none of that volume provenance.
|
||
return True
|
||
if a_kind == "relative":
|
||
a_path, _a_nodeid = _selection_path_parts(criterion_token)
|
||
b_path, _b_nodeid = _selection_path_parts(negated_value)
|
||
a_is_drive_relative = bool(_WINDOWS_DRIVE_QUALIFIED_RE.match(a_path.replace("\\", "/")))
|
||
b_is_drive_relative = bool(_WINDOWS_DRIVE_QUALIFIED_RE.match(b_path.replace("\\", "/")))
|
||
if a_is_drive_relative != b_is_drive_relative:
|
||
# ``tests/x`` and ``D:tests/x`` can name the same path when the
|
||
# process cwd and D:'s remembered cwd coincide. Neither value is
|
||
# absolute, and the execution evidence carries neither cwd.
|
||
return True
|
||
a, a_nodeid, a_is_windows = _normalize_selection_path(criterion_token, windows_path_context=windows_path_context)
|
||
b, b_nodeid, b_is_windows = _normalize_selection_path(negated_value, windows_path_context=windows_path_context)
|
||
if not a or not b or a == "." or b == ".":
|
||
return False
|
||
if a == b:
|
||
return _nodeids_overlap(a_nodeid, b_nodeid)
|
||
if a_is_windows != b_is_windows:
|
||
# A drive-relative spelling can resolve to the same path as an
|
||
# ordinary relative token, but the per-drive cwd is not recorded.
|
||
return True
|
||
separator = "\\" if a_is_windows else "/"
|
||
a_prefix = a if a.endswith(separator) else a + separator
|
||
b_prefix = b if b.endswith(separator) else b + separator
|
||
return a.startswith(b_prefix) or b.startswith(a_prefix)
|
||
|
||
|
||
def _normalize_command(command: str) -> str:
|
||
return " ".join(command.split())
|
||
|
||
|
||
def _shell_parse_line(line: str, *, posix: bool = True) -> tuple[str | None, list[list[str]], list[str]] | None:
|
||
"""Tokenize one physical line into segments plus the operators joining them.
|
||
|
||
Returns ``(leading_op, segments, ops)``: ``ops[i]`` is the operator
|
||
between segment ``i`` and segment ``i+1`` (``;``, ``&&``, ``||``, ``|``,
|
||
``&``, or a rarer punctuation run), and ``leading_op`` is an operator
|
||
the line STARTS with (``cmd1\\n|| cmd2``) — real control flow the caller
|
||
must join with, never drop: a continuation ``||`` after a successful
|
||
first line SKIPS the line's commands while exiting 0, so parsing it as
|
||
``;`` would overstate what provably ran. Comments are stripped (a
|
||
``# pytest ...`` remark executes nothing) and quotes are honored, so an
|
||
operator inside an argument cannot split a segment. Returns ``None`` on
|
||
malformed shell (unbalanced quotes). ``posix=False`` is reserved for the
|
||
raw-token safety pass: it keeps backslashes and surrounding quotes visible
|
||
before the normal POSIX parse can consume them as escaping syntax.
|
||
"""
|
||
lexer = shlex.shlex(line, posix=posix, punctuation_chars=_SHELL_OPERATORS)
|
||
lexer.whitespace_split = True
|
||
lexer.commenters = "#"
|
||
try:
|
||
tokens = list(lexer)
|
||
except ValueError:
|
||
return None
|
||
segments: list[list[str]] = []
|
||
ops: list[str] = []
|
||
current: list[str] = []
|
||
leading_op: str | None = None
|
||
for token in tokens:
|
||
if token and all(char in _SHELL_OPERATORS for char in token):
|
||
if current:
|
||
segments.append(current)
|
||
current = []
|
||
ops.append(token)
|
||
elif not segments and leading_op is None:
|
||
leading_op = token
|
||
# A doubled operator (``cmd ;; esac`` style) attaches no
|
||
# following segment; it can never make evidence more provable,
|
||
# so it is simply not recorded.
|
||
else:
|
||
current.append(token)
|
||
if current:
|
||
segments.append(current)
|
||
# ops[i] is the operator following segment i; a trailing operator (e.g.
|
||
# ``make test &``) leaves ops as long as segments and must stay visible —
|
||
# backgrounding makes the execution unprovable.
|
||
return leading_op, segments, ops
|
||
|
||
|
||
def _shell_parse(command: str, *, posix: bool = True) -> tuple[list[list[str]], list[str]] | None:
|
||
"""Tokenize a shell command into segments plus the operators joining them.
|
||
|
||
Physical newlines are command separators with ``;`` semantics — bash
|
||
executes ``pytest tests/\\nseq 1 90000\\necho '3 passed'`` as three
|
||
sequential commands whose overall exit status is the LAST one's. shlex
|
||
treats ``\\n`` as ordinary whitespace, so parsing the raw string would
|
||
merge the lines into one segment: the trailing ``echo`` would pass for an
|
||
extra positional, its exit status for the run's, and its text for the
|
||
test summary while bulk output (``seq``) pushes the real one out of the
|
||
bounded tail. Each line is parsed on its own and joined with a ``;`` op —
|
||
with the previous line's trailing operator when it ends on one
|
||
(``cmd &``), or with the continuation operator the next line opens with
|
||
(``cmd1\\n&& cmd2`` parses as ``&&``, and ``cmd1\\n|| cmd2`` as ``||`` —
|
||
a continuation ``||`` after a successful first command skips the rest
|
||
while exiting 0, which ``;`` would overstate). A newline inside an open
|
||
quote breaks that line's parse and falls back to exact-equality matching
|
||
— fail closed.
|
||
"""
|
||
segments: list[list[str]] = []
|
||
ops: list[str] = []
|
||
for line in command.split("\n"):
|
||
parsed = _shell_parse_line(line, posix=posix)
|
||
if parsed is None:
|
||
return None
|
||
leading_op, line_segments, line_ops = parsed
|
||
if not line_segments:
|
||
continue # blank line: no command, no separator effect
|
||
if segments and len(ops) < len(segments):
|
||
# The previous line ended without an operator: the newline
|
||
# itself separates the two commands — with the continuation
|
||
# operator this line opens with (``cmd1\\n&& cmd2``), not a
|
||
# hardcoded ``;``.
|
||
ops.append(leading_op or ";")
|
||
segments.extend(line_segments)
|
||
ops.extend(line_ops)
|
||
return segments, ops
|
||
|
||
|
||
def _has_shell_ambiguous_whitespace(command: str) -> bool:
|
||
"""Whether whitespace can split differently across supported shells.
|
||
|
||
ASCII space, tab, LF, and CRLF are shared separators. PowerShell also
|
||
separates on bare CR, vertical tab, form feed, NEL, and Unicode separator
|
||
characters, while Python's POSIX ``shlex`` can retain them inside a token.
|
||
"""
|
||
command_without_crlf = command.replace("\r\n", "\n")
|
||
return any(character in "\r\v\f\x85" or (character != " " and unicodedata.category(character) in {"Zs", "Zl", "Zp"}) for character in command_without_crlf)
|
||
|
||
|
||
def _has_cmd_control_operator_in_single_quotes(command: str) -> bool:
|
||
"""Whether POSIX single quotes hide cmd.exe control syntax.
|
||
|
||
Cmd does not use single quotes for grouping, so metacharacters within them
|
||
remain active there. Ordinary single-quoted text stays verifiable for the
|
||
POSIX execution path. Shell-specific ways to escape quotes are already
|
||
rejected by the other provenance checks below.
|
||
"""
|
||
in_double_quotes = False
|
||
in_single_quotes = False
|
||
for character in command:
|
||
if character == '"':
|
||
in_double_quotes = not in_double_quotes
|
||
elif character == "'" and not in_double_quotes:
|
||
in_single_quotes = not in_single_quotes
|
||
elif in_single_quotes and character in "&|<>()":
|
||
return True
|
||
return False
|
||
|
||
|
||
def _has_unquoted_parenthesis(command: str) -> bool:
|
||
"""Whether a parenthesis appears outside a quoted string.
|
||
|
||
PowerShell evaluates an unquoted parenthesized command expression and
|
||
expands its output into native arguments. POSIX tokenization instead
|
||
leaves the parentheses attached to ordinary tokens, which can hide an
|
||
injected runner option. Parentheses inside double- or single-quoted
|
||
strings are data; cmd-specific control syntax inside single quotes is
|
||
rejected separately because cmd does not honor those quotes.
|
||
"""
|
||
in_double_quotes = False
|
||
in_single_quotes = False
|
||
for character in command:
|
||
if character == '"' and not in_single_quotes:
|
||
in_double_quotes = not in_double_quotes
|
||
elif character == "'" and not in_double_quotes:
|
||
in_single_quotes = not in_single_quotes
|
||
elif character in "()" and not in_double_quotes and not in_single_quotes:
|
||
return True
|
||
return False
|
||
|
||
|
||
def _has_unquoted_single_quote(text: str) -> bool:
|
||
"""Whether *text* contains a single quote outside double quotes.
|
||
|
||
The raw ``posix=False`` token stream retains surrounding quotes. Cmd.exe
|
||
passes a POSIX single quote to the child process instead of using it for
|
||
grouping, so such a token cannot be compared with its quote-stripped
|
||
POSIX form without knowing which shell executed it.
|
||
"""
|
||
in_double_quotes = False
|
||
for character in text:
|
||
if character == '"':
|
||
in_double_quotes = not in_double_quotes
|
||
elif character == "'" and not in_double_quotes:
|
||
return True
|
||
return False
|
||
|
||
|
||
def _raw_segment_has_unquoted_single_quote(segment: list[str]) -> bool:
|
||
return any(_has_unquoted_single_quote(token) for token in segment)
|
||
|
||
|
||
def _matched_span_has_ambiguous_single_quotes(
|
||
expected_raw: list[list[str]],
|
||
actual_raw: list[list[str]],
|
||
actual: list[list[str]],
|
||
*,
|
||
start: int,
|
||
span: int,
|
||
thread_data: Mapping[str, Any] | None,
|
||
) -> bool:
|
||
"""Whether cmd.exe single-quote semantics can change a candidate match.
|
||
|
||
Every expected segment and the matching actual span define the invocation
|
||
being certified, so a POSIX single quote in any of them is ambiguous.
|
||
Before the span, only segments accepted as provably silent can contribute
|
||
to a successful match; a quoted ``cd`` or assignment can change the state
|
||
in which the runner executes and therefore also requires shell provenance.
|
||
Non-silent prefixes remain governed by output-attribution checks, preserving
|
||
authoritative failure results such as ``echo '12 passed'; make test``.
|
||
"""
|
||
if any(_raw_segment_has_unquoted_single_quote(segment) for segment in expected_raw):
|
||
return True
|
||
if any(_raw_segment_has_unquoted_single_quote(segment) for segment in actual_raw[start : start + span]):
|
||
return True
|
||
return any(_raw_segment_has_unquoted_single_quote(actual_raw[index]) and _is_silent_segment(actual[index], thread_data) for index in range(start))
|
||
|
||
|
||
def _command_requires_shell_provenance(command: str) -> bool:
|
||
"""Whether raw command tokens have shell-dependent Windows semantics.
|
||
|
||
The acceptance evidence does not currently record which shell executed a
|
||
command. Inspect a non-POSIX tokenization before the authoritative POSIX
|
||
parser can discard backslashes anywhere in the candidate command: PowerShell
|
||
and native Windows runners preserve them as path separators while POSIX
|
||
shells treat them as escapes. Cmd percent/bang references and ``^``, Bash
|
||
tilde/brace expansion, and PowerShell splatting, typographic quotes, or
|
||
unquoted parentheses can all rewrite the native argv differently. ``#``
|
||
starts a comment for the POSIX parser but is an ordinary argument to
|
||
cmd.exe. Cmd also does not treat single quotes as quoting, so cmd control
|
||
syntax hidden inside POSIX single quotes is unsafe. PowerShell recognizes
|
||
more separators than POSIX ``shlex``, including bare carriage returns and
|
||
Unicode separator characters; normal CRLF remains unambiguous. These forms
|
||
fail closed rather than certifying arguments that depend on an unknown
|
||
shell.
|
||
"""
|
||
if (
|
||
_has_shell_ambiguous_whitespace(command)
|
||
or _has_cmd_control_operator_in_single_quotes(command)
|
||
or "#" in command
|
||
or "^" in command
|
||
or _CMD_DELAYED_ENV_EXPANSION_RE.search(command)
|
||
or _BASH_BRACE_EXPANSION_RE.search(command)
|
||
or any(quote in command for quote in _POWERSHELL_QUOTE_DELIMITERS)
|
||
or _has_unquoted_parenthesis(command)
|
||
):
|
||
return True
|
||
parsed = _shell_parse(command, posix=False)
|
||
if parsed is None:
|
||
return "\\" in command or bool(_CMD_ENV_EXPANSION_RE.search(command))
|
||
segments, _ops = parsed
|
||
return any("\\" in token or token.startswith(("~", "@")) or (token.startswith("(") and token.endswith(")")) or _CMD_ENV_EXPANSION_RE.search(token) for segment in segments for token in segment)
|
||
|
||
|
||
def _strip_env_assignments(tokens: list[str]) -> list[str]:
|
||
index = 0
|
||
while index < len(tokens) and _ENV_ASSIGNMENT_RE.match(tokens[index]):
|
||
index += 1
|
||
return tokens[index:]
|
||
|
||
|
||
def _leading_assignment_tokens(tokens: list[str]) -> list[str]:
|
||
"""The leading ``NAME=value`` assignment tokens (the env-setup prefix)."""
|
||
assignments: list[str] = []
|
||
for token in tokens:
|
||
if not _ENV_ASSIGNMENT_RE.match(token):
|
||
break
|
||
assignments.append(token)
|
||
return assignments
|
||
|
||
|
||
def _effective_env_assignments(tokens: list[str]) -> dict[str, str]:
|
||
"""The effective leading environment as a name → final-value mapping.
|
||
|
||
A set of raw tokens is only order-insensitive when names are distinct:
|
||
shell assignments may repeat a name and the LAST value wins, so
|
||
``CI=0 CI=1`` and ``CI=1 CI=0`` are the same token set but different
|
||
environments (effective ``CI`` of 1 vs 0).
|
||
"""
|
||
effective: dict[str, str] = {}
|
||
for token in _leading_assignment_tokens(tokens):
|
||
name, _, value = token.partition("=")
|
||
effective[name] = value
|
||
return effective
|
||
|
||
|
||
def _segment_pollutes_state(tokens: list[str]) -> bool:
|
||
"""Whether a preceding segment mutates shell state the matcher cannot
|
||
see: ANY assignment (prefix or pure-assignment segment) or any
|
||
``export``/``unset`` with arguments. No variable is provably inert across
|
||
repositories — ``CI``/``DEBUG``/``VERBOSE`` are routinely read by tests
|
||
and can change or skip execution, and PATH/LD_PRELOAD/PYTHONPATH/
|
||
PYTEST_ADDOPTS/MAKEFILES/BASH_ENV change what runs outright."""
|
||
if _leading_assignment_tokens(tokens):
|
||
return True
|
||
stripped = _strip_env_assignments(tokens)
|
||
if not stripped:
|
||
return False
|
||
if os.path.basename(stripped[0]) in ("export", "unset"):
|
||
# ``export NAME`` marks the inherited value for later children,
|
||
# ``export NAME=…`` sets it, ``unset NAME`` removes it — all mutate
|
||
# the state the matched run executes in. (An argument-less
|
||
# ``export`` prints the environment; the silence check rejects it.)
|
||
return bool(stripped[1:])
|
||
return False
|
||
|
||
|
||
#: Tokens whose runtime expansion the matcher cannot see: command/parameter
|
||
#: substitution (``$(cat args)``, ``$TARGET``, backticks). In the matched
|
||
#: span a substitution can inject selection-changing flags or an unknown
|
||
#: exclusion; anywhere it makes the run's arguments unknowable.
|
||
_EXPANSION_CHAR_RE = re.compile(r"[$`]")
|
||
#: Glob metacharacters in an *extra* executed token: the expanded file set
|
||
#: is unknowable — and crafted option-looking filenames (``-k``/``smoke``)
|
||
#: turn a widening glob into an invisible narrowing. Criterion tokens are
|
||
#: matched literally, so a criterion-side glob stays self-consistent.
|
||
_GLOB_CHAR_RE = re.compile(r"[*?[]")
|
||
|
||
|
||
#: Options that consume the NEXT token as their value (separate form), so
|
||
#: that token is not a positional target: the negating family (the value
|
||
#: names what did NOT run), selection flags, and the output/config family.
|
||
#: Glued forms (``--opt=value``) need no entry — the whole token is an
|
||
#: option either way.
|
||
_VALUE_TAKING_OPTION_TOKENS = frozenset(
|
||
{
|
||
"-k",
|
||
"-m",
|
||
"-c",
|
||
"-p",
|
||
"-n",
|
||
"-r",
|
||
"--maxfail",
|
||
"--junitxml",
|
||
"--basetemp",
|
||
"--cov",
|
||
"--cov-report",
|
||
"--durations-min",
|
||
"--capture",
|
||
"--tb",
|
||
"--color",
|
||
"--dist",
|
||
"--ignore",
|
||
"--ignore-glob",
|
||
"--deselect",
|
||
"--exclude",
|
||
"--exclude-glob",
|
||
"--skip",
|
||
"--skip-file",
|
||
}
|
||
)
|
||
|
||
#: A criterion argument scopes the run's selection only when it is path-like
|
||
#: (``tests/security``, ``tests/test_auth.py``); dotted module names, make
|
||
#: targets, and bare runner invocations leave the runner default in charge,
|
||
#: so extra positionals after them narrow rather than widen.
|
||
_CRITERION_PATHLIKE_ARG_RE = re.compile(r"[/\\]|\.(?:py|jsx?|tsx?|go|rs|java|rb|php)$")
|
||
|
||
|
||
def _criterion_positional_args(expected: list[str]) -> list[str] | None:
|
||
"""Criterion tokens that are positional arguments — the tokens that can
|
||
name a test selection. Options and their values are skipped by arity,
|
||
so a path embedded in an option (``--basetemp=/tmp/p``,
|
||
``--junitxml=/tmp/r.xml``) is never mistaken for a selection target.
|
||
|
||
Returns ``None`` when the positional set is unknowable: an option whose
|
||
arity is NOT known (absent from the value-taking table, no glued
|
||
``=``) immediately followed by a path-like token — that token may be
|
||
the option's separate value (``--rootdir /tmp/project``) rather than a
|
||
selection target, and the table stays incomplete across runners and
|
||
plugins by construction, so the unknown case must fail closed rather
|
||
than lend the criterion a scoped-selection proof it does not have."""
|
||
args: list[str] = []
|
||
tokens = expected[1:]
|
||
index = 0
|
||
while index < len(tokens):
|
||
token = tokens[index]
|
||
if token.startswith("-"):
|
||
if token in _VALUE_TAKING_OPTION_TOKENS:
|
||
index += 2
|
||
continue
|
||
if "=" not in token:
|
||
following = tokens[index + 1] if index + 1 < len(tokens) else None
|
||
if following is not None and not following.startswith("-") and _CRITERION_PATHLIKE_ARG_RE.search(following):
|
||
return None
|
||
index += 1
|
||
continue
|
||
args.append(token)
|
||
index += 1
|
||
return args
|
||
|
||
|
||
def _criterion_scopes_selection(expected: list[str]) -> bool:
|
||
positionals = _criterion_positional_args(expected)
|
||
# Unknown arity (None) fails closed: no scoped-selection proof.
|
||
return positionals is not None and any(_CRITERION_PATHLIKE_ARG_RE.search(token) for token in positionals)
|
||
|
||
|
||
#: Extra flags that provably do not change *which* tests run: verbosity,
|
||
#: output formatting, parallelism, coverage, exit-on-failure. Anything else
|
||
#: (``-k``/``-m`` selection, ``--lf``, ``--collect-only``, ``-c`` config,
|
||
#: ``-p`` plugins, …) makes the recorded run a *different* test selection
|
||
#: than the criterion's and must not anchor it.
|
||
_EXTRA_TOKEN_SAFE_RE = re.compile(
|
||
r"^(-[vqxsl]+|-r\S*|-n\d*"
|
||
r"|--verbose|--quiet|--capture=\S+|--tb=\S+|--color=\S+"
|
||
r"|--durations(=\S+)?|--durations-min=\S+|--disable-warnings"
|
||
r"|--junitxml=\S+|--basetemp=\S+|--dist=\S+"
|
||
r"|--cov(=\S*)?|--cov-report=\S+"
|
||
r"|--strict-markers|--strict-config|--exitfirst|--showlocals|--maxfail=\d+"
|
||
r"|--no-header|--no-summary)$"
|
||
)
|
||
|
||
|
||
def _normalize_executable(token: str) -> str:
|
||
"""Canonical spelling of an executable token: forward slashes, no ``.`` or
|
||
duplicate-separator noise — ``./venv/bin/pytest`` and ``venv/bin/pytest``
|
||
are the same invocation. Lexical only: callers must reject ``..``
|
||
components BEFORE comparing (normpath collapses them textually, but the
|
||
OS resolves them after following symlinks)."""
|
||
return os.path.normpath(token.replace("\\", "/"))
|
||
|
||
|
||
def _segment_matches(expected: list[str], actual: list[str], *, windows_path_context: bool = False) -> str:
|
||
"""Match one segment against the criterion's, classifying extra flags.
|
||
|
||
Returns ``"match"`` when the executable agrees — directional: a bare
|
||
criterion executable (``pytest``) accepts any path spelling of the same
|
||
name, while an explicitly path-spelled criterion
|
||
(``/opt/project/.venv/bin/pytest``, and also ``./pytest`` — spelling is
|
||
judged on the raw token because normpath collapses ``./``) requires a
|
||
path-spelled execution of the same normalized path, and a ``..``
|
||
component on either side is unprovable outright (``link/../pytest``
|
||
normalizes to ``pytest`` textually, but the OS follows ``link`` first —
|
||
it may be a different binary) —
|
||
the criterion's arguments appear in order among the executed ones
|
||
(tokens consumed by a negating option — ``--ignore tests/security`` —
|
||
are ineligible evidence, they name what did NOT run), and every extra
|
||
executed token is provably selection-preserving. Extra *positional*
|
||
targets are safe only when the criterion itself scopes the selection
|
||
with a path-like argument (``pytest tests/security``): they widen it,
|
||
so the criterion's tests still ran and the overall result covers them.
|
||
After a bare criterion the same extra positional NARROWS the runner's
|
||
default selection (``python -m unittest pkg.OneTest``). Returns
|
||
``"unprovable"`` when the textual match carries a behavior-changing
|
||
extra (``pytest -k smoke tests/security``), a negating option excludes
|
||
the matched target or a sub-path of it (``--deselect
|
||
tests/unit/test_auth.py`` against ``pytest tests``), the env-assignment
|
||
prefix differs at all (extra, missing, or different value — no variable
|
||
is provably inert across repositories), or any span token carries a
|
||
runtime expansion (``$VAR``/``$( )``/backticks — expanded arguments are
|
||
unknowable) or an extra token carries glob metacharacters
|
||
(option-looking filenames can narrow invisibly), ``"no_match"``
|
||
otherwise.
|
||
"""
|
||
if _effective_env_assignments(expected) != _effective_env_assignments(actual):
|
||
# The environment is part of the invocation: an assignment the
|
||
# criterion does not make, or makes with a different value, can
|
||
# change or skip execution — no variable is provably inert across
|
||
# repositories, so only an exactly equal environment matches. The
|
||
# comparison is the effective name → final-value mapping, not the
|
||
# raw token set: distinct-name order is insignificant, but a
|
||
# repeated name is last-wins — ``CI=0 CI=1`` vs ``CI=1 CI=0`` are
|
||
# equal sets with opposite effective ``CI`` values.
|
||
return "unprovable"
|
||
expected = _strip_env_assignments(expected)
|
||
actual = _strip_env_assignments(actual)
|
||
if not expected or not actual:
|
||
return "no_match"
|
||
if any(_EXPANSION_CHAR_RE.search(token) for token in (*expected, *actual)):
|
||
# A substitution expands at runtime to arguments the matcher cannot
|
||
# see — hidden flags (``pytest tests/security $(cat args)``) or
|
||
# unknown targets (``pytest $T``).
|
||
return "unprovable"
|
||
if expected[0] != actual[0]:
|
||
# A ``..`` component makes the executable's identity lexically
|
||
# unprovable: ``os.path.normpath`` collapses ``link/../pytest`` to
|
||
# ``pytest`` TEXTUALLY, but the OS resolves ``..`` AFTER following
|
||
# symlinks — with ``link`` → ``/tmp/attacker/subdir`` the executed
|
||
# binary is ``/tmp/attacker/pytest``, not the project-local
|
||
# ``./pytest`` the normalized form claims. Two tokens that normalize
|
||
# alike can name different binaries, so any parent-traversal
|
||
# component on either side fails closed. (An identical token on
|
||
# both sides skips this branch entirely — the criterion and the
|
||
# execution then name the same odd path, which is fine.)
|
||
if ".." in expected[0].replace("\\", "/").split("/") or ".." in actual[0].replace("\\", "/").split("/"):
|
||
return "unprovable"
|
||
# Path-spelling is judged on the RAW token, not the normalized form:
|
||
# ``./pytest`` names the project-local file, but normpath collapses
|
||
# it to bare ``pytest`` — deciding on the normalized form would let a
|
||
# PATH lookup or ``/tmp/fake/pytest`` stand in for the explicit
|
||
# local executable the criterion asked for.
|
||
if "/" in expected[0] or "\\" in expected[0]:
|
||
# An explicitly path-spelled criterion names THAT executable: a
|
||
# same-basename binary at a different path (or a bare PATH lookup
|
||
# resolving who-knows-where) may select a different environment —
|
||
# only a path-spelled execution of the same normalized path is
|
||
# evidence for it (``venv/bin/pytest`` ≡ ``./venv/bin/pytest``).
|
||
if "/" not in actual[0] and "\\" not in actual[0]:
|
||
return "no_match"
|
||
if _normalize_executable(expected[0]) != _normalize_executable(actual[0]):
|
||
return "no_match"
|
||
elif os.path.basename(expected[0]) != os.path.basename(actual[0]):
|
||
# A bare criterion deliberately leaves the runner to PATH, so any
|
||
# path spelling of the same executable name is evidence for it.
|
||
return "no_match"
|
||
option_positions, negated = _negated_positions(actual)
|
||
consumed: set[int] = {0}
|
||
index = 1
|
||
for token in expected[1:]:
|
||
found = False
|
||
while index < len(actual):
|
||
candidate = actual[index]
|
||
eligible = index not in negated
|
||
if candidate == token and eligible:
|
||
consumed.add(index)
|
||
index += 1
|
||
found = True
|
||
break
|
||
index += 1
|
||
if not found:
|
||
return "no_match"
|
||
if negated and not _criterion_scopes_selection(expected):
|
||
# A criterion with no positional selection target (bare ``pytest``,
|
||
# ``make test``) stands for the runner's DEFAULT selection: any
|
||
# negating option narrows it, and there is no consumed criterion
|
||
# token for the overlap check below to catch it with —
|
||
# ``pytest --ignore tests/security`` never ran the selection the
|
||
# criterion means.
|
||
return "unprovable"
|
||
# An expected target negated elsewhere in the same command was excluded
|
||
# even though a positional occurrence matched — the passing summary comes
|
||
# from the remaining targets. The overlap check is by path/nodeid
|
||
# prefix, not exact token equality: excluding a SUB-PATH of the
|
||
# criterion's target (``pytest tests --deselect tests/unit/test_auth.py``)
|
||
# means the excluded tests never ran, so the summary does not cover the
|
||
# criterion's selection; excluding a PARENT (``pytest tests/unit
|
||
# --ignore tests``) excludes the target itself. Unrelated exclusions
|
||
# (``--ignore tests/slow`` against ``pytest tests/unit``) keep matching.
|
||
negated_values = [_negated_value(actual[position]) for position in negated]
|
||
if any(_EXPANSION_CHAR_RE.search(value) or _GLOB_CHAR_RE.search(value) or ".." in value.replace("\\", "/").split("/") for value in negated_values):
|
||
# An unknown, glob, or parent-traversal exclusion (``--ignore $X``,
|
||
# ``--ignore tests/slow*``, ``--ignore link/../tests/security``):
|
||
# the overlap check cannot reason about what did not run — the
|
||
# ``..`` form because lexical prefixes lie (the OS follows ``link``
|
||
# before resolving ``..``, so a textually unrelated value can name
|
||
# the criterion's target).
|
||
return "unprovable"
|
||
compared_values = [actual[position] for position in consumed if position != 0] + negated_values
|
||
if negated_values and any(_has_ambiguous_windows_component(value, windows_path_context=windows_path_context) for value in compared_values):
|
||
return "unprovable"
|
||
if negated_values and any(_has_parent_path_component(actual[position]) for position in consumed if position != 0):
|
||
# The matched target itself may normalize onto an excluded path, but
|
||
# collapsing its ``..`` components would be unsound across symlinks.
|
||
return "unprovable"
|
||
if any(_negation_overlaps(actual[position], value, windows_path_context=windows_path_context) for position in consumed if position != 0 for value in negated_values):
|
||
return "unprovable"
|
||
for position, token in enumerate(actual):
|
||
if position in consumed or position in negated or position in option_positions:
|
||
continue
|
||
if _EXPANSION_CHAR_RE.search(token) or _GLOB_CHAR_RE.search(token):
|
||
# An extra token whose expansion or glob result is unknowable —
|
||
# it may inject flags (``$(cat args)``) or option-looking
|
||
# filenames that narrow the run invisibly.
|
||
return "unprovable"
|
||
if token.startswith("-"):
|
||
if not _EXTRA_TOKEN_SAFE_RE.fullmatch(token):
|
||
return "unprovable"
|
||
elif not _criterion_scopes_selection(expected):
|
||
# A bare criterion (``python -m unittest``, bare ``pytest``) means
|
||
# the runner's default selection; an extra positional NARROWS it
|
||
# to specific targets (``pkg.OneTest``), so the recorded run is a
|
||
# different selection than the criterion's — unprovable.
|
||
return "unprovable"
|
||
return "match"
|
||
|
||
|
||
def _span_attributable(ops_before: list[str], ops_within: list[str], ops_after: list[str], executed_success: bool) -> bool:
|
||
"""Whether the matching span provably ran with the recorded exit status.
|
||
|
||
The whole-command exit status belongs to the *last executed* segment, so
|
||
the span must end at the last segment (checked by the caller) and every
|
||
operator around it must keep execution provable:
|
||
|
||
- ``;`` is unconditional; ``|`` before the span is unconditional too
|
||
(pipeline stages all run), but ``|`` *within* the span breaks exit
|
||
attribution (the pipeline's status is its last stage's, not the test's).
|
||
- ``&&`` makes the next segment conditional on success — provable only
|
||
when the recorded status is success.
|
||
- ``||`` makes the next segment conditional on failure — provable only
|
||
when the recorded status is failure.
|
||
- ``&`` (background) and exotic punctuation runs are never provable.
|
||
"""
|
||
if any(op != ";" for op in ops_after):
|
||
# A trailing ``&`` (backgrounding) or dangling conditional means the
|
||
# recorded status is not the matched command's own outcome. A
|
||
# trailing ``;`` is everyday shell punctuation and harmless.
|
||
return False
|
||
for op in ops_within:
|
||
if op == ";":
|
||
continue
|
||
if op == "&&" and executed_success:
|
||
continue
|
||
return False
|
||
for op in ops_before:
|
||
if op in (";", "|", "|&"):
|
||
continue
|
||
if op == "&&" and executed_success:
|
||
continue
|
||
if op == "||" and not executed_success:
|
||
continue
|
||
return False
|
||
return True
|
||
|
||
|
||
def _criterion_connectors_preserved(expected_ops: list[str], executed_within_ops: list[str], executed_success: bool) -> bool:
|
||
"""Whether the executed span keeps the criterion's control-flow connectors.
|
||
|
||
Criterion operators carry semantics the match must preserve: an expected
|
||
``&&`` makes the next segment conditional on the previous segment's
|
||
success, so executing it as ``;`` (``cd missing; pytest
|
||
tests/test_auth.py`` against criterion ``cd missing && pytest
|
||
tests/test_auth.py``) lets a failed preceding step be bypassed while the
|
||
run still succeeds from the wrong state. The reverse substitution is
|
||
sound: an unconditional criterion connector (``;``) executed as ``&&``
|
||
is the stricter run — with a recorded success the final segment provably
|
||
ran (a recorded failure already fails span attribution on its own). A
|
||
trailing criterion operator other than ``;`` (``make test &``) has no
|
||
executed counterpart — the span must end at the last executed segment —
|
||
so it is never preserved.
|
||
"""
|
||
trailing = expected_ops[len(executed_within_ops) :]
|
||
if any(op != ";" for op in trailing):
|
||
return False
|
||
# Trailing criterion ``;`` operators are validated above; the pairwise
|
||
# comparison covers only the connector prefix between the span's
|
||
# segments, or a criterion spelled ``make test;`` (one more operator
|
||
# than connectors) would raise on the strict zip — and the task tool
|
||
# discards the whole verdict on an exception.
|
||
for expected_op, executed_op in zip(expected_ops[: len(executed_within_ops)], executed_within_ops, strict=True):
|
||
if expected_op == executed_op:
|
||
continue
|
||
if expected_op == ";" and executed_op == "&&" and executed_success:
|
||
continue
|
||
return False
|
||
return True
|
||
|
||
|
||
def _commands_match(
|
||
criterion_command: str,
|
||
executed_command: str,
|
||
*,
|
||
executed_success: bool,
|
||
thread_data: Mapping[str, Any] | None = None,
|
||
) -> str:
|
||
"""Shell-structure match with control-flow attribution.
|
||
|
||
Returns ``"match"`` when the criterion's segment sequence appears as
|
||
consecutive executed segments ending at the last segment AND the span
|
||
provably ran with the recorded status; ``"unprovable"`` when a span
|
||
matches textually but control flow (``false && pytest x; echo done``,
|
||
backgrounding, pipelines inside the span) means it cannot be proven to
|
||
have executed; ``"no_match"`` otherwise. Containment of raw strings is
|
||
deliberately not enough — ``echo '12 passed'; # pytest x.py`` must not
|
||
anchor ``tests_passed:pytest x.py``.
|
||
"""
|
||
expected_parsed = _shell_parse(criterion_command)
|
||
actual_parsed = _shell_parse(executed_command)
|
||
expected_raw_parsed = _shell_parse(criterion_command, posix=False)
|
||
actual_raw_parsed = _shell_parse(executed_command, posix=False)
|
||
needs_shell_provenance = _command_requires_shell_provenance(criterion_command) or _command_requires_shell_provenance(executed_command)
|
||
if expected_parsed is None or actual_parsed is None:
|
||
# Malformed shell: only exact normalized equality survives.
|
||
expected_norm = _normalize_command(criterion_command)
|
||
if not expected_norm or expected_norm != _normalize_command(executed_command):
|
||
return "no_match"
|
||
has_ambiguous_single_quote = _has_unquoted_single_quote(criterion_command) or _has_unquoted_single_quote(executed_command)
|
||
return "unprovable" if needs_shell_provenance or has_ambiguous_single_quote else "match"
|
||
expected, expected_ops = expected_parsed
|
||
actual, ops = actual_parsed
|
||
if not expected or not actual or len(expected) > len(actual):
|
||
return "no_match"
|
||
raw_segments_align = expected_raw_parsed is not None and actual_raw_parsed is not None and len(expected_raw_parsed[0]) == len(expected) and len(actual_raw_parsed[0]) == len(actual)
|
||
if not raw_segments_align:
|
||
needs_shell_provenance = True
|
||
expected_raw = expected_raw_parsed[0] if raw_segments_align and expected_raw_parsed is not None else []
|
||
actual_raw = actual_raw_parsed[0] if raw_segments_align and actual_raw_parsed is not None else []
|
||
span = len(expected)
|
||
saw_unprovable = False
|
||
windows_path_context = _thread_uses_windows_paths(thread_data)
|
||
for start in range(len(actual) - span + 1):
|
||
if any(_segment_pollutes_state(segment) for segment in actual[:start]):
|
||
# A preceding segment mutated shell state the matcher cannot see
|
||
# (PATH/exports): nothing later is provable.
|
||
saw_unprovable = True
|
||
continue
|
||
outcomes = [_segment_matches(expected[i], actual[start + i], windows_path_context=windows_path_context) for i in range(span)]
|
||
if any(outcome == "no_match" for outcome in outcomes):
|
||
continue
|
||
if any(outcome == "unprovable" for outcome in outcomes):
|
||
saw_unprovable = True
|
||
continue
|
||
# The exit status is attributable only to the command's last segment.
|
||
if start + span != len(actual):
|
||
saw_unprovable = True
|
||
continue
|
||
if not _criterion_connectors_preserved(expected_ops, ops[start : start + span - 1], executed_success):
|
||
# Textually equal segments wired with weaker control flow than the
|
||
# criterion's (an expected ``&&`` executed as ``;``) — a failed
|
||
# preceding step may have been bypassed.
|
||
saw_unprovable = True
|
||
continue
|
||
if _span_attributable(ops[:start], ops[start : start + span - 1], ops[start + span - 1 :], executed_success):
|
||
if needs_shell_provenance or _matched_span_has_ambiguous_single_quotes(
|
||
expected_raw,
|
||
actual_raw,
|
||
actual,
|
||
start=start,
|
||
span=span,
|
||
thread_data=thread_data,
|
||
):
|
||
saw_unprovable = True
|
||
continue
|
||
return "match"
|
||
saw_unprovable = True
|
||
return "unprovable" if saw_unprovable else "no_match"
|
||
|
||
|
||
#: ``<``/``>`` are ordinary word characters to the parser (only ``;&|`` are
|
||
#: punctuation), so a redirection in the matched final segment is invisible
|
||
#: to the matcher: ``pytest tests/ > /dev/null`` still matches, while the
|
||
#: runner's real summary went to the redirection target and the recorded
|
||
#: tail carries whatever remains — text any preceding segment (or nothing)
|
||
#: produced. Any token carrying a redirection char makes the tail
|
||
#: non-attributable. (``&>``/``2>&1`` already degrade upstream: the bare
|
||
#: ``&`` parses as an operator and breaks span/exit attribution.)
|
||
_REDIRECTION_CHAR_RE = re.compile(r"[<>]")
|
||
|
||
|
||
def _output_attribution(executed_command: str, thread_data: Mapping[str, Any] | None = None) -> str | None:
|
||
"""Why the recorded output tail cannot be attributed to the matched
|
||
final segment, or ``None`` when it can.
|
||
|
||
The matched segment is always the command's last (the matcher requires
|
||
the span to end there). Two channels break attribution:
|
||
|
||
- A redirection token in that final segment (``pytest tests/ > log``):
|
||
the real summary may have gone to the target while the recorded tail
|
||
carries text from anywhere, so neither a pass nor a fail shape in it
|
||
is test evidence.
|
||
- A preceding segment that is not provably silent by invocation form
|
||
(``echo '12 passed'; make test``, a ``pushd``/``export -p`` that
|
||
prints): it could have emitted the very summary the shape check reads.
|
||
"""
|
||
parsed = _shell_parse(executed_command)
|
||
if parsed is None:
|
||
return None # unparseable commands already fell back to exact equality
|
||
segments, _ops = parsed
|
||
if any(_REDIRECTION_CHAR_RE.search(token) for token in segments[-1]):
|
||
return "matched segment redirects its output; the recorded tail is not test evidence"
|
||
if not all(_is_silent_segment(segment, thread_data) for segment in segments[:-1]):
|
||
return "recorded output is not attributable to the matched segment"
|
||
return None
|
||
|
||
|
||
def _check_tests_passed_leaf(command: str, bash_executions: list[dict[str, Any]] | None, thread_data: Mapping[str, Any] | None = None) -> AcceptanceLeaf:
|
||
base: AcceptanceLeaf = {"criterion": "", "family": "tests_passed", "checked": False, "holds": False, "detail": ""}
|
||
matches: list[tuple[str, dict[str, Any]]] = []
|
||
for execution in bash_executions or []:
|
||
status = str(execution.get("status") or "")
|
||
outcome = _commands_match(
|
||
command,
|
||
str(execution.get("command") or ""),
|
||
executed_success=status == "success",
|
||
thread_data=thread_data,
|
||
)
|
||
if outcome == "match" and execution.get("command_truncated"):
|
||
# The recorded command lost its suffix to the evidence cap; a
|
||
# selection-changing tail (``-k smoke``) may have been cut away.
|
||
outcome = "unprovable"
|
||
if outcome != "no_match":
|
||
matches.append((outcome, execution))
|
||
if not matches:
|
||
base["detail"] = "no matching bash execution recorded"
|
||
return base
|
||
# The latest matching run is decisive: earlier failing attempts superseded
|
||
# by a later pass must not fail the leaf.
|
||
latest_outcome, latest = matches[-1]
|
||
if latest_outcome == "unprovable":
|
||
base["detail"] = "recorded command is truncated; the match cannot be proven" if latest.get("command_truncated") else "matching segment cannot be proven to have executed"
|
||
return base
|
||
shell_persistent = latest.get("shell_persistent")
|
||
if shell_persistent is not False:
|
||
# Provenance comes from the harvest stamp (``_harvest_bash_executions``
|
||
# resolves ``Sandbox.persistent_shell_sessions`` against the sandbox
|
||
# recorded in the state that CARRIED the evidence — the subagent's own
|
||
# graph state — never the parent task runtime, which has no
|
||
# ``sandbox`` key when the parent delegated before touching one).
|
||
# True: the matched run shared one persistent shell with every earlier
|
||
# call — any of them (including one since capped away or compacted
|
||
# out) could have exported PATH or redefined the runner. None: the
|
||
# producing sandbox could not be identified OR never declared its
|
||
# session semantics (a custom provider's silence is not fresh-shell
|
||
# proof) — fail closed either way. Re-execution belongs to the
|
||
# read-only verifier (RFC §6).
|
||
if shell_persistent is True:
|
||
base["detail"] = "recorded bash evidence comes from a persistent shell session; earlier calls' state cannot be proven clean"
|
||
else:
|
||
base["detail"] = "the producing sandbox does not declare one-shot shell sessions (or could not be identified); shell state cannot be proven clean"
|
||
return base
|
||
status = str(latest.get("status") or "")
|
||
if status != "success":
|
||
base["checked"] = True
|
||
marker = latest.get("status_marker")
|
||
if isinstance(marker, str) and marker.strip():
|
||
# The recorded failure comes from a trailing exit marker, which
|
||
# the harness cannot distinguish from the command's own output
|
||
# ending in the same shape — report what was actually seen.
|
||
base["detail"] = f"recorded output carries an exit marker ({_bound_detail(marker)}); the harness cannot tell it from the command's own text"
|
||
else:
|
||
base["detail"] = f"latest matching run recorded status={status or 'unknown'}"
|
||
return base
|
||
output_tail = str(latest.get("output_tail") or "")
|
||
unattributable = _output_attribution(str(latest.get("command") or ""), thread_data)
|
||
if unattributable is not None:
|
||
# Neither a pass nor a fail shape here can be trusted either way.
|
||
base["detail"] = unattributable
|
||
return base
|
||
if _TEST_FAIL_SHAPE_RE.search(output_tail):
|
||
base["checked"] = True
|
||
base["detail"] = "recorded output carries a failing test summary"
|
||
return base
|
||
if _TEST_PASS_SHAPE_RE.search(output_tail) and not _TEST_ZERO_SHAPE_RE.search(output_tail):
|
||
base["checked"] = True
|
||
base["holds"] = True
|
||
base["detail"] = "recorded output carries a passing test summary"
|
||
return base
|
||
base["detail"] = "matching run recorded no test-summary shape"
|
||
return base
|
||
|
||
|
||
def check_acceptance_criteria(
|
||
acceptance_criteria: list[str] | None,
|
||
*,
|
||
runtime: Any = None,
|
||
thread_data: Mapping[str, Any] | None = None,
|
||
bash_executions: list[dict[str, Any]] | None = None,
|
||
content_reader: Callable[[Any, str], str] | None = None,
|
||
size_prober: Callable[[Any, str, Mapping[str, Any] | None], int | None] | None = None,
|
||
readable_prober: Callable[[Any, str, Mapping[str, Any] | None], bool | None] | None = None,
|
||
) -> AcceptanceVerdict | None:
|
||
"""Check each decidable criterion against recorded execution evidence.
|
||
|
||
Returns ``None`` when no usable criterion exists (caller stamps nothing).
|
||
Synchronous: the async call site offloads via ``asyncio.to_thread`` —
|
||
``content_reader`` performs sandbox IO. The shared normalizer keeps the
|
||
checked list identical to the persisted and delegated list.
|
||
"""
|
||
criteria = normalize_acceptance_criteria(acceptance_criteria)
|
||
if not criteria:
|
||
return None
|
||
|
||
if content_reader is None:
|
||
# Lazy import: sandbox.tools pulls the provider stack (see
|
||
# _resolve_scoped_path).
|
||
from deerflow.sandbox.tools import read_current_file_content
|
||
|
||
content_reader = read_current_file_content
|
||
if size_prober is None:
|
||
size_prober = _probe_file_size
|
||
if readable_prober is None:
|
||
readable_prober = _probe_file_readable
|
||
leaves: list[AcceptanceLeaf] = []
|
||
for criterion in criteria:
|
||
file_criterion = parse_file_criterion(criterion)
|
||
tests_match = _TESTS_PASSED_RE.match(criterion)
|
||
if file_criterion is not None:
|
||
family, path = file_criterion
|
||
leaf = _check_file_leaf(family, path, runtime=runtime, thread_data=thread_data, content_reader=content_reader, size_prober=size_prober, readable_prober=readable_prober)
|
||
elif tests_match is not None:
|
||
leaf = _check_tests_passed_leaf(tests_match.group("command"), bash_executions, thread_data)
|
||
else:
|
||
leaf = AcceptanceLeaf(criterion="", family="undecidable", checked=False, holds=False, detail="not deterministically checkable")
|
||
leaf["criterion"] = criterion
|
||
leaf["detail"] = _bound_detail(leaf["detail"])
|
||
leaves.append(leaf)
|
||
|
||
return AcceptanceVerdict(
|
||
source=CHECK_SOURCE,
|
||
requirement=CHECK_REQUIREMENT,
|
||
leaves=leaves,
|
||
unchecked=[leaf["criterion"] for leaf in leaves if not leaf["checked"]],
|
||
all_hold=all(leaf["checked"] and leaf["holds"] for leaf in leaves),
|
||
)
|
||
|
||
|
||
def validate_acceptance_verdict(value: object) -> AcceptanceVerdict | None:
|
||
"""Structural check for a persisted verdict (read side trusts nothing)."""
|
||
if not isinstance(value, dict):
|
||
return None
|
||
source = value.get("source")
|
||
requirement = value.get("requirement")
|
||
all_hold = value.get("all_hold")
|
||
if not isinstance(source, str) or not isinstance(requirement, str):
|
||
return None
|
||
if not isinstance(all_hold, bool):
|
||
return None
|
||
raw_leaves = value.get("leaves")
|
||
raw_unchecked = value.get("unchecked")
|
||
if not isinstance(raw_leaves, list) or len(raw_leaves) > MAX_ACCEPTANCE_CRITERIA:
|
||
return None
|
||
if not isinstance(raw_unchecked, list) or any(not isinstance(item, str) for item in raw_unchecked):
|
||
return None
|
||
leaves: list[AcceptanceLeaf] = []
|
||
for entry in raw_leaves:
|
||
if not isinstance(entry, dict):
|
||
return None
|
||
criterion = entry.get("criterion")
|
||
family = entry.get("family")
|
||
checked = entry.get("checked")
|
||
holds = entry.get("holds")
|
||
detail = entry.get("detail")
|
||
if not all(isinstance(field, str) for field in (criterion, family, detail)):
|
||
return None
|
||
if not isinstance(checked, bool) or not isinstance(holds, bool):
|
||
return None
|
||
leaves.append(AcceptanceLeaf(criterion=criterion, family=family, checked=checked, holds=holds, detail=detail))
|
||
return AcceptanceVerdict(
|
||
source=source,
|
||
requirement=requirement,
|
||
leaves=leaves,
|
||
unchecked=list(raw_unchecked),
|
||
all_hold=all_hold,
|
||
)
|
||
|
||
|
||
def render_acceptance_section(verdict: AcceptanceVerdict) -> str:
|
||
"""Render the per-criterion checklist section for the result text.
|
||
|
||
One leaf is exactly one line: the criterion is model-supplied untrusted
|
||
text (tag-neutralized, but newlines are not tags), so it is rendered
|
||
whitespace-collapsed — a multi-line criterion would otherwise inject a
|
||
forged ``- [holds] …`` line into the checklist the lead reads. The
|
||
stored verdict keeps the verbatim criterion; only the display collapses.
|
||
"""
|
||
lines = [f"Acceptance checklist (deterministic checks; {_LIMITATION}):"]
|
||
for leaf in verdict["leaves"]:
|
||
if not leaf["checked"]:
|
||
marker = "UNVERIFIED"
|
||
elif leaf["holds"]:
|
||
marker = "holds"
|
||
else:
|
||
marker = "does not hold"
|
||
lines.append(f"- [{marker}] {' '.join(leaf['criterion'].split())} — {leaf['detail']}")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def render_acceptance_segment(verdict: AcceptanceVerdict) -> str:
|
||
"""Render the compact delegation-ledger segment (counts only)."""
|
||
holds = sum(1 for leaf in verdict["leaves"] if leaf["checked"] and leaf["holds"])
|
||
does_not_hold = sum(1 for leaf in verdict["leaves"] if leaf["checked"] and not leaf["holds"])
|
||
unverified = sum(1 for leaf in verdict["leaves"] if not leaf["checked"])
|
||
parts: list[str] = []
|
||
if holds:
|
||
parts.append(f"{holds} hold")
|
||
if does_not_hold:
|
||
parts.append(f"{does_not_hold} does not hold")
|
||
if unverified:
|
||
parts.append(f"{unverified} UNVERIFIED")
|
||
if not parts:
|
||
return ""
|
||
return f"acceptance: {', '.join(parts)} — {_LIMITATION}"
|