Skip to content

[FEATURE]: Capability registration validation: policy at tool/prompt/resource advertisement time #119

Description

@terylt

Status: RFC / request for input
Area: cpex-core (CMF phases + hooks), apl-core/apl-cpex (route scope), policy filter

Problem / gap

CPEX policy today fires at invocation time — tool_pre_invoke,
prompt_pre_invoke, resource_pre_fetch — and the payload it reasons about is
the call arguments. But a meaningful class of policy applies before any call
exists
: when a server advertises its capabilities (tools/list,
prompts/list, resources/list, and the notifications/*/list_changed
follow-ups). At that point the relevant data isn't the args — it's the
capability definition: name, description, inputSchema, annotations.

Requested by an adopter who wants to run validation at registration/discovery
time. It doesn't map onto the invoke-args hooks — but not because it's
"payload-less"; it has a different payload (the definition, which is exactly
what you'd validate).

Proposed model: a new CMF phase, not a new model

CMF is already typed on entity {tool, prompt, resource} × phase
{pre_invoke, post_invoke} (+ resource fetch). Registration is those same
entities
at a new phase — register / advertise:

entity phase payload direction
today tool/prompt/resource pre_invoke / post_invoke call args / result request/response
new tool/prompt/resource register definition (name, description, schema, annotations) response (*/list) + notification (list_changed)

So we reuse the entity taxonomy, hook dispatch, and scoping — we add a phase
whose payload is the definition. The things that make it feel distinct:

  • It fires on the response side of */list (and on unsolicited
    list_changed notifications), server → client — closer to the result.* /
    post-phase machinery than the request path.
  • It is naturally catalog-scoped (the set of capabilities), though per-entity
    rules also make sense.

Triggers

  1. tools/list / prompts/list / resources/list responses — the full
    advertised catalog (validate/filter the set).
  2. notifications/tools/list_changed (+ prompt/resource) — server-initiated
    catalog updates (re-validate; catch a definition that changed after initial
    registration).

Scope

  • Global / tag for catalog-wide policy: allow/deny-list which tools an agent
    may even see; apply a scanner to every advertised description.
  • Per-entity for specific rules: on_register: on a route keyed by name
    (delete_all must never be advertised; transfer_funds must carry a schema
    hash). Same scoping shape as invoke policy.

The composition that motivates it: enrich-at-register, enforce-at-invoke

Registration policy enriches; invoke policy enforces:

  • A register-time classifier tags a tool risk: high → invoke-time policy
    require_approval for risk: high tools. (Risk assessment done once, at
    discovery, not on every call.)
  • Register-time pins hash(inputSchema) → invoke-time verifies the live schema
    hash matches. Anti-rug-pull: a server that swaps a benign tool for a
    malicious one after approval is caught.

This two-phase enrich→enforce split is the strongest argument for a first-class
registration phase (vs. ad-hoc filtering).

Use cases

  • Allow/deny-listing which capabilities are exposed to the agent.
  • Schema hygiene — reject under-specified or dangerous parameter shapes.
  • Tool-description / prompt-injection scanning — poisoned descriptions that
    manipulate the agent (a live threat class; MCP descriptions are model-visible).
  • Risk tagging — attach metadata consumed by invoke-time policy.
  • Rug-pull detection — definition drift between register and invoke.

Custom plugins / hook family

A registration.validate (or capability.register) hook family that plugins
register for — e.g. an allowlist plugin, a JSON-Schema linter, a
description-injection scanner. A plugin's registration for this hook is its
declaration that it operates on definitions, not args (mirrors how the header/body
phase split infers a plugin's body-need from its hook — see related issue).

Result shape parallels the invoke hooks: continue (advertise as-is),
modify (filter/annotate the entry — strip a field, attach a tag), or deny
(drop the capability from the advertised set / reject the catalog).

Design questions (input wanted)

  1. Filter vs annotate vs reject — should a denied capability be dropped from
    the advertised list transparently, or should the whole */list response be
    rejected? (Dropping is friendlier to the agent; rejecting is stricter.)
  2. Where enrichment lives — a register-time tag needs to persist to invoke
    time. Session-scoped store keyed by (server, entity)? Part of the catalog
    snapshot? This is the load-bearing plumbing question.
  3. Notification handlinglist_changed is unsolicited server→client; does
    the gateway re-run registration policy and reconcile, or only validate on the
    next explicit */list?
  4. APL surface — does this get DSL verbs (e.g. deny_registration(...),
    tag(risk, high) on an on_register: block), or is it plugin-only at first?
  5. Interaction with the header/body phase split — registration is also a
    non-invoke-args phase; should both be modeled under one "policy phases"
    framework, or kept as independent lifecycle hooks?

Metadata

Metadata

Assignees

Projects

Status
In review

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions