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.
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) checkscorpus.startsWith("context-"), andIntegrationsInterface.vueuses 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 insrc/beaker_notebook/lib/context.py(getattr(provider, function)(**kwargs)) → the provider's mutation method. No layer in this chain inspects thecorpusprefix or any read-only flag. The providers are mutable, and their existing guards concern something else:source_type(local/remote), not provenance. Context-bundled skills are generallylocal, soupdate_integration,remove_integration,add_resource,update_resource, andremove_resourceall execute against them.remove_integrationrunsshutil.rmtree(skill.base_path)— deleting the skill directory shipped alongside the context.update_integrationexecutes and callsserver_config.update_config_file(), rewriting the context's own MCP config file. (Its resource/remove methods areNotImplementedError, 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
context-<slug>integration are refused.Design decisions (to settle during implementation)
call_in_contextand/or the API handlers) vs. per-provider in the mutation methods. Provenance (corpusprefix) is currently the only signal distinguishing these integrations.corpusstring in multiple places. Note thatMutableBaseIntegrationProvider.mutableis 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.