Skip to content

fix(cmf): finish the extension bridge audit and bind the catalog to the bridge #78

Description

@terylt

Description

PR #59 wrote docs/cmf-extensions.md, which catalogs the keys each of the twelve extension slots emits. What it does not do is check that the catalog is complete, and it is not: SecurityExtension.objects and SecurityExtension.data are unbridged and appear nowhere in the document. Populating both and running extract_extensions emits 19 keys, none referencing either.

That is the failure mode this issue closes. The catalog is hand-maintained, so it drifts by the next extension, and constants.rs already flags the same risk in its own header. A test that walks a fully populated Extensions and asserts the emitted key set is what makes the document stay true, and it is also the machine-readable vocabulary the absent-key contract issue needs before it can start.

The remaining unbridged fields below are the sweep #7 never finished. #7 fixed the shape problem for roles and permissions, because a boolean per role answers "does alice have hr" but cannot answer "what roles does alice have". The same pass over the other ten slots did not happen. Each field here becomes either a bridged key or a recorded decision with a reason.

Fields to resolve

SecurityExtension.objects and .data. Unbridged and undocumented. DataPolicy carries apply_labels, allowed_actions, denied_actions and retention, which is a field built to express data policy that the policy language cannot read. Check whether praxis reads them off the typed extension first; if it does, they are host-only by design and that is the decision.

DelegationExtension.chain. Nothing from it is bridged, so delegation.depth <= 2 is expressible but "deny if any hop granted write:payroll" is not. This does not need an array value type: union sets delegation.scopes and delegation.audiences plus an any-flag delegation.from_cache cover the common intents. Largest gap on the list for a delegation-aware engine, and possibly its own issue.

MCP annotations on ToolMetadata, ResourceMetadata and PromptMetadata. Skipped as free-form, but framework.metadata.*, custom.*, claim.* and client.claim.* are all free-form maps that go through payload::walk. Three walked, three not, no stated principle. Annotations are where readOnlyHint and destructiveHint live, which is what a tool-gating rule wants.

AgentExtension.conversation.history. Skipping the transcript is right. There is no agent.conversation.history_len either, and turn-count gating is an ordinary thing to want.

WorkloadIdentity.attested_at. Deferred for "no datetime type" while request.timestamp and completion.created_at ship as plain strings. Either decision is fine, it should be one decision.

ToolMetadata.input_schema / output_schema, PromptMetadata.arguments. Plausibly out of scope, currently undocumented rather than decided.

client.teams has no flattened booleans. subject.teams writes the set and team.<name> (security.rs:115-120); client.teams writes only the set (:188). Five collections get both projections and nine get only the set, and nothing states the rule, so require(tag.pii) is silently false forever with no way for an author to tell that is by design. The recommendation is to keep the shape and write the rule down: the set is the primary form, the five flattened collections exist only because that DSL idiom predates the sets, and no more are added. Mirroring booleans across the other nine would double the key count per request to buy sugar over contains. client.teams is the one worth a second look, since the asymmetry inside a single namespace pair reads as an oversight rather than a decision.

Also in scope

capability_namespaces.rs is the crate's existing capability-to-bag-prefix table and it disagrees with the new catalog in two places. CAP_READ_LABELS maps to &[] although security.labels is written. CAP_READ_WORKLOAD maps to BAG_WORKLOAD_PREFIX ("workload."), which nothing writes and which has exactly one use in the tree, that table entry; the bridge writes caller_workload.* and this_workload.*. Whatever binds the catalog to the bridge should cover this table too, so the crate stops carrying three hand-maintained key lists that can disagree.

Acceptance criteria

  • A test walks a fully populated Extensions through extract_extensions and asserts the exact emitted key set and value types, so a new extension field cannot land unbridged and unnoticed
  • docs/cmf-extensions.md is generated from, or checked against, whatever that test asserts, rather than maintained by hand alongside it
  • Every field in "Fields to resolve" is either bridged with a key and type in the catalog, or carries a recorded decision naming the reason
  • security.objects and security.data specifically are confirmed host-only by design or bridged
  • The set-only versus set-plus-booleans rule is stated in docs/cmf-extensions.md, naming the five collections that carry booleans and the nine that do not, and saying the list is closed
  • client.teams is decided either way: flattened to client.team.<name> to match subject.teams, or recorded as set-only with the reason
  • capability_namespaces.rs agrees with the emitted key set, enforced by the same test rather than by review
  • BAG_WORKLOAD_PREFIX is removed or corrected, and this_workload.* is reachable through read_workload
  • The resulting key universe is exported in a form the Cedar schema generator and the CEL load-time check can consume

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions