Skip to content

feat: MCP/ToolSafetyContract security hardening (#356, #357, #358, #359, #371)#385

Merged
dgenio merged 5 commits into
mainfrom
claude/github-issues-triage-vo7u14
Jun 13, 2026
Merged

feat: MCP/ToolSafetyContract security hardening (#356, #357, #358, #359, #371)#385
dgenio merged 5 commits into
mainfrom
claude/github-issues-triage-vo7u14

Conversation

@dgenio

@dgenio dgenio commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the recommended five-issue MCP / ToolSafetyContract security hardening cluster as a single coherent PR. The spine is the ToolSafetyContract: MCPToolAdapter now populates it conservatively from untrusted remote metadata (#371), the executor enforces it at run time (#356) and previews via it (#357), and the adapter import boundary is hardened against name/description abuse (#359) and silent schema drift (#358). All five are opt-in or behaviour-preserving by default.

Changes

#371 — Ingest MCP ToolAnnotationsToolSafetyContract (mcp/adapter.py)

  • MCPToolAdapter(annotation_trust="trust"|"ignore"|"cap") (default "cap"). readOnlyHint → READ (never NONE — a remote call still observes the world), destructiveHint → DESTRUCTIVE, unannotated → EXTERNAL; remote determinism_level always NONE. Contract source recorded on tool.metadata.

#359 — Trust controls for server-provided metadata (mcp/adapter.py)

  • New frozen MetadataPolicy (control-char stripping, whitespace normalisation, length cap, description_mode="placeholder", name validation/sanitisation, permissive() escape hatch). Raw server description preserved on tool.metadata["mcp_raw_description"].

#358 — Pin & verify MCP-adapted tool schemas (mcp/adapter.py, compat.py)

  • compat.schema_dict_fingerprint(); raw input/output schemas fingerprinted at discovery (tool.metadata["mcp_schema_hash"]); discover_tools(pins=/pins_path=) + on_drift="error"|"warn"|"accept"; build_pin_file() / load_pins() lockfile helpers.

#356 — Enforce ToolSafetyContract at execution (new approvals.py, executor.py, contracts.py)

  • New ApprovalCallback/ApprovalContext/ApprovalDecision/ApprovalRecord seam (mirrors decision_callback — executor only calls the user seam, so the no-LLM/no-network/no-randomness invariants hold). FlowExecutor(approval_callback=, strict_safety=, max_side_effect_level=); decisions recorded on StepRecord.approval; enforced on both sync and async lanes. contracts.side_effect_exceeds() helper.

#357 — Execution-time dry-run (executor.py, tools.py)

  • Tool(dry_run_fn=) + Tool.run_dry()/supports_dry_run; execute_flow(dry_run=, dry_run_unsupported=): read-only steps run, declared dry_run_fn previews run, other side-effecting steps skip (stub) or abort; cache + checkpointer bypassed; ExecutionResult.dry_run set; composed sub-flows inherit the mode.

Public API exports + tests/fixtures/public_api.json snapshot updated; docs/security.md, README error table, and AGENTS.md repo map / field tables updated.

Testing

  • Linting passes (ruff check chainweaver/ tests/ examples/) — All checks passed!
  • Formatting check passes (ruff format --check chainweaver/ tests/ examples/) — 168 files already formatted
  • Type checking passes (python -m mypy chainweaver/ tests/) — Success: no issues in 168 source files
  • All existing tests pass (python -m pytest tests/) — 1592 passed, 1 skipped, 92.58% coverage
  • New tests added: tests/test_mcp_adapter_trust.py (25 cases), tests/test_execution_safety.py (20 cases) — annotation-mapping matrix, metadata policy, schema-pin drift, approve/deny/raise/invalid/strict/ceiling, dry-run dispatch + cache bypass + serialization round-trip.

Related Issues

Closes #356, #357, #358, #359, #371.

Mode B scope notes / deltas

Checklist

  • Code follows project conventions (see AGENTS.md and docs/agent-context/)
  • Public API changes are documented (__all__, snapshot, docs/security.md, README, AGENTS.md)
  • No secrets or credentials included

https://claude.ai/code/session_01TRXw5DESD7cYUkPiKQqBKU


Generated by Claude Code

claude added 3 commits June 13, 2026 16:50
Treat remote MCP server metadata as untrusted input at the adapter boundary:

- #371: ingest server ToolAnnotations into a conservative ToolSafetyContract
  (annotation_trust trust/ignore/cap; cap default). readOnlyHint -> READ,
  destructiveHint -> DESTRUCTIVE, unannotated -> EXTERNAL; remote determinism
  pinned to NONE. Provenance recorded on tool.metadata.
- #359: MetadataPolicy controls for server-provided names/descriptions
  (length cap, control-char stripping, whitespace normalisation, placeholder
  mode, name validation/sanitisation, permissive() escape hatch). Raw server
  description preserved on tool.metadata for audit.
- #358: fingerprint each tool's raw JSON Schema at discovery
  (schema_dict_fingerprint); verify against supplied pins with on_drift
  error/warn/accept; build_pin_file/load_pins lockfile helpers.

Adds Tool.metadata provenance carrier; MCPMetadataError / MCPSchemaDriftError;
ApprovalDeniedError / SafetyCeilingError (for the execution-time enforcement
that follows). Exports + public-API snapshot updated.

Note: the chainweaver mcp lock CLI command and doctor --check-drift wiring
are deferred as a documented follow-up to keep this off the CLI surface.
#356, #357)

#356 — execution-time safety enforcement (all opt-in, defaults unchanged):
- new chainweaver/approvals.py: ApprovalCallback/ApprovalContext/
  ApprovalDecision/ApprovalRecord + coerce_approval_callback, mirroring the
  decision_callback seam (executor only *calls* the user callback, so the
  no-LLM/no-network/no-randomness invariants hold).
- FlowExecutor(approval_callback=, strict_safety=, max_side_effect_level=):
  steps whose effective contract has requires_approval=True invoke the
  callback before the tool runs; DENY / raise / invalid-return / strict-without
  -callback abort the step with ApprovalDeniedError; max_side_effect_level
  refuses over-ceiling steps with SafetyCeilingError. Decisions recorded on
  the new StepRecord.approval, enforced on both sync and async lanes.
- contracts.side_effect_exceeds() ordering helper.

#357 — dry-run mode:
- Tool(dry_run_fn=) + Tool.run_dry()/supports_dry_run; supports_dry_run=True
  now requires a dry_run_fn at construction.
- execute_flow(dry_run=, dry_run_unsupported=): read-only steps run, declared
  dry_run_fn previews run, other side-effecting steps skip (stub) or abort;
  step cache and checkpointer bypassed; ExecutionResult.dry_run set. Composed
  sub-flows inherit the mode via per-thread run-scoped state.

Exports + public-API snapshot updated; 1592 passed, 92.58% coverage.
…el (#356, #357, #358, #359, #371)

- docs/security.md: consolidated sections for the approval/ceiling enforcement
  seam (#356), dry-run rehearsals (#357), and the MCP-imported-metadata trust
  model (annotations->contract / MetadataPolicy / schema pinning).
- README error table: ApprovalDeniedError, SafetyCeilingError, MCPMetadataError,
  MCPSchemaDriftError.
- AGENTS.md repo map + StepRecord.approval / ExecutionResult.dry_run field rows.
- Fix mypy nits in the new safety tests.
Copilot AI review requested due to automatic review settings June 13, 2026 17:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the MCP import boundary and makes ToolSafetyContract actionable at execution time via opt-in enforcement (approvals + side-effect ceilings) and an execution-time dry-run rehearsal mode, with accompanying docs, exports, and tests.

Changes:

  • Add execution-time safety gating to FlowExecutor (approval callback seam, strict_safety, max_side_effect_level) and record approval outcomes on traces.
  • Add execute_flow(dry_run=True) to run read-only steps, run dry_run_fn previews where available, and skip/abort other side-effecting steps while bypassing cache/checkpointing.
  • Harden MCPToolAdapter by deriving conservative safety contracts from annotations, sanitizing remote metadata, and pinning/verifying remote JSON-schema fingerprints.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/test_mcp_adapter_trust.py Covers MCP adapter hardening: annotation mapping, metadata policy, schema pinning/drift.
tests/test_execution_safety.py Covers approval gating, side-effect ceilings, and dry-run dispatch/bypass behavior.
tests/fixtures/public_api.json Updates public API snapshot for new exports and signature changes.
README.md Documents newly added typed exceptions and their meanings.
docs/security.md Documents approval enforcement, dry-run mode, and MCP trust/pinning model.
chainweaver/tools.py Adds Tool.metadata, dry_run_fn, and dry-run execution helpers.
chainweaver/mcp/adapter.py Implements annotation→contract mapping, metadata sanitization, schema pinning/drift handling.
chainweaver/mcp/init.py Exposes new MCP adapter surface (policies + pin helpers).
chainweaver/executor.py Enforces safety contracts at runtime and adds dry-run execution path + trace fields.
chainweaver/exceptions.py Adds typed exceptions for approvals/ceilings and MCP metadata/schema drift.
chainweaver/contracts.py Adds side_effect_exceeds() helper for ceiling enforcement.
chainweaver/compat.py Adds schema_dict_fingerprint() for raw JSON Schema pinning.
chainweaver/approvals.py Introduces approval callback protocol/models and callback coercion helper.
chainweaver/init.py Exports new public symbols for approvals, exceptions, and helpers.
AGENTS.md Updates repo map and trace field tables for new modules/fields.

Comment thread chainweaver/executor.py Outdated
Comment thread chainweaver/executor.py Outdated
Comment thread chainweaver/executor.py
Comment thread chainweaver/tools.py Outdated
Comment thread chainweaver/mcp/adapter.py Outdated
Comment thread chainweaver/executor.py
Comment thread chainweaver/mcp/adapter.py
Comment thread chainweaver/exceptions.py Outdated
claude added 2 commits June 13, 2026 17:15
- Record an explicit ApprovalRecord(DENY, reason) on every denial path
  (callback raised / invalid return / strict_safety with no callback), so the
  trace distinguishes 'not gated' from 'gated but denied' (was approval=None).
- Async lane now passes step_id=getattr(step, 'step_id', None) into the
  approval gate so DAG step ids populate ApprovalContext.step_id.
- Validate annotation_trust / on_drift in MCPToolAdapter.__init__ so a typo
  (e.g. on_drift='erorr') fails loudly instead of silently disabling drift
  protection.
- ApprovalDeniedError message normalised to end with exactly one period
  (repo exception-message convention).
- Docs: dry_run_fn is synchronous (not 'same signature as fn' incl. async);
  MetadataPolicy.permissive() shown called, not as a bare method reference;
  added a dry_run_fn Args entry.
- Tests: assert DENY ApprovalRecord on raise/invalid/strict paths; add adapter
  policy-literal validation tests.

1594 passed, 92.59% coverage.
…anitisation

Address PR #385 audit findings:

- Export ApprovalCallable and coerce_approval_callback from the chainweaver
  package __all__ so the approval seam mirrors its sibling decisions.py seam
  (which exports DecisionCallable / coerce_decision_callback). Users writing
  typed approval callbacks can now import the alias and coercion helper from
  the top-level package.
- MetadataPolicy.apply_name now re-validates a sanitised tool name against a
  custom name_pattern and raises MCPMetadataError when it still does not match,
  instead of returning a name that silently violates the configured policy.
  The default pattern is unaffected (its charset matches the sanitiser).
- Refresh tests/fixtures/public_api.json for the two new public exports.
@dgenio dgenio merged commit ca13535 into main Jun 13, 2026
20 checks passed
@dgenio dgenio deleted the claude/github-issues-triage-vo7u14 branch June 13, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce ToolSafetyContract at execution time with approval callbacks and side-effect ceilings

3 participants