fix(author bootstrap): restore missing runtime identity and session helpers (Closes #943)

gitea_bootstrap_author_issue_worktree referenced four globals that commit
a942afe (#850) introduced without ever defining:

    _active_username        1 reference, 0 definitions
    _active_profile_name    1 reference, 0 definitions
    _current_session_id     1 reference, 0 definitions
    _author_mutation_block  1 reference, 0 definitions

Evaluating the call arguments therefore raised

    NameError: name '_active_username' is not defined

before author_issue_bootstrap.bootstrap_author_issue_worktree was entered, so
the capability was unusable for every caller including dry_run=true. The fourth
name, _author_mutation_block, sits on the reviewer-stop refusal path and was
found by the generalised regression test rather than by the original report.

The defect was unreachable until PR #942 (#941) wired the bootstrap scope into
workflow_scope_guard: before that, verify_preflight_purity refused first with
missing_issue_worktree, masking it.

Changes:

* _active_username reads the immutable #714 session context that gitea_whoami
  seeds — the identity pin every other mutation gate already consults. An
  unbound context returns None so callers fail closed rather than acting as an
  unverified actor; a profile's expected_username is never substituted.
* _active_profile_name prefers the live get_profile() and falls back to the
  bound session context only when the profile cannot be read.
* _current_session_id mints the same "<profile>-<pid>-<hex>" shape as the three
  pre-existing lease call sites, bound once per process so repeated calls
  describe one session instead of a fresh owner per call, which would make
  lease-ownership comparisons unsatisfiable. None is never memoised.
* _author_mutation_block returns the uniform refusal shape the other author
  mutations already return for the same check_author_mutation_after_reviewer_stop
  block.

No guard, signature, or permission changes. Wrong-role, wrong-profile,
wrong-identity, stale-runtime, expected-base and workflow-scope enforcement all
still gate the call; the helpers only supply values the service then validates
fail-closed (missing_active_identity / missing_active_profile /
missing_owner_session / stale_concurrency_pin).

Regression: tests/test_issue_943_runtime_context_helpers.py (27 tests). The
generalised test resolves every global the wrapper's body references against
module globals and builtins, so the next missing reference fails too rather
than only the three named here — that test is what found
_author_mutation_block. Coverage also coversdry-run reaching and completing the
service with helper-produced bindings, dry-run leaving no branch, worktree,
assignment or lease, apply reaching its intended transition, each fail-closed
mismatch, expected-base mismatch, and the #941/PR #942 scope wiring.

Pre-fix 20 failed / 13 passed against unmodified aab54d48; post-fix 27 passed.
Targeted bootstrap and guard suites: 245 passed, 59 subtests.
Full suite: 28 failed, 5552 passed, 6 skipped, 1006 subtests — the 28 are the
standing baseline, every one of which also fails on the unmodified base.

Closes #943

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_013ygVZQLbbhJTChuVuLaJWb
This commit is contained in:
2026-07-26 07:56:14 -05:00
co-authored by Claude Opus 5
parent aab54d4825
commit f49e781102
2 changed files with 534 additions and 0 deletions
+76
View File
@@ -3580,6 +3580,64 @@ def _authenticated_username(host: str):
return user
# #943: process-local session identifier. The pre-existing call sites that mint a
# session id (workflow dashboard, lease adopt, lease reclaim) all build the same
# "<profile>-<pid>-<hex>" shape when the caller supplies none. Binding it once per
# process keeps lease-ownership comparisons stable for the life of the session
# instead of minting a fresh identifier — and therefore a fresh owner — on every
# call. Process-local only, never shared to a file (same rationale as the
# immutable session context in session_context_binding).
_ACTIVE_SESSION_ID: str | None = None
def _active_username() -> str | None:
"""Authenticated identity bound to this session, or None when unbound.
Reads the immutable #714 session context that ``gitea_whoami`` seeds; it is
the authoritative identity pin every other mutation gate already consults.
Never re-derives or fabricates an identity: an unbound context returns None
so callers fail closed instead of acting as an unverified actor.
"""
ctx = session_ctx.get_session_context() or {}
return ((ctx.get("identity") or "").strip()) or None
def _active_profile_name() -> str | None:
"""Active runtime profile name, or None when it cannot be determined.
The live profile is authoritative (``get_profile``); the bound session
context is consulted only when the profile cannot be read, so the reported
name always describes the profile actually serving this process.
"""
try:
profile = get_profile() or {}
except Exception:
profile = {}
name = (profile.get("profile_name") or "").strip()
if name:
return name
ctx = session_ctx.get_session_context() or {}
return ((ctx.get("profile_name") or "").strip()) or None
def _current_session_id() -> str | None:
"""Session identifier for this process, or None when the profile is unknown.
Uses the same "<profile>-<pid>-<hex>" shape as the existing lease call
sites. Bound once per process so repeated calls describe one session; fails
soft to None when the profile is undeterminable, letting callers fail closed
rather than inventing an owner.
"""
global _ACTIVE_SESSION_ID
if _ACTIVE_SESSION_ID:
return _ACTIVE_SESSION_ID
profile_name = _active_profile_name()
if not profile_name:
return None
_ACTIVE_SESSION_ID = f"{profile_name}-{os.getpid()}-{uuid.uuid4().hex[:8]}"
return _ACTIVE_SESSION_ID
def _authenticated_actor(host: str) -> dict:
"""Resolve the authenticated actor's stable identity (#709 F7 review 438).
@@ -9902,6 +9960,24 @@ def gitea_commit_files(
}
def _author_mutation_block(reasons: list[str], **extra) -> dict:
"""Uniform fail-closed shape for an author mutation refused after a reviewer stop.
#943: referenced by ``gitea_bootstrap_author_issue_worktree`` and never
defined, so the reviewer-stop refusal path raised ``NameError`` instead of
returning its refusal. Mirrors the inline shape the other author mutations
return for the same ``check_author_mutation_after_reviewer_stop`` block.
"""
payload = {
"success": False,
"performed": False,
"outcome": "REFUSED",
"reasons": reasons,
}
payload.update(extra)
return payload
def _publication_block(reasons: list[str], **extra) -> dict:
"""Uniform fail-closed shape for publication refusals (#812 AC20)."""
payload = {