Skip to content

Ingest manifest and MCP tool annotations as untrusted claims instead of hardcoding read_only: false #3287

Description

@macanderson

crates/stella-tools/src/custom.rs hardcodes read_only: false for every .stella/tools/*.toml tool, and MCP tools get the same treatment — so a genuinely read-only third-party tool cannot say so, and the MCP spec's own readOnlyHint / destructiveHint / idempotentHint annotations are discarded entirely.

PR #3281 built the machinery that makes ingesting them safe: ToolContract::declared records a claim while grading it High, and trusted_read_only() is the one place the distinction is load-bearing. This is #2716 §6.

Definition of done

  • .stella/tools/*.toml gains read_only, risk, idempotent and an optional [output_schema]. The manifest is the TOML serialization of the same contract, not a second format.
  • MCP tool annotations map into the same contract fields.
  • Both arrive as Provenance::Declared. A claim is displayed and available to policy; it never admits the tool to the read-only/speculation dispatch sets on its own — that is trusted_read_only(), and crates/stella-tools/src/contracts.rs's tests already pin it.
  • The one route from claim to trust is the existing foundry adoption gate (crates/stella-tools/src/foundry_gate.rs): witnessed adoption + human enablement + byte-digest match. Nothing else upgrades provenance.
  • Add the idempotent field to ToolContract here, with this as its consumer — it was omitted from feat(stella-tools): ToolContract, AuthzGate port and the gate decorator — the governance half of tool-first #3281 rather than shipped unread.
  • Document the asymmetry in the stella-tools and stella-mcp crate READMEs.

Why the untrusted posture is not paranoia

The read-only bit is not decorative: it admits a tool into concurrent dispatch alongside other reads, and it fences a verifier into a set it cannot mutate the workspace from. A false claim there is a data race and a broken verification boundary, not a cosmetic error. An MCP server's metadata is untrusted for the same reason its output is (#2689).

Witness

A manifest declaring read_only = true is displayed as read-only, is refused by a Medium risk ceiling, and does not appear in the executor's read-only dispatch set — with the built-in equivalent doing all three the other way.

Refs #2716, #2689, #3281.

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageUntyped request — convert by adding bug / feature / epic

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions