Skip to content

Scaffold trust/privacy extensions from the SEP-1913 carve#2

Open
SamMorrowDrums wants to merge 4 commits into
mainfrom
sammorrowdrums/extensions-scaffold-trust-ifc-action
Open

Scaffold trust/privacy extensions from the SEP-1913 carve#2
SamMorrowDrums wants to merge 4 commits into
mainfrom
sammorrowdrums/extensions-scaffold-trust-ifc-action

Conversation

@SamMorrowDrums

Copy link
Copy Markdown
Contributor

What this is

The first scaffold for incubating the SEP-1913 (Trust & Sensitivity Annotations) work as small, independently-shippable experimental extensions, following the Extensions Track (SEP-2133) and the Tool Annotations IG's 2026-05-28 decision to pursue this extension-first.

SEP-1913 stalled because it bundled four concerns into one schema change with a taxonomy that's hard to remove once shipped (@localden: "If the taxonomy turns out to be wrong, I worry that we can't remove it… Can we do a potential narrower first cut?"). This repo is that narrower first cut.

The carve

Extension Scope
io.modelcontextprotocol/trust-annotations Headline. Narrow data-classification (sensitive, untrusted) + open-ended evidenceRef pointer.
io.modelcontextprotocol/action-metadata Carries forward SEP-2061inputMetadata/returnMetadata/outcomes + requiresReview.
io.modelcontextprotocol/ifc-fides FIDES information-flow control as a profile of evidenceRef, not a wire root.

Key decisions

  • FIDES → profile, not headline. IFC is one of several enforcement models raised in review (capability tokens, caller/tool cosigning, sequence-shape); a top-level ifc/ root would silently foreclose them. It lives as type: "ifc.fides.v1" inside the open evidenceRef slot.
  • evidenceRef.type stays an open string + non-binding registry, so future schemes fit without a spec change.
  • requiresReview → action-metadata (workflow signal, not a data property).
  • DataClass → data-class.v1 profile, off the wire.
  • Parked on the SEP-1913 umbrella: maliciousActivityHint (structural objections) and session propagation.
  • SEP-1862 stays core — pre-flight/tool-resolution is a protocol-level change, not an extension.

See docs/sep-disposition.md for the per-SEP plan and the SEP-2127 migration precedent, and docs/decisions.md for the rationale log.

Follow-ups (not in this PR)

  • Intent comments on SEP-1913 and SEP-2061 explaining the split + Extensions Track move.
  • Fleshing out the three drafts (stacked PRs on top of this scaffold).

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

SamMorrowDrums and others added 4 commits June 10, 2026 23:50
Carve the schema-bearing parts of SEP-1913 into three independent
experimental extensions, per @localden's "narrower first cut" ask and
the Tool Annotations IG's 2026-05-28 extension-first decision:

- io.modelcontextprotocol/trust-annotations (headline): narrow
  data-classification taxonomy (sensitive, untrusted) + open-ended
  evidenceRef pointer.
- io.modelcontextprotocol/action-metadata: carries forward SEP-2061
  (inputMetadata/returnMetadata/outcomes + requiresReview).
- io.modelcontextprotocol/ifc-fides: FIDES information-flow control as
  a profile of evidenceRef, not a wire root.

Adds repo meta (README, MAINTAINERS, CONTRIBUTING, LICENSE) and docs
(sep-disposition, intent-comment, decisions, open-questions,
trust-model, related-work). FIDES demoted to a profile; DataClass and
requiresReview rehomed; maliciousActivityHint and propagation parked on
the SEP-1913 umbrella. Citations index on public sources only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace 'carve'/'headline' throughout; reframe the front-facing docs
(README, spec drafts, related-work) to describe what the repo contains
and link to it rather than narrate the process. Process framing stays in
the decision log and the intent-comment/sep-disposition planning docs.
Live SEP-1913/SEP-2061 comments updated to match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Official extension specs (ext-auth) use only `title:` in frontmatter
plus an <Info> protocol-revision marker. Remove the invented
extension_identifier / status / maintainers / related / profile_of
fields and restate the identifier and profile relationship in the body
prose where they belong.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Rul1an

Rul1an commented Jun 11, 2026

Copy link
Copy Markdown

This carve is the right shape, and the evidenceRef slot captures the small-annotation-on-the-wire / richer-evidence-out-of-band split well.

One small editorial point on trust-annotations.mdx: every evidenceRef example currently uses canonicalization: "cbor/rfc8949". Since canonicalization is per-reference precisely so different evidence producers can be re-derived, it would read more neutrally to show a second example with JSON canonicalization, for example jcs/rfc8785, alongside the CBOR one.

With only CBOR shown, implementers may treat it as the default rather than one valid envelope choice, which would quietly narrow the extension point the field is meant to keep open.

On the open questions, keeping type as an open string with a non-binding registry seems right. The minimum a client needs to do useful local checking is type, digest, and canonicalization; schema seems better as recommended, and ref as optional. That keeps digest-only references useful while letting deployments with an audit/evidence stream add a locator.

Happy to help exercise the shape with worked emitter/consumer cases for policy-decision and sequence types, especially the cases where the annotation stays small and the sequence or policy record remains out of band.

```jsonc
{
"integrity": "trusted", // "trusted" | "untrusted" (FIDES §4.1 two-level lattice)
"confidentiality": "public" // "public" | "private" | ["login1","login2", …]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We can't emit user logins as part of confidentiality for a few reasons:

  • user identity is not uniform across mcp servers that use different auth methods
  • user identity is itself an access-restricted data
  • in real systems there can be hundreds of users that can access given resource and returning them will cause
    I'd propose limiting it to public and private

| Field | Meaning |
| :--- | :--- |
| `integrity` | Two-level integrity lattice (`trusted` ⊑ `untrusted`): trusted data may flow to untrusted sinks, not vice versa. |
| `confidentiality` | `"public"` = world-readable; `"private"` = an opaque marker the host resolves to a reader set (e.g. repo collaborators); an explicit `string[]` = pre-resolved reader logins. Fewer readers = more confidential = higher in the lattice. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
| `confidentiality` | `"public"` = world-readable; `"private"` = an opaque marker the host resolves to a reader set (e.g. repo collaborators); an explicit `string[]` = pre-resolved reader logins. Fewer readers = more confidential = higher in the lattice. |
| `confidentiality` | `"public"` = world-readable; `"private"` = an opaque marker meaning "restricted to some reader set". The concrete reader set is resolved host-side at policy-decision time (see Reader-set resolution). |


### Label semantics

- **Join on accumulation.** As a session ingests labeled results, the context

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'd update the bullets to

- Join on accumulation. As a session ingests labeled results, the context 
- label is the join of what it has seen: integrity degrades toward untrusted, 
- confidentiality narrows toward the smallest permitted reader set. Integrity 
- join is total and computable from the wire values alone (untrusted 
- dominates). **Confidentiality join is NOT computable from the opaque wire 
- markers alone** — see Reader-set resolution.

> The normative integrity/confidentiality lattice definitions follow the FIDES
> paper, §4.1 and §4.3. This draft references the model rather than restating
> the proofs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we add a separate section to descrive reader set resolution?

Reader-set resolution
    
    "private" is intentionally opaque on the wire. Two distinct "private"
    markers (e.g. file contents from two different private repositories) are **not equal** and their confidentiality join is not the same "private" token: data
    derived from both may flow only to principals who can read both sources — the
    intersection of their reader sets. The opaque marker cannot express this
    intersection, so a host that needs to make a precise cross-source flow decision
    MUST resolve each "private" marker to a concrete reader set before joining.
    
    Resolution is a host-side concern, performed at policy-decision time:
    
    1. The host maps each contributing "private" label back to its source
       (e.g. via the evidenceRef.ref locator, or its own record of which tool
       result carried the label).
    2. The host queries the originating system for the current reader set
       (e.g. a repository collaborators lookup) using its own credentials.
    3. The host computes the flow decision over the resolved sets (intersection for
       a join of multiple private sources) and then discards them.

@Rul1an

Rul1an commented Jun 11, 2026

Copy link
Copy Markdown

+1 to this direction — it's the precision that was missing behind keeping private opaque on the wire. The load-bearing part is that private must not collapse into one global bucket: two private sources can have different reader sets, and data derived from both may flow only to a sink valid for all of them, the intersection, which is exactly what an opaque token can't carry. Resolving each source host-side at decision time is the right shape.

One case worth adding to the section: when resolution isn't available — ref is optional, a label may be digest-only, or the originating system is unreachable at decision time — the host must not treat the opaque labels as equivalent or as public. It should deny, prompt, or apply its configured fail-closed policy; two private labels are equal only once resolution proves their sources are. Without that line the optional/digest-only reference can quietly become permissive. And reinforcing your "then discards them": the resolved set is a decision-time read under the host's own credentials, so it shouldn't be cached as a durable grant or serialized back into annotations/evidence unless a deployment explicitly opts in.

Net, the wire stays small and free of user identities while hosts still have enough to make a precise flow decision when they hold source provenance plus host-side credentials — the same small-marker-on-the-wire, richer-state-host-side split as evidenceRef.

confidentiality}` label, maintains a context label across tool results, and
applies a flow policy before egress operations already exists in practice.
(Linked once a public reference is available.)
- **Emitter (gap / proof point):** [`github-mcp-server`](https://github.com/github/github-mcp-server)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note that repository visibility is not the whole story - a public repository can contain sub-resources that are not world-readable (draft security advisories, draft releases, the collaborator roster itself, authenticated-user fields), so a correct emitter MUST classify per resource, not per repository. This makes the emitter side a non-trivial proof point rather than a one-line repo.private read.

@Rul1an

Rul1an commented Jun 11, 2026

Copy link
Copy Markdown

+1 repository visibility is a default hint, not a classification boundary, so the emitter has to classify per resource returned (a public repo still serves things that aren't world-readable: draft advisories, the collaborator roster, authenticated-user fields), not per repo.

It divides cleanly: the label rides the actual resource (emitter-side), the host resolves the concrete reader set later only when it needs a precise decision (the reader-set-resolution section), and unknown or mixed provenance classifies private until the source is established, never defaulted to public from a repo-level shortcut, and without serializing reader sets. Happy to turn this into a small set of emitter conformance cases if useful, the public-repo-with-private-subresource ones especially.

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.

3 participants