Remediates reviewer findings F1, F2, and F3 from review 483 on PR #770. F1 (blocking): _current_runtime_mode_report derived the runtime-gate alignment input as realpath(workspace_root) == process_project_root, redefining workspace_roots_aligned. Its established meaning is ctx["roots_aligned"] (canonical_repo_root == process_project_root) — a repository-level question. Because the global worktree rule requires all task work to live in a branches/ worktree, the old derivation reported aligned False for exactly the sessions that are configured correctly, raising unsafe_process_root_workspace_alignment and refusing every non-read operation once an author, reviewer, or merger namespace held a worktree binding. The gate is now fed ctx["roots_aligned"]. F2 (blocking): _current_runtime_mode_report cached its first result into _STARTUP_RUNTIME_MODE, which was initialised to None rather than captured at import, and the read-only refresh=True path seeded it too. Session-scoped fields (active_task_workspace and its derived alignment) and mutable fields (dirty_files) were frozen for the process lifetime, so the acceptance criterion 7 dirty-runtime blocker stopped applying after the snapshot and one session's binding decided alignment for every later session. Immutable process facts (process root, branch, head, checkout-ness) are now captured at import as _STARTUP_RUNTIME_FACTS, matching the #420 parity baseline. Dirty state, workspace binding, and alignment are recomputed on every call. A new stable_control_runtime.observe_dirty_files() splits out the one runtime fact that legitimately changes during a process lifetime; observe_runtime() delegates to it so the parsing lives in one place. refresh is retained for the read-only reporting path but no longer selects a cache, so a read-only call can neither seed nor weaken a later mutation decision. F3 (major): no test exercised the real derivation — every server-wiring test asserting a healthy runtime permits mutations patched _current_runtime_mode_report with a fixture whose workspace_roots_aligned was True. New TestServerWiringRealDerivation patches only the derivation's inputs and lets the real function build the report: - a clean stable control checkout plus a correctly bound branches/ worktree permits an otherwise authorized mutation; - misaligned process/canonical roots fail closed; - newly dirty task state is detected after an earlier clean read; - a read-only refresh cannot freeze a permissive mutation result; - an unresolvable binding reports unknown alignment, never alignment proof. Acceptance criteria 6, 8, 10, and 11 are unchanged and untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+43
-28
@@ -2010,12 +2010,21 @@ import stable_control_runtime # noqa: E402
|
||||
# Read-only operations are never blocked by staleness.
|
||||
_STARTUP_PARITY = master_parity_gate.capture_startup_parity(PROJECT_ROOT)
|
||||
|
||||
# Stable-control runtime snapshot (#615): which runtime this process serves
|
||||
# from. Captured lazily on first use and then reused, so the mutation gate never
|
||||
# shells out per call. Whether the gate enforces at all is decided from how the
|
||||
# process was loaded -- a suite running from a branches/ worktree is dev-test by
|
||||
# design, and per-test production simulation must not switch this gate on.
|
||||
_STARTUP_RUNTIME_MODE: dict | None = None
|
||||
# Stable-control runtime facts (#615): which runtime this process serves from.
|
||||
# These are the *immutable* facts -- process root, branch, head, checkout-ness --
|
||||
# and they are captured at import exactly like the #420 parity baseline, because
|
||||
# the runtime a process serves from is fixed when it loads its code.
|
||||
#
|
||||
# Dirty state and the session's workspace binding are deliberately NOT captured
|
||||
# here. Both change during a process lifetime, so freezing them would retire the
|
||||
# acceptance-criterion-7 dirty-runtime blocker after the first snapshot and let
|
||||
# whichever session happened to call first decide alignment for every session
|
||||
# after it. _current_runtime_mode_report() re-evaluates them per call.
|
||||
#
|
||||
# Whether the gate enforces at all is decided from how the process was loaded --
|
||||
# a suite running from a branches/ worktree is dev-test by design, and per-test
|
||||
# production simulation must not switch this gate on.
|
||||
_STARTUP_RUNTIME_FACTS = stable_control_runtime.observe_runtime(PROJECT_ROOT)
|
||||
_RUNTIME_MODE_GATE_UNDER_TEST = "pytest" in sys.modules or "unittest" in sys.modules
|
||||
import worktree_cleanup_audit # noqa: E402
|
||||
import canonical_comment_validator as ccv # noqa: E402
|
||||
@@ -11372,18 +11381,22 @@ def _current_runtime_mode_report(refresh: bool = False) -> dict:
|
||||
binding this session resolved, so the report answers both "what code is the
|
||||
control plane running" and "does it agree with the task workspace".
|
||||
|
||||
The mutation gate uses the **startup snapshot** (``refresh=False``), for the
|
||||
same reason the #420 parity gate compares against a startup baseline: the
|
||||
runtime a process serves from is fixed when it loads its code, and editing
|
||||
files afterwards does not change what is already in memory. That also keeps
|
||||
the mutation path free of per-call subprocess reads. ``refresh=True`` re-reads
|
||||
live state for the read-only reporting path.
|
||||
"""
|
||||
global _STARTUP_RUNTIME_MODE
|
||||
The immutable facts -- process root, branch, head, checkout-ness -- come from
|
||||
the import-time snapshot, for the same reason the #420 parity gate compares
|
||||
against a startup baseline: the runtime a process serves from is fixed when
|
||||
it loads its code, and editing files afterwards does not change what is
|
||||
already in memory.
|
||||
|
||||
if not refresh and _STARTUP_RUNTIME_MODE is not None:
|
||||
return _STARTUP_RUNTIME_MODE
|
||||
observed = stable_control_runtime.observe_runtime(PROJECT_ROOT)
|
||||
Everything mutation-sensitive is recomputed on **every** call: the dirty-file
|
||||
list, the session's workspace binding, and the alignment derived from it.
|
||||
Caching those would mean a checkout that goes dirty after the first call is
|
||||
never caught again, and that one session's binding decides alignment for the
|
||||
whole process. ``refresh`` is retained for the read-only reporting path; it
|
||||
no longer selects between a cache and live state, so a read-only call can
|
||||
neither seed nor weaken a later mutation decision.
|
||||
"""
|
||||
facts = _STARTUP_RUNTIME_FACTS
|
||||
dirty_files = stable_control_runtime.observe_dirty_files(PROJECT_ROOT)
|
||||
workspace_root = None
|
||||
aligned = None
|
||||
canonical_root = None
|
||||
@@ -11391,9 +11404,14 @@ def _current_runtime_mode_report(refresh: bool = False) -> dict:
|
||||
ctx = _resolve_namespace_mutation_context(None)
|
||||
workspace_root = ctx.get("workspace_path")
|
||||
canonical_root = ctx.get("canonical_repo_root")
|
||||
aligned = os.path.realpath(workspace_root or "") == (
|
||||
ctx.get("process_project_root") or ""
|
||||
)
|
||||
# Alignment keeps its established repository-level meaning (#615 F1):
|
||||
# does this namespace target the repository the process is installed in,
|
||||
# i.e. canonical_repo_root == process_project_root. It is deliberately
|
||||
# NOT path equality between the task workspace and the process root --
|
||||
# the global worktree rule requires task work to live in a branches/
|
||||
# worktree, so that comparison would classify every correctly bound
|
||||
# author, reviewer, and merger session as unsafe.
|
||||
aligned = ctx.get("roots_aligned")
|
||||
except Exception:
|
||||
# An unresolvable binding is reported as unknown alignment, never as
|
||||
# proof of alignment.
|
||||
@@ -11402,21 +11420,18 @@ def _current_runtime_mode_report(refresh: bool = False) -> dict:
|
||||
profile_name = get_profile()["profile_name"]
|
||||
except Exception:
|
||||
profile_name = None
|
||||
report = stable_control_runtime.build_runtime_report(
|
||||
return stable_control_runtime.build_runtime_report(
|
||||
process_root=PROJECT_ROOT,
|
||||
checkout_branch=observed["checkout_branch"],
|
||||
runtime_head=observed["runtime_head"],
|
||||
is_git_checkout=observed["is_git_checkout"],
|
||||
dirty_files=observed["dirty_files"],
|
||||
checkout_branch=facts["checkout_branch"],
|
||||
runtime_head=facts["runtime_head"],
|
||||
is_git_checkout=facts["is_git_checkout"],
|
||||
dirty_files=dirty_files,
|
||||
active_task_workspace=workspace_root,
|
||||
canonical_repository_root=canonical_root,
|
||||
workspace_roots_aligned=aligned,
|
||||
profile=profile_name,
|
||||
declared_mode=stable_control_runtime.declared_runtime_mode(),
|
||||
)
|
||||
if _STARTUP_RUNTIME_MODE is None:
|
||||
_STARTUP_RUNTIME_MODE = report
|
||||
return report
|
||||
|
||||
|
||||
def _runtime_mode_block(op: str) -> list[str]:
|
||||
|
||||
Reference in New Issue
Block a user