fix(runtime): support validated cross-repository canonical roots (#973)
This commit is contained in:
@@ -8,8 +8,10 @@ poison workspace purity checks in another namespace.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import author_mutation_worktree as amw
|
||||
import canonical_repository_root as crr
|
||||
|
||||
ACTIVE_WORKTREE_ENV = amw.ACTIVE_WORKTREE_ENV
|
||||
AUTHOR_WORKTREE_ENV = amw.AUTHOR_WORKTREE_ENV
|
||||
@@ -152,6 +154,60 @@ def resolve_namespace_workspace(
|
||||
return os.path.realpath(process_project_root), "MCP server process root (default)"
|
||||
|
||||
|
||||
def verify_git_common_directory_membership(
|
||||
workspace_path: str,
|
||||
canonical_repo_root: str,
|
||||
) -> tuple[bool, str | None]:
|
||||
"""Verify that workspace_path belongs to canonical_repo_root via git common-dir or branches containment."""
|
||||
ws = (workspace_path or "").strip()
|
||||
root = (canonical_repo_root or "").strip()
|
||||
if not ws or not root:
|
||||
return False, "empty workspace or canonical root path"
|
||||
|
||||
try:
|
||||
real_ws = os.path.realpath(os.path.abspath(ws))
|
||||
real_root = os.path.realpath(os.path.abspath(root))
|
||||
except Exception as exc:
|
||||
return False, f"invalid workspace or root path: {exc}"
|
||||
|
||||
if real_ws == real_root:
|
||||
return True, None
|
||||
|
||||
if not os.path.isdir(real_ws):
|
||||
return False, f"workspace directory '{real_ws}' does not exist"
|
||||
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["git", "-C", real_ws, "rev-parse", "--git-common-dir"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if res.returncode == 0:
|
||||
common_raw = (res.stdout or "").strip()
|
||||
common_dir = amw._realpath_git_common_dir(real_ws, common_raw)
|
||||
real_common = os.path.realpath(common_dir)
|
||||
canonical_git = os.path.realpath(os.path.join(real_root, ".git"))
|
||||
|
||||
if real_common in (canonical_git, real_root) or os.path.dirname(real_common) == real_root:
|
||||
return True, None
|
||||
return (
|
||||
False,
|
||||
f"workspace '{real_ws}' git common directory '{real_common}' does not match "
|
||||
f"canonical repository root '{real_root}' (.git at '{canonical_git}')"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if amw.is_path_under_branches(real_ws, real_root):
|
||||
return True, None
|
||||
|
||||
return (
|
||||
False,
|
||||
f"workspace '{real_ws}' does not belong to canonical repository root '{real_root}'"
|
||||
)
|
||||
|
||||
|
||||
def resolve_namespace_mutation_context(
|
||||
*,
|
||||
role_kind: str,
|
||||
@@ -163,6 +219,8 @@ def resolve_namespace_mutation_context(
|
||||
worktree: str | None = None,
|
||||
profile_name: str | None = None,
|
||||
configured_canonical_root: str | None = None,
|
||||
expected_slug: str | None = None,
|
||||
remote: str | None = None,
|
||||
) -> dict:
|
||||
"""Shared workspace resolution for runtime_context and mutation guards.
|
||||
|
||||
@@ -180,11 +238,30 @@ def resolve_namespace_mutation_context(
|
||||
env_map = env if env is not None else os.environ
|
||||
process_root = os.path.realpath(process_project_root)
|
||||
role = normalize_role_kind(role_kind, profile_name=profile_name)
|
||||
configured = (configured_canonical_root or "").strip()
|
||||
if configured:
|
||||
canonical_root = os.path.realpath(configured)
|
||||
|
||||
configured_val = (configured_canonical_root or "").strip()
|
||||
if configured_val:
|
||||
crr_assessment = crr.assess_canonical_repository_root(
|
||||
configured_value=configured_val,
|
||||
source="configured_canonical_root",
|
||||
expected_slug=expected_slug,
|
||||
process_project_root=process_root,
|
||||
remote=remote,
|
||||
)
|
||||
canonical_root = crr_assessment["canonical_repo_root"]
|
||||
roots_aligned = crr_assessment["proven"]
|
||||
else:
|
||||
canonical_root = amw.resolve_canonical_repo_root(process_root, process_root)
|
||||
crr_assessment = {
|
||||
"proven": True,
|
||||
"block": False,
|
||||
"reasons": [],
|
||||
"configured": False,
|
||||
"canonical_repo_root": amw.resolve_canonical_repo_root(process_root, process_root),
|
||||
"resolved_slug": None,
|
||||
"source": None,
|
||||
}
|
||||
canonical_root = crr_assessment["canonical_repo_root"]
|
||||
roots_aligned = (canonical_root == process_root)
|
||||
|
||||
durable: dict | None = None
|
||||
if role == "author":
|
||||
@@ -234,7 +311,8 @@ def resolve_namespace_mutation_context(
|
||||
"ignored_bindings": demotions + (pollution.get("ignored_bindings") or []),
|
||||
"process_project_root": process_root,
|
||||
"canonical_repo_root": canonical_root,
|
||||
"roots_aligned": canonical_root == process_root,
|
||||
"roots_aligned": roots_aligned,
|
||||
"canonical_root_assessment": crr_assessment,
|
||||
}
|
||||
if durable is not None:
|
||||
result["author_worktree_resolution"] = durable
|
||||
@@ -387,6 +465,8 @@ def assess_namespace_mutation_workspace(
|
||||
profile_name: str | None = None,
|
||||
current_branch: str | None = None,
|
||||
configured_canonical_root: str | None = None,
|
||||
expected_slug: str | None = None,
|
||||
remote: str | None = None,
|
||||
) -> dict:
|
||||
"""Evaluate namespace workspace binding before preflight/mutation."""
|
||||
ctx = resolve_namespace_mutation_context(
|
||||
@@ -399,6 +479,8 @@ def assess_namespace_mutation_workspace(
|
||||
session_lock_worktree=session_lock_worktree,
|
||||
profile_name=profile_name,
|
||||
configured_canonical_root=configured_canonical_root,
|
||||
expected_slug=expected_slug,
|
||||
remote=remote,
|
||||
)
|
||||
mutation_workspace = ctx["workspace_path"]
|
||||
binding_source = ctx["workspace_binding_source"]
|
||||
@@ -422,6 +504,25 @@ def assess_namespace_mutation_workspace(
|
||||
|
||||
reasons = list(metadata.get("reasons") or [])
|
||||
operator_recovery = ctx.get("operator_recovery")
|
||||
|
||||
crr_reasons = list(ctx.get("canonical_root_assessment", {}).get("reasons") or [])
|
||||
if crr_reasons:
|
||||
reasons.extend(crr_reasons)
|
||||
|
||||
if not ctx.get("roots_aligned"):
|
||||
if not crr_reasons:
|
||||
reasons.append(
|
||||
f"unsafe_process_root_workspace_alignment: process root '{process_root}' "
|
||||
f"and canonical root '{ctx['canonical_repo_root']}' disagree"
|
||||
)
|
||||
|
||||
if os.path.exists(mutation_workspace):
|
||||
valid_common, common_err = verify_git_common_directory_membership(
|
||||
mutation_workspace, ctx["canonical_repo_root"]
|
||||
)
|
||||
if not valid_common and common_err:
|
||||
reasons.append(common_err)
|
||||
|
||||
if role == "author":
|
||||
# #618 durable resolution already validated existence, membership,
|
||||
# branches/, lock ownership, and traversal safety when present.
|
||||
|
||||
Reference in New Issue
Block a user