{"record":{"id":"8476051970932d88","repo":"abhigyanpatwari/GitNexus","slug":"phase-artifact-escapes-the-workspace-allowed-art","errorCode":null,"errorMessage":"phase artifact escapes the workspace: {allowed_artifact}","messagePattern":"phase artifact escapes the workspace: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/runner_artifacts.py","lineNumber":201,"sourceCode":"                os.close(descriptor)\n            snapshot[relative.as_posix()] = f\"f:{permissions:o}:{metadata.st_size}:{digest.hexdigest()}\"\n    return snapshot\n\n\ndef enforce_phase_workspace(\n    worktree: Path,\n    before: dict[str, str],\n    *,\n    allowed_artifact: Path,\n) -> None:\n    \"\"\"Require a phase to change only its one explicit workspace artifact.\"\"\"\n\n    root = worktree.expanduser().absolute()\n    artifact = allowed_artifact.expanduser().absolute()\n    try:\n        relative = PurePosixPath(artifact.relative_to(root).as_posix())\n    except ValueError as exc:\n        raise ValueError(f\"phase artifact escapes the workspace: {allowed_artifact}\") from exc\n    after = workspace_snapshot(root)\n    changed = {path for path in before.keys() | after.keys() if before.get(path) != after.get(path)}\n    artifact_key = relative.as_posix()\n    artifact_state = after.get(artifact_key)\n    if before.get(artifact_key) == artifact_state:\n        raise ValueError(f\"phase did not create or change its required artifact: {relative}\")\n    if artifact_state is None or not artifact_state.startswith(\"f:\"):\n        raise ValueError(f\"phase artifact must be a regular non-symlink file: {relative}\")\n\n    try:\n        metadata = artifact.lstat()\n        descriptor = os.open(artifact, os.O_RDONLY | getattr(os, \"O_NOFOLLOW\", 0))\n    except OSError as exc:\n        raise ValueError(f\"phase artifact must be a readable regular non-symlink file: {relative}\") from exc\n    try:\n        opened = os.fstat(descriptor)\n        if (\n            stat.S_ISLNK(metadata.st_mode)","sourceCodeStart":183,"sourceCodeEnd":219,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/runner_artifacts.py#L183-L219","documentation":"Raised by enforce_phase_workspace (runner_artifacts.py:201) when the allowed_artifact path cannot be expressed relative to the workspace root. The phase contract requires the artifact to live strictly inside the worktree; an artifact outside it (absolute path on the host, or reachable only via '..') cannot be diffed against the workspace snapshot and is rejected.","triggerScenarios":"PurePosixPath(artifact.relative_to(root)) raises ValueError because artifact is not under root. Caused by passing an allowed_artifact that is an absolute host path, a symlink that resolves outside the worktree, or a path constructed with '..' segments.","commonSituations":"Caller computes allowed_artifact from a tempfile outside the worktree; the artifact was written to /tmp instead of inside the clone; the worktree root was passed un-expanded so the relative computation mismatches.","solutions":["Construct allowed_artifact as a path inside the worktree, e.g. worktree / 'docs' / 'plans' / 'plan.md', and pass that.","If the artifact must live outside, copy/symlink it into the worktree first and point allowed_artifact at the in-worktree location.","Double-check that worktree and allowed_artifact are both expanded/absolute and that artifact is genuinely under root (artifact.relative_to(root) should not raise)."],"exampleFix":"# before\nenforce_phase_workspace(worktree, before, allowed_artifact=Path('/tmp/plan.md'))\n\n# after\nartifact = worktree / 'docs' / 'plans' / 'plan.md'\nenforce_phase_workspace(worktree, before, allowed_artifact=artifact)","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\nroot = Path(worktree).expanduser().absolute()\nartifact = Path(allowed_artifact).expanduser().absolute()\n# Must be inside root, or relative_to raises\nrel = artifact.relative_to(root)\nassert rel.parts and \"..\" not in rel.parts, (\n    f\"artifact {artifact} is not strictly inside worktree root {root}\")","typeGuard":"from pathlib import Path\n\ndef is_inside_worktree(artifact, root) -> bool:\n    root = Path(root).expanduser().absolute()\n    artifact = Path(artifact).expanduser().absolute()\n    try:\n        artifact.relative_to(root)\n        return True\n    except ValueError:\n        return False","tryCatchPattern":null,"preventionTips":["Construct allowed_artifact as worktree / <rel-path>, never an absolute host path.","Pass already-resolved (resolve()) paths for both worktree and artifact."],"tags":["phase-boundary","workspace","integrity"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}