620ed6e9a9550b8da2ceb82d9ab8744e8920490f
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1ec4672fad |
fix(mcp): block manual daemon killing as workflow recovery (Closes #630)
A session that ran `pkill -f mcp_server.py`, waited for the IDE to respawn the daemons, and then closed an issue left no trace distinguishing that closure from one performed over a sanctioned runtime. Detection existed only as advisory strings (`native_mcp_preference.classify_command_path`, `review_workflow_boundary`) and never failed closed on the mutations that followed. Adds `runtime_recovery_guard.py`, mirroring the #671 stable-branch contamination model so the two cannot drift apart: classification of kill/pkill/killall commands and known-pid kills, a durable `runtime_recovery_contamination` marker, a fail-closed pre-flight gate over the shared review/merge/close/completion task set, and reconciler-only clearing. `comment_issue` and `lock_issue` stay allowed so a contaminated worker can still post its audit comment and hand off. The marker is recovery-critical, so contamination cannot expire into cleanliness with the session-state TTL. Read-only inspection (`ps aux | grep mcp_server`), sanctioned reconnects, and process management unrelated to the daemons are never flagged; a bare `kill` of an unknown pid is reported as ambiguous rather than contaminating, so ordinary subprocess work is not false-blocked. A pattern broad enough to sweep unrelated namespaces (`pkill -f python`) is contamination even when it never names MCP. Operator-authorized host maintenance stays permitted, but the authorization is read from GITEA_OPERATOR_DAEMON_MAINTENANCE_AUTHORIZATION in the process environment only and never from a tool argument, so a session cannot authorize itself. Final-report rules reject clean-session claims and require the contaminated recovery to be surfaced. Two tools are registered (inventory 112 to 114) and the sanctioned-versus-forbidden contrast is documented in the namespace recovery doc and the workflow skill. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
0589ec8069 |
feat(control-plane): persist allocator dependency edges as durable state (Closes #784)
Umbrella #628 scope item 6 requires dependencies to be durable structured
state carrying source, target, type, blocking condition, completion
condition, current state, and evidence. Nothing stored any of that.
Dependency knowledge existed only as a per-run computation:
allocator_dependencies re-parsed the Depends: declaration out of every
issue body on every allocation, _allocator_candidates_from_gitea resolved
each reference against live issue state, and the result collapsed into two
in-memory WorkCandidate fields that classify_skip consumed and discarded.
Three consequences followed. Nothing could answer "what is waiting on #N"
without re-listing every open issue and re-parsing every body, so the
reverse edge automatic resumption needs did not exist in any form. Only
issue-blocked-by-issue was expressible, leaving the other six #628
relationships with nowhere to live. And no observation was recorded, so a
transient lookup failure and a real block were indistinguishable after the
fact.
Add the store:
- dependency_edges table under schema v4. Creating the table is itself the
v3 to v4 migration: additive, idempotent, and it never touches the
existing tables. Uniqueness is (scope, source, target, edge_type), so
re-observation updates one row rather than appending duplicates.
- dependency_graph.py owns the vocabulary: the seven #628 relationship
types, the three states, and fail-closed normalization for both plus
endpoint kinds. An unrecognized value writes nothing rather than landing
as unqueryable free text. Evidence is sanitized before storage, so no
credential or endpoint URL can be persisted or read back.
- upsert_dependency_edge, list_dependency_edges, and
record_dependency_edge_observation on ControlPlaneDB. Filtering by target
makes reverse lookup a single query. State transitions append to the
existing events table rather than a parallel audit table.
- The allocator persists what it already resolved. States map one-to-one
from the resolver's met/unmet/unavailable partitions, so nothing is
re-classified and unavailable evidence is never recorded as met.
- gitea_list_dependency_edges exposes stored edges read-only, gated on
gitea.read, and is added to the documented inventory the #781 drift guard
checks.
Selection is deliberately untouched: classify_skip still consumes the
in-memory dependency_unmet field. The write is best-effort and reports
failures through reasons, so a broken or absent store leaves allocation
behaving exactly as it did before — proven by allocating the same candidate
set through a store whose writes all raise and comparing the selection,
skip set, and candidate count.
Automatic blocking and resumption (#628 item 7), non-issue edge creation,
and defect auto-linking are later slices; this one only makes the graph
durable and queryable.
Tests: 31 new cases covering fresh-schema creation, a real v3-to-v4
migration with row retention, idempotent re-migration, enum rejection,
upsert idempotence, forward and reverse lookup, scope isolation, transition
events, redaction at rest, live allocation-run ingestion, write-failure
tolerance, and the tool's permission gate.
Verification: 4126 passed in the branch worktree. The 11 failures in
test_commit_payloads, test_issue_702_review_findings_f1_f6, test_mcp_server,
test_post_merge_moot_lease, and test_reconciler_supersession_close reproduce
identically on a clean detached checkout of master at
|
||
|
|
a002864a06 |
fix(mcp): add sanctioned gitea_edit_issue and a doc/registry drift guard (Closes #781)
The gitea-workflow documentation named a gitea_edit_issue tool that no
namespace had ever registered. There was no sanctioned MCP path to change an
issue title or body at all: the only edit tool, gitea_edit_pr, PATCHes the
pull-request endpoint. An authorized body correction on issue #780 therefore
had to be recorded as a discussion comment instead.
Add edit_issue.py as the authoritative rule and gitea_edit_issue as the tool
built on it:
- Only the fields the caller names are sent, so labels, state, assignee, and
milestone cannot be overwritten from a stale read.
- A pull-request number is refused. Gitea serves pull requests from the same
/issues/{n} collection, so without that check the issue path would quietly
become a second, ungated PR edit path. gitea_edit_pr stays PR-only.
- Structurally invalid requests raise before any credential or network work;
a request that would change nothing is reported as an explicit no-op with a
next action rather than a silent success.
- Read-after-write proves the applied title/body and proves that state,
labels, assignees, and milestone did not move. Transport failures on the
pre-read, the PATCH, and the read-back are each reported, redacted, with
the correct performed/verified state.
Gates match every other issue mutation: profile permission via the shared
capability map (resolver task edit_issue, gitea.issue.comment), preflight
purity, branches worktree validation, anti-stomp inventory membership, and
audited mutation.
Fix the drift that hid this. docs/mcp-tool-inventory.md is now the canonical
registered-tool list, and mcp_tool_inventory.py compares it to the live
registry in both directions, plus checks that every tool named under skills/
is registered. The new guard immediately found a second instance of the same
defect: gitea_record_pre_review_command had lost its @mcp.tool() decorator
while the canonical review workflow still instructed reviewers to call it, so
that registration is restored.
Validation:
PASSED: venv/bin/python -m pytest tests/test_issue_781_edit_issue_tool.py -s -q
— 50 passed
FAILED: venv/bin/python -m pytest -s -q — 4095 passed, 11 failed, 6 skipped.
The identical 11 tests fail on clean master
|