Skip to content

Hardening: Enforce read-only guard for context-bundled integrations in the back-end #251

Description

@mattprintz

Summary

Integrations bundled with a context (skills, MCP servers) are meant to be read-only, but that guard is enforced only in the front-end. The back-end will execute mutation and deletion operations against context-bundled integrations whenever a request reaches it by any path other than the UI (a direct API call, a non-UI client, or a future front-end regression). This should be enforced server-side.

Background

Context-bundled integrations are namespaced under a context-<slug> corpus (SkillIntegrationProvider.from_context, MCPIntegrationProvider.from_context). The front-end keys "read-only" off exactly this: isContextProvidedIntegration() (beaker-vue/src/util/integration.ts) checks corpus.startsWith("context-"), and IntegrationsInterface.vue uses it to render a read-only viewer instead of an editor, so the UI never issues mutation requests.

The gap

The mutation path is: POST/DELETE /beaker/integrations/... (src/beaker_notebook/app/api/integrations.py) → call_in_context → the kernel action in src/beaker_notebook/lib/context.py (getattr(provider, function)(**kwargs)) → the provider's mutation method. No layer in this chain inspects the corpus prefix or any read-only flag. The providers are mutable, and their existing guards concern something else:

  • Skills guard on source_type (local/remote), not provenance. Context-bundled skills are generally local, so update_integration, remove_integration, add_resource, update_resource, and remove_resource all execute against them. remove_integration runs shutil.rmtree(skill.base_path) — deleting the skill directory shipped alongside the context.
  • MCP update_integration executes and calls server_config.update_config_file(), rewriting the context's own MCP config file. (Its resource/remove methods are NotImplementedError, so current exposure is narrower.)

A direct API call can therefore modify or delete context-bundled files on disk, bypassing the only guard that exists today.

Acceptance criteria

  • Mutation and deletion attempts against a context-bundled integration are rejected by the back-end regardless of the calling client, with a clear error surfaced to the caller.
  • The guard covers both skill and MCP providers, and both the integration- and resource-level mutation entry points.
  • A regression test exercises the back-end path directly (not just the UI) to confirm mutations on a context-<slug> integration are refused.

Design decisions (to settle during implementation)

  • Where the guard lives — centrally at the dispatch layer (call_in_context and/or the API handlers) vs. per-provider in the mutation methods. Provenance (corpus prefix) is currently the only signal distinguishing these integrations.
  • How read-only is represented — whether to model it explicitly (e.g. a flag on the integration) rather than re-deriving it from the corpus string in multiple places. Note that MutableBaseIntegrationProvider.mutable is per-provider-class, but read-only-ness here is per-integration (one skill provider serves both editable user skills and read-only context skills), so the class flag alone can't express it.

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

    Labels

    HardeningDefensive/robustness improvement to existing behavior

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions