feat(restart): audit lifecycle events and durable incidents (Closes #665) #909
Open
jcwalker3
wants to merge 1 commits from
feat/issue-665-restart-audit into master
pull from: feat/issue-665-restart-audit
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#909
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.
Summary
Implements #665 under the restart-governance umbrella (#655): structured forensic trail for MCP restart evaluation so failed drains and break-glass cannot be silently repeated.
Changes
restart_audit.py—mcp.restart.*event schema, redacted payload builder, append-only emission viagitea_audit, correlation ids (rst-…), incident descriptors, and injected materializer for Gitea issues.gitea_request_mcp_restart— mints a correlation id; always audits impact previews; on apply, audits apply-gate / break-glass and materializes durable incidents for failed drain and break-glass; privileged apply fails closed when audit is enabled but the sink write fails.docs/mcp-restart-audit.md+ coordinator cross-link.tests/test_restart_audit.py— schema, redaction, emission, deny policy, incident mocks (22 tests).Acceptance map
Non-goals (unchanged)
Proof
Test plan
gitea_request_mcp_restartshowscorrelation_id+restart_auditCloses #665
Also #655 #658 #661 #663 #664 #652 #653
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #909
issue: #665
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 81615-94558e39a724
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-909-restart-audit
phase: claimed
candidate_head:
22e0a41bd5target_branch: master
target_branch_sha:
2b4e43042alast_activity: 2026-07-25T23:06:53Z
expires_at: 2026-07-25T23:16:53Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #909
issue: #665
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 81615-94558e39a724
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-909-restart-audit
phase: validated
candidate_head:
22e0a41bd5target_branch: master
target_branch_sha:
2b4e43042alast_activity: 2026-07-25T23:07:52Z
expires_at: 2026-07-25T23:17:52Z
blocker: none
Canonical PR State
STATE: changes_requested
WHO_IS_NEXT: author
NEXT_ACTION: fix incident kind overwrite so #661 kind restart_drain_gate_denied is preserved; re-run #886 + restart_audit tests; push new head for re-review
NEXT_PROMPT:
WHAT_HAPPENED: Independent reviewer sysadmin validated PR #909 at
22e0a41. Restart audit unit tests pass; #886 regression fails only on this head (passes on master).WHY: gitea_request_mcp_restart overwrites payload["incident"] from drain_proof.gate.incident (kind=restart_drain_gate_denied) with restart_audit.incident_from_apply_gate (kind=restart_failed_drain), breaking the established #661/#886 contract.
ISSUE: #665
HEAD_SHA:
22e0a41bd5REVIEW_STATUS: REQUEST_CHANGES
MERGE_READY: no
BLOCKERS: incident kind restart_failed_drain overwrites restart_drain_gate_denied; test_issue_886 TestExistingPathsStillWork.test_apply_still_denies_without_any_proof fails on PR head only
VALIDATION: pytest tests/test_restart_audit.py -q → 22 passed; pytest tests/test_issue_886_apply_authorization_conjunction.py -q → 1 failed on PR head / passes on master 2b4e430; worktree branches/review-pr-909-restart-audit clean at pinned head
LAST_UPDATED_BY: sysadmin / prgs-reviewer
Review detail
Author safety: reviewer sysadmin != author jcwalker3.
Positive: #665 audit module, redaction, correlation ids, no process restart, docs.
Required fix: preserve #661 incident kind on drain-gate deny path (or dual-support with intentional migration + test updates).
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #909
issue: #665
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 52249-5fe108392e0d
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-909
phase: claimed
candidate_head:
22e0a41bd5target_branch: master
target_branch_sha: none
last_activity: 2026-07-25T23:27:36Z
expires_at: 2026-07-25T23:37:36Z
blocker: none
Second-reviewer findings at head
22e0a41bd5dfe4f47d2417b1f020e7d17f18c2fb. No new verdict is recorded by this comment — review612already carries an unresolved REQUEST_CHANGES at this same head, andgitea_mark_final_review_decisioncorrectly refused a duplicate (#332). This adds two blockers that review 612 does not cover, plus an independent reproduction of its finding.Reproduction of review 612's blocker
Confirmed at this head, in
branches/review-pr-909:incident_from_apply_gate(restart_audit.py:398-413) rebuilds the gate's descriptor under its ownINCIDENT_FAILED_DRAINkind, andgitea_mcp_server.pythen assigns it overpayload["incident"], discarding the #661 kind. Review 612's account of this is accurate.What the module gets right
restart_audit.pyis the strongest piece of this series. The event vocabulary is a closed set andbuild_restart_eventraises on anything outside it. Emission is append-only throughgitea_auditrather than overwrite-by-key state — which is exactly what PR #908 should be calling instead ofmcp_session_state.save_state. Reasons and details are redacted before both the audit write and the issue body, andrequire_audit_or_denyis genuinely fail-closed for privileged paths. Both blockers below are in the wiring, not the module.F1 — a read-only preview now mutates session preflight state
gitea_mcp_server.py:22760, reached on every invocation including the dry-run preview:The value is used once, as the
authenticated_usernamefield of a best-effort audit event.gitea_whoamiis not read-only with respect to session state: its first statement isrecord_preflight_check("whoami")(gitea_mcp_server.py:842), which has two session-wide effects.First, it can clear the caller's resolved capability. At lines 860-875 the resolved-capability stamp survives an interleaved whoami only when
preserve_capabilityholds — whoami already called earlier in the session, a capability already resolved, and no outstanding whoami violation. Otherwise the branch falls to_clear_preflight_capability_state()at line 875 and the resolve is gone.Second, it recomputes the whoami violation flag against workspace porcelain at line 879:
_preflight_whoami_violation = bool(whoami_delta), measured from process start. A session holding legitimate in-flight tracked edits acquires the violation from this call alone.Both feed a fail-closed gate:
verify_preflight_purityraises_PreflightOrderErroron that flag at lines 1510-1514, and it also surfaces as apreflight_block_reasonat line 762.The concrete failure: an operator inspects the blast radius with the preview — whose payload sets
read_onlyat line 22738 — and the next gated mutation is refused, either for a capability that was resolved or for a workspace edit that predates the preview. The tool meant to make a restart safer to look at becomes one you cannot look at mid-workflow.Nothing about the audit needs this.
_authenticated_username(host)(gitea_mcp_server.py:3533) returns the same login from the same endpoint, is cached, never raises, and touches no preflight state.F2 — the durable incident cannot be created by any profile that reaches this path
AC2 and AC3 require a failed drain and a break-glass to materialize a durable Gitea issue. The materializer injected at
gitea_mcp_server.py:22866-22877forwards togitea_create_issue, which is doubly gated: it requirestask_capability_map.required_permission("create_issue")—gitea.issue.create(line 3862) — and it runsverify_preflight_purity(..., task="create_issue")(lines 3876-3879), so the session's resolved capability must becreate_issueat that moment.Neither condition holds here. In the live profile inventory only the two author profiles carry
gitea.issue.create;prgs-controller,prgs-reconciler,prgs-mergerandprgs-reviewerdo not. And a session reaching a restart apply resolved the restart task, notcreate_issue, so even an author profile trips the preflight task check.The outcome looks correct and is empty:
materialize_incidentcatches the refusal atrestart_audit.py:307-310, returnscreated: Falsewith a reason, the payload still reports the deny, and no durable incident exists. AC2's "a failed drain cannot be silently repeated" is the property that does not hold — the only remaining trace is the audit line, which is itself off unlessGITEA_AUDIT_LOGis configured.This is the mirror image of PR #908's incident-permission finding. There the write reached for raw
api_requestand inherited no permission check; here it routes through the gated tool and is refused. Both need the same missing piece: a sanctioned incident-write path a privileged non-author caller may actually use. Failing that, the apply should fail loudly when no capable writer is injected rather than reporting a deny whose incident never landed.Lesser observations
payload["restart_audit"]reportsimpact_preview_written: Falsefor two different states — sink not configured, and sink configured but write failed. For a forensic feature that distinction is the point; surfacegitea_audit.audit_enabled()alongside the written flags. The same conflation hidesrequire_audit_or_deny's no-op for the unconfigured case (restart_audit.py:172-180), whichdocs/mcp-restart-audit.md:16,48describes as intended rollout policy.incident_from_apply_gate(restart_audit.py:414) raises a failed-drain incident only whendrain_gate_allowis falsy, so a denial for another reason — an unauthorized restart class, say — leaves nothing durable behind. AC2 names failed drain specifically, so this is scope rather than defect, but the doc should say it.tests/test_restart_audit.pyexercises the module directly and none importsgitea_mcp_server, so neither F1 nor F2 is reachable by that suite. The PR's own test plan leaves both integration checkboxes unticked, consistent with that. The one suite that does exercise the wiring,test_issue_886, is the one review 612 found failing.payload["success"] = Trueis reasserted inside the audit-deny branch at line 22923 where it is already true; harmless, but it reads as if the deny changed it.build_restart_eventmints a fallback correlation id atrestart_audit.py:132whilerecord_restart_lifecyclerequires one — unreachable from the wired path, and it would quietly break the one-id-per-lifecycle invariant for a future caller.Canonical PR State
STATE: changes-requested
WHO_IS_NEXT: author
NEXT_ACTION: Preserve the #661 incident kind per review 612, replace the internal gitea_whoami call with _authenticated_username, and give the incident materializer a write path a privileged non-author caller can use.
NEXT_PROMPT:
WHAT_HAPPENED: PR #909 was read in full at head
22e0a41bd5against merge base76f293eb28— 975 insertions across restart_audit.py, gitea_mcp_server.py, two docs and one test module. restart_audit.py was read end to end, then each integration point in gitea_request_mcp_restart was traced into the function it calls: gitea_whoami into record_preflight_check and on into the preflight violation flags and their fail-closed consumer, and the injected incident writer into gitea_create_issue and its permission plus resolved-task gates. The live profile inventory was read to determine which profiles hold gitea.issue.create. Review 612's failing-test claim was reproduced locally. Two additional blockers and five lesser observations were found.WHY: The module delivers what #665 promises — closed event vocabulary, append-only sink, redaction, privileged fail-closed rule. The wiring does not. F1 makes a read-only preview change session-scoped preflight state that other tools fail closed on, so an operator inspecting a restart can lose the ability to act, and it does so for an identity string a pure helper already provides. F2 leaves the one artifact AC2 exists to guarantee unreachable for every profile that can reach the apply path, turning a promised durable record into an audit line that is off by default.
ISSUE: #665
BASE: master
HEAD: feat/issue-665-restart-audit
HEAD_SHA:
22e0a41bd5RELATED_PRS: #909
REVIEW_STATUS: REQUEST_CHANGES
MERGE_READY: no — review 612's blocker plus two further blockers in the server wiring
BLOCKERS: review 612's incident-kind overwrite, reproduced here as a failing test_issue_886 case on the PR head; F1 the read-only impact preview calls gitea_whoami, which clears the resolved-capability stamp and recomputes the whoami violation flag, both fail-closed inputs to verify_preflight_purity; F2 the AC2/AC3 durable incident is created through gitea_create_issue, whose permission and resolved-task requirements no caller on this path satisfies, so the incident never lands.
SUPERSEDES: none
SUPERSEDED_BY: none
VALIDATION: Static review at head
22e0a41bd5against merge base76f293eb28, in the bound worktree branches/review-pr-909, confirmed clean by git status --porcelain --untracked-files=all. Every cited line was read at this head. tests/test_issue_886_apply_authorization_conjunction.py was executed here: 1 failed, 18 passed, 45 subtests passed, the failure being 'restart_failed_drain' != 'restart_drain_gate_denied' — an independent reproduction of review 612's finding. F1 was traced through the call at 22760, record_preflight_check at 842-882 including the preserve_capability branch and the clear at 875, and the consumers at 762 and 1510-1514, with _authenticated_username at 3533 read to confirm the pure alternative. F2 rests on gitea_create_issue's permission constant at 3862 and its preflight task at 3876-3879, together with the live profile inventory showing gitea.issue.create only on the author profiles. The PR reports no conflict against its base. Live head SHA at the time of this comment:22e0a41bd5. Author changes during the read: none.LAST_UPDATED_BY: sysadmin (prgs-reviewer)
NATIVE_REVIEW_PROOF: findings posted via gitea_create_issue_comment on native MCP namespace gitea-reviewer, profile prgs-reviewer, identity sysadmin, at head
22e0a41bd5. No formal review verdict was recorded by this call. No offline, import, or helper path was used.[THREAD STATE LEDGER]
what is true now
PR #909 is in open state at head
22e0a41bd5against master, and reports no conflict against that base.Server-side decision state: review 612 records an unresolved REQUEST_CHANGES at this head; this comment records no decision and adds findings only.
Local verdict/state: REQUEST_CHANGES concurrence, two additional blockers, plus a local reproduction of review 612's failing test.
what changed
Nothing on the PR itself. A second finding set now exists on the thread alongside review 612, and a reviewer lease was taken and will be released.
what is blocked
Blocker classification: code blocker
Recording a second formal verdict is refused by the duplicate-verdict rule (#332) while review 612 stands unresolved at this head; that is a process constraint, not a defect in this PR. The code blockers are review 612's incident-kind overwrite, reproduced as a failing
test_issue_886case; F1 atgitea_mcp_server.py:22760, where aread_onlypreview callsgitea_whoamiand thereby clears the resolved-capability stamp (line 875) and recomputes_preflight_whoami_violation(line 879), both of which raise atverify_preflight_puritylines 1510-1514; and F2 at lines 22866-22877, where the AC2/AC3 incident is written throughgitea_create_issueundergitea.issue.create(line 3862) and a resolvedcreate_issuetask (lines 3876-3879) that no caller on this path holds.who/what acts next
Next actor: author
Required action: Preserve the #661
restart_drain_gate_deniedkind on the drain-gate deny path and maketest_issue_886pass on the PR head; replace the internalgitea_whoamicall with_authenticated_username(h); give the incident materializer a write path a privileged non-author caller can use, or fail the apply loudly when no capable writer is injected; surfacegitea_audit.audit_enabled()inpayload["restart_audit"]; and add tests that drivegitea_request_mcp_restartitself.Do not do: Do not resolve F1 by reordering the internal whoami or by documenting the side effect — the preview is consumed mid-workflow by design, so any preflight mutation inside it harms callers who did nothing wrong. Do not resolve F2 by reaching for raw
api_requestto skip the permission gate; PR #908 does exactly that and it is a blocker there. Do not read the 22 passing restart_audit tests as evidence against F1 or F2 — that module never imports the server and cannot observe the wiring.repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #909
issue: #665
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 52249-5fe108392e0d
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-909
phase: released
candidate_head:
22e0a41bd5target_branch: master
target_branch_sha: none
last_activity: 2026-07-25T23:31:53Z
expires_at: 2026-07-25T23:41:53Z
blocker: manual-release
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.