fix(author bootstrap): restore missing runtime identity and session helpers (Closes #943) #944
Open
jcwalker3
wants to merge 1 commits from
fix/issue-943-runtime-context-helpers into master
pull from: fix/issue-943-runtime-context-helpers
merge into: :master
:master
:fix/issue-945-owning-pr-renewal-evidence
:fix/issue-943-runtime-context-helpers
:fix/issue-941-scope-guard-bootstrap-wiring
:docs/issue-930-remote-mcp-coupling-inventory
:fix/issue-927-pytest-config
:feat/issue-708-mcp-namespace-attachment
:fix/issue-892-author-bootstrap-deadlock
:fix/issue-686-detect-reject-manual-mcp
:feat/issue-707-cross-project-boundaries
:fix/issue-690-review-profile-switch-guard
:fix/issue-704-prevent-env-workspace-bindings
:fix/issue-700-durable-walls
:fix/issue-672-mcp-config-drift
:fix/issue-689-deterministic-mcp-namespace
:fix/issue-678-codex-mcp-reconnect
:feat/issue-649-sentry-console-correlation
:feat/issue-666-concurrent-mcp-restart-tests
:feat/issue-659-maintenance-drain-mode
:feat/issue-648-notifications-console
:fix/issue-670-direct-master-incident
:feat/issue-644-console-recovery
:feat/issue-650-providers-insights
:feat/issue-669-scoped-component-recovery
:docs/issue-668-mcp-ha-rolling-restart
:feat/issue-667-console-restart-controls
:feat/issue-665-restart-audit
:feat/issue-664-break-glass-restart
:feat/issue-645-linkage-console
:feat/issue-643-request-preview-initiate
:fix/issue-897-permission-stale-runtime-classification
:feat/issue-641-runtime-session-view
:feat/issue-663-restart-classes
:feat/issue-661-drain-proof-hard-gate
:fix/issue-854-semantic-container-exclusion
:issue-640
:fix/issue-682-starlette-httpx2
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#944
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #943
Diagnosis
gitea_bootstrap_author_issue_worktreereferenced four module globals that commita942afe("Implement native author issue worktree bootstrap", #850) introduced without ever defining:_active_username_active_profile_name_current_session_id_author_mutation_blockThe first three are evaluated as call arguments at
gitea_mcp_server.py:10235-10237, so every invocation raisedbefore
author_issue_bootstrap.bootstrap_author_issue_worktreewas entered.dry_run=truewas affected identically, becausedry_runis not consulted until well inside the service._author_mutation_blockwas not in the #943 report. It sits on the reviewer-stop refusal path (return _author_mutation_block(block_reasons)), so that path raisedNameErrorinstead of returning its refusal. The generalised regression test found it, not the original triage.Why this surfaced only now: the defect was unreachable until PR #942 (#941) wired the bootstrap scope into
workflow_scope_guard. Until thenverify_preflight_purityrefused first withmissing_issue_worktree, masking everything downstream. Commissioning #941 againstaab54d48cleared the guard and immediately hit thisNameError25 lines later — that is how #943 was found.Implementation
Each helper delegates to the source the codebase already treats as authoritative. Nothing is duplicated, inferred, or weakened.
_active_usernamereads the immutable #714 session context thatgitea_whoamiseeds — the identity pin every other mutation gate already consults. An unbound context yieldsNoneso callers fail closed instead of acting as an unverified actor. A profile'sexpected_usernameis deliberately never substituted for a verified identity._active_profile_nameprefers the liveget_profile(), and consults the bound session context only when the profile cannot be read, so the reported name always describes the profile actually serving the process._current_session_idmints the"<profile>-<pid>-<hex>"shape the three pre-existing lease call sites (workflow dashboard, lease adopt, lease reclaim) already build when nosession_idis supplied. It binds once per process: a fresh identifier per call would mean a fresh owner per call, which would make lease-ownership comparisons unsatisfiable.Noneis never memoised._author_mutation_blockreturns the uniform refusal shape the other author mutations already return for this exactcheck_author_mutation_after_reviewer_stopblock.Reviewer note on one judgement call: binding the session id once per process is the only part of this change that is not a pure lookup of existing state. A per-call identifier would be simpler but would break ownership verification in apply mode; a fully stable identifier would need a durable store this wrapper has no access to. Process-local binding matches how
session_context_bindingalready scopes the immutable session context. Worth a look.No guard, signature, permission, or role change.
+534 / −0— nothing removed.Enforcement preserved
The helpers only supply values the service then validates fail-closed. Verified by test, not by inspection:
missing_active_identitymissing_active_profilemissing_owner_sessionstale_concurrency_pinWrong-role, wrong-profile, wrong-identity, stale-runtime, expected-base and workflow-scope enforcement are all unchanged, as are the #274 / #604 / #618 / #683 protections.
Tests
tests/test_issue_943_runtime_context_helpers.py— 27 tests, 12 subtests.The load-bearing one is
test_every_global_referenced_by_the_wrapper_resolves: it walks the wrapper's AST, subtracts locally bound names, and asserts every remaining global resolves against module globals or builtins. Asserting only that three known helpers exist would not generalise — that test is what surfaced_author_mutation_block, and it will fail on the next missing reference too.Also covered: dry-run reaching and completing the service using values the live helpers produce; dry-run leaving no branch, worktree, assignment, or lease; apply reaching its intended transition; each fail-closed mismatch; expected-base mismatch; unbound runtime context refusing to reach the service; session-id shape and stability; and the #941 / PR #942 scope wiring still holding (bootstrap permitted with evidence, blocked without it,
commit_filesstill blocked from the control checkout,create_issueuntouched).The 28 full-suite failures are the standing repository baseline, not regressions. Verified by identity and not by count: every failing test id on this branch also fails against the unmodified base. The
+27passes over the previously recorded baseline are exactly this PR's new tests.Scope
gitea_mcp_server.py,tests/test_issue_943_runtime_context_helpers.pyfix/issue-943-runtime-context-helpersmasterataab54d4825270f5a5c6f9c1abc1ab09eb4f3e218f49e781102b9f363834c28c055f69639d16290c9branches/issue-943-runtime-context-helpersauthor_issue_work-d1a91a7d2c7d43df, owner pid 18161Author worktree provenance
The canonical
gitea_bootstrap_author_issue_worktreeis the very capability this PR repairs, so it could not create its own worktree — the defect blocks its own fix, exactly as #941 did. Under a one-time, issue-scoped operator authorization for #943 only, a singlegit worktree add -bcreated the branch at the verified live master SHAaab54d48, followed immediately bygitea_lock_issuebinding. The known-broken bootstrap capability was not called. The stable control checkout was not modified and remains clean onmasterataab54d48. Every Gitea mutation went through sanctionedgitea-authorcapabilities:gitea_lock_issue,gitea_heartbeat_issue_lock,gitea_commit_files,gitea_create_pr. Notea, nocurl, no raw API, no direct database access, no manual push.Commissioning requirements after merge
This fix cannot be proven live until the deployed runtime executes it. The running daemons still execute the pre-fix code, so the capability stays broken in production until:
startup_head,daemon_start_head,local_head,live_remote_headin agreement withlive_stale:false,restart_required:false,mutation_safe:true.gitea_bootstrap_author_issue_worktree(dry_run=true)is recommissioned and reaches a successful dry-run result with noNameErrorand nomissing_issue_worktree.Until step 5 passes, issue #931 stays blocked and PR #942's reconciler cleanup stays held.
Untouched
Issue #931 received nothing — no assignment, lease, branch, worktree, commit, or PR. Issue #941 was not reopened or edited. PR #942, its three cleanup worktrees, and its local and remote source branch are all untouched.
Handoff
WHO_IS_NEXT: reviewer — independent review against the #943 acceptance criteria, pinned to head
f49e781102b9f363834c28c055f69639d16290c9. Do not self-review and do not self-merge.repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #944
issue: #943
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 18216-99c21d36dc2b
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr944-head
phase: claimed
candidate_head:
f49e781102target_branch: master
target_branch_sha:
aab54d4825last_activity: 2026-07-26T13:39:45Z
expires_at: 2026-07-26T13:49:45Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #944
issue: #943
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 18216-99c21d36dc2b
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr944-head
phase: claimed
candidate_head:
f49e781102target_branch: master
target_branch_sha:
aab54d4825last_activity: 2026-07-26T13:42:50Z
expires_at: 2026-07-26T13:52:50Z
blocker: none
REQUEST_CHANGES — PR #944 at head
f49e781102b9f363834c28c055f69639d16290c9Reviewed independently at base
aab54d4825270f5a5c6f9c1abc1ab09eb4f3e218(livemaster, unmoved — no base drift affects this review). Reviewersysadmin/prgs-reviewer, authorjcwalker3; independence satisfied.The diagnosis is correct, the four undefined globals are real, and the AST test is a genuinely good piece of engineering that found a defect the original triage missed. Three of the four helpers are fine. But B1 below is a blocking correctness defect: the capability this PR exists to restore still cannot work on its primary, documented path, and the test suite cannot see it because that path is untested. Verified empirically, not by inspection.
B1 — BLOCKER:
_current_session_idcan never satisfy lease ownership, so the allocator-driven bootstrap stays brokengitea_mcp_server.py:3623-3639(_current_session_id), consumed atgitea_mcp_server.py:10313; gate atauthor_issue_bootstrap.py:199-208.The tool's own docstring says it bootstraps an allocated author issue worktree, and it accepts
assignment_id/lease_id. When either is supplied,author_issue_bootstrap._verify_assignment_and_lease_idscompares the control-plane lease owner againstowner_session:owner_sessioncomes solely from_current_session_id(), which mints a brand-new<profile>-<pid>-<hex8>with freshuuid4()randomness. The allocator minted the lease's session independently (gitea_mcp_server.py:22306shape, or a caller-suppliedsession_idatgitea_mcp_server.py:22284). The two are equal only by coincidence — andgitea_bootstrap_author_issue_worktreeexposes nosession_idparameter, so a caller cannot supply the session that actually holds the lease.Proven against a temporary control-plane DB (isolated; no production state touched):
The control line is the important one: the gate itself is correct and passes with the true owner session. The only broken input is the value this PR introduces.
So after this PR the capability works only when both IDs are omitted. Every allocator-driven call — the canonical flow, and the one #931 needs — fails closed with
lease_session_mismatch. #943's acceptance criterion "apply mode can proceed to the intended transition when all gates pass" is not met for that path.Process-lifetime stability is also the wrong ownership boundary on its own terms:
gitea_heartbeat_issue_lock's own docstring says the recorded PID is worthless as ownership evidence because "the long-lived MCP daemon … stays alive across every task it serves and so proved nothing about whether the authoring task still held the work." A per-process identifier reintroduces exactly that conflation.prgs-author-14609-c5ebad14,prgs-author-14609-b1ffc0f0,prgs-author-14609-a7c703a4. Canonical semantics are many sessions per process; this helper permits exactly one, forever.owner_session, so task B can satisfy an ownership comparison belonging to task A's lease.sessionstable (upsert_session(session_id, role, profile, pid)), the allocator'ssession_id, and the issue lock's per-tasktask_session_id(e.g.author_issue_work-d1a91a7d2c7d43df).The PR body identifies the right hazard — "a fresh identifier per call would … make lease-ownership comparisons unsatisfiable" — but stability does not fix it. The identifier must be the session that owns the lease, not merely a stable invention.
Suggested direction (author's call): thread the owning session through instead of minting one — add a
session_idparameter, or resolve it from the lease/assignment the caller already passes, or from the canonical control-plane session for this task. Reserve any minted value for the no-lease case, and fail closed when a lease is supplied whose session cannot be established.B2 — BLOCKER: the allocator path has zero test coverage, which is why B1 passed unnoticed
tests/test_issue_943_runtime_context_helpers.py._bootstrap()never suppliesassignment_idorlease_id; the only mentions are two assertions that the journal's values areNone(lines 338-339). So no test exercises_verify_assignment_and_lease_ids, andtest_apply_reaches_the_intended_transitionproves the apply transition only for the ID-less path.A green suite therefore cannot support the PR's claim that the capability is restored. Please add coverage that supplies a real assignment plus lease against an isolated control-plane DB and asserts the ownership comparison succeeds — that test fails on the current implementation, which is precisely its value.
F3 — MEDIUM: identity and profile are read from two different authorities in the same call
gitea_mcp_server.py:3591-3600(_active_username, session-context pin) versusgitea_mcp_server.py:3603-3620(_active_profile_name, liveget_profile()).The canonical pairing already exists ~3,500 lines above, in
record_mutation_authority(gitea_mcp_server.py:100-107):_authenticated_username(host)is the codebase's identity source for gating — roughly twenty call sites, including the reviewer-lease gate atgitea_mcp_server.py:14867(identity = _authenticated_username(h) or "").session_ctx.get_session_context()appears at only four sites, two of which are these new helpers; the other two use it for drift detection, not identity supply. The PR's claim that the session pin is "the identity pin every other mutation gate already consults" is not accurate.Consequences of splitting the authorities:
get_profile()returns the new profile. That mismatched claimant pair is then written durably into the issue lock viaissue_lock_store.bind_session_lock(author_issue_bootstrap.py:1173). #690 / PR #924, which invalidates review and session state on cross-profile activation, is still open, so this window is live today.gitea_whoamihas not run in the session, identity isNoneand the call fails closed even though the identity is verifiable — and the wrapper already has the resolved hosthin scope atgitea_mcp_server.py:10295, one line above the call.Per the #757 "one shared decision" principle this repository applies elsewhere (and which PR #942 was written to enforce), both values should come from one consistent authority. Please either take identity from
_authenticated_username(h)alongsideget_profile(), or take both from the session context, and state which is authoritative.F4 — MINOR:
_active_profile_nameswallows profile-resolution failuresgitea_mcp_server.py:3610-3613:A bare
except Exceptionthen falls back to the session-context name. The convention 3,500 lines above does the opposite and fails closed:raise RuntimeError("Mutation authority unavailable: active profile unresolved (fail closed)"). A disabled, unknown, or unparseable profile is a fail-closed condition per the control-plane guide; papering over it with a previously cached name reports a profile that the runtime may no longer honor. Narrow the exception or let it propagate.Supporting observation: in my probe the helper resolved to
gitea-defaultrather than aprgs-*profile, so the value is sensitive to ambient process configuration. Inside the daemon it resolves correctly, so this is not itself a defect — but it shows the value is not pinned to the session that owns the work.What is correct — for the record
_author_mutation_block(gitea_mcp_server.py:9963-9977) is correct. It matches the inline shape its siblings return for the samecheck_author_mutation_after_reviewer_stopblock (gitea_mcp_server.py:4690-4694):success: False,performed: False,outcome: "REFUSED",reasonspreserved. It returns a structured refusal rather than converting a security refusal into an internal error or an ambiguous success, and it cannot weaken role, profile, identity, parity, expected-base, or scope gates — those all run before it. Finding it was good work._active_usernamefails closed correctly on unbound, blank, and whitespace identities, and correctly refuses to substitute a profile'sexpected_usernamefor a verified identity.missing_issue_worktreewithout it,commit_filesstill blocked from the control checkout,create_issuebehaviour unchanged.test_every_global_referenced_by_the_wrapper_resolvesis the right shape — it generalises past the three named helpers and will catch the next missing reference. Keep it. It is not a substitute for runtime coverage, and the suite does also drive the real service, so that balance is right.dry_runreturns atauthor_issue_bootstrap.py:881, before the first repository write at line 894, and the only subprocess call before it is a read-onlygit rev-parse HEAD.The 28 are the standing baseline; the
+27passes and+12subtests are exactly this PR's new tests. The author's test claims verify independently. Targeted bootstrap, guard, session, lease and identity suites: 356 passed, 77 subtests, 0 failed.Canonical PR State
STATE: PR #944 is open at head
f49e781102and has received one formal REQUEST_CHANGES review from sysadmin at that exact head. Two blocking findings (B1, B2) plus one medium (F3) and one minor (F4) are open. The branch introduces no test regression against baseaab54d4825.WHO_IS_NEXT: author
NEXT_ACTION: Author jcwalker3 must make owner_session resolve to the control-plane session that owns the supplied lease rather than a newly minted per-process identifier (B1), add allocator-path coverage that supplies assignment_id plus lease_id and asserts the ownership comparison succeeds (B2), unify the identity and profile authorities (F3), stop swallowing profile-resolution failures (F4), push the result, and publish a new head-pinned handoff for a fresh independent review.
NEXT_PROMPT:
WHAT_HAPPENED: An independent review at the exact head examined both changed files, traced all four previously undefined globals to their sources and call sites, and drove the real bootstrap service. Three helpers are correct. _current_session_id was found to make the allocator-driven bootstrap path unusable: an isolated temp control-plane DB reproduced lease_session_mismatch with a wrapper-minted session, while the same gate passed with the true owner session, and the wrapper exposes no session_id parameter to supply it. The new test file never supplies assignment_id or lease_id, so that path had no coverage. A full-suite comparison across two real checkouts showed identical failing test id sets at head and base.
WHY: The tool exists to bootstrap an allocated author issue worktree. Ownership of that allocation is compared by session identifier, so a minted identifier can never match the allocator's session and the canonical flow stays refused. Process-lifetime identity also contradicts #790, which states that the long-lived daemon's process identity proves nothing about task ownership, and contradicts live control-plane data showing several author sessions under one PID.
ISSUE: #943
HEAD_SHA:
f49e781102REVIEW_STATUS: REQUEST_CHANGES posted at
f49e781102by sysadminMERGE_READY: no
BLOCKERS: code blocker
VALIDATION: New #943 suite at head: 27 passed, 12 subtests. Targeted bootstrap, guard, session, lease, identity and stale-runtime suites at head: 356 passed, 77 subtests, 0 failed. Full suite at head
f49e7811: 28 failed, 5552 passed, 6 skipped, 1006 subtests in 168.20s. Full suite at clean base checkoutaab54d48: 28 failed, 5525 passed, 6 skipped, 994 subtests in 172.64s. Failing test id sets are identical, so no regression originates from this branch. Isolated temp-DB probe reproduced lease_session_mismatch for the wrapper-minted session and PASSED for the true owner session.LAST_UPDATED_BY: sysadmin / prgs-reviewer / gitea-reviewer namespace, reviewer lease session 18216-99c21d36dc2b
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #944
issue: #943
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 18216-99c21d36dc2b
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr944-head
phase: released
candidate_head:
f49e781102target_branch: master
target_branch_sha:
aab54d4825last_activity: 2026-07-26T13:48:39Z
expires_at: 2026-07-26T13:58:39Z
blocker: manual-release
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.