From 5e14a642be895247d5843a8ed674518980ef8104 Mon Sep 17 00:00:00 2001 From: Alexander Rymdeko-Harvey <8761706+killswitch-GUI@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:52:45 -0400 Subject: [PATCH 01/11] feat(plugins): install trusted OCI plugins and enable MCP connections --- apps/desktop/playwright.config.ts | 8 +- apps/desktop/src/App.tsx | 6 +- .../src/components/ManagedSettingsPane.tsx | 2 +- .../src/components/OperationsSurface.tsx | 5 +- apps/desktop/src/components/PluginDetail.tsx | 25 ++- .../src/components/PluginMcpControls.test.ts | 11 +- .../src/components/PluginMcpControls.tsx | 82 ++++++-- .../src/components/PluginOperationForm.tsx | 53 +++-- .../desktop/src/components/PluginsSurface.tsx | 7 + apps/desktop/src/dev/plugin-studio.tsx | 1 + apps/desktop/src/pluginConnection.test.ts | 60 ++++++ apps/desktop/src/pluginConnection.ts | 56 ++++++ apps/desktop/tests/browser/plugins.spec.ts | 63 ++++++ .../src/bin/colossus-mcp-test-server.rs | 12 ++ crates/colossus-cli/src/commands.rs | 1 + crates/colossus-cli/src/extension_args.rs | 13 +- crates/colossus-cli/src/plugin_requests.rs | 20 +- crates/colossus-cli/src/tests.rs | 42 ++++ crates/colossus-cli/tests/plugin_mcp_smoke.rs | 183 ++++++++++++++++++ crates/colossus-mcp/src/config.rs | 21 +- crates/colossus-mcp/src/tests.rs | 84 ++++++++ crates/colossus-plugins/src/registry.rs | 73 ++++++- .../src/registry_acceptance.rs | 89 +++++++++ crates/colossus-runtime/src/config.rs | 23 ++- crates/colossus-runtime/src/plugin_catalog.rs | 30 +++ .../src/plugin_catalog_tests.rs | 92 +++++++++ .../colossus-runtime/src/plugin_extensions.rs | 36 ++-- .../colossus-runtime/src/plugin_inventory.rs | 16 +- .../colossus-runtime/src/plugin_management.rs | 23 +-- .../src/plugin_registry_tests.rs | 36 ++++ crates/colossus-runtime/src/services.rs | 41 ++-- docs/extend/plugins.md | 36 +++- docs/reference/configuration.md | 22 ++- 33 files changed, 1155 insertions(+), 117 deletions(-) create mode 100644 apps/desktop/src/pluginConnection.test.ts create mode 100644 apps/desktop/src/pluginConnection.ts create mode 100644 crates/colossus-cli/tests/plugin_mcp_smoke.rs diff --git a/apps/desktop/playwright.config.ts b/apps/desktop/playwright.config.ts index 1348dd94d..ca614c88b 100644 --- a/apps/desktop/playwright.config.ts +++ b/apps/desktop/playwright.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from "@playwright/test"; +const port = Number(process.env.COLOSSUS_PLAYWRIGHT_PORT ?? "1421"); + export default defineConfig({ testDir: "./tests/browser", fullyParallel: false, @@ -8,14 +10,14 @@ export default defineConfig({ workers: 1, reporter: process.env.CI === "true" ? "github" : "line", use: { - baseURL: "http://127.0.0.1:1421", + baseURL: `http://127.0.0.1:${port}`, screenshot: "only-on-failure", trace: "retain-on-failure", viewport: { width: 880, height: 640 }, }, webServer: { - command: "npm run dev -- --port 1421", - url: "http://127.0.0.1:1421/?fixture=operations-studio", + command: `npm run dev -- --port ${port}`, + url: `http://127.0.0.1:${port}/?fixture=operations-studio`, reuseExistingServer: process.env.CI !== "true", timeout: 120_000, }, diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index cfba968e0..1e7a2c374 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -990,7 +990,7 @@ export default function App() { const [showOnboarding, setShowOnboarding] = useState(false); const [surface, setSurface] = useState("work"); const [settingsStartTab, setSettingsStartTab] = useState< - "runtime" | "providers" + "runtime" | "providers" | "plugins" >("runtime"); const [workNavigationOpen, setWorkNavigationOpen] = useState(false); const [workspaceFileOpenRequest, setWorkspaceFileOpenRequest] = @@ -5423,6 +5423,10 @@ export default function App() { ) : ( { + setSettingsStartTab("plugins"); + setSurface("settings"); + }} onReturnToWork={() => selectSurface("work")} pluginSelections={pluginSelections} onUsePluginSkill={(id) => { diff --git a/apps/desktop/src/components/ManagedSettingsPane.tsx b/apps/desktop/src/components/ManagedSettingsPane.tsx index 8ff4dc11f..57830468a 100644 --- a/apps/desktop/src/components/ManagedSettingsPane.tsx +++ b/apps/desktop/src/components/ManagedSettingsPane.tsx @@ -214,7 +214,7 @@ type SpaceTab = | "effective"; interface ManagedSettingsPaneProps { - initialSpaceTab?: "runtime" | "providers" | undefined; + initialSpaceTab?: "runtime" | "providers" | "plugins" | undefined; desktop: DesktopStatus; connecting: boolean; updateChecking: boolean; diff --git a/apps/desktop/src/components/OperationsSurface.tsx b/apps/desktop/src/components/OperationsSurface.tsx index 1437ad55b..ccedca217 100644 --- a/apps/desktop/src/components/OperationsSurface.tsx +++ b/apps/desktop/src/components/OperationsSurface.tsx @@ -32,7 +32,8 @@ import { PluginsSurface } from "./PluginsSurface"; import type { WorkspaceSurface } from "./ProductRail"; interface OperationsSurfaceProps { - initialSettingsTab?: "runtime" | "providers" | undefined; + initialSettingsTab?: "runtime" | "providers" | "plugins" | undefined; + onConfigurePluginConnection?: () => void; pluginSelections?: readonly string[]; onUsePluginSkill?: (id: string) => void; surface: Exclude; @@ -1016,6 +1017,8 @@ export function OperationsSurface(props: OperationsSurfaceProps) { void) | undefined; + onConnectionChanged?: (() => void) | undefined; managementAvailable: boolean; selections: readonly string[]; onUseSkill: ((id: string) => void) | undefined; @@ -127,11 +133,13 @@ export function PluginDetail({ {server.id} · {server.transport} ·{" "} {server.status}

- {managementAvailable && ( + {managementAvailable && spaceId && ( )) )} + {managementAvailable && + plugin.mcp_servers.length > 0 && + onConfigureConnection && ( + + )}

Enable individual MCP servers explicitly in plugin settings. Credential configuration does not enable them. diff --git a/apps/desktop/src/components/PluginMcpControls.test.ts b/apps/desktop/src/components/PluginMcpControls.test.ts index 1767ec4ca..9b8e47b13 100644 --- a/apps/desktop/src/components/PluginMcpControls.test.ts +++ b/apps/desktop/src/components/PluginMcpControls.test.ts @@ -4,25 +4,28 @@ import { describe, expect, it } from "vitest"; import { PluginMcpControls } from "./PluginMcpControls"; describe("plugin MCP controls", () => { - it("keeps all connection actions disabled until runtime enablement", () => { + it("offers explicit enablement while keeping diagnostics disabled", () => { const markup = renderToStaticMarkup( createElement(PluginMcpControls, { - targetId: "workspace", + spaceId: "workspace", server: "example/docs", enabled: false, + pluginActive: true, http: true, }), ); - expect(markup).toContain("Enable and apply this server explicitly"); + expect(markup).toContain("Enable all plugin tools"); + expect(markup).toContain("including tools"); expect(markup.match(/disabled=""/g)).toHaveLength(2); expect(markup).not.toContain("Sign in"); }); it("does not offer HTTP OAuth controls for a stdio server", () => { const markup = renderToStaticMarkup( createElement(PluginMcpControls, { - targetId: "workspace", + spaceId: "workspace", server: "example/local", enabled: true, + pluginActive: true, http: false, }), ); diff --git a/apps/desktop/src/components/PluginMcpControls.tsx b/apps/desktop/src/components/PluginMcpControls.tsx index 1a3b24bc9..8332d10a6 100644 --- a/apps/desktop/src/components/PluginMcpControls.tsx +++ b/apps/desktop/src/components/PluginMcpControls.tsx @@ -5,22 +5,28 @@ import { beginManagedMcpOAuth, completeManagedMcpOAuth, diagnoseManagedMcpServer, + getManagedConfiguration, logoutManagedMcpOAuth, managedMcpOAuthStatus, + saveSpaceConfiguration, } from "../api"; import type { ManagedMcpOAuthLogin, ManagedMcpOAuthStatus } from "../types"; +import { pluginConnectionRequest } from "../pluginConnection"; -/** Explicit diagnostics and OAuth only; this component cannot enable a server. */ export function PluginMcpControls({ - targetId, + spaceId, server, enabled, + pluginActive, http, + onChanged, }: { - targetId: string; + spaceId: string; server: string; enabled: boolean; + pluginActive: boolean; http: boolean; + onChanged?: (() => void) | undefined; }) { const [busy, setBusy] = useState(false); const [error, setError] = useState(""); @@ -32,7 +38,7 @@ export function PluginMcpControls({ const [login, setLogin] = useState(null); const [callback, setCallback] = useState(""); async function run(operation: () => Promise) { - if (busy || !enabled) return; + if (busy || !enabled || !pluginActive) return; setBusy(true); setError(""); try { @@ -47,17 +53,51 @@ export function PluginMcpControls({ setBusy(false); } } + async function setEnabled(next: boolean) { + if (busy) return; + setBusy(true); + setError(""); + setMessage(""); + try { + const snapshot = await getManagedConfiguration(); + const request = pluginConnectionRequest(snapshot, spaceId, server, next); + await saveSpaceConfiguration(request); + setDiagnostic(null); + setStatus(null); + setMessage( + next + ? "Connection enabled. Test it after the runtime restarts." + : "Connection disabled for this workspace.", + ); + onChanged?.(); + } catch (error) { + setError( + error instanceof Error + ? error.message + : "Could not update this plugin connection.", + ); + } finally { + setBusy(false); + } + } return (

+ )}
- {!enabled && ( + {!pluginActive ? ( - Enable and apply this server explicitly in plugin settings first. + Activate this plugin digest before configuring its connection. - )} - {busy &&

Checking MCP connection…

} + ) : !enabled ? ( + + A new connection permits every tool from this plugin, including tools + added by a later update. Configure an exact tool list in plugin + settings for narrower access. + + ) : null} + {busy &&

Updating or checking MCP connection…

} {diagnostic ? ( ) : ( @@ -107,14 +153,14 @@ export function PluginMcpControls({ {status?.configured && ( + + {status.enabled && ( +

+ {status.activeDigest + ? `Helper running from verified digest ${status.activeDigest}.` + : "Enabled; start this Workspace to connect."} +

+ )} + {check &&

{check}

} + {error &&

{error}

} + + ); +} diff --git a/apps/desktop/src/components/PluginDetail.tsx b/apps/desktop/src/components/PluginDetail.tsx index f58bdcc02..645c4e49b 100644 --- a/apps/desktop/src/components/PluginDetail.tsx +++ b/apps/desktop/src/components/PluginDetail.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { readPluginPreview } from "../api"; import type { PluginEntry, PluginResource, PluginSkill } from "../plugins"; import { PluginMcpControls } from "./PluginMcpControls"; +import { OutlookCompanionControls } from "./OutlookCompanionControls"; import { PluginIcon } from "./PluginIcon"; import type { PluginAction } from "./PluginOperationForm"; @@ -139,6 +140,10 @@ export function PluginDetail({ server={server.id} enabled={server.enabled} pluginActive={plugin.available} + sessionRequired={ + plugin.manifest.name === "outlook-classic" && + server.id === "outlook-classic/mail" + } onChanged={onConnectionChanged} http={ server.transport === "http" || @@ -150,6 +155,15 @@ export function PluginDetail({
)) )} + {managementAvailable && + spaceId && + plugin.manifest.name === "outlook-classic" && ( + + )} {managementAvailable && plugin.mcp_servers.length > 0 && onConfigureConnection && ( diff --git a/apps/desktop/src/components/PluginMcpControls.tsx b/apps/desktop/src/components/PluginMcpControls.tsx index 8332d10a6..135af6b34 100644 --- a/apps/desktop/src/components/PluginMcpControls.tsx +++ b/apps/desktop/src/components/PluginMcpControls.tsx @@ -19,6 +19,7 @@ export function PluginMcpControls({ enabled, pluginActive, http, + sessionRequired = false, onChanged, }: { spaceId: string; @@ -26,6 +27,7 @@ export function PluginMcpControls({ enabled: boolean; pluginActive: boolean; http: boolean; + sessionRequired?: boolean; onChanged?: (() => void) | undefined; }) { const [busy, setBusy] = useState(false); @@ -85,7 +87,7 @@ export function PluginMcpControls({
{status.enabled && (

diff --git a/apps/desktop/src/pluginConnection.test.ts b/apps/desktop/src/pluginConnection.test.ts index d9f55ae96..aa5fd297b 100644 --- a/apps/desktop/src/pluginConnection.test.ts +++ b/apps/desktop/src/pluginConnection.test.ts @@ -51,10 +51,46 @@ describe("plugin connection save", () => { { fieldId: "plugins.mcpServers", value: { + $colossusPatchV1: true, "example/mail": { enabled: true, allowedTools: ["*"] }, - "other/docs": { enabled: true, allowedTools: ["search"] }, }, }, ]); }); + + it("keeps an existing whole-map workspace override as a whole-map override", () => { + const snapshot = { + globalConfiguration: { revision: 17 }, + spaces: [ + { + id: "workspace", + archived: false, + configuration: { + fieldOverrides: [ + { + fieldId: "plugins.mcpServers", + value: { "local/docs": { enabled: true } }, + }, + ], + catalogRevisions: {}, + searchRoles: {}, + modelRoles: {}, + credentialOverrides: {}, + }, + }, + ], + } as unknown as ManagedSettingsSnapshot; + const request = pluginConnectionRequest( + snapshot, + "workspace", + "local/docs", + false, + ); + expect(request.fieldOverrides).toEqual([ + { + fieldId: "plugins.mcpServers", + value: { "local/docs": { enabled: false, allowedTools: [] } }, + }, + ]); + }); }); diff --git a/apps/desktop/src/pluginConnection.ts b/apps/desktop/src/pluginConnection.ts index 6f4dc9347..76e7bcdf1 100644 --- a/apps/desktop/src/pluginConnection.ts +++ b/apps/desktop/src/pluginConnection.ts @@ -18,10 +18,12 @@ export function pluginConnectionRequest( const space = snapshot.spaces.find((candidate) => candidate.id === spaceId); if (!space || space.archived) throw new Error("The selected workspace is no longer available."); - const current = space.effectiveValues.find( + const current = space.configuration.fieldOverrides.find( (entry) => entry.fieldId === "plugins.mcpServers", ); - const servers = { ...record(current?.value ?? {}) }; + const servers = { + ...record(current?.value ?? { $colossusPatchV1: true }), + }; const existing = record(servers[serverId] ?? {}); servers[serverId] = { ...existing, diff --git a/docs/desktop/settings.md b/docs/desktop/settings.md index 5d6325cd5..4056612c0 100644 --- a/docs/desktop/settings.md +++ b/docs/desktop/settings.md @@ -62,7 +62,7 @@ For a command requiring approval, **Review command…** opens a dedicated window ## Connections, search, and telemetry -Define reusable standalone MCP servers under **Global → MCP**, then enable and test the selected servers under **Workspace → MCP**. Plugin-provided servers are managed through **Plugins** settings. A health check reports bounded connection diagnostics from the authenticated workspace runtime. If a server works in the CLI but not Desktop, compare its effective endpoint, credential bindings, CA trust, and runtime permissions; Desktop and CLI can use different configuration and state. [MCP guide](../use/mcp-servers.md) +Define reusable standalone MCP servers under **Global → MCP**, then enable and test the selected servers under **Workspace → MCP**. Plugin-provided servers are managed through **Plugins** settings. A new per-workspace plugin connection change overrides only that server; other plugin servers continue to inherit their accepted Global settings. Existing Workspace overrides that replaced the entire plugin server map keep that behavior. A health check reports bounded connection diagnostics from the authenticated workspace runtime. If a server works in the CLI but not Desktop, compare its effective endpoint, credential bindings, CA trust, and runtime permissions; Desktop and CLI can use different configuration and state. [MCP guide](../use/mcp-servers.md) **Global → Search** stores search services; **Workspace → Search** chooses routing for this workspace. **Workspace → Research** contains its research behavior. **Global → Telemetry** defines connections, while **Workspace → Telemetry** controls what this workspace exports. Saved destinations do not imply successful delivery; use their available health tests and diagnostics. diff --git a/docs/extend/plugins.md b/docs/extend/plugins.md index 28eb2758d..77a534910 100644 --- a/docs/extend/plugins.md +++ b/docs/extend/plugins.md @@ -183,6 +183,10 @@ digest. A CLI activation change revokes a running helper within 15 seconds. The published alpha.3 package does not contain this companion and the switch rejects it until a signed companion-capable release is installed. +**Discover tools** checks the authenticated MCP transport and allowlist. Outlook COM +attachment is checked when an authorized Outlook tool runs; discovery alone does not +prove that classic Outlook is open. + ```bash colossus plugins validate ./example-plugin colossus plugins package ./example-plugin --output ./example-plugin.oci From 696190be0b9e15f757b76f9845586f5be245a1cd Mon Sep 17 00:00:00 2001 From: Alexander Rymdeko-Harvey <8761706+killswitch-GUI@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:20:13 -0400 Subject: [PATCH 04/11] docs(plugins): point OCI setup to signed Outlook alpha.4 --- docs/extend/plugins.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/extend/plugins.md b/docs/extend/plugins.md index 77a534910..22ee730e2 100644 --- a/docs/extend/plugins.md +++ b/docs/extend/plugins.md @@ -157,19 +157,15 @@ On a fresh configuration, the built-in GHCR profile and its required Sigstore si identity are ready for a published Obscurity Labs plugin: ```bash -colossus plugin install oci://ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:0.1.0-alpha.3.ci.3.1-windows-amd64 +colossus plugin install oci://ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:0.1.0-alpha.4.ci.7.1-windows-amd64 colossus plugins list -colossus plugins enable outlook-classic --digest sha256:6ca3c7a1e753e39c85b1345feda29cac368137a67918b03fed31a8300059bced +colossus plugins enable outlook-classic --digest sha256:e95973e1eb55062d92d8c8810788d142cf62ac94bc7f0b3d5bf49f70e9b5342a ``` The install resolves the tag once and records the verified manifest digest. Check the reported digest before activation. A different registry needs a configured exact-origin profile; pass `--registry NAME` when more than one profile matches its origin. -The Outlook Classic alpha.3 server uses legacy COM and cannot attach to Outlook from -the `windows_job` AppContainer. Installation and activation do not make its mail tools -usable there. A signed package with the user-session companion is required. - -When a newer signed Outlook Classic package includes companion support, select its +The Outlook Classic alpha.4 release includes the user-session companion. Select its active digest in Desktop **Plugins**, then choose **Connect Outlook session** in its details. This per-Workspace switch starts the verified executable as the logged-in Windows user and passes a fresh bearer token to Managed Local through its protected @@ -177,11 +173,11 @@ bootstrap. The runtime connects to one loopback MCP endpoint; the ordinary `windows_job` boundary remains in place for agent and plugin subprocesses. The connection exposes the package's 14 current tool names and keeps ordinary MCP policy, approval, and audit. **Disconnect Outlook session** stops the helper and rotates the -token on the next connection. Updating, disabling, or uninstalling the plugin through -Desktop revokes running Outlook helpers; restart a Workspace to use a newly activated -digest. A CLI activation change revokes a running helper within 15 seconds. The -published alpha.3 package does not contain this companion and the switch -rejects it until a signed companion-capable release is installed. +token on the next connection. Disabling or removing the active plugin through Desktop +revokes running Outlook helpers; downloading an update leaves the current session in +place until a different digest is activated. Restart a Workspace to use a newly +activated digest. A CLI activation change revokes a running helper within 15 seconds. +The earlier alpha.3 package does not contain this companion. **Discover tools** checks the authenticated MCP transport and allowlist. Outlook COM attachment is checked when an authorized Outlook tool runs; discovery alone does not From 540d8ce99e2450b24554acab5d515713e221c5f9 Mon Sep 17 00:00:00 2001 From: Alexander Rymdeko-Harvey <8761706+killswitch-GUI@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:48:25 -0400 Subject: [PATCH 05/11] test: expose plugin MCP smoke registration diagnostics --- crates/colossus-cli/tests/plugin_mcp_smoke.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/colossus-cli/tests/plugin_mcp_smoke.rs b/crates/colossus-cli/tests/plugin_mcp_smoke.rs index 9a3258d63..7f72bab07 100644 --- a/crates/colossus-cli/tests/plugin_mcp_smoke.rs +++ b/crates/colossus-cli/tests/plugin_mcp_smoke.rs @@ -42,6 +42,9 @@ fn installed_plugin_stdio_discovers_and_calls_with_host_bound_paths() { store .enable("fixture-plugin", &installed.digest, true, actor) .expect("enable fixture"); + let snapshot = store.snapshot(&[], &[]).expect("installed plugin snapshot"); + assert_eq!(snapshot.len(), 1, "{snapshot:?}"); + assert_eq!(snapshot[0].mcp_servers.len(), 1, "{snapshot:?}"); let data = store.data_path("fixture-plugin").expect("plugin data"); let workflows = workspace.join("workflows"); @@ -140,10 +143,14 @@ sandbox: "--server", "fixture-plugin/mail", ]); + let inventory = run(&["plugins", "list"]); + let servers = run(&["mcp", "servers"]); assert!( tools.status.success(), - "{}", - String::from_utf8_lossy(&tools.stderr) + "tools stderr: {}\nplugin inventory: {}\nMCP servers: {}", + String::from_utf8_lossy(&tools.stderr), + String::from_utf8_lossy(&inventory.stdout), + String::from_utf8_lossy(&servers.stdout), ); let discovered: Value = serde_json::from_slice(&tools.stdout).expect("tool discovery"); let names = discovered From 193b142391b45cf15faf26e3a81a3a4c87c24541 Mon Sep 17 00:00:00 2001 From: Alexander Rymdeko-Harvey <8761706+killswitch-GUI@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:10:16 -0400 Subject: [PATCH 06/11] fix: verify normalized Unix plugin file modes --- crates/colossus-cli/tests/plugin_mcp_smoke.rs | 8 ++------ crates/colossus-plugins/src/store.rs | 9 ++++++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/colossus-cli/tests/plugin_mcp_smoke.rs b/crates/colossus-cli/tests/plugin_mcp_smoke.rs index 7f72bab07..a16be66f6 100644 --- a/crates/colossus-cli/tests/plugin_mcp_smoke.rs +++ b/crates/colossus-cli/tests/plugin_mcp_smoke.rs @@ -143,14 +143,10 @@ sandbox: "--server", "fixture-plugin/mail", ]); - let inventory = run(&["plugins", "list"]); - let servers = run(&["mcp", "servers"]); assert!( tools.status.success(), - "tools stderr: {}\nplugin inventory: {}\nMCP servers: {}", - String::from_utf8_lossy(&tools.stderr), - String::from_utf8_lossy(&inventory.stdout), - String::from_utf8_lossy(&servers.stdout), + "{}", + String::from_utf8_lossy(&tools.stderr) ); let discovered: Value = serde_json::from_slice(&tools.stdout).expect("tool discovery"); let names = discovered diff --git a/crates/colossus-plugins/src/store.rs b/crates/colossus-plugins/src/store.rs index 655097f83..64352ab98 100644 --- a/crates/colossus-plugins/src/store.rs +++ b/crates/colossus-plugins/src/store.rs @@ -49,7 +49,14 @@ fn verify_content_trees(expected: &Path, actual: &Path) -> Result<(), StoreError .map_err(adapter)? .permissions() .mode(); - if expected_mode & 0o111 != actual_mode & 0o111 || actual_mode & 0o222 != 0 { + // Published content is owner-only (0500/0400), while the extracted + // artifact carries archive modes (0755/0644). + let published_mode = if expected_mode & 0o111 != 0 { + 0o500 + } else { + 0o400 + }; + if actual_mode & 0o777 != published_mode { return Err(mismatch()); } } From 3d66c1d9c74fc7f66159df0a12cc5b14b5310b30 Mon Sep 17 00:00:00 2001 From: Alexander Rymdeko-Harvey <8761706+killswitch-GUI@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:12:24 -0400 Subject: [PATCH 07/11] fix: reject special mode bits on cached plugins --- crates/colossus-plugins/src/store.rs | 2 +- .../colossus-plugins/src/store/bundled_tests.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/colossus-plugins/src/store.rs b/crates/colossus-plugins/src/store.rs index 64352ab98..68d627298 100644 --- a/crates/colossus-plugins/src/store.rs +++ b/crates/colossus-plugins/src/store.rs @@ -56,7 +56,7 @@ fn verify_content_trees(expected: &Path, actual: &Path) -> Result<(), StoreError } else { 0o400 }; - if actual_mode & 0o777 != published_mode { + if actual_mode & 0o7777 != published_mode { return Err(mismatch()); } } diff --git a/crates/colossus-plugins/src/store/bundled_tests.rs b/crates/colossus-plugins/src/store/bundled_tests.rs index 76755579b..ae8f89bf8 100644 --- a/crates/colossus-plugins/src/store/bundled_tests.rs +++ b/crates/colossus-plugins/src/store/bundled_tests.rs @@ -211,3 +211,19 @@ fn corrupt_content_is_never_reused_or_overwritten_even_when_leased() { assert!(store.snapshot(&[], &[]).is_err()); assert_eq!(fs::read(&path).expect("not overwritten"), b"corrupted"); } + +#[cfg(unix)] +#[test] +fn cached_plugin_special_mode_bits_are_rejected() { + use std::os::unix::fs::PermissionsExt as _; + + let home = tempfile::tempdir().expect("home"); + let store = PluginStore::new(home.path()).expect("store"); + let core = store + .bootstrap_bundled(artifact("1.0.0"), actor()) + .expect("bootstrap"); + let path = Path::new(&core.root).join("skills/test/assets/binary.dat"); + fs::set_permissions(&path, fs::Permissions::from_mode(0o4400)) + .expect("set unexpected setuid bit"); + assert!(store.snapshot(&[], &[]).is_err()); +} From b2fee35367cbe75f4a10948e7586593efa999cbd Mon Sep 17 00:00:00 2001 From: Alexander Rymdeko-Harvey <8761706+killswitch-GUI@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:43:18 -0400 Subject: [PATCH 08/11] fix(desktop): gate Outlook companion code by platform --- apps/desktop/src-tauri/src/managed_runtime.rs | 43 +++++++++++-------- apps/desktop/src-tauri/src/plugin_commands.rs | 13 +++--- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src-tauri/src/managed_runtime.rs b/apps/desktop/src-tauri/src/managed_runtime.rs index 3a8886668..b8c429a85 100644 --- a/apps/desktop/src-tauri/src/managed_runtime.rs +++ b/apps/desktop/src-tauri/src/managed_runtime.rs @@ -777,35 +777,42 @@ struct CompanionBootstrap { credential: HostSecret, } +#[cfg(windows)] async fn start_outlook_companion( state: &AppState, settings: &DesktopSettings, space_id: &str, colossus_home: &Path, ) -> Result, (CommandErrorDto, RuntimeFailureCodeDto)> { - #[cfg(windows)] + if settings + .space(space_id) + .is_some_and(|space| space.outlook_companion_enabled) { - if settings - .space(space_id) - .is_some_and(|space| space.outlook_companion_enabled) - { - let (process, credential) = - crate::outlook_companion::OutlookCompanion::start(colossus_home) - .await - .map_err(|error| (error, RuntimeFailureCodeDto::Configuration))?; - let registration = CompanionBootstrap { - endpoint: process.endpoint.clone(), - credential, - }; - state.install_outlook_companion_for(space_id, process).await; - return Ok(Some(registration)); - } + let (process, credential) = crate::outlook_companion::OutlookCompanion::start(colossus_home) + .await + .map_err(|error| (error, RuntimeFailureCodeDto::Configuration))?; + let registration = CompanionBootstrap { + endpoint: process.endpoint.clone(), + credential, + }; + state.install_outlook_companion_for(space_id, process).await; + return Ok(Some(registration)); } - #[cfg(not(windows))] - let _ = (state, settings, space_id, colossus_home); Ok(None) } +#[cfg(not(windows))] +fn start_outlook_companion( + state: &AppState, + settings: &DesktopSettings, + space_id: &str, + colossus_home: &Path, +) -> std::future::Ready, (CommandErrorDto, RuntimeFailureCodeDto)>> +{ + let _ = (state, settings, space_id, colossus_home); + std::future::ready(Ok(None)) +} + async fn prepare_managed_bootstrap( workspace: &Path, workspace_identity: WorkspaceIdentity, diff --git a/apps/desktop/src-tauri/src/plugin_commands.rs b/apps/desktop/src-tauri/src/plugin_commands.rs index 0a165f064..774e06045 100644 --- a/apps/desktop/src-tauri/src/plugin_commands.rs +++ b/apps/desktop/src-tauri/src/plugin_commands.rs @@ -11,13 +11,16 @@ use crate::managed_configuration::FieldOverrideSetting; use crate::plugin_adapter::{self, PluginInventoryDto, PluginPreviewInput, PluginPreviewKind}; use crate::{ commands::{target, unary_slot}, - desktop_commands::{connect_guard, settings_store}, + desktop_commands::settings_store, dto::CommandErrorDto, - managed_configuration_commands::persist_and_restart, managed_diagnostics::worker_for, - managed_runtime, state::{AppState, TargetConsentContext}, }; +#[cfg(windows)] +use crate::{ + desktop_commands::connect_guard, managed_configuration_commands::persist_and_restart, + managed_runtime, +}; #[derive(Serialize)] #[serde(rename_all = "camelCase")] @@ -59,10 +62,10 @@ pub(crate) async fn configure_outlook_companion( #[cfg(not(windows))] { let _ = (state, space_id, enabled); - return Err(CommandErrorDto::invalid( + Err(CommandErrorDto::invalid( "spaceId", "Classic Outlook integration requires Windows Desktop.", - )); + )) } #[cfg(windows)] { From 5dae956c31304f4165bb2d03c1bd722cd2bff577 Mon Sep 17 00:00:00 2001 From: Alexander Rymdeko-Harvey <8761706+killswitch-GUI@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:49:13 -0400 Subject: [PATCH 09/11] style(desktop): format Outlook companion platform gate --- apps/desktop/src-tauri/src/managed_runtime.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src-tauri/src/managed_runtime.rs b/apps/desktop/src-tauri/src/managed_runtime.rs index b8c429a85..a87b38c9c 100644 --- a/apps/desktop/src-tauri/src/managed_runtime.rs +++ b/apps/desktop/src-tauri/src/managed_runtime.rs @@ -788,9 +788,10 @@ async fn start_outlook_companion( .space(space_id) .is_some_and(|space| space.outlook_companion_enabled) { - let (process, credential) = crate::outlook_companion::OutlookCompanion::start(colossus_home) - .await - .map_err(|error| (error, RuntimeFailureCodeDto::Configuration))?; + let (process, credential) = + crate::outlook_companion::OutlookCompanion::start(colossus_home) + .await + .map_err(|error| (error, RuntimeFailureCodeDto::Configuration))?; let registration = CompanionBootstrap { endpoint: process.endpoint.clone(), credential, From 5ecc46870bd69e409c603d943360429ee4b3b0f3 Mon Sep 17 00:00:00 2001 From: Alexander Rymdeko-Harvey <8761706+killswitch-GUI@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:39:38 -0400 Subject: [PATCH 10/11] fix(plugins): preserve built-in trust with custom profiles --- crates/colossus-runtime/src/composition.rs | 1 + crates/colossus-runtime/src/config.rs | 87 ++++++++++++++---- .../src/plugin_registry_tests.rs | 92 +++++++++++++++++++ docs/reference/configuration/extensions.md | 6 ++ 4 files changed, 169 insertions(+), 17 deletions(-) diff --git a/crates/colossus-runtime/src/composition.rs b/crates/colossus-runtime/src/composition.rs index b98ea553d..7ef0411d0 100644 --- a/crates/colossus-runtime/src/composition.rs +++ b/crates/colossus-runtime/src/composition.rs @@ -225,6 +225,7 @@ impl Runtime { codex_auth: Option, ) -> Result { validate_storage_config(&config.storage)?; + validate_builtin_plugin_trust_identity(&config.plugins)?; let storage_adapter = match config.storage.adapter { StorageAdapter::Redb => "redb", StorageAdapter::Ephemeral => "ephemeral", diff --git a/crates/colossus-runtime/src/config.rs b/crates/colossus-runtime/src/config.rs index c83981512..75a650a60 100644 --- a/crates/colossus-runtime/src/config.rs +++ b/crates/colossus-runtime/src/config.rs @@ -354,6 +354,7 @@ pub struct PluginsConfig { /// Exact denylist applied after `include`. pub exclude: Vec, /// Reusable supply-chain trust policies. + #[serde(deserialize_with = "deserialize_plugin_trust_profiles")] pub trust_profiles: BTreeMap, /// Exact-origin OCI registry profiles. pub registries: BTreeMap, @@ -367,28 +368,79 @@ impl Default for PluginsConfig { enabled: true, include: Vec::new(), exclude: Vec::new(), - trust_profiles: BTreeMap::from([ - ("default".into(), PluginTrustProfile::default()), - ("obscuritylabs".into(), PluginTrustProfile { - identities: vec![colossus_plugins::SigstoreIdentity { - issuer: "https://token.actions.githubusercontent.com".into(), - subject: "https://github.com/obscuritylabs/colossus-plugins/.github/workflows/plugins.yml@refs/heads/main".into(), - }], - ..PluginTrustProfile::default() - }), - ]), - registries: BTreeMap::from([("obscuritylabs".into(), PluginRegistryProfile { - origin: "https://ghcr.io".into(), - trust_profile: "obscuritylabs".into(), - token_origins: vec!["https://ghcr.io".into()], - blob_redirect_origins: vec!["https://pkg-containers.githubusercontent.com".into()], - ..PluginRegistryProfile::default() - })]), + trust_profiles: default_plugin_trust_profiles(), + registries: default_plugin_registries(), mcp_servers: BTreeMap::new(), } } } +fn default_plugin_trust_profiles() -> BTreeMap { + BTreeMap::from([ + ("default".into(), PluginTrustProfile::default()), + ( + "obscuritylabs".into(), + PluginTrustProfile { + identities: vec![colossus_plugins::SigstoreIdentity { + issuer: "https://token.actions.githubusercontent.com".into(), + subject: "https://github.com/obscuritylabs/colossus-plugins/.github/workflows/plugins.yml@refs/heads/main".into(), + }], + ..PluginTrustProfile::default() + }, + ), + ]) +} + +fn default_plugin_registries() -> BTreeMap { + BTreeMap::from([( + "obscuritylabs".into(), + PluginRegistryProfile { + origin: "https://ghcr.io".into(), + trust_profile: "obscuritylabs".into(), + token_origins: vec!["https://ghcr.io".into()], + blob_redirect_origins: vec!["https://pkg-containers.githubusercontent.com".into()], + ..PluginRegistryProfile::default() + }, + )]) +} + +fn deserialize_plugin_trust_profiles<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let mut profiles = default_plugin_trust_profiles(); + let configured = BTreeMap::::deserialize(deserializer)?; + if configured + .get("obscuritylabs") + .is_some_and(|profile| profiles.get("obscuritylabs") != Some(profile)) + { + return Err(serde::de::Error::custom( + "plugins.trustProfiles.obscuritylabs is a reserved built-in signing identity", + )); + } + profiles.extend(configured); + Ok(profiles) +} + +pub(super) fn validate_builtin_plugin_trust_identity( + config: &PluginsConfig, +) -> Result<(), RuntimeError> { + if config + .trust_profiles + .get("obscuritylabs") + .is_some_and(|profile| { + default_plugin_trust_profiles().get("obscuritylabs") != Some(profile) + }) + { + return Err(RuntimeError::Config( + "plugins.trustProfiles.obscuritylabs is a reserved built-in signing identity".into(), + )); + } + Ok(()) +} + /// Workspace authority overlay for one canonical `/` MCP identity. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] @@ -1599,6 +1651,7 @@ impl RuntimeConfig { } fn validate_plugins_config(config: &PluginsConfig) -> Result<(), RuntimeError> { + validate_builtin_plugin_trust_identity(config)?; fn valid_name(value: &str) -> bool { !value.is_empty() && value.len() <= 128 diff --git a/crates/colossus-runtime/src/plugin_registry_tests.rs b/crates/colossus-runtime/src/plugin_registry_tests.rs index e7f516544..c390bcba2 100644 --- a/crates/colossus-runtime/src/plugin_registry_tests.rs +++ b/crates/colossus-runtime/src/plugin_registry_tests.rs @@ -43,6 +43,98 @@ fn default_oci_profile_pins_obscurity_labs_signing_identity() { ); } +#[test] +fn custom_plugin_profiles_retain_builtin_oci_trust() { + let plugins: PluginsConfig = serde_json::from_value(json!({ + "trustProfiles": { + "offline": { "mode": "optional" } + } + })) + .expect("custom plugin profiles"); + assert!(plugins.trust_profiles.contains_key("default")); + assert!(plugins.trust_profiles.contains_key("obscuritylabs")); + assert!(plugins.trust_profiles.contains_key("offline")); + assert!(plugins.registries.contains_key("obscuritylabs")); + let temporary = crate::test_support::private_tempdir(); + let runtime = open(temporary.path(), plugins); + assert_eq!( + runtime + .plugin_registry_for_reference( + "ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:v1" + ) + .expect("built-in registry remains usable"), + "obscuritylabs" + ); +} + +#[test] +fn explicit_registry_map_can_exclude_builtin_oci_registry() { + let plugins: PluginsConfig = serde_json::from_value(json!({ + "trustProfiles": { + "offline": { "mode": "optional" } + }, + "registries": { + "private": { + "origin": "https://registry.example", + "trustProfile": "offline" + } + } + })) + .expect("private-only registry configuration"); + assert!(!plugins.registries.contains_key("obscuritylabs")); + assert!(plugins.registries.contains_key("private")); + let temporary = crate::test_support::private_tempdir(); + let runtime = open(temporary.path(), plugins); + assert!( + runtime + .plugin_registry_for_reference( + "ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:v1" + ) + .is_err() + ); + + let empty: PluginsConfig = + serde_json::from_value(json!({ "registries": {} })).expect("empty registry map"); + assert!(empty.registries.is_empty()); +} + +#[test] +fn builtin_signing_identity_cannot_be_redefined() { + let default = PluginsConfig::default(); + let round_trip: PluginsConfig = + serde_json::from_value(serde_json::to_value(&default).expect("serialize defaults")) + .expect("deserialize defaults"); + assert!(round_trip.trust_profiles.contains_key("obscuritylabs")); + + let redefined = serde_json::from_value::(json!({ + "trustProfiles": { + "obscuritylabs": { "mode": "disabled" } + } + })); + assert!(redefined.is_err()); + + let temporary = crate::test_support::private_tempdir(); + let workspace = temporary.path().join("workspace"); + fs::create_dir(&workspace).expect("workspace"); + let mut config = RuntimeConfig::offline_template(workspace.join("state.redb")); + config.plugins.trust_profiles.insert( + "obscuritylabs".into(), + PluginTrustProfile { + mode: colossus_plugins::PluginTrustMode::Disabled, + ..PluginTrustProfile::default() + }, + ); + let result = Runtime::open_with_options( + &config, + Arc::new(DenyApproval), + None, + RuntimeOpenOptions::for_workspace(&workspace).expect("workspace binding"), + ); + assert!( + matches!(result, Err(RuntimeError::Config(message)) if message.contains("reserved built-in signing identity")) + ); +} + struct DenyHelperRelease(BuiltInPolicy); #[async_trait] diff --git a/docs/reference/configuration/extensions.md b/docs/reference/configuration/extensions.md index 9217426cd..72afe23a9 100644 --- a/docs/reference/configuration/extensions.md +++ b/docs/reference/configuration/extensions.md @@ -58,6 +58,12 @@ workflows: | `plugins.registries` | Exact-origin OCI Distribution profiles | | `plugins.mcpServers` | Explicit workspace enablement and authority overlay keyed by `PLUGIN/SERVER` | +The built-in `obscuritylabs` trust profile pins the `colossus-plugins` signing workflow. +Custom `trustProfiles` entries are added alongside it; the built-in signing identity cannot +be redefined. When `registries` is omitted, the built-in `obscuritylabs` GHCR registry is +available. An explicit `registries` map replaces that default, so `registries: {}` disables +OCI registry profiles for the workspace. + The owner-scoped plugin store is always `$COLOSSUS_HOME/plugins`; it is not configurable by a workspace. Trust roots, CA bundles, Docker config files, and Docker helper executables must use absolute paths. Registry credentials are references, never literal values. From 70a32cd544ab350a0a221eb474c89783125163cd Mon Sep 17 00:00:00 2001 From: Alexander Rymdeko-Harvey <8761706+killswitch-GUI@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:46:26 -0400 Subject: [PATCH 11/11] style(desktop): format merged settings tab types --- apps/desktop/src/components/ManagedSettingsPane.tsx | 3 ++- apps/desktop/src/components/OperationsSurface.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ManagedSettingsPane.tsx b/apps/desktop/src/components/ManagedSettingsPane.tsx index a8cba1b04..c934b9243 100644 --- a/apps/desktop/src/components/ManagedSettingsPane.tsx +++ b/apps/desktop/src/components/ManagedSettingsPane.tsx @@ -229,7 +229,8 @@ type SpaceTab = | "effective"; interface ManagedSettingsPaneProps { - initialSpaceTab?: "runtime" | "providers" | "plugins" | "terminal" | undefined; + initialSpaceTab?: + "runtime" | "providers" | "plugins" | "terminal" | undefined; desktop: DesktopStatus; connecting: boolean; updateChecking: boolean; diff --git a/apps/desktop/src/components/OperationsSurface.tsx b/apps/desktop/src/components/OperationsSurface.tsx index 2f0acb786..b12d43634 100644 --- a/apps/desktop/src/components/OperationsSurface.tsx +++ b/apps/desktop/src/components/OperationsSurface.tsx @@ -32,7 +32,8 @@ import { PluginsSurface } from "./PluginsSurface"; import type { WorkspaceSurface } from "./ProductRail"; interface OperationsSurfaceProps { - initialSettingsTab?: "runtime" | "providers" | "plugins" | "terminal" | undefined; + initialSettingsTab?: + "runtime" | "providers" | "plugins" | "terminal" | undefined; onConfigurePluginConnection?: () => void; pluginSelections?: readonly string[]; onUsePluginSkill?: (id: string) => void;