feat(hitl): interactive mode + scoped_mcp_hitl_confirm tool - #61
Merged
Conversation
added 3 commits
July 21, 2026 12:56
Add hitl.mode: enforce|interactive (default enforce — no behavior change for any manifest that doesn't opt in). - manifest.py: new HitlConfig.mode enum field, default enforce. - registry.py: register scoped_mcp_hitl_confirm ONLY for interactive-mode agents that actually gate tools. An enforce-mode / unattended agent never gets the tool registered, so it can't self-resolve a headless run's gated calls. - hitl_endpoint.py / hitl_http.py / registry_db.py: thread a resolved_via audit tag (matrix_bot | courier | interactive_self_service) so the in-session shortcut is always distinguishable from the real out-of-band paths. The interactive tool reuses hitl_endpoint.approve/deny unchanged — no duplicated approval logic. resolved_via is written only when supplied, so a later consume/expire transition never clobbers the approve-time channel. - migrations/0002_hitl_resolved_via.sql: ADD COLUMN IF NOT EXISTS resolved_via (fail-open until applied; safe to run before the code). - docs/manifest-schema.md: document mode + the interactive trust tradeoff. Trust tradeoff (interactive only, by design): scoped_mcp_hitl_confirm trusts the agent's own report that Ted approved in the current turn — the same trust level every other interactive tool call already runs under. Agent-facing contract (only call after explicit in-conversation approve/deny) lives in the tool docstring, manifest-schema.md, and the developer/sysadmin CLAUDE.md. Does not touch Vikunja #203 (broken enforce-mode bot->endpoint path, deferred). Build: scoped-mcp-hitl-bypass-mode-2026-07 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> agent-id: developer
Audit INFO (scoped-mcp-hitl-bypass-mode-2026-07): assert that a state-backend error inside the shared approve logic makes scoped_mcp_hitl_confirm return backend_unavailable and write no pre-approval token — never fabricate an approval. Closes the one coverage gap the audit noted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> agent-id: developer
Stamp CHANGELOG 1.11.0 and bump version. Records the audit outcome (1 HIGH accepted + tracked, 2 Info) for scoped-mcp-hitl-bypass-mode-2026-07. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> agent-id: developer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
hitl.mode: enforce | interactiveto scoped-mcp manifests. Default isenforce— no behavior change for any manifest that doesn't opt in.interactivemode fires the same Matrix notify and the same immediate reject (the agent still stops and asks), but resolves the pending request in-session via a new companion toolscoped_mcp_hitl_confirm(approval_id, decision)that the agent calls directly — replacing the token-expensive "read the Matrix message and self-resolve via asystem-opsshell call" workaround. It reuses the sharedhitl_endpoint.approve/denylogic (no duplicated approval path).Build:
scoped-mcp-hitl-bypass-mode-2026-07. Does not touch Vikunja #203 (the broken enforce-mode bot→endpoint path — deferred, out of scope).Changes
manifest.py— newHitlConfig.modeenum (enforce/interactive, defaultenforce).registry.py—_register_hitl_confirm_tool; registered only whenhitl.mode == interactiveAND the agent actually gates tools AND a state backend exists. Anenforce-mode / unattended agent never gets the tool, so it can't self-resolve a headless run's gated calls.hitl_endpoint.py/hitl_http.py/registry_db.py— thread aresolved_viaaudit tag (matrix_bot|courier|interactive_self_service) so the in-session shortcut is always distinguishable from the real out-of-band paths. Written only when supplied, so a later consume/expire transition never clobbers the approve-time channel.migrations/0002_hitl_resolved_via.sql—ADD COLUMN IF NOT EXISTS resolved_via(fail-open until applied; safe to run before the code).docs/manifest-schema.md— documentsmode+ the interactive trust tradeoff.Trust tradeoff (interactive only, by design)
scoped_mcp_hitl_confirmtrusts the agent's own report that the operator approved in the current conversation turn — the same trust level every other interactive tool call already runs under. It is registered only for interactive-mode agents, soenforce-mode (unattended) agents can never reach it. The agent-facing contract ("only after an explicit in-conversation approve/deny") lives in the tool docstring,docs/manifest-schema.md, and the developer/sysadmin CLAUDE.md.Tests
test_hitl_interactive.py(new) — registration gating (present for interactive, absent for enforce/default/no-approval), approve/deny behavior via the FastMCP client, invalid decision, cross-agent isolation,resolved_viatagging.test_manifest.py—modedefault/accept/reject.test_registry_db.py—resolved_viaSQL param positioning (guarded + unguarded), and that it's omitted when not supplied.Full suite: 737 passed, 11 skipped (Dragonfly/Postgres/otel-gated).
ruff check+ruff format --checkclean.Deploy note
Venv upgrade to this release must land before the companion manifest flip (host-forge-scripts) —
HitlConfig(extra="forbid")rejects themodefield on older builds. Apply0002_hitl_resolved_via.sqlto agent-postgres as part of deploy.🤖 Generated with Claude Code