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 && (
+
+ Configure plugin connections
+
+ )}
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 (
void setEnabled(!enabled)}
+ >
+ {enabled ? "Disable connection" : "Enable all plugin tools"}
+
+
void run(async () => {
setDiagnostic(null);
setMessage("");
- const result = await diagnoseManagedMcpServer(targetId, server);
+ const result = await diagnoseManagedMcpServer(spaceId, server);
setDiagnostic(result);
setMessage(
result.healthy
@@ -72,10 +112,10 @@ export function PluginMcpControls({
{http && (
void run(async () => {
- setStatus(await managedMcpOAuthStatus(targetId, server));
+ setStatus(await managedMcpOAuthStatus(spaceId, server));
})
}
>
@@ -83,12 +123,18 @@ export function PluginMcpControls({
)}
- {!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 && (
void run(async () => {
if (status.authenticated) {
- setStatus(await logoutManagedMcpOAuth(targetId, server));
+ setStatus(await logoutManagedMcpOAuth(spaceId, server));
setLogin(null);
} else {
- setLogin(await beginManagedMcpOAuth(targetId, server));
+ setLogin(await beginManagedMcpOAuth(spaceId, server));
}
})
}
@@ -139,16 +185,16 @@ export function PluginMcpControls({
value={callback}
placeholder={login.callbackUrl}
onChange={(event) => setCallback(event.target.value)}
- disabled={busy || !enabled}
+ disabled={busy || !enabled || !pluginActive}
/>
void run(async () => {
setStatus(
- await completeManagedMcpOAuth(targetId, server, callback),
+ await completeManagedMcpOAuth(spaceId, server, callback),
);
setLogin(null);
setCallback("");
diff --git a/apps/desktop/src/components/PluginOperationForm.tsx b/apps/desktop/src/components/PluginOperationForm.tsx
index c677ce89b..9a5aaf70f 100644
--- a/apps/desktop/src/components/PluginOperationForm.tsx
+++ b/apps/desktop/src/components/PluginOperationForm.tsx
@@ -40,7 +40,7 @@ export function PluginOperationForm({
trigger.focus({ preventScroll: true });
};
}, []);
- const [source, setSource] = useState("directory");
+ const [source, setSource] = useState("reference");
const [digest, setDigest] = useState("");
const [registry, setRegistry] = useState("");
const [reference, setReference] = useState("");
@@ -60,7 +60,11 @@ export function PluginOperationForm({
case "install": {
const input: PluginSource =
source === "reference"
- ? { kind: source, registry, reference }
+ ? {
+ kind: source,
+ registry: registry.trim(),
+ reference: reference.trim().replace(/^oci:\/\//, ""),
+ }
: source === "directory"
? { kind: source, path: "" }
: { kind: source, path: "", digest: digest || null };
@@ -147,33 +151,49 @@ export function PluginOperationForm({
setSource(event.target.value as PluginSource["kind"])
}
>
+ OCI registry
Plugin directory
OCI layout directory
OCI layout archive
- Registry reference
)}
{network && (
<>
- Registry profile
- setRegistry(event.target.value)}
- placeholder="Configured registry name"
- />
-
-
- Registry reference
+ {action === "install"
+ ? "OCI plugin reference"
+ : "Registry reference"}
setReference(event.target.value)}
- placeholder="registry.example.com/team/plugin:version"
+ placeholder="oci://ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:version"
/>
+ {action === "install" ? (
+
+ Registry options
+
+ Registry profile
+ setRegistry(event.target.value)}
+ placeholder="Selected automatically when one profile matches"
+ />
+
+
+ ) : (
+
+ Registry profile
+ setRegistry(event.target.value)}
+ placeholder="Configured registry name"
+ />
+
+ )}
>
)}
{((action === "install" && source !== "reference") ||
@@ -189,8 +209,9 @@ export function PluginOperationForm({
)}
{action === "install" && source === "reference" && (
- Registry installations enforce the trust profile configured for this
- registry.
+ The matching registry profile verifies the package signature. The
+ built-in Obscurity Labs profile accepts only its published plugin
+ workflow identity.
)}
{(action === "verify" ||
diff --git a/apps/desktop/src/components/PluginsSurface.tsx b/apps/desktop/src/components/PluginsSurface.tsx
index 339e3d436..25a73f6ab 100644
--- a/apps/desktop/src/components/PluginsSurface.tsx
+++ b/apps/desktop/src/components/PluginsSurface.tsx
@@ -27,11 +27,15 @@ function failure(error: unknown): string {
export function PluginsSurface({
targetId,
+ spaceId,
+ onConfigureConnection,
supported,
selections = [],
onUseSkill,
}: {
targetId: string | null;
+ spaceId?: string | null;
+ onConfigureConnection?: (() => void) | undefined;
supported: boolean;
selections?: readonly string[] | undefined;
onUseSkill?: ((id: string) => void) | undefined;
@@ -342,6 +346,9 @@ export function PluginsSurface({
key={plugin.digest}
plugin={plugin}
targetId={targetId}
+ spaceId={spaceId ?? null}
+ onConfigureConnection={onConfigureConnection}
+ onConnectionChanged={() => void refresh()}
managementAvailable={inventory?.managementAvailable === true}
selections={selections}
onUseSkill={onUseSkill}
diff --git a/apps/desktop/src/dev/plugin-studio.tsx b/apps/desktop/src/dev/plugin-studio.tsx
index a4239cd7d..b1371430c 100644
--- a/apps/desktop/src/dev/plugin-studio.tsx
+++ b/apps/desktop/src/dev/plugin-studio.tsx
@@ -51,6 +51,7 @@ export default function PluginStudio() {
diff --git a/apps/desktop/src/pluginConnection.test.ts b/apps/desktop/src/pluginConnection.test.ts
new file mode 100644
index 000000000..d9f55ae96
--- /dev/null
+++ b/apps/desktop/src/pluginConnection.test.ts
@@ -0,0 +1,60 @@
+import { describe, expect, it } from "vitest";
+import type { ManagedSettingsSnapshot } from "./types";
+import { pluginConnectionRequest } from "./pluginConnection";
+
+describe("plugin connection save", () => {
+ it("preserves unrelated workspace settings while explicitly allowing all plugin tools", () => {
+ const snapshot = {
+ globalConfiguration: { revision: 17 },
+ spaces: [
+ {
+ id: "workspace",
+ archived: false,
+ effectiveValues: [
+ {
+ fieldId: "plugins.mcpServers",
+ value: {
+ "example/mail": { enabled: false, allowedTools: ["list_mail"] },
+ "other/docs": { enabled: true, allowedTools: ["search"] },
+ },
+ },
+ ],
+ configuration: {
+ accessProfileOverride: "pinned",
+ executionBoundaryOverride: null,
+ terminalEnabledOverride: false,
+ fieldOverrides: [{ fieldId: "sandbox.profile", value: "strict" }],
+ catalogRevisions: {
+ "provider:p1": { resourceId: "p1", revision: 4 },
+ "mcp:m1": { resourceId: "m1", revision: 2 },
+ },
+ searchRoles: { agent: "search-1" },
+ modelRoles: { chat: "model-1" },
+ credentialOverrides: { token: "cred-1" },
+ },
+ },
+ ],
+ } as unknown as ManagedSettingsSnapshot;
+
+ const request = pluginConnectionRequest(
+ snapshot,
+ "workspace",
+ "example/mail",
+ true,
+ );
+ expect(request.expectedGlobalRevision).toBe(17);
+ expect(request.selectedProviderResourceIds).toEqual(["p1"]);
+ expect(request.selectedMcpResourceIds).toEqual(["m1"]);
+ expect(request.credentialOverrides).toEqual({ token: "cred-1" });
+ expect(request.fieldOverrides).toEqual([
+ { fieldId: "sandbox.profile", value: "strict" },
+ {
+ fieldId: "plugins.mcpServers",
+ value: {
+ "example/mail": { enabled: true, allowedTools: ["*"] },
+ "other/docs": { enabled: true, allowedTools: ["search"] },
+ },
+ },
+ ]);
+ });
+});
diff --git a/apps/desktop/src/pluginConnection.ts b/apps/desktop/src/pluginConnection.ts
new file mode 100644
index 000000000..6f4dc9347
--- /dev/null
+++ b/apps/desktop/src/pluginConnection.ts
@@ -0,0 +1,56 @@
+import type {
+ ManagedSettingsSnapshot,
+ SaveSpaceConfigurationRequest,
+} from "./types";
+
+function record(value: unknown): Record {
+ if (value && typeof value === "object" && !Array.isArray(value))
+ return value as Record;
+ throw new Error("The effective plugin connection settings are invalid.");
+}
+
+export function pluginConnectionRequest(
+ snapshot: ManagedSettingsSnapshot,
+ spaceId: string,
+ serverId: string,
+ enabled: boolean,
+): SaveSpaceConfigurationRequest {
+ 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(
+ (entry) => entry.fieldId === "plugins.mcpServers",
+ );
+ const servers = { ...record(current?.value ?? {}) };
+ const existing = record(servers[serverId] ?? {});
+ servers[serverId] = {
+ ...existing,
+ enabled,
+ allowedTools: enabled ? ["*"] : (existing.allowedTools ?? []),
+ };
+ const fields = space.configuration.fieldOverrides.filter(
+ (entry) => entry.fieldId !== "plugins.mcpServers",
+ );
+ fields.push({ fieldId: "plugins.mcpServers", value: servers });
+ const selections = Object.entries(space.configuration.catalogRevisions);
+ const selected = (kind: string) =>
+ selections
+ .filter(([key]) => key.startsWith(`${kind}:`))
+ .map(([, entry]) => entry.resourceId);
+ return {
+ expectedGlobalRevision: snapshot.globalConfiguration.revision,
+ spaceId,
+ accessProfileOverride: space.configuration.accessProfileOverride,
+ executionBoundaryOverride: space.configuration.executionBoundaryOverride,
+ terminalEnabledOverride: space.configuration.terminalEnabledOverride,
+ fieldOverrides: fields,
+ selectedProviderResourceIds: selected("provider"),
+ selectedModelResourceIds: selected("model"),
+ selectedMcpResourceIds: selected("mcp"),
+ selectedSearchResourceIds: selected("search"),
+ selectedTelemetryResourceId: selected("telemetry")[0] ?? null,
+ searchRoles: space.configuration.searchRoles,
+ modelRoles: space.configuration.modelRoles,
+ credentialOverrides: space.configuration.credentialOverrides,
+ };
+}
diff --git a/apps/desktop/tests/browser/plugins.spec.ts b/apps/desktop/tests/browser/plugins.spec.ts
index c78d161b8..bae508f4b 100644
--- a/apps/desktop/tests/browser/plugins.spec.ts
+++ b/apps/desktop/tests/browser/plugins.spec.ts
@@ -103,6 +103,31 @@ test.beforeEach(async ({ page }) => {
}
return snapshot;
}
+ if (command === "get_managed_configuration")
+ return {
+ globalConfiguration: { revision: 4 },
+ spaces: [
+ {
+ id: "local",
+ archived: false,
+ effectiveValues: [{ fieldId: "plugins.mcpServers", value: {} }],
+ configuration: {
+ accessProfileOverride: null,
+ executionBoundaryOverride: null,
+ terminalEnabledOverride: null,
+ fieldOverrides: [],
+ catalogRevisions: {},
+ searchRoles: {},
+ modelRoles: {},
+ credentialOverrides: {},
+ },
+ },
+ ],
+ };
+ if (command === "save_space_configuration") {
+ state.pluginMcpEnabled = true;
+ return {};
+ }
if (command === "managed_mcp_oauth_status")
return {
server: "colossus/docs",
@@ -213,6 +238,37 @@ test.beforeEach(async ({ page }) => {
).toBeVisible();
});
+test("enables a plugin connection from its detail view", async ({ page }) => {
+ const controls = page.getByRole("group", {
+ name: "colossus/docs connection",
+ });
+ await controls
+ .getByRole("button", { name: "Enable all plugin tools" })
+ .click();
+ await expect(
+ controls.getByRole("button", { name: "Disable connection" }),
+ ).toBeVisible();
+ const request = await page.evaluate(() => {
+ const state = window as unknown as {
+ pluginCalls: { command: string; args: Record }[];
+ };
+ return state.pluginCalls.find(
+ (entry) => entry.command === "save_space_configuration",
+ )?.args.request;
+ });
+ expect(request).toMatchObject({
+ spaceId: "local",
+ fieldOverrides: [
+ {
+ fieldId: "plugins.mcpServers",
+ value: {
+ "colossus/docs": { enabled: true, allowedTools: ["*"] },
+ },
+ },
+ ],
+ });
+});
+
test("failed plugin MCP diagnostics keep TLS evidence readable before expansion", async ({
page,
}) => {
@@ -583,6 +639,13 @@ for (const action of [
.click();
const form = page.getByRole("form", { name: `${action} plugin` });
await expect(form).toBeFocused();
+ if (action === "install") {
+ await form
+ .getByLabel("OCI plugin reference")
+ .fill(
+ "oci://ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:0.1.0-alpha.3.ci.3.1-windows-amd64",
+ );
+ }
if (action === "pull" || action === "push") {
await form
.getByLabel("Registry profile", { exact: true })
diff --git a/crates/colossus-cli/src/bin/colossus-mcp-test-server.rs b/crates/colossus-cli/src/bin/colossus-mcp-test-server.rs
index 333661cd2..b6389a9cc 100644
--- a/crates/colossus-cli/src/bin/colossus-mcp-test-server.rs
+++ b/crates/colossus-cli/src/bin/colossus-mcp-test-server.rs
@@ -50,6 +50,10 @@ fn handle_request(request: &Value) -> Result<(), Box> {
"properties": {},
"additionalProperties": false
}
+ }, {
+ "name": "plugin_paths",
+ "description": "Return runtime-bound plugin root and data paths.",
+ "inputSchema": {"type": "object", "properties": {}, "additionalProperties": false}
}]
})
} else {
@@ -104,6 +108,14 @@ fn handle_request(request: &Value) -> Result<(), Box> {
"content": [{"type": "text", "text": secret}],
"isError": false
}),
+ Some("plugin_paths") => json!({
+ "content": [{"type": "text", "text": "plugin paths available"}],
+ "structuredContent": {
+ "root": std::env::var("PLUGIN_ROOT").unwrap_or_default(),
+ "data": std::env::var("PLUGIN_DATA").unwrap_or_default()
+ },
+ "isError": false
+ }),
_ => {
write_message(&json!({
"jsonrpc": "2.0",
diff --git a/crates/colossus-cli/src/commands.rs b/crates/colossus-cli/src/commands.rs
index 35978d386..9b0777666 100644
--- a/crates/colossus-cli/src/commands.rs
+++ b/crates/colossus-cli/src/commands.rs
@@ -63,6 +63,7 @@ pub(super) enum Command {
/// Inspect metadata-only persisted run telemetry.
Telemetry(TelemetryCommand),
/// Validate, distribute, and lifecycle-manage Agent Plugins over OCI.
+ #[command(alias = "plugin")]
Plugins(PluginsCommand),
/// Build, verify, and install signed offline release bundles.
Bundle(BundleCommand),
diff --git a/crates/colossus-cli/src/extension_args.rs b/crates/colossus-cli/src/extension_args.rs
index 155391ee2..d4edde169 100644
--- a/crates/colossus-cli/src/extension_args.rs
+++ b/crates/colossus-cli/src/extension_args.rs
@@ -26,15 +26,18 @@ pub(super) enum PluginsAction {
#[arg(long, default_value = "default")]
trust_profile: String,
},
- /// Install exactly one source as disabled.
+ /// Install exactly one source as disabled. Use `oci://HOST/REPOSITORY:TAG` for a configured registry.
Install {
- #[arg(long, conflicts_with_all = ["reference", "layout", "archive"], required_unless_present_any = ["reference", "layout", "archive"])]
+ /// OCI registry reference, for example `oci://ghcr.io/obscuritylabs/plugin:v1`.
+ #[arg(conflicts_with_all = ["directory", "reference", "layout", "archive"])]
+ source: Option,
+ #[arg(long, conflicts_with_all = ["source", "reference", "layout", "archive"])]
directory: Option,
- #[arg(long, conflicts_with_all = ["directory", "layout", "archive"])]
+ #[arg(long, conflicts_with_all = ["source", "directory", "layout", "archive"])]
reference: Option,
- #[arg(long, conflicts_with_all = ["directory", "reference", "archive"])]
+ #[arg(long, conflicts_with_all = ["source", "directory", "reference", "archive"])]
layout: Option,
- #[arg(long, conflicts_with_all = ["directory", "reference", "layout"])]
+ #[arg(long, conflicts_with_all = ["source", "directory", "reference", "layout"])]
archive: Option,
/// Required when an OCI layout contains multiple plugin manifests.
#[arg(long)]
diff --git a/crates/colossus-cli/src/plugin_requests.rs b/crates/colossus-cli/src/plugin_requests.rs
index 2c4587944..b7d9b6cc2 100644
--- a/crates/colossus-cli/src/plugin_requests.rs
+++ b/crates/colossus-cli/src/plugin_requests.rs
@@ -21,6 +21,7 @@ impl PluginsAction {
trust_profile: trust_profile.clone(),
},
Self::Install {
+ source,
directory,
reference,
layout,
@@ -29,10 +30,21 @@ impl PluginsAction {
registry,
trust_profile,
} => {
- if reference.is_none() && registry.is_some() {
- return Err("--registry requires --reference".into());
+ if reference.is_none() && source.is_none() && registry.is_some() {
+ return Err("--registry requires an OCI reference".into());
}
- let source = if let Some(path) = directory {
+ let source = if let Some(value) = source {
+ if digest.is_some() {
+ return Err("pin the OCI reference with @sha256:DIGEST".into());
+ }
+ let reference = value
+ .strip_prefix("oci://")
+ .ok_or("positional plugin sources must start with oci://")?;
+ Source::Reference {
+ registry: registry.clone().unwrap_or_default(),
+ reference: reference.into(),
+ }
+ } else if let Some(path) = directory {
if digest.is_some() {
return Err(
"--digest selects an OCI candidate, not a source directory".into()
@@ -56,7 +68,7 @@ impl PluginsAction {
return Err("pin the registry reference with @sha256:DIGEST".into());
}
Source::Reference {
- registry: registry.clone().ok_or("--reference requires --registry")?,
+ registry: registry.clone().unwrap_or_default(),
reference: reference.clone(),
}
} else {
diff --git a/crates/colossus-cli/src/tests.rs b/crates/colossus-cli/src/tests.rs
index c298f1ae6..830ba23e1 100644
--- a/crates/colossus-cli/src/tests.rs
+++ b/crates/colossus-cli/src/tests.rs
@@ -756,6 +756,48 @@ fn config_init_from_requires_development_mode() {
);
}
+#[test]
+fn plugin_install_accepts_one_oci_reference_without_registry_flags() {
+ assert!(matches!(
+ Cli::try_parse_from([
+ "colossus",
+ "plugin",
+ "install",
+ "oci://ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:v1",
+ ])
+ .expect("singular plugin alias")
+ .command,
+ Command::Plugins(_)
+ ));
+ let parsed = Cli::try_parse_from([
+ "colossus", "plugins", "install",
+ "oci://ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:0.1.0-alpha.3.ci.3.1-windows-amd64",
+ ]).expect("OCI install syntax");
+ let Command::Plugins(PluginsCommand { command: action }) = parsed.command else {
+ panic!("plugin install command");
+ };
+ let request = action.request().expect("typed request");
+ assert!(matches!(request,
+ colossus_contracts::PluginManagementRequest::Install {
+ source: colossus_contracts::PluginInstallSource::Reference { registry, reference },
+ trust_profile,
+ } if registry.is_empty()
+ && reference == "ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:0.1.0-alpha.3.ci.3.1-windows-amd64"
+ && trust_profile == "default"
+ ));
+ let invalid = Cli::try_parse_from([
+ "colossus",
+ "plugins",
+ "install",
+ "https://ghcr.io/example/plugin:v1",
+ ])
+ .expect("a positional string is parsed before request validation");
+ let Command::Plugins(PluginsCommand { command: action }) = invalid.command else {
+ panic!("plugin install command");
+ };
+ assert!(action.request().is_err());
+}
+
#[test]
fn config_init_local_conflicts_with_an_explicit_config() {
let parsed = Cli::try_parse_from([
diff --git a/crates/colossus-cli/tests/plugin_mcp_smoke.rs b/crates/colossus-cli/tests/plugin_mcp_smoke.rs
new file mode 100644
index 000000000..9a3258d63
--- /dev/null
+++ b/crates/colossus-cli/tests/plugin_mcp_smoke.rs
@@ -0,0 +1,183 @@
+//! An installed portable stdio MCP declaration must execute with host-bound plugin paths.
+#![cfg(any(target_os = "linux", target_os = "macos", windows))]
+
+#[path = "support/process.rs"]
+#[allow(dead_code)]
+mod process_support;
+
+use colossus_contracts::{Actor, ActorType};
+use colossus_home::ColossusHome;
+use colossus_plugins::PluginStore;
+use serde_json::Value;
+use std::{fs, path::Path, process::Command};
+
+const JOURNAL_KEY: &str = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
+const SIGNING_KEY: &str = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
+
+#[test]
+fn installed_plugin_stdio_discovers_and_calls_with_host_bound_paths() {
+ let directory = process_support::tempdir().expect("temporary workspace");
+ let workspace = directory.path().canonicalize().expect("workspace");
+ let isolated = process_support::isolated_user_home(&workspace);
+ let home = ColossusHome::ensure_at(isolated.colossus_home()).expect("private home");
+ let source = workspace.join("fixture-plugin");
+ fs::create_dir_all(source.join("bin")).expect("plugin source");
+ fs::write(source.join("plugin.json"), r#"{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"fixture-plugin","version":"1.0.0"}"#).expect("plugin manifest");
+ let server = Path::new(env!("CARGO_BIN_EXE_colossus-mcp-test-server"));
+ let executable_name = if cfg!(windows) {
+ "server.exe"
+ } else {
+ "server"
+ };
+ fs::copy(server, source.join("bin").join(executable_name)).expect("portable MCP binary");
+ fs::write(source.join("mcp.json"), format!(r#"{{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{{"mail":{{"type":"stdio","command":"./bin/{executable_name}","cwd":"${{PLUGIN_ROOT}}"}}}}}}"#)).expect("portable MCP declaration");
+ let store = PluginStore::new(home.root()).expect("plugin store");
+ let actor = Actor {
+ actor_type: ActorType::User,
+ id: "plugin-mcp-smoke".into(),
+ };
+ let installed = store
+ .install_directory(&source, actor.clone())
+ .expect("install fixture");
+ store
+ .enable("fixture-plugin", &installed.digest, true, actor)
+ .expect("enable fixture");
+ let data = store.data_path("fixture-plugin").expect("plugin data");
+
+ let workflows = workspace.join("workflows");
+ fs::create_dir(&workflows).expect("workflows");
+ let config = workspace.join("config.yaml");
+ let backend = if cfg!(windows) {
+ "windows_job"
+ } else {
+ "native"
+ };
+ let timeout_ms = if cfg!(windows) { 10_000 } else { 5_000 };
+ fs::write(
+ &config,
+ format!(
+ r#"schemaVersion: 3
+storage:
+ path: {state}
+ keys:
+ kind: environment
+ journal_variable: COLOSSUS_PLUGIN_TEST_JOURNAL_KEY
+ journal_key_id: plugin-test-journal-v1
+ signing_variable: COLOSSUS_PLUGIN_TEST_SIGNING_KEY
+ anchor_path: {anchor}
+access:
+ profile: development
+ tools:
+ include: []
+ exclude: []
+ actions:
+ allow: []
+ requireApproval: []
+ deny: []
+policy:
+ kind: built_in
+ require_post_effect: false
+workflows:
+ repository: {workflows}
+ user: {workflows}
+plugins:
+ mcpServers:
+ fixture-plugin/mail:
+ enabled: true
+ allowedTools: [echo, plugin_paths]
+sandbox:
+ backend: {backend}
+ profile: plugin-mcp-smoke-v1
+ allowBrokerFallback: false
+ helperPath: null
+ ociRuntime: null
+ ociImage: null
+ ociProxyImage: null
+ filesystem:
+ - root: {workspace}
+ mode: read
+ executables: []
+ environment: []
+ networkDestinations: []
+ timeoutMs: {timeout_ms}
+ maxOutputBytes: 1048576
+ maxProcesses: 4
+ maxMemoryBytes: 134217728
+ maxConcurrency: 1
+"#,
+ state = serde_json::to_string(&workspace.join("state.redb")).unwrap(),
+ anchor = serde_json::to_string(&workspace.join("anchor.json")).unwrap(),
+ workflows = serde_json::to_string(&workflows).unwrap(),
+ workspace = serde_json::to_string(&workspace).unwrap(),
+ ),
+ )
+ .expect("runtime config");
+
+ let run = |arguments: &[&str]| {
+ let mut command = Command::new(env!("CARGO_BIN_EXE_colossus"));
+ command
+ .current_dir(&workspace)
+ .arg("--config")
+ .arg(&config)
+ .args(arguments)
+ .env("COLOSSUS_HOME", home.root())
+ .env("HOME", isolated.path())
+ .env("COLOSSUS_PLUGIN_TEST_JOURNAL_KEY", JOURNAL_KEY)
+ .env("COLOSSUS_PLUGIN_TEST_SIGNING_KEY", SIGNING_KEY);
+ #[cfg(windows)]
+ command
+ .env("USERPROFILE", isolated.path())
+ .env("LOCALAPPDATA", isolated.local_app_data())
+ .env("TEMP", isolated.temporary_directory())
+ .env("TMP", isolated.temporary_directory());
+ command.output().expect("run CLI")
+ };
+ let tools = run(&[
+ "--approval-mode",
+ "full-access",
+ "mcp",
+ "tools",
+ "--server",
+ "fixture-plugin/mail",
+ ]);
+ assert!(
+ tools.status.success(),
+ "{}",
+ String::from_utf8_lossy(&tools.stderr)
+ );
+ let discovered: Value = serde_json::from_slice(&tools.stdout).expect("tool discovery");
+ let names = discovered
+ .as_array()
+ .expect("tool list")
+ .iter()
+ .filter_map(|tool| tool["name"].as_str())
+ .collect::>();
+ assert_eq!(names, vec!["echo", "plugin_paths"]);
+
+ let called = run(&[
+ "--approval-mode",
+ "full-access",
+ "mcp",
+ "call",
+ "fixture-plugin/mail",
+ "plugin_paths",
+ "{}",
+ ]);
+ assert!(
+ called.status.success(),
+ "{}",
+ String::from_utf8_lossy(&called.stderr)
+ );
+ let output: Value = serde_json::from_slice(&called.stdout).expect("tool result");
+ let paths = &output["result"]["structuredContent"];
+ let root = paths["root"].as_str().expect("host plugin root");
+ let child_data = paths["data"].as_str().expect("host plugin data");
+ assert_eq!(
+ fs::canonicalize(root).expect("returned plugin root"),
+ fs::canonicalize(&installed.root).expect("installed root")
+ );
+ assert_eq!(
+ fs::canonicalize(child_data).expect("returned plugin data"),
+ fs::canonicalize(data).expect("plugin data")
+ );
+}
diff --git a/crates/colossus-mcp/src/config.rs b/crates/colossus-mcp/src/config.rs
index 502888c2b..40c9e63fa 100644
--- a/crates/colossus-mcp/src/config.rs
+++ b/crates/colossus-mcp/src/config.rs
@@ -658,6 +658,8 @@ fn validate_stdio_server(
}
for (child_name, reference) in &server.environment {
if !valid_environment_name(child_name)
+ || child_name.eq_ignore_ascii_case("PLUGIN_ROOT")
+ || child_name.eq_ignore_ascii_case("PLUGIN_DATA")
|| (!ambient_resources && !allowed_environment.contains(child_name))
|| !valid_credential_reference(reference)
{
@@ -668,15 +670,32 @@ fn validate_stdio_server(
}
for (child_name, value) in &server.literal_environment {
if !valid_environment_name(child_name)
- || matches!(child_name.as_str(), "PLUGIN_ROOT" | "PLUGIN_DATA")
|| value.len() > 64 * 1024
|| value.contains('\0')
+ || ((child_name.eq_ignore_ascii_case("PLUGIN_ROOT")
+ || child_name.eq_ignore_ascii_case("PLUGIN_DATA"))
+ && (server.effect_action_prefix.is_none() || server.provenance.is_none()))
+ || ((child_name.eq_ignore_ascii_case("PLUGIN_ROOT")
+ || child_name.eq_ignore_ascii_case("PLUGIN_DATA"))
+ && !matches!(child_name.as_str(), "PLUGIN_ROOT" | "PLUGIN_DATA"))
{
return Err(McpError::Invalid(format!(
"server {name} contains an invalid literal plugin environment entry"
)));
}
}
+ if server.effect_action_prefix.is_some() {
+ let root = server.literal_environment.get("PLUGIN_ROOT");
+ let data = server.literal_environment.get("PLUGIN_DATA");
+ if !root.is_some_and(|value| Path::new(value).is_absolute())
+ || !data.is_some_and(|value| Path::new(value).is_absolute())
+ || !root.is_some_and(|value| server.command.starts_with(value))
+ {
+ return Err(McpError::Invalid(format!(
+ "server {name} requires runtime-bound PLUGIN_ROOT and PLUGIN_DATA"
+ )));
+ }
+ }
Ok(())
}
diff --git a/crates/colossus-mcp/src/tests.rs b/crates/colossus-mcp/src/tests.rs
index 85df07838..9e8f859c7 100644
--- a/crates/colossus-mcp/src/tests.rs
+++ b/crates/colossus-mcp/src/tests.rs
@@ -566,6 +566,90 @@ fn ambient_validation_keeps_exact_mcp_declarations_but_omits_duplicate_sandbox_g
);
}
+#[test]
+fn plugin_stdio_accepts_only_runtime_bound_root_and_data_environment() {
+ let temporary = tempfile::tempdir().expect("plugin roots");
+ let root = temporary.path().join("content");
+ let data = temporary.path().join("data");
+ std::fs::create_dir_all(root.join("bin")).expect("content");
+ std::fs::create_dir_all(&data).expect("data");
+ let mut server = remote_server("http://127.0.0.1:8787/mcp");
+ server.transport = McpTransportKind::Stdio;
+ server.url = None;
+ server.command = root.join("bin/server.exe");
+ server.working_directory = Some(root.clone());
+ server.effect_action_prefix = Some(colossus_contracts::plugin_mcp_action_prefix(
+ "fixture", "mail",
+ ));
+ server.provenance = Some(json!({"plugin":"fixture"}));
+ server.literal_environment = BTreeMap::from([
+ ("PLUGIN_ROOT".into(), root.display().to_string()),
+ ("PLUGIN_DATA".into(), data.display().to_string()),
+ ]);
+ let mut config = McpConfig {
+ oauth_credential_store: McpOAuthCredentialStoreKind::Auto,
+ servers: BTreeMap::from([("fixture/mail".into(), server)]),
+ };
+ let validate = |config: &McpConfig| {
+ validate_config(
+ config,
+ &root,
+ validation_context(ResourceAuthority::Ambient, &[]),
+ )
+ };
+ validate(&config).expect("runtime-owned plugin paths");
+
+ let server = config.servers.get_mut("fixture/mail").expect("server");
+ server
+ .environment
+ .insert("PLUGIN_ROOT".into(), "env:ATTACK".into());
+ assert!(
+ validate(&config).is_err(),
+ "credential overlay cannot replace a root"
+ );
+ config
+ .servers
+ .get_mut("fixture/mail")
+ .expect("server")
+ .environment
+ .clear();
+ config
+ .servers
+ .get_mut("fixture/mail")
+ .expect("server")
+ .literal_environment
+ .insert("plugin_data".into(), data.display().to_string());
+ assert!(
+ validate(&config).is_err(),
+ "Windows environment names are case insensitive"
+ );
+ config
+ .servers
+ .get_mut("fixture/mail")
+ .expect("server")
+ .literal_environment
+ .remove("plugin_data");
+ config
+ .servers
+ .get_mut("fixture/mail")
+ .expect("server")
+ .literal_environment
+ .insert("PLUGIN_ROOT".into(), data.display().to_string());
+ assert!(
+ validate(&config).is_err(),
+ "command must remain under the trusted root"
+ );
+ let server = config.servers.get_mut("fixture/mail").expect("server");
+ server
+ .literal_environment
+ .insert("PLUGIN_ROOT".into(), root.display().to_string());
+ server.effect_action_prefix = None;
+ assert!(
+ validate(&config).is_err(),
+ "unbound values cannot be accepted"
+ );
+}
+
struct McpEffectShapeExecutor {
reference: &'static str,
}
diff --git a/crates/colossus-plugins/src/registry.rs b/crates/colossus-plugins/src/registry.rs
index cbdccafc1..df4153b6e 100644
--- a/crates/colossus-plugins/src/registry.rs
+++ b/crates/colossus-plugins/src/registry.rs
@@ -707,7 +707,7 @@ impl PluginRegistryClient {
})
}
- /// Fetch the OCI 1.1 referrers index for an exact subject digest.
+ /// Fetch the OCI referrers index for an exact subject digest, including the tag fallback.
pub async fn referrers(
&self,
reference: &str,
@@ -723,6 +723,9 @@ impl PluginRegistryClient {
false,
)
.await?;
+ if response.status() == StatusCode::NOT_FOUND {
+ return self.pull_referrers_tag(&parsed, subject_digest).await;
+ }
require_status(&response, StatusCode::OK, "referrers pull")?;
serde_json::from_slice(&read_response(response, MAX_MANIFEST_BYTES).await?).map_err(adapter)
}
@@ -846,13 +849,13 @@ impl PluginRegistryClient {
false,
)
.await?;
- if response.status() == StatusCode::NOT_FOUND {
- return Ok((Vec::new(), Vec::new()));
- }
- require_status(&response, StatusCode::OK, "referrers pull")?;
- let index: Value =
+ let index: Value = if response.status() == StatusCode::NOT_FOUND {
+ self.pull_referrers_tag(reference, subject_digest).await?
+ } else {
+ require_status(&response, StatusCode::OK, "referrers pull")?;
serde_json::from_slice(&read_response(response, MAX_MANIFEST_BYTES).await?)
- .map_err(adapter)?;
+ .map_err(adapter)?
+ };
if index.get("schemaVersion") != Some(&json!(2)) {
return Err(StoreError::Adapter("invalid OCI referrers index".into()));
}
@@ -886,7 +889,7 @@ impl PluginRegistryClient {
{
continue;
}
- let manifest = self.pull_blob(reference, digest, size).await?;
+ let manifest = self.pull_referrer_manifest(reference, digest, size).await?;
let value: Value = serde_json::from_slice(&manifest).map_err(adapter)?;
if value
.get("subject")
@@ -927,6 +930,60 @@ impl PluginRegistryClient {
Ok((retained, blobs))
}
+ async fn pull_referrers_tag(
+ &self,
+ reference: &RegistryReference,
+ subject_digest: &str,
+ ) -> Result {
+ let encoded = subject_digest
+ .strip_prefix("sha256:")
+ .ok_or_else(|| StoreError::Adapter("unsupported referrer subject digest".into()))?;
+ let tag = format!("sha256-{encoded}");
+ let url = self.registry_url(reference, "manifests", &tag)?;
+ let response = self
+ .authenticated_get(
+ url,
+ Some(format!("repository:{}:pull", reference.repository)),
+ false,
+ )
+ .await?;
+ if response.status() == StatusCode::NOT_FOUND {
+ return Ok(json!({"schemaVersion": 2, "manifests": []}));
+ }
+ require_status(&response, StatusCode::OK, "referrers tag pull")?;
+ require_media_type(&response, OCI_IMAGE_INDEX_MEDIA_TYPE, "referrers tag")?;
+ serde_json::from_slice(&read_response(response, MAX_MANIFEST_BYTES).await?).map_err(adapter)
+ }
+
+ async fn pull_referrer_manifest(
+ &self,
+ reference: &RegistryReference,
+ digest: &str,
+ size: u64,
+ ) -> Result, StoreError> {
+ let url = self.registry_url(reference, "manifests", digest)?;
+ let response = self
+ .authenticated_get(
+ url,
+ Some(format!("repository:{}:pull", reference.repository)),
+ false,
+ )
+ .await?;
+ require_status(&response, StatusCode::OK, "referrer manifest pull")?;
+ require_media_type(
+ &response,
+ OCI_IMAGE_MANIFEST_MEDIA_TYPE,
+ "referrer manifest",
+ )?;
+ let bytes = read_response(response, MAX_MANIFEST_BYTES).await?;
+ if u64::try_from(bytes.len()).map_err(adapter)? != size || sha256_digest(&bytes) != digest {
+ return Err(StoreError::Verification(
+ "OCI referrer manifest digest or size does not match its descriptor".into(),
+ ));
+ }
+ Ok(bytes)
+ }
+
async fn push_referrer_material(
&self,
layout: &Path,
diff --git a/crates/colossus-plugins/src/registry_acceptance.rs b/crates/colossus-plugins/src/registry_acceptance.rs
index 985668c8b..b5b8cf7aa 100644
--- a/crates/colossus-plugins/src/registry_acceptance.rs
+++ b/crates/colossus-plugins/src/registry_acceptance.rs
@@ -8,6 +8,8 @@ use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
struct RegistryState {
blobs: BTreeMap>,
manifest: Vec,
+ referrers_tag: Option>,
+ referrer_manifests: BTreeMap>,
uploads: BTreeMap>,
requests: Vec<(String, String, Option)>,
deny: bool,
@@ -143,6 +145,9 @@ fn respond(state: &mut RegistryState, head: &str, body: Vec) -> (u16, String
);
}
if path.contains("/referrers/") {
+ if state.referrers_tag.is_some() {
+ return (404, String::new(), Vec::new());
+ }
return (
200,
"Content-Type: application/json\r\n".into(),
@@ -154,6 +159,23 @@ fn respond(state: &mut RegistryState, head: &str, body: Vec) -> (u16, String
state.manifest = body;
return (201, String::new(), Vec::new());
}
+ let selector = path.rsplit('/').next().expect("manifest selector");
+ if selector.starts_with("sha256-")
+ && let Some(index) = &state.referrers_tag
+ {
+ return (
+ 200,
+ format!("Content-Type: {OCI_IMAGE_INDEX_MEDIA_TYPE}\r\n"),
+ index.clone(),
+ );
+ }
+ if let Some(manifest) = state.referrer_manifests.get(selector) {
+ return (
+ 200,
+ format!("Content-Type: {OCI_IMAGE_MANIFEST_MEDIA_TYPE}\r\n"),
+ manifest.clone(),
+ );
+ }
return (
200,
format!(
@@ -234,6 +256,73 @@ fn respond(state: &mut RegistryState, head: &str, body: Vec) -> (u16, String
}
}
+#[tokio::test]
+async fn pull_uses_referrers_tag_when_registry_lacks_referrers_api() {
+ let directory = tempfile::tempdir().expect("temporary");
+ let artifact = layout(directory.path());
+ let registry = RegistryFixture::start().await;
+ registry
+ .client()
+ .push(&directory.path().join("layout"), ®istry.reference(":v1"))
+ .await
+ .expect("push plugin");
+ let bundle = br#"{"fixture":"signature material"}"#.to_vec();
+ let config = b"{}".to_vec();
+ let manifest = serde_json::to_vec(&json!({
+ "schemaVersion": 2,
+ "mediaType": OCI_IMAGE_MANIFEST_MEDIA_TYPE,
+ "artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
+ "config": {"mediaType": "application/vnd.oci.empty.v1+json", "digest": sha256_digest(&config), "size": config.len()},
+ "layers": [{"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "digest": sha256_digest(&bundle), "size": bundle.len()}],
+ "subject": {"mediaType": OCI_IMAGE_MANIFEST_MEDIA_TYPE, "digest": artifact.manifest_digest, "size": artifact.manifest.len()}
+ }))
+ .expect("referrer manifest");
+ let digest = sha256_digest(&manifest);
+ let index = serde_json::to_vec(&json!({
+ "schemaVersion": 2,
+ "mediaType": OCI_IMAGE_INDEX_MEDIA_TYPE,
+ "manifests": [{"mediaType": OCI_IMAGE_MANIFEST_MEDIA_TYPE, "digest": digest, "size": manifest.len()}]
+ }))
+ .expect("referrers tag");
+ {
+ let mut state = registry.state.lock().expect("fixture state");
+ state.blobs.insert(sha256_digest(&bundle), bundle.clone());
+ state.blobs.insert(sha256_digest(&config), config);
+ state.referrer_manifests.insert(digest, manifest);
+ state.referrers_tag = Some(index);
+ }
+ let destination = directory.path().join("pulled");
+ registry
+ .client()
+ .pull(®istry.reference(":v1"), &destination)
+ .await
+ .expect("pull fallback referrer");
+ assert_eq!(
+ sigstore_bundles_for_subject(&destination, &artifact.manifest_digest)
+ .expect("retained signature material"),
+ vec![bundle]
+ );
+ let state = registry.state.lock().expect("fixture state");
+ assert!(
+ state
+ .requests
+ .iter()
+ .any(|(_, path, _)| path.ends_with(&format!(
+ "/manifests/sha256-{}",
+ &artifact.manifest_digest[7..]
+ )))
+ );
+ assert!(
+ state
+ .requests
+ .iter()
+ .any(|(_, path, _)| path.ends_with(&format!(
+ "/manifests/{}",
+ sha256_digest(state.referrer_manifests.values().next().expect("referrer"))
+ )))
+ );
+}
+
fn layout(path: &Path) -> BuiltPluginArtifact {
let source = path.join("source");
fs::create_dir(&source).expect("source");
diff --git a/crates/colossus-runtime/src/config.rs b/crates/colossus-runtime/src/config.rs
index 37ec94f98..c83981512 100644
--- a/crates/colossus-runtime/src/config.rs
+++ b/crates/colossus-runtime/src/config.rs
@@ -367,8 +367,23 @@ impl Default for PluginsConfig {
enabled: true,
include: Vec::new(),
exclude: Vec::new(),
- trust_profiles: BTreeMap::from([("default".into(), PluginTrustProfile::default())]),
- registries: BTreeMap::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()
+ })]),
mcp_servers: BTreeMap::new(),
}
}
@@ -1700,7 +1715,9 @@ fn validate_plugins_config(config: &PluginsConfig) -> Result<(), RuntimeError> {
)));
}
if server.environment.keys().any(|name| {
- !valid_environment_name(name) || matches!(name.as_str(), "PLUGIN_ROOT" | "PLUGIN_DATA")
+ !valid_environment_name(name)
+ || name.eq_ignore_ascii_case("PLUGIN_ROOT")
+ || name.eq_ignore_ascii_case("PLUGIN_DATA")
}) {
return Err(RuntimeError::Config(format!(
"plugins.mcpServers.{id}.environment contains an invalid or reserved variable"
diff --git a/crates/colossus-runtime/src/plugin_catalog.rs b/crates/colossus-runtime/src/plugin_catalog.rs
index 23a58c782..64b559e74 100644
--- a/crates/colossus-runtime/src/plugin_catalog.rs
+++ b/crates/colossus-runtime/src/plugin_catalog.rs
@@ -138,6 +138,36 @@ pub(super) struct PluginCatalogSource {
}
impl PluginCatalogSource {
+ pub(super) fn live_inventory(&self) -> Result, RuntimeError> {
+ let Some(store) = &self.store else {
+ return Ok(Vec::new());
+ };
+ let mut inventory = store.inventory()?;
+ if self.configuration.enabled {
+ let (records, _lease) = store.available_snapshot_with_lease(
+ &self.configuration.include,
+ &self.configuration.exclude,
+ )?;
+ let extensions = compile_active_plugin_extensions(
+ &records,
+ &self.configuration,
+ &self.standalone_mcp,
+ &self.sandbox,
+ Some(store),
+ )?;
+ for entry in &mut inventory {
+ if records.iter().any(|record| {
+ record.installation.manifest.name == entry.manifest.name
+ && record.installation.digest == entry.digest
+ }) && let Some(diagnostics) = extensions.diagnostics.get(&entry.manifest.name)
+ {
+ entry.diagnostics.extend(diagnostics.iter().cloned());
+ }
+ }
+ }
+ Ok(narrow_plugin_inventory(inventory, &self.configuration))
+ }
+
pub(super) fn capture(&self) -> Result, RuntimeError> {
if let Some(catalog) = active_plugin_catalog() {
return Ok(catalog);
diff --git a/crates/colossus-runtime/src/plugin_catalog_tests.rs b/crates/colossus-runtime/src/plugin_catalog_tests.rs
index 07ee3e06b..e4b8e8fb7 100644
--- a/crates/colossus-runtime/src/plugin_catalog_tests.rs
+++ b/crates/colossus-runtime/src/plugin_catalog_tests.rs
@@ -21,6 +21,98 @@ fn operator_dispatch_future_fits_a_normal_worker_stack() {
);
}
+#[test]
+fn installed_plugin_stdio_status_matches_runtime_component_validation() {
+ let temporary = private_tempdir();
+ let root = temporary.path().canonicalize().expect("root");
+ let source = root.join("fixture");
+ fs::create_dir_all(source.join("bin")).expect("plugin directory");
+ fs::write(source.join("plugin.json"), r#"{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"fixture","version":"1.0.0","description":"MCP fixture"}"#)
+ .expect("plugin manifest");
+ fs::write(source.join("mcp.json"), r#"{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"mail":{"type":"stdio","command":"./bin/server.exe","cwd":"${PLUGIN_ROOT}"}}}"#)
+ .expect("MCP declaration");
+ fs::write(source.join("bin/server.exe"), b"fixture executable").expect("server fixture");
+ let store = Arc::new(PluginStore::new(root.join("home")).expect("plugin store"));
+ let installed = store
+ .install_directory(&source, terminal_actor())
+ .expect("install");
+ store
+ .enable("fixture", &installed.digest, true, terminal_actor())
+ .expect("enable");
+ let configuration = Arc::new(PluginsConfig {
+ mcp_servers: BTreeMap::from([(
+ "fixture/mail".into(),
+ PluginMcpServerConfig {
+ enabled: true,
+ allowed_tools: vec!["echo".into()],
+ ..PluginMcpServerConfig::default()
+ },
+ )]),
+ ..PluginsConfig::default()
+ });
+ let catalog = PluginCatalogSource {
+ store: Some(store),
+ configuration,
+ standalone_mcp: McpConfig::default(),
+ sandbox: SandboxConfig::platform_isolating(),
+ workspace: root.join("workspace"),
+ mcp_template: std::sync::OnceLock::new(),
+ };
+ let inventory = catalog.live_inventory().expect("live inventory");
+ let entry = inventory
+ .iter()
+ .find(|entry| entry.manifest.name == "fixture")
+ .expect("fixture");
+ assert_eq!(entry.mcp_servers[0].status, "Configured");
+ assert!(entry.diagnostics.is_empty(), "{:?}", entry.diagnostics);
+ let snapshot = catalog.capture().expect("runtime snapshot");
+ assert!(
+ snapshot
+ .records
+ .iter()
+ .any(|record| record.installation.manifest.name == "fixture"
+ && record.diagnostics.is_empty())
+ );
+
+ let mut broken = (*catalog.configuration).clone();
+ broken
+ .mcp_servers
+ .get_mut("fixture/mail")
+ .expect("overlay")
+ .allowed_tools
+ .clear();
+ let catalog = PluginCatalogSource {
+ configuration: Arc::new(broken),
+ ..catalog
+ };
+ let inventory = catalog.live_inventory().expect("invalid inventory");
+ let entry = inventory
+ .iter()
+ .find(|entry| entry.manifest.name == "fixture")
+ .expect("fixture");
+ assert_eq!(
+ entry.mcp_servers[0].status,
+ "Invalid component configuration; see diagnostics"
+ );
+ assert!(
+ entry
+ .diagnostics
+ .iter()
+ .any(|diagnostic| diagnostic.code == "mcp_configuration_unavailable")
+ );
+ let snapshot = catalog.capture().expect("invalid runtime snapshot");
+ assert!(
+ snapshot
+ .records
+ .iter()
+ .find(|record| record.installation.manifest.name == "fixture")
+ .expect("fixture record")
+ .diagnostics
+ .iter()
+ .any(|diagnostic| diagnostic.code == "mcp_configuration_unavailable")
+ );
+}
+
#[test]
fn core_instruction_preview_runs_on_a_standard_worker_thread() {
let temporary = private_tempdir();
diff --git a/crates/colossus-runtime/src/plugin_extensions.rs b/crates/colossus-runtime/src/plugin_extensions.rs
index 621d7d18f..161664934 100644
--- a/crates/colossus-runtime/src/plugin_extensions.rs
+++ b/crates/colossus-runtime/src/plugin_extensions.rs
@@ -243,7 +243,7 @@ fn compile_plugin_extensions(
portable.id.clone(),
McpServerConfig {
transport,
- command,
+ command: command.clone(),
args: portable
.args
.iter()
@@ -283,19 +283,31 @@ fn compile_plugin_extensions(
for suffix in ["tools", "call"] {
let action = format!("{action_prefix}.{suffix}");
actions.push(action.clone());
+ let mut action_filesystem = vec![
+ FilesystemGrant {
+ root: root.display().to_string(),
+ mode: "read".into(),
+ },
+ FilesystemGrant {
+ root: data.display().to_string(),
+ mode: "write".into(),
+ },
+ ];
+ if transport == colossus_mcp::McpTransportKind::Stdio {
+ action_filesystem.push(FilesystemGrant {
+ root: command.display().to_string(),
+ mode: "execute".into(),
+ });
+ }
+ let mut allowed_environment =
+ overlay.environment.keys().cloned().collect::>();
+ if transport == colossus_mcp::McpTransportKind::Stdio {
+ allowed_environment.extend(["PLUGIN_ROOT".into(), "PLUGIN_DATA".into()]);
+ }
restrictions.push(PluginActionRestriction {
action,
- filesystem: vec![
- FilesystemGrant {
- root: root.display().to_string(),
- mode: "read".into(),
- },
- FilesystemGrant {
- root: data.display().to_string(),
- mode: "write".into(),
- },
- ],
- allowed_environment: overlay.environment.keys().cloned().collect(),
+ filesystem: action_filesystem,
+ allowed_environment,
network_destinations: portable
.url
.as_deref()
diff --git a/crates/colossus-runtime/src/plugin_inventory.rs b/crates/colossus-runtime/src/plugin_inventory.rs
index bce6aaf11..4ae543c23 100644
--- a/crates/colossus-runtime/src/plugin_inventory.rs
+++ b/crates/colossus-runtime/src/plugin_inventory.rs
@@ -3,8 +3,7 @@
use super::*;
struct PluginInventoryExecutor {
- store: Option>,
- configuration: Arc,
+ catalog: Arc,
}
#[async_trait]
@@ -23,13 +22,9 @@ impl EffectExecutor for PluginInventoryExecutor {
));
}
let inventory = self
- .store
- .as_ref()
- .map(|store| store.inventory())
- .transpose()
- .map_err(|error| ExecutionError::Failed(error.to_string()))?
- .unwrap_or_default();
- let inventory = narrow_plugin_inventory(inventory, &self.configuration);
+ .catalog
+ .live_inventory()
+ .map_err(|error| ExecutionError::Failed(error.to_string()))?;
Ok(QuarantinedEffectResult {
media_type: "application/json".into(),
bytes: serde_json::to_vec(&inventory)
@@ -53,8 +48,7 @@ impl Runtime {
let executor = WorkspaceBoundEffectExecutor::new(
self._workspace_lease.identity(),
Arc::new(PluginInventoryExecutor {
- store: self.plugin_store.clone(),
- configuration: Arc::clone(&self.plugin_configuration),
+ catalog: Arc::clone(&self.plugin_catalog),
}),
);
let released = self.gateway.execute(request, &executor).await?;
diff --git a/crates/colossus-runtime/src/plugin_management.rs b/crates/colossus-runtime/src/plugin_management.rs
index 4df0a77ce..0c7cf66ff 100644
--- a/crates/colossus-runtime/src/plugin_management.rs
+++ b/crates/colossus-runtime/src/plugin_management.rs
@@ -6,6 +6,7 @@ use colossus_contracts::{PluginInstallSource, PluginManagementRequest, PluginOri
struct PluginManagementExecutor {
store: Option>,
configuration: Arc,
+ catalog: Arc,
}
impl PluginManagementExecutor {
@@ -30,18 +31,14 @@ impl PluginManagementExecutor {
) -> Result {
use PluginManagementRequest as Op;
let value = match operation {
- Op::Inventory => serde_json::to_value(super::plugin_catalog::narrow_plugin_inventory(
- self.store()?.inventory().map_err(failed)?,
- &self.configuration,
- )),
+ Op::Inventory => serde_json::to_value(self.catalog.live_inventory().map_err(failed)?),
Op::Show { name } => serde_json::to_value(
- super::plugin_catalog::narrow_plugin_inventory(
- self.store()?.inventory().map_err(failed)?,
- &self.configuration,
- )
- .into_iter()
- .filter(|plugin| plugin.manifest.name == name)
- .collect::>(),
+ self.catalog
+ .live_inventory()
+ .map_err(failed)?
+ .into_iter()
+ .filter(|plugin| plugin.manifest.name == name)
+ .collect::>(),
),
Op::SkillRead { skill_id, digest }
| Op::ResourceList { skill_id, digest }
@@ -465,6 +462,9 @@ impl Runtime {
},
trust_profile,
} => {
+ if registry.is_empty() {
+ *registry = self.plugin_registry_for_reference(reference)?;
+ }
let enforced = &self.plugin_registry_profile(registry)?.trust_profile;
if trust_profile != "default" && trust_profile != enforced {
return Err(RuntimeError::Config(format!(
@@ -505,6 +505,7 @@ impl Runtime {
Arc::new(PluginManagementExecutor {
store: self.plugin_store.clone(),
configuration: Arc::clone(&self.plugin_configuration),
+ catalog: Arc::clone(&self.plugin_catalog),
}),
);
let released = self.gateway.execute(request, &executor).await?;
diff --git a/crates/colossus-runtime/src/plugin_registry_tests.rs b/crates/colossus-runtime/src/plugin_registry_tests.rs
index 4af53dee9..e7f516544 100644
--- a/crates/colossus-runtime/src/plugin_registry_tests.rs
+++ b/crates/colossus-runtime/src/plugin_registry_tests.rs
@@ -7,6 +7,42 @@ use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
const SECRET: &str = "runtime-registry-fixture-secret";
+#[test]
+fn default_oci_profile_pins_obscurity_labs_signing_identity() {
+ let defaults = PluginsConfig::default();
+ let registry = defaults
+ .registries
+ .get("obscuritylabs")
+ .expect("built-in registry");
+ assert_eq!(registry.origin, "https://ghcr.io");
+ assert_eq!(registry.trust_profile, "obscuritylabs");
+ assert!(matches!(registry.auth, RegistryAuthConfig::Anonymous));
+ let trust = defaults
+ .trust_profiles
+ .get("obscuritylabs")
+ .expect("built-in trust");
+ assert_eq!(trust.mode, colossus_plugins::PluginTrustMode::Required);
+ assert_eq!(trust.identities.len(), 1);
+ assert_eq!(
+ trust.identities[0].issuer,
+ "https://token.actions.githubusercontent.com"
+ );
+ assert_eq!(
+ trust.identities[0].subject,
+ "https://github.com/obscuritylabs/colossus-plugins/.github/workflows/plugins.yml@refs/heads/main"
+ );
+ let temporary = crate::test_support::private_tempdir();
+ let runtime = open(temporary.path(), defaults);
+ assert_eq!(runtime.plugin_registry_for_reference(
+ "ghcr.io/obscuritylabs/colossus-plugin-outlook-classic:0.1.0-alpha.3.ci.3.1-windows-amd64"
+ ).expect("one matching profile"), "obscuritylabs");
+ assert!(
+ runtime
+ .plugin_registry_for_reference("registry.example/team/plugin:v1")
+ .is_err()
+ );
+}
+
struct DenyHelperRelease(BuiltInPolicy);
#[async_trait]
diff --git a/crates/colossus-runtime/src/services.rs b/crates/colossus-runtime/src/services.rs
index 02c468bc8..4ceaf9766 100644
--- a/crates/colossus-runtime/src/services.rs
+++ b/crates/colossus-runtime/src/services.rs
@@ -77,16 +77,7 @@ impl Runtime {
/// Live management inventory, including disabled installations and workspace exclusions.
pub fn plugin_inventory(&self) -> Result, RuntimeError> {
- let inventory = self
- .plugin_store
- .as_ref()
- .map(|store| store.inventory())
- .transpose()?
- .unwrap_or_default();
- Ok(super::plugin_catalog::narrow_plugin_inventory(
- inventory,
- &self.plugin_configuration,
- ))
+ self.plugin_catalog.live_inventory()
}
/// Return every machine-scoped plugin installation, including inactive digests.
@@ -364,8 +355,9 @@ impl Runtime {
.prefix(".plugin-pull-")
.tempdir_in(&self.workspace)?;
let layout = temporary.path().join("layout");
- self.pull_plugin(registry, reference, &layout).await?;
- let artifact = colossus_plugins::verify_plugin_layout(&layout, None)?;
+ let transfer = self.pull_plugin(registry, reference, &layout).await?;
+ let artifact =
+ colossus_plugins::verify_plugin_layout(&layout, Some(&transfer.manifest_digest))?;
let config: colossus_contracts::AgentPluginOciConfig =
serde_json::from_slice(&artifact.config)
.map_err(|error| RuntimeError::Config(error.to_string()))?;
@@ -413,6 +405,31 @@ impl Runtime {
})
}
+ pub(super) fn plugin_registry_for_reference(
+ &self,
+ reference: &str,
+ ) -> Result {
+ let origin = colossus_plugins::RegistryReference::parse(reference)
+ .map_err(|error| RuntimeError::Config(error.to_string()))?
+ .origin;
+ let matches = self
+ .plugin_configuration
+ .registries
+ .iter()
+ .filter(|(_, profile)| profile.origin == origin)
+ .map(|(name, _)| name.as_str())
+ .collect::>();
+ match matches.as_slice() {
+ [name] => Ok((*name).to_owned()),
+ [] => Err(RuntimeError::Config(format!(
+ "no plugin registry profile is configured for {origin}; use --registry after configuring one"
+ ))),
+ _ => Err(RuntimeError::Config(format!(
+ "multiple plugin registry profiles match {origin}; select one with --registry"
+ ))),
+ }
+ }
+
async fn execute_plugin_registry_operation(
&self,
profile: PluginRegistryProfile,
diff --git a/docs/extend/plugins.md b/docs/extend/plugins.md
index d84d49735..30b13f54d 100644
--- a/docs/extend/plugins.md
+++ b/docs/extend/plugins.md
@@ -42,6 +42,15 @@ Desktop's **Plugins** surface lists installed candidates, availability, source,
trust, component diagnostics, skills and MCP servers. Managed Local owns lifecycle
operations and native import/export dialogs. External targets provide authorized
discovery and bounded previews when they advertise support; they are not managed locally.
+Choose **Install → OCI registry**, paste an `oci://` reference, and continue. When one
+configured registry profile matches the host, Desktop selects it automatically. The
+registry options let you choose a named profile when several match. Local directories
+and OCI layout imports remain available in the source selector.
+After installation, open the candidate and select **Activate this digest**. For an
+installed MCP server, **Enable all plugin tools** applies a Workspace overlay and restarts
+Managed Local after preflight. This wildcard also covers tools added by later versions;
+use **Configure plugin connections** to narrow the tool list or add credentials.
+**Test connection** checks the applied server after the restart.
Public API clients can request unavailable metadata with `include_disabled`; instruction
and resource reads still require the plugin to be available in the workspace. An empty
kind filter or `EXTENSION_KIND_UNSPECIFIED` includes Agent Plugins.
@@ -144,6 +153,23 @@ a read/execute grant.
## Validate, package, and install
+On a fresh configuration, the built-in GHCR profile and its required Sigstore signing
+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 plugins list
+colossus plugins enable outlook-classic --digest sha256:6ca3c7a1e753e39c85b1345feda29cac368137a67918b03fed31a8300059bced
+```
+
+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; the managed user-session companion tracked in [issue #246](https://github.com/obscuritylabs/Colossus/issues/246)
+is required for that connection.
+
```bash
colossus plugins validate ./example-plugin
colossus plugins package ./example-plugin --output ./example-plugin.oci
@@ -176,6 +202,10 @@ at startup and no ambient Docker credentials are used unless `auth.kind: docker`
selected explicitly. Bearer/basic values remain credential references. Docker helpers
require an exact configured executable and run through the normal process permit and audit
boundary.
+The built-in `obscuritylabs` profile allows anonymous pulls from `https://ghcr.io` and
+requires a keyless Sigstore certificate issued by GitHub Actions for the exact
+`obscuritylabs/colossus-plugins` main-branch plugin workflow. It does not trust every
+artifact on GHCR. Explicit workspace configuration can replace or remove this profile.
Docker configuration is opened only inside an authorized registry transfer, and its file
must be covered by that transfer's permit. Denied transfers do not inspect credentials.
@@ -198,8 +228,10 @@ colossus plugins install --reference registry.example/acme/review@sha256:DIGEST
--registry production
```
-Cosign signatures and attestations remain standard OCI 1.1 referrers. Colossus does not
-invent a signing envelope.
+Cosign signatures and attestations remain standard OCI referrers. Pulls also read the
+OCI referrers tag fallback when a registry does not serve the referrers API, verifying
+each attached manifest's digest and exact subject before checking its signature.
+Colossus does not invent a signing envelope.
## MCP enablement and data
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index b4d76782b..029d8d731 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -152,7 +152,27 @@ plugins:
publicKeys: []
identities: []
trustRootPath: null
- registries: {}
+ obscuritylabs:
+ mode: required
+ publicKeys: []
+ identities:
+ - issuer: https://token.actions.githubusercontent.com
+ subject: https://github.com/obscuritylabs/colossus-plugins/.github/workflows/plugins.yml@refs/heads/main
+ trustRootPath: null
+ registries:
+ obscuritylabs:
+ origin: https://ghcr.io
+ trustProfile: obscuritylabs
+ auth:
+ kind: anonymous
+ tokenOrigins:
+ - https://ghcr.io
+ blobRedirectOrigins:
+ - https://pkg-containers.githubusercontent.com
+ caBundlePath: null
+ tokenCaBundlePaths: {}
+ blobRedirectCaBundlePaths: {}
+ allowNonPublic: false
mcpServers: {}
bundles:
trustedPublishers: {}
From 856f8e6c143f01ef4d590b3d45c428685e36d12b Mon Sep 17 00:00:00 2001
From: Alexander Rymdeko-Harvey
<8761706+killswitch-GUI@users.noreply.github.com>
Date: Fri, 2 Oct 2026 09:14:26 -0400
Subject: [PATCH 02/11] feat(desktop): manage signed Outlook session companion
---
apps/desktop/src-tauri/Cargo.lock | 842 +++++++++++++++++-
apps/desktop/src-tauri/Cargo.toml | 1 +
apps/desktop/src-tauri/build.rs | 2 +
.../src-tauri/capabilities/main-chat.json | 2 +
.../src-tauri/src/configuration_import.rs | 1 +
.../desktop/src-tauri/src/desktop_settings.rs | 7 +
apps/desktop/src-tauri/src/lib.rs | 9 +-
.../src-tauri/src/managed_configuration.rs | 1 +
.../src/managed_configuration_commands.rs | 1 +
.../catalog_deletion/tests.rs | 1 +
.../mcp_deletion/tests.rs | 1 +
apps/desktop/src-tauri/src/managed_runtime.rs | 163 +++-
.../managed_runtime/credential_acceptance.rs | 1 +
.../src-tauri/src/outlook_companion.rs | 235 +++++
apps/desktop/src-tauri/src/plugin_commands.rs | 116 ++-
.../desktop/src-tauri/src/provider_catalog.rs | 1 +
apps/desktop/src-tauri/src/state.rs | 86 +-
apps/desktop/src/api.ts | 17 +-
.../components/OutlookCompanionControls.tsx | 125 +++
apps/desktop/src/components/PluginDetail.tsx | 14 +
.../src/components/PluginMcpControls.tsx | 10 +-
apps/desktop/src/plugins.ts | 6 +
docs/desktop/capabilities.md | 5 +
docs/extend/plugins.md | 17 +-
24 files changed, 1647 insertions(+), 17 deletions(-)
create mode 100644 apps/desktop/src-tauri/src/outlook_companion.rs
create mode 100644 apps/desktop/src/components/OutlookCompanionControls.tsx
diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock
index ff291c74b..9f03451a2 100644
--- a/apps/desktop/src-tauri/Cargo.lock
+++ b/apps/desktop/src-tauri/Cargo.lock
@@ -38,6 +38,7 @@ dependencies = [
"cfg-if",
"getrandom 0.3.4",
"once_cell",
+ "serde",
"version_check",
"zerocopy",
]
@@ -329,6 +330,30 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+[[package]]
+name = "aws-lc-rs"
+version = "1.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
+dependencies = [
+ "aws-lc-sys",
+ "untrusted 0.7.1",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "pkg-config",
+]
+
[[package]]
name = "axum"
version = "0.8.9"
@@ -475,6 +500,12 @@ dependencies = [
"piper",
]
+[[package]]
+name = "borrow-or-share"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
+
[[package]]
name = "brotli"
version = "8.0.4"
@@ -505,12 +536,28 @@ dependencies = [
"tinyvec",
]
+[[package]]
+name = "bstr"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79"
+dependencies = [
+ "memchr",
+ "serde_core",
+]
+
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+[[package]]
+name = "bytecount"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
+
[[package]]
name = "bytemuck"
version = "1.25.2"
@@ -729,6 +776,39 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cmpv2"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "961b955a666e25ee5a1091d219128d6e6401e3dab84efb1a2bf6b4035d797b39"
+dependencies = [
+ "crmf",
+ "der",
+ "spki",
+ "x509-cert",
+]
+
+[[package]]
+name = "cms"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730"
+dependencies = [
+ "const-oid",
+ "der",
+ "spki",
+ "x509-cert",
+]
+
[[package]]
name = "colossus-api"
version = "0.11.6"
@@ -833,6 +913,7 @@ dependencies = [
"colossus-native-browser",
"colossus-native-credential-ui",
"colossus-network",
+ "colossus-plugins",
"colossus-ports",
"colossus-sdk",
"colossus-sidecar-protocol",
@@ -929,6 +1010,27 @@ dependencies = [
"thiserror 2.0.19",
]
+[[package]]
+name = "colossus-journal-redb"
+version = "0.11.6"
+dependencies = [
+ "base64 0.22.1",
+ "chacha20poly1305",
+ "colossus-contracts",
+ "colossus-ports",
+ "ed25519-dalek",
+ "fs4",
+ "getrandom 0.4.3",
+ "hex",
+ "keyring",
+ "redb",
+ "serde",
+ "serde_json",
+ "sha2",
+ "time",
+ "uuid",
+]
+
[[package]]
name = "colossus-linux-native"
version = "0.11.6"
@@ -1013,6 +1115,37 @@ dependencies = [
"url",
]
+[[package]]
+name = "colossus-plugins"
+version = "0.11.6"
+dependencies = [
+ "base64 0.22.1",
+ "colossus-contracts",
+ "colossus-journal-redb",
+ "colossus-network",
+ "colossus-ports",
+ "colossus-windows-native",
+ "flate2",
+ "fs4",
+ "futures",
+ "hex",
+ "http",
+ "image",
+ "jsonschema",
+ "reqwest 0.12.28",
+ "rustix",
+ "serde",
+ "serde-saphyr",
+ "serde_json",
+ "sha2",
+ "sigstore-verify",
+ "tar",
+ "time",
+ "tokio",
+ "url",
+ "uuid",
+]
+
[[package]]
name = "colossus-ports"
version = "0.11.6"
@@ -1204,6 +1337,18 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "crmf"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36fe21b96d5b87f5de4b5b7202ec41c00110ac817ce6728fe75fb2fe5962ed92"
+dependencies = [
+ "cms",
+ "der",
+ "spki",
+ "x509-cert",
+]
+
[[package]]
name = "crossbeam-channel"
version = "0.5.16"
@@ -1281,6 +1426,33 @@ version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "darling"
version = "0.23.0"
@@ -1332,6 +1504,37 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.19",
+]
+
[[package]]
name = "der"
version = "0.7.10"
@@ -1339,6 +1542,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
+ "der_derive",
+ "flagset",
"pem-rfc7468",
"zeroize",
]
@@ -1357,6 +1562,17 @@ dependencies = [
"rusticata-macros",
]
+[[package]]
+name = "der_derive"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "deranged"
version = "0.5.8"
@@ -1572,6 +1788,31 @@ dependencies = [
"spki",
]
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand_core 0.6.4",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "either"
version = "1.16.0"
@@ -1598,6 +1839,15 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "email_address"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "embed-resource"
version = "3.0.11"
@@ -1710,6 +1960,17 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "fancy-regex"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
+dependencies = [
+ "bit-set",
+ "regex-automata",
+ "regex-syntax",
+]
+
[[package]]
name = "fastrand"
version = "2.5.0"
@@ -1735,6 +1996,12 @@ dependencies = [
"subtle",
]
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
[[package]]
name = "field-offset"
version = "0.3.6"
@@ -1778,6 +2045,12 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+[[package]]
+name = "flagset"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
+
[[package]]
name = "flate2"
version = "1.1.9"
@@ -1788,6 +2061,17 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "fluent-uri"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
+dependencies = [
+ "borrow-or-share",
+ "ref-cast",
+ "serde",
+]
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1842,6 +2126,16 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fraction"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872"
+dependencies = [
+ "lazy_static",
+ "num",
+]
+
[[package]]
name = "fs4"
version = "0.13.1"
@@ -1852,6 +2146,12 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
[[package]]
name = "futures"
version = "0.3.33"
@@ -2201,6 +2501,19 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+[[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
[[package]]
name = "gobject-sys"
version = "0.18.0"
@@ -2751,6 +3064,48 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "jiff"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e"
+dependencies = [
+ "defmt",
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
[[package]]
name = "jni"
version = "0.21.1"
@@ -2767,6 +3122,36 @@ dependencies = [
"windows-sys 0.45.0",
]
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.19",
+ "walkdir",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
[[package]]
name = "jni-sys"
version = "0.3.1"
@@ -2838,6 +3223,33 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "jsonschema"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d46662859bc5f60a145b75f4632fbadc84e829e45df6c5de74cfc8e05acb96b5"
+dependencies = [
+ "ahash",
+ "base64 0.22.1",
+ "bytecount",
+ "email_address",
+ "fancy-regex",
+ "fraction",
+ "idna",
+ "itoa",
+ "num-cmp",
+ "num-traits",
+ "once_cell",
+ "percent-encoding",
+ "referencing",
+ "regex",
+ "regex-syntax",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "uuid-simd",
+]
+
[[package]]
name = "keyboard-types"
version = "0.7.0"
@@ -3230,6 +3642,12 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "num-cmp"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
+
[[package]]
name = "num-complex"
version = "0.4.6"
@@ -3590,6 +4008,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
[[package]]
name = "opentelemetry"
version = "0.32.0"
@@ -3652,6 +4076,12 @@ dependencies = [
"thiserror 2.0.19",
]
+[[package]]
+name = "outref"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
+
[[package]]
name = "p256"
version = "0.13.2"
@@ -3930,6 +4360,15 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
+]
+
[[package]]
name = "portable-pty"
version = "0.9.0"
@@ -4246,6 +4685,7 @@ version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
dependencies = [
+ "aws-lc-rs",
"bytes",
"getrandom 0.4.3",
"lru-slab",
@@ -4431,6 +4871,20 @@ dependencies = [
"syn 3.0.2",
]
+[[package]]
+name = "referencing"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9c261f7ce75418b3beadfb3f0eb1299fe8eb9640deba45ffa2cb783098697d"
+dependencies = [
+ "ahash",
+ "fluent-uri",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "serde_json",
+]
+
[[package]]
name = "regex"
version = "1.13.1"
@@ -4468,6 +4922,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
+ "futures-channel",
"futures-core",
"futures-util",
"http",
@@ -4515,15 +4970,22 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
+ "hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
"serde",
"serde_json",
+ "serde_urlencoded",
"sync_wrapper",
"tokio",
+ "tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -4579,7 +5041,7 @@ dependencies = [
"cfg-if",
"getrandom 0.2.17",
"libc",
- "untrusted",
+ "untrusted 0.9.0",
"windows-sys 0.52.0",
]
@@ -4626,6 +5088,7 @@ version = "0.23.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [
+ "aws-lc-rs",
"log",
"once_cell",
"ring",
@@ -4635,6 +5098,18 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
[[package]]
name = "rustls-pki-types"
version = "1.15.0"
@@ -4645,15 +5120,43 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "jni 0.22.4",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
[[package]]
name = "rustls-webpki"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
+ "aws-lc-rs",
"ring",
"rustls-pki-types",
- "untrusted",
+ "untrusted 0.9.0",
]
[[package]]
@@ -4668,6 +5171,12 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+[[package]]
+name = "ryu-js"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15"
+
[[package]]
name = "same-file"
version = "1.0.6"
@@ -4677,6 +5186,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "schemars"
version = "0.8.22"
@@ -4904,6 +5422,17 @@ dependencies = [
"zmij",
]
+[[package]]
+name = "serde_json_canonicalizer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe52319a927259afbfa5180c5157cd8167edfd3e8c254f9558c7fef44c5649f2"
+dependencies = [
+ "ryu-js",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "serde_repr"
version = "0.1.21"
@@ -5019,6 +5548,17 @@ dependencies = [
"stable_deref_trait",
]
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
[[package]]
name = "sha2"
version = "0.10.9"
@@ -5081,12 +5621,209 @@ dependencies = [
"rand_core 0.6.4",
]
+[[package]]
+name = "sigstore-bundle"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94ba33ef96bafd16f5dfc954113c37a689dcf721816d11680667626b1ebb0496"
+dependencies = [
+ "base64 0.22.1",
+ "hex",
+ "serde",
+ "serde_json",
+ "sigstore-crypto",
+ "sigstore-rekor",
+ "sigstore-tsa",
+ "sigstore-types",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "sigstore-crypto"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ecc48bf9facc710e772b3e7ab6a3ebc81c34be208a6ebf6c872d0ccbb8b52f"
+dependencies = [
+ "aws-lc-rs",
+ "base64 0.22.1",
+ "const-oid",
+ "der",
+ "digest",
+ "pem",
+ "rand_core 0.9.5",
+ "sha2",
+ "signature",
+ "sigstore-types",
+ "spki",
+ "thiserror 2.0.19",
+ "tracing",
+ "x509-cert",
+]
+
+[[package]]
+name = "sigstore-merkle"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f6fc5c295b5b9fa929e38acc53b29c8ed9ba3ee93e5fa87e7daa44072f0b26e"
+dependencies = [
+ "base64 0.22.1",
+ "hex",
+ "sigstore-crypto",
+ "sigstore-types",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "sigstore-rekor"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ece0d614b93c94e2ddab5c2142959396ce5a5f1d315435779e8ce0f9da250f5d"
+dependencies = [
+ "base64 0.22.1",
+ "hex",
+ "reqwest 0.13.4",
+ "serde",
+ "serde_json",
+ "sigstore-crypto",
+ "sigstore-merkle",
+ "sigstore-types",
+ "thiserror 2.0.19",
+ "url",
+]
+
+[[package]]
+name = "sigstore-trust-root"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a91dbe81d236075c568926b5540d86dbc0e272cd0c6cfc5523e35d5200e71cf0"
+dependencies = [
+ "base64 0.22.1",
+ "hex",
+ "jiff",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "sigstore-crypto",
+ "sigstore-tuf",
+ "sigstore-types",
+ "thiserror 2.0.19",
+ "x509-cert",
+]
+
+[[package]]
+name = "sigstore-tsa"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a04c841cdc52fc5da6284d6aa1fbf93f6b65d71437b2cc04751d117eac8aa33"
+dependencies = [
+ "aws-lc-rs",
+ "base64 0.22.1",
+ "cmpv2",
+ "cms",
+ "const-oid",
+ "der",
+ "hex",
+ "jiff",
+ "rand 0.9.5",
+ "reqwest 0.13.4",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "sigstore-crypto",
+ "sigstore-types",
+ "thiserror 2.0.19",
+ "tracing",
+ "x509-cert",
+ "x509-tsp",
+]
+
+[[package]]
+name = "sigstore-tuf"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556727ed8c836bcfee6a151a3201e8605af30ef0e01dd9021a2fd43c3f5269f4"
+dependencies = [
+ "globset",
+ "hex",
+ "jiff",
+ "reqwest 0.13.4",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sigstore-crypto",
+ "sigstore-types",
+ "tempfile",
+ "thiserror 2.0.19",
+ "tokio",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "sigstore-types"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a48661f5c7a4f7496de80865e5b258adac0bc514164e6c944b93b65ba5de334b"
+dependencies = [
+ "base64 0.22.1",
+ "hex",
+ "pem",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "sigstore-verify"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c4fed99a3609ebb236284b781674d8c45beea93bbc823ef2facd2261b3801c7"
+dependencies = [
+ "base64 0.22.1",
+ "cms",
+ "const-oid",
+ "hex",
+ "jiff",
+ "pem",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "serde",
+ "serde_json",
+ "serde_json_canonicalizer",
+ "sigstore-bundle",
+ "sigstore-crypto",
+ "sigstore-merkle",
+ "sigstore-rekor",
+ "sigstore-trust-root",
+ "sigstore-tsa",
+ "sigstore-types",
+ "thiserror 2.0.19",
+ "tls_codec",
+ "tracing",
+ "x509-cert",
+]
+
[[package]]
name = "simd-adler32"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+[[package]]
+name = "simd_cesu8"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
[[package]]
name = "siphasher"
version = "1.0.3"
@@ -5323,7 +6060,7 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
- "jni",
+ "jni 0.21.1",
"libc",
"log",
"ndk",
@@ -5391,7 +6128,7 @@ dependencies = [
"heck 0.5.0",
"http",
"image",
- "jni",
+ "jni 0.21.1",
"libc",
"log",
"mime",
@@ -5621,7 +6358,7 @@ dependencies = [
"dpi",
"gtk",
"http",
- "jni",
+ "jni 0.21.1",
"objc2",
"objc2-ui-kit",
"objc2-web-kit",
@@ -5644,7 +6381,7 @@ checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
dependencies = [
"gtk",
"http",
- "jni",
+ "jni 0.21.1",
"log",
"objc2",
"objc2-app-kit",
@@ -5848,6 +6585,27 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+[[package]]
+name = "tls_codec"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b"
+dependencies = [
+ "tls_codec_derive",
+ "zeroize",
+]
+
+[[package]]
+name = "tls_codec_derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "tokio"
version = "1.53.1"
@@ -5857,6 +6615,7 @@ dependencies = [
"bytes",
"libc",
"mio",
+ "parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@@ -6343,6 +7102,12 @@ dependencies = [
"subtle",
]
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -6392,6 +7157,17 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "uuid-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
+dependencies = [
+ "outref",
+ "uuid",
+ "vsimd",
+]
+
[[package]]
name = "valuable"
version = "0.1.1"
@@ -6416,6 +7192,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
[[package]]
name = "vswhom"
version = "0.1.0"
@@ -6627,6 +7409,15 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "webpki-roots"
version = "1.0.9"
@@ -7243,7 +8034,7 @@ dependencies = [
"gtk",
"http",
"javascriptcore-rs",
- "jni",
+ "jni 0.21.1",
"libc",
"ndk",
"objc2",
@@ -7290,6 +8081,20 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "x509-cert"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
+dependencies = [
+ "const-oid",
+ "der",
+ "sha1",
+ "signature",
+ "spki",
+ "tls_codec",
+]
+
[[package]]
name = "x509-parser"
version = "0.18.1"
@@ -7308,6 +8113,17 @@ dependencies = [
"time",
]
+[[package]]
+name = "x509-tsp"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5ceece934a21607055b7ac5c25adb56a2ff559804b10705dc674d1d838c15e1"
+dependencies = [
+ "cmpv2",
+ "cms",
+ "der",
+]
+
[[package]]
name = "xattr"
version = "1.6.1"
@@ -7471,6 +8287,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
dependencies = [
"serde",
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
]
[[package]]
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
index 8c7b91785..3bfd7ba8a 100644
--- a/apps/desktop/src-tauri/Cargo.toml
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -58,6 +58,7 @@ colossus-network = { version = "=0.11.6", path = "../../../crates/colossus-netwo
colossus-native-credential-ui = { version = "=0.1.0", path = "../native-credential-ui" }
colossus-native-browser = { version = "=0.1.0", path = "../native-browser" }
colossus-ports = { version = "=0.11.6", path = "../../../crates/colossus-ports" }
+colossus-plugins = { version = "=0.11.6", path = "../../../crates/colossus-plugins" }
colossus-sdk = { version = "=0.11.6", path = "../../../crates/colossus-sdk", features = ["daemon", "keyring", "sidecar"] }
colossus-sidecar-protocol = { version = "=0.11.6", path = "../../../crates/colossus-sidecar-protocol" }
colossus-worker-protocol = { version = "=0.11.6", path = "../../../crates/colossus-worker-protocol" }
diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs
index 8701293a8..02090461b 100644
--- a/apps/desktop/src-tauri/build.rs
+++ b/apps/desktop/src-tauri/build.rs
@@ -23,6 +23,8 @@ const COMMANDS: &[&str] = &[
"read_plugin_preview",
"manage_plugin",
"cancel_plugin_operation",
+ "outlook_companion_status",
+ "configure_outlook_companion",
"desktop_release_channel",
"desktop_release_metadata",
"check_desktop_update",
diff --git a/apps/desktop/src-tauri/capabilities/main-chat.json b/apps/desktop/src-tauri/capabilities/main-chat.json
index 8ad9c0893..a2c997bb5 100644
--- a/apps/desktop/src-tauri/capabilities/main-chat.json
+++ b/apps/desktop/src-tauri/capabilities/main-chat.json
@@ -20,6 +20,8 @@
"allow-read-plugin-preview",
"allow-manage-plugin",
"allow-cancel-plugin-operation",
+ "allow-outlook-companion-status",
+ "allow-configure-outlook-companion",
"allow-desktop-release-channel",
"allow-desktop-release-metadata",
"allow-export-diagnostics",
diff --git a/apps/desktop/src-tauri/src/configuration_import.rs b/apps/desktop/src-tauri/src/configuration_import.rs
index b381c2aba..1e7d16ed2 100644
--- a/apps/desktop/src-tauri/src/configuration_import.rs
+++ b/apps/desktop/src-tauri/src/configuration_import.rs
@@ -1349,6 +1349,7 @@ mod tests {
access_profile: AccessProfileSetting::AllowAll,
execution_boundary: ExecutionBoundarySetting::FullAccess,
terminal_enabled: false,
+ outlook_companion_enabled: false,
configuration: crate::managed_configuration::SpaceConfigurationSetting {
accepted_global_revision: 1,
..crate::managed_configuration::SpaceConfigurationSetting::default()
diff --git a/apps/desktop/src-tauri/src/desktop_settings.rs b/apps/desktop/src-tauri/src/desktop_settings.rs
index 1c84a1817..32945589a 100644
--- a/apps/desktop/src-tauri/src/desktop_settings.rs
+++ b/apps/desktop/src-tauri/src/desktop_settings.rs
@@ -157,6 +157,10 @@ pub(crate) struct WorkspaceProfile {
pub(crate) access_profile: AccessProfileSetting,
pub(crate) execution_boundary: ExecutionBoundarySetting,
pub(crate) terminal_enabled: bool,
+ /// Explicit per-Workspace consent to attach a trusted Outlook plugin to the
+ /// logged-in Windows session. This is never inherited by a new Workspace.
+ #[serde(default)]
+ pub(crate) outlook_companion_enabled: bool,
/// Sparse inherited configuration and pinned global catalog revisions. The legacy
/// concrete fields above remain the selected-runtime compatibility projection.
#[serde(default)]
@@ -599,6 +603,7 @@ impl DesktopSettings {
terminal_enabled: defaults
.and_then(|d| d.terminal_enabled)
.unwrap_or(self.terminal_enabled),
+ outlook_companion_enabled: false,
configuration: SpaceConfigurationSetting {
accepted_global_revision: self.global_configuration.revision,
..SpaceConfigurationSetting::default()
@@ -687,6 +692,7 @@ impl DesktopSettings {
access_profile: self.access_profile,
execution_boundary: self.execution_boundary,
terminal_enabled: self.terminal_enabled,
+ outlook_companion_enabled: false,
configuration: SpaceConfigurationSetting {
accepted_global_revision: self.global_configuration.revision,
..SpaceConfigurationSetting::default()
@@ -2396,6 +2402,7 @@ mod tests {
access_profile: settings.access_profile,
execution_boundary: settings.execution_boundary,
terminal_enabled: settings.terminal_enabled,
+ outlook_companion_enabled: false,
configuration: SpaceConfigurationSetting::default(),
});
let mut encoded = serde_json::to_value(settings).expect("settings");
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index a69fc7962..971fd0476 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -22,6 +22,8 @@ mod managed_configuration_commands;
mod managed_diagnostics;
mod managed_runtime;
mod mcp_health;
+#[cfg(windows)]
+mod outlook_companion;
mod plugin_adapter;
mod plugin_commands;
mod plugin_selection;
@@ -87,7 +89,8 @@ use managed_diagnostics::{
managed_mcp_oauth_status,
};
use plugin_commands::{
- cancel_plugin_operation, get_plugin_inventory, manage_plugin, read_plugin_preview,
+ cancel_plugin_operation, configure_outlook_companion, get_plugin_inventory, manage_plugin,
+ outlook_companion_status, read_plugin_preview,
};
use plugin_selection::resolve_plugin_selection;
use provider_catalog::{discover_managed_provider_models, get_provider_presets};
@@ -156,6 +159,8 @@ pub fn run() {
.setup(|app| {
status_bar::setup(app)?;
browser::start_watchdog(app.handle().clone());
+ #[cfg(windows)]
+ outlook_companion::start_watchdog(app.handle().clone());
terminal_commands::pane::start_watchdog(app.handle().clone());
Ok(())
})
@@ -187,6 +192,8 @@ pub fn run() {
read_plugin_preview,
manage_plugin,
cancel_plugin_operation,
+ outlook_companion_status,
+ configure_outlook_companion,
desktop_release_channel,
desktop_release_metadata,
check_desktop_update,
diff --git a/apps/desktop/src-tauri/src/managed_configuration.rs b/apps/desktop/src-tauri/src/managed_configuration.rs
index 24ad00789..ea321f4f3 100644
--- a/apps/desktop/src-tauri/src/managed_configuration.rs
+++ b/apps/desktop/src-tauri/src/managed_configuration.rs
@@ -1042,6 +1042,7 @@ mod tests {
access_profile: AccessProfileSetting::AllowAll,
execution_boundary: ExecutionBoundarySetting::FullAccess,
terminal_enabled: false,
+ outlook_companion_enabled: false,
configuration: SpaceConfigurationSetting::default(),
}
}
diff --git a/apps/desktop/src-tauri/src/managed_configuration_commands.rs b/apps/desktop/src-tauri/src/managed_configuration_commands.rs
index 8907d3d7f..b52df018e 100644
--- a/apps/desktop/src-tauri/src/managed_configuration_commands.rs
+++ b/apps/desktop/src-tauri/src/managed_configuration_commands.rs
@@ -2253,6 +2253,7 @@ mod tests {
access_profile: AccessProfileSetting::AllowAll,
execution_boundary: ExecutionBoundarySetting::FullAccess,
terminal_enabled: false,
+ outlook_companion_enabled: false,
configuration: SpaceConfigurationSetting {
accepted_global_revision: 1,
..SpaceConfigurationSetting::default()
diff --git a/apps/desktop/src-tauri/src/managed_configuration_commands/catalog_deletion/tests.rs b/apps/desktop/src-tauri/src/managed_configuration_commands/catalog_deletion/tests.rs
index 0947cecbc..6aaa66c54 100644
--- a/apps/desktop/src-tauri/src/managed_configuration_commands/catalog_deletion/tests.rs
+++ b/apps/desktop/src-tauri/src/managed_configuration_commands/catalog_deletion/tests.rs
@@ -103,6 +103,7 @@ fn workspace(name: &str, revision: u64) -> WorkspaceProfile {
access_profile: AccessProfileSetting::Minimal,
execution_boundary: ExecutionBoundarySetting::WorkspaceIsolated,
terminal_enabled: false,
+ outlook_companion_enabled: false,
configuration: SpaceConfigurationSetting {
accepted_global_revision: revision,
..SpaceConfigurationSetting::default()
diff --git a/apps/desktop/src-tauri/src/managed_configuration_commands/mcp_deletion/tests.rs b/apps/desktop/src-tauri/src/managed_configuration_commands/mcp_deletion/tests.rs
index e2a514b1e..f3d301c79 100644
--- a/apps/desktop/src-tauri/src/managed_configuration_commands/mcp_deletion/tests.rs
+++ b/apps/desktop/src-tauri/src/managed_configuration_commands/mcp_deletion/tests.rs
@@ -76,6 +76,7 @@ fn workspace(name: &str, accepted_revision: u64) -> WorkspaceProfile {
access_profile: AccessProfileSetting::Minimal,
execution_boundary: ExecutionBoundarySetting::WorkspaceIsolated,
terminal_enabled: false,
+ outlook_companion_enabled: false,
configuration: SpaceConfigurationSetting {
accepted_global_revision: accepted_revision,
..SpaceConfigurationSetting::default()
diff --git a/apps/desktop/src-tauri/src/managed_runtime.rs b/apps/desktop/src-tauri/src/managed_runtime.rs
index 4711f2c31..3a8886668 100644
--- a/apps/desktop/src-tauri/src/managed_runtime.rs
+++ b/apps/desktop/src-tauri/src/managed_runtime.rs
@@ -1,3 +1,4 @@
+use colossus_contracts::HostSecret;
use colossus_sdk::{
ApiMajor, ApiScope, AppPrivateInstanceDir, Colossus, CreateRunRequest, GetRunRequest,
IdempotencyKey, InputContentPart, InstanceId, ListRunsRequest, ManagedAccessProfile,
@@ -55,6 +56,8 @@ const ACTIVE_RUN_PAGE_SIZE: u32 = 100;
const MAX_ACTIVE_RUN_PAGES: usize = 4_096;
const CONFIGURATION_DRAIN_POLL_INTERVAL: Duration = Duration::from_millis(250);
const CONFIGURATION_DRAIN_TIMEOUT: Duration = Duration::from_mins(5);
+const OUTLOOK_SESSION_SERVER: &str = "outlook-session";
+const OUTLOOK_SESSION_CREDENTIAL: &str = "outlook-companion-session";
const PRIMARY_SCOPES: [&str; 7] = [
scopes::EXTENSIONS_READ,
scopes::RUNS_EXECUTE,
@@ -227,6 +230,8 @@ async fn start_after_operation_drain(
Ok(())
}
Err((error, failure_code)) => {
+ #[cfg(windows)]
+ state.stop_outlook_companion_for(space_id).await;
state
.clear_managed_lifecycle_for(space_id, lifecycle_generation)
.await;
@@ -681,6 +686,7 @@ async fn start_inner(
ApiMajor::new(1).map_err(|error| classify_sdk(error, RuntimeFailureCodeDto::Internal))?,
)
.map_err(|error| classify_sdk(error, RuntimeFailureCodeDto::Configuration))?;
+ let companion = start_outlook_companion(state, settings, space_id, &colossus_home).await?;
let PreparedManagedBootstrap {
bootstrap,
worker_authentication,
@@ -692,6 +698,7 @@ async fn start_inner(
settings,
&DesktopCredentials::for_settings(state, store)
.map_err(|error| (error, RuntimeFailureCodeDto::Provider))?,
+ companion,
)
.await?;
let lifecycle = NativeSidecarLifecycle::new(bootstrap);
@@ -765,12 +772,47 @@ struct PreparedManagedBootstrap {
terminal_enabled: bool,
}
+struct CompanionBootstrap {
+ endpoint: String,
+ credential: HostSecret,
+}
+
+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)
+ {
+ 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)
+}
+
async fn prepare_managed_bootstrap(
workspace: &Path,
workspace_identity: WorkspaceIdentity,
store: &SettingsStore,
settings: &DesktopSettings,
credentials: &std::sync::Arc,
+ companion: Option,
) -> Result {
let space = settings
.selected_space_id
@@ -817,6 +859,7 @@ async fn prepare_managed_bootstrap(
approval_broker_grant,
worker_bootstrap_secret.as_ref(),
&paths,
+ companion,
)
.map_err(|error| classify_sdk(error, RuntimeFailureCodeDto::Configuration))?;
if let Some(identity) = settings.client_identity.as_ref() {
@@ -848,8 +891,17 @@ fn managed_bootstrap(
approval_broker_grant: SidecarApprovalBrokerGrant,
worker_authentication: &[u8],
paths: &ManagedBootstrapPaths<'_>,
+ companion: Option,
) -> Result {
- let runtime = managed_runtime_config(resolved);
+ let mut runtime = managed_runtime_config(resolved);
+ let mut host_credentials = host_credentials;
+ if let Some(companion) = companion {
+ configure_outlook_companion(&mut runtime, &companion.endpoint)?;
+ host_credentials.push(SidecarHostCredential::new(
+ OUTLOOK_SESSION_CREDENTIAL,
+ companion.credential,
+ )?);
+ }
let bootstrap = SidecarBootstrapConfig::new(
workspace,
runtime,
@@ -873,6 +925,115 @@ fn managed_bootstrap(
}
}
+fn configure_outlook_companion(
+ runtime: &mut ManagedRuntimeConfig,
+ endpoint: &str,
+) -> Result<(), SdkError> {
+ if runtime
+ .mcp_servers
+ .iter()
+ .any(|server| server.name == OUTLOOK_SESSION_SERVER)
+ {
+ return Err(SdkError::InvalidConfiguration(
+ "Outlook companion server name conflicts with another MCP server",
+ ));
+ }
+ // An enabled portable stdio declaration still runs inside windows_job. The
+ // companion replaces that one connection for this Workspace only.
+ if let Some(field) = runtime
+ .field_overrides
+ .iter_mut()
+ .find(|field| field.field_id == "plugins.mcpServers")
+ && let Some(overlay) = field.value.as_object_mut()
+ && let Some(mail) = overlay.get_mut("outlook-classic/mail")
+ && let Some(mail) = mail.as_object_mut()
+ {
+ mail.insert("enabled".into(), serde_json::Value::Bool(false));
+ }
+ runtime.mcp_servers.push(ManagedMcpServerConfig {
+ name: OUTLOOK_SESSION_SERVER.into(),
+ transport: ManagedMcpTransport::StreamableHttp,
+ command: None,
+ args: Vec::new(),
+ working_directory: None,
+ environment_credentials: BTreeMap::new(),
+ url: Some(endpoint.into()),
+ headers: BTreeMap::new(),
+ credential_headers: BTreeMap::from([(
+ "Authorization".into(),
+ ManagedMcpCredentialHeader {
+ scheme: Some("Bearer".into()),
+ credential_id: OUTLOOK_SESSION_CREDENTIAL.into(),
+ },
+ )]),
+ allow_stateless: true,
+ oauth: None,
+ allowed_tools: [
+ "get_status",
+ "list_stores",
+ "list_folders",
+ "get_mail_folders",
+ "list_messages",
+ "search_messages",
+ "get_message",
+ "list_attachments",
+ "mark_message_read",
+ "move_message",
+ "archive_message",
+ "delete_message",
+ "create_draft",
+ "update_draft",
+ ]
+ .into_iter()
+ .map(str::to_owned)
+ .collect(),
+ research_tools: Vec::new(),
+ timeout_ms: Some(30_000),
+ max_output_bytes: Some(65_536),
+ });
+ Ok(())
+}
+
+#[cfg(test)]
+mod outlook_companion_tests {
+ use super::*;
+ use serde_json::json;
+
+ #[test]
+ fn companion_uses_exact_http_endpoint_and_disables_portable_stdio() {
+ let mut runtime = ManagedRuntimeConfig::echo(ManagedAccessProfile::Development);
+ runtime.field_overrides.push(ManagedFieldOverride {
+ field_id: "plugins.mcpServers".into(),
+ value: json!({
+ "outlook-classic/mail": {"enabled": true, "allowedTools": ["*"]},
+ "other/plugin": {"enabled": true}
+ }),
+ });
+ configure_outlook_companion(&mut runtime, "http://127.0.0.1:64123/mcp")
+ .expect("configure companion");
+ let server = runtime.mcp_servers.last().expect("companion server");
+ assert_eq!(server.name, OUTLOOK_SESSION_SERVER);
+ assert_eq!(server.url.as_deref(), Some("http://127.0.0.1:64123/mcp"));
+ assert_eq!(server.transport, ManagedMcpTransport::StreamableHttp);
+ assert!(server.allow_stateless);
+ assert_eq!(
+ server.credential_headers["Authorization"].scheme.as_deref(),
+ Some("Bearer")
+ );
+ assert_eq!(server.allowed_tools.len(), 14);
+ assert!(!server.allowed_tools.iter().any(|tool| tool == "*"));
+ assert_eq!(
+ runtime.field_overrides[0].value["outlook-classic/mail"]["enabled"],
+ false
+ );
+ assert_eq!(
+ runtime.field_overrides[0].value["other/plugin"]["enabled"],
+ true
+ );
+ runtime.validate().expect("valid managed transport");
+ }
+}
+
#[allow(clippy::too_many_lines)]
pub(crate) fn managed_runtime_config(
resolved: &ResolvedSpaceConfiguration,
diff --git a/apps/desktop/src-tauri/src/managed_runtime/credential_acceptance.rs b/apps/desktop/src-tauri/src/managed_runtime/credential_acceptance.rs
index 38706d43f..b90c793fb 100644
--- a/apps/desktop/src-tauri/src/managed_runtime/credential_acceptance.rs
+++ b/apps/desktop/src-tauri/src/managed_runtime/credential_acceptance.rs
@@ -183,6 +183,7 @@ async fn native_backend_process_child() {
codex_auth: None,
colossus_home: &home,
},
+ None,
)
.unwrap();
let identity = settings.load().unwrap().client_identity.unwrap();
diff --git a/apps/desktop/src-tauri/src/outlook_companion.rs b/apps/desktop/src-tauri/src/outlook_companion.rs
new file mode 100644
index 000000000..8097b5b86
--- /dev/null
+++ b/apps/desktop/src-tauri/src/outlook_companion.rs
@@ -0,0 +1,235 @@
+//! Native-owned bridge to classic Outlook in the logged-in Windows session.
+//! The ordinary plugin MCP process continues to use the configured sandbox.
+
+use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
+use colossus_contracts::{HostSecret, PluginMcpTransport};
+use colossus_plugins::{PluginSnapshotLease, PluginStore};
+use serde::Deserialize;
+use std::{os::windows::process::CommandExt as _, path::Path, process::Stdio, time::Duration};
+use tauri::{AppHandle, Manager as _};
+use tokio::{
+ io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _, BufReader},
+ process::{Child, ChildStdin, Command},
+ time::timeout,
+};
+use url::Url;
+use zeroize::{Zeroize as _, Zeroizing};
+
+use crate::dto::CommandErrorDto;
+
+const PLUGIN_NAME: &str = "outlook-classic";
+const COMPONENT_ID: &str = "outlook-classic/mail";
+const SIGNER: &str = "https://token.actions.githubusercontent.com|https://github.com/obscuritylabs/colossus-plugins/.github/workflows/plugins.yml@refs/heads/main";
+const START_TIMEOUT: Duration = Duration::from_secs(15);
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+struct Handshake {
+ endpoint: String,
+ process_id: u32,
+}
+
+pub(crate) struct OutlookCompanion {
+ child: Child,
+ stdin: ChildStdin,
+ // A plugin update or GC cannot replace this verified executable while it runs.
+ _lease: PluginSnapshotLease,
+ pub(crate) endpoint: String,
+ pub(crate) digest: String,
+}
+
+impl OutlookCompanion {
+ pub(crate) async fn start(home: &Path) -> Result<(Self, HostSecret), CommandErrorDto> {
+ let home = home.to_path_buf();
+ let (executable, digest, lease) = tokio::task::spawn_blocking(move || {
+ let store = PluginStore::new(home).map_err(|_| unavailable())?;
+ let (records, lease) = store
+ .snapshot_with_lease(&[PLUGIN_NAME.into()], &[])
+ .map_err(|_| unavailable())?;
+ let plugin = records
+ .into_iter()
+ .find(|record| record.installation.manifest.name == PLUGIN_NAME)
+ .ok_or_else(unavailable)?;
+ if !plugin.installation.trust.trusted
+ || plugin.installation.trust.profile.as_deref() != Some("obscuritylabs")
+ || plugin.installation.trust.method != "sigstore-keyless"
+ || plugin.installation.trust.signer.as_deref() != Some(SIGNER)
+ || plugin.installation.manifest.repository.as_deref()
+ != Some("https://github.com/obscuritylabs/colossus-plugins")
+ || !plugin.mcp_servers.iter().any(|server| {
+ server.id == COMPONENT_ID
+ && server.transport == PluginMcpTransport::Stdio
+ && server.command.as_deref() == Some("./bin/outlook-classic-mcp.exe")
+ })
+ {
+ return Err(unavailable());
+ }
+ let root =
+ std::fs::canonicalize(&plugin.installation.root).map_err(|_| unavailable())?;
+ let executable = std::fs::canonicalize(root.join("bin/outlook-classic-mcp.exe"))
+ .map_err(|_| unavailable())?;
+ if !executable.starts_with(&root) || !executable.is_file() {
+ return Err(unavailable());
+ }
+ Ok((executable, plugin.installation.digest, lease))
+ })
+ .await
+ .map_err(|_| unavailable())??;
+
+ let mut random = [0_u8; 32];
+ getrandom::fill(&mut random).map_err(|_| unavailable())?;
+ let token = Zeroizing::new(URL_SAFE_NO_PAD.encode(random));
+ random.zeroize();
+ let credential = HostSecret::new(token.to_string()).map_err(|_| unavailable())?;
+ let mut command = Command::new(executable);
+ command
+ .arg("--http-companion")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .kill_on_drop(true);
+ command.as_std_mut().creation_flags(0x0800_0000); // CREATE_NO_WINDOW
+ let mut child = command.spawn().map_err(|_| unavailable())?;
+ let mut stdin = child.stdin.take().ok_or_else(unavailable)?;
+ let stdout = child.stdout.take().ok_or_else(unavailable)?;
+ stdin
+ .write_all(token.as_bytes())
+ .await
+ .map_err(|_| unavailable())?;
+ stdin.write_all(b"\n").await.map_err(|_| unavailable())?;
+ stdin.flush().await.map_err(|_| unavailable())?;
+ let mut line = String::new();
+ let mut reader = BufReader::new(stdout.take(513));
+ let bytes = timeout(START_TIMEOUT, reader.read_line(&mut line))
+ .await
+ .map_err(|_| unavailable())?
+ .map_err(|_| unavailable())?;
+ if bytes == 0 || bytes > 512 {
+ return Err(unavailable());
+ }
+ let handshake: Handshake = serde_json::from_str(&line).map_err(|_| unavailable())?;
+ if child.id() != Some(handshake.process_id) || !valid_endpoint(&handshake.endpoint) {
+ return Err(unavailable());
+ }
+ Ok((
+ Self {
+ child,
+ stdin,
+ _lease: lease,
+ endpoint: handshake.endpoint,
+ digest,
+ },
+ credential,
+ ))
+ }
+
+ pub(crate) async fn stop(mut self) {
+ drop(self.stdin);
+ if timeout(Duration::from_secs(3), self.child.wait())
+ .await
+ .is_err()
+ {
+ let _ = self.child.kill().await;
+ }
+ }
+
+ pub(crate) fn active_digest(&mut self) -> Option {
+ self.child
+ .try_wait()
+ .ok()
+ .filter(Option::is_none)
+ .map(|_| self.digest.clone())
+ }
+}
+
+fn valid_endpoint(endpoint: &str) -> bool {
+ Url::parse(endpoint).is_ok_and(|url| {
+ url.scheme() == "http"
+ && url.host_str() == Some("127.0.0.1")
+ && url.port().is_some_and(|port| port != 0)
+ && url.path() == "/mcp"
+ && url.username().is_empty()
+ && url.password().is_none()
+ && url.query().is_none()
+ && url.fragment().is_none()
+ })
+}
+
+fn unavailable() -> CommandErrorDto {
+ CommandErrorDto::local_sanitized(
+ "outlook_companion_unavailable",
+ "Install and activate a signed Obscurity Labs Outlook Classic plugin with companion support, then restart this Workspace.",
+ false,
+ )
+}
+
+/// Revoke a helper when a CLI or another Workspace activates a different
+/// digest. Desktop management revokes immediately; this covers external changes.
+pub(crate) fn start_watchdog(app: AppHandle) {
+ tauri::async_runtime::spawn(async move {
+ let mut interval = tokio::time::interval(Duration::from_secs(15));
+ loop {
+ interval.tick().await;
+ let state = app.state::();
+ let snapshots = state.outlook_companion_snapshots().await;
+ if snapshots.is_empty() {
+ continue;
+ }
+ let Ok(store) = crate::desktop_commands::settings_store() else {
+ for (space_id, digest) in snapshots {
+ state
+ .stop_outlook_companion_if_digest(&space_id, &digest)
+ .await;
+ }
+ continue;
+ };
+ let home = if let Ok(home) = store.home_root() {
+ home.to_path_buf()
+ } else {
+ for (space_id, digest) in snapshots {
+ state
+ .stop_outlook_companion_if_digest(&space_id, &digest)
+ .await;
+ }
+ continue;
+ };
+ let active = tokio::task::spawn_blocking(move || {
+ PluginStore::new(home)
+ .and_then(|store| store.active(PLUGIN_NAME))
+ .ok()
+ .flatten()
+ .map(|installation| installation.digest)
+ })
+ .await
+ .ok()
+ .flatten();
+ for (space_id, digest) in snapshots {
+ if active.as_deref() != Some(&digest) {
+ state
+ .stop_outlook_companion_if_digest(&space_id, &digest)
+ .await;
+ }
+ }
+ }
+ });
+}
+
+#[cfg(test)]
+mod tests {
+ use super::valid_endpoint;
+
+ #[test]
+ fn endpoint_accepts_only_one_ipv4_loopback_mcp_route() {
+ assert!(valid_endpoint("http://127.0.0.1:64123/mcp"));
+ for value in [
+ "http://localhost:64123/mcp",
+ "http://127.0.0.1/mcp",
+ "http://127.0.0.1:64123/other",
+ "http://127.0.0.1:64123/mcp?x=1",
+ "http://x@127.0.0.1:64123/mcp",
+ "https://127.0.0.1:64123/mcp",
+ ] {
+ assert!(!valid_endpoint(value), "{value}");
+ }
+ }
+}
diff --git a/apps/desktop/src-tauri/src/plugin_commands.rs b/apps/desktop/src-tauri/src/plugin_commands.rs
index 6dadc0131..54ce6e288 100644
--- a/apps/desktop/src-tauri/src/plugin_commands.rs
+++ b/apps/desktop/src-tauri/src/plugin_commands.rs
@@ -1,7 +1,7 @@
//! Native-owned target binding, file dialogs, and policy approval for plugin management.
use colossus_worker_protocol::PluginManagementRequest;
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use tauri::{AppHandle, State};
use tauri_plugin_dialog::{DialogExt as _, MessageDialogButtons, MessageDialogKind};
@@ -9,11 +9,105 @@ use tauri_plugin_dialog::{DialogExt as _, MessageDialogButtons, MessageDialogKin
use crate::plugin_adapter::{self, PluginInventoryDto, PluginPreviewInput, PluginPreviewKind};
use crate::{
commands::{target, unary_slot},
+ desktop_commands::{connect_guard, settings_store},
dto::CommandErrorDto,
+ managed_configuration_commands::persist_and_restart,
managed_diagnostics::worker_for,
+ managed_runtime,
state::{AppState, TargetConsentContext},
};
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct OutlookCompanionStatusDto {
+ supported: bool,
+ enabled: bool,
+ active_digest: Option,
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) async fn outlook_companion_status(
+ state: State<'_, AppState>,
+ space_id: String,
+) -> Result {
+ let settings = settings_store()?.load()?;
+ let space = settings
+ .space(&space_id)
+ .ok_or_else(|| CommandErrorDto::invalid("spaceId", "The Workspace is unknown."))?;
+ #[cfg(windows)]
+ let active_digest = state.outlook_companion_digest_for(&space_id).await;
+ #[cfg(not(windows))]
+ let active_digest = {
+ let _ = state;
+ None
+ };
+ Ok(OutlookCompanionStatusDto {
+ supported: cfg!(windows),
+ enabled: space.outlook_companion_enabled,
+ active_digest,
+ })
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) async fn configure_outlook_companion(
+ state: State<'_, AppState>,
+ space_id: String,
+ enabled: bool,
+) -> Result {
+ #[cfg(not(windows))]
+ {
+ let _ = (state, space_id, enabled);
+ return Err(CommandErrorDto::invalid(
+ "spaceId",
+ "Classic Outlook integration requires Windows Desktop.",
+ ));
+ }
+ #[cfg(windows)]
+ {
+ let _guard = connect_guard(&state)?;
+ let store = settings_store()?;
+ let mut settings = store.load()?;
+ if settings.selected_space_id.as_deref() != Some(&space_id) {
+ return Err(CommandErrorDto::invalid(
+ "spaceId",
+ "Select this Workspace before changing its Outlook connection.",
+ ));
+ }
+ let previous = settings.clone();
+ let space = settings
+ .spaces
+ .iter_mut()
+ .find(|space| space.id == space_id)
+ .ok_or_else(|| CommandErrorDto::invalid("spaceId", "The Workspace is unknown."))?;
+ if space.archived {
+ return Err(CommandErrorDto::invalid(
+ "spaceId",
+ "Restore this Workspace first.",
+ ));
+ }
+ if space.outlook_companion_enabled != enabled {
+ if enabled {
+ // Verify both the active signed digest and its companion handshake
+ // before persisting a setting that would fail every future startup.
+ let (probe, _) =
+ crate::outlook_companion::OutlookCompanion::start(store.home_root()?).await?;
+ probe.stop().await;
+ }
+ space.outlook_companion_enabled = enabled;
+ let drain =
+ managed_runtime::drain_active_runs_for_configuration(&state, &space_id).await?;
+ persist_and_restart(&state, &store, &mut settings, previous, &space_id).await?;
+ drop(drain);
+ }
+ let active_digest = state.outlook_companion_digest_for(&space_id).await;
+ Ok(OutlookCompanionStatusDto {
+ supported: true,
+ enabled,
+ active_digest,
+ })
+ }
+}
+
#[tauri::command(rename_all = "camelCase")]
pub(crate) async fn get_plugin_inventory(
state: State<'_, AppState>,
@@ -156,7 +250,16 @@ pub(crate) async fn manage_plugin(
else {
return Ok(json!({"cancelled": true}));
};
- plugin_adapter::manage(&worker, request, cancellation, |prompt| {
+ #[cfg(windows)]
+ let revokes_outlook = matches!(
+ &request,
+ PluginManagementRequest::Enable { name, .. }
+ | PluginManagementRequest::Disable { name }
+ | PluginManagementRequest::Update { name, .. }
+ | PluginManagementRequest::Uninstall { name, .. }
+ if name == "outlook-classic"
+ );
+ let result = plugin_adapter::manage(&worker, request, cancellation, |prompt| {
let app = app.clone();
let target_id = target_id.clone();
async move {
@@ -168,7 +271,14 @@ pub(crate) async fn manage_plugin(
.buttons(MessageDialogButtons::OkCancelCustom(approve.clone(), deny)).blocking_show()).await.ok()?;
accepted.then_some(prompt.choices[0].clone())
}
- }).await.map_err(operation_error)
+ }).await.map_err(operation_error)?;
+ #[cfg(windows)]
+ if revokes_outlook && result.get("cancelled").and_then(Value::as_bool) != Some(true) {
+ // The store's immutable-content lease protects an in-flight process, but a
+ // lifecycle change must revoke the old session token and endpoint now.
+ state.stop_all_outlook_companions().await;
+ }
+ Ok(result)
}
#[tauri::command(rename_all = "camelCase")]
diff --git a/apps/desktop/src-tauri/src/provider_catalog.rs b/apps/desktop/src-tauri/src/provider_catalog.rs
index c6cdd4210..bd75166cc 100644
--- a/apps/desktop/src-tauri/src/provider_catalog.rs
+++ b/apps/desktop/src-tauri/src/provider_catalog.rs
@@ -581,6 +581,7 @@ mod tests {
access_profile: AccessProfileSetting::Minimal,
execution_boundary: ExecutionBoundarySetting::OfflineIsolated,
terminal_enabled: false,
+ outlook_companion_enabled: false,
configuration: crate::managed_configuration::SpaceConfigurationSetting::default(),
});
assert_eq!(
diff --git a/apps/desktop/src-tauri/src/state.rs b/apps/desktop/src-tauri/src/state.rs
index f2e00b509..812639bdc 100644
--- a/apps/desktop/src-tauri/src/state.rs
+++ b/apps/desktop/src-tauri/src/state.rs
@@ -257,6 +257,8 @@ pub(crate) struct AppState {
pub(crate) mcp_health_history:
StdMutex>,
pub(crate) plugin_operations: StdMutex)>>,
+ #[cfg(windows)]
+ outlook_companions: Mutex>,
targets: RwLock>,
selected_target_id: RwLock>,
selection_epoch: AtomicU64,
@@ -422,6 +424,8 @@ impl Default for AppState {
mcp_health_history: StdMutex::new(std::collections::VecDeque::new()),
selected_target_id: RwLock::new(None),
plugin_operations: StdMutex::new(HashMap::new()),
+ #[cfg(windows)]
+ outlook_companions: Mutex::new(HashMap::new()),
selection_epoch: AtomicU64::new(0),
selection_updates,
run_targets: RwLock::new(HashMap::new()),
@@ -557,7 +561,85 @@ impl AppState {
}
pub(crate) async fn remove_target(&self, target_id: &str) -> Option {
- self.targets.write().await.remove(target_id)
+ let target = self.targets.write().await.remove(target_id);
+ #[cfg(windows)]
+ self.stop_outlook_companion_for(target_id).await;
+ target
+ }
+
+ #[cfg(windows)]
+ pub(crate) async fn install_outlook_companion_for(
+ &self,
+ space_id: &str,
+ companion: crate::outlook_companion::OutlookCompanion,
+ ) {
+ let previous = self
+ .outlook_companions
+ .lock()
+ .await
+ .insert(space_id.to_owned(), companion);
+ if let Some(previous) = previous {
+ previous.stop().await;
+ }
+ }
+
+ #[cfg(windows)]
+ pub(crate) async fn stop_outlook_companion_for(&self, space_id: &str) {
+ let companion = self.outlook_companions.lock().await.remove(space_id);
+ if let Some(companion) = companion {
+ companion.stop().await;
+ }
+ }
+
+ #[cfg(windows)]
+ pub(crate) async fn stop_all_outlook_companions(&self) {
+ let companions = self
+ .outlook_companions
+ .lock()
+ .await
+ .drain()
+ .map(|(_, companion)| companion)
+ .collect::>();
+ for companion in companions {
+ companion.stop().await;
+ }
+ }
+
+ #[cfg(windows)]
+ pub(crate) async fn outlook_companion_digest_for(&self, space_id: &str) -> Option {
+ self.outlook_companions
+ .lock()
+ .await
+ .get_mut(space_id)
+ .and_then(crate::outlook_companion::OutlookCompanion::active_digest)
+ }
+
+ #[cfg(windows)]
+ pub(crate) async fn outlook_companion_snapshots(&self) -> Vec<(String, String)> {
+ self.outlook_companions
+ .lock()
+ .await
+ .iter()
+ .map(|(space_id, companion)| (space_id.clone(), companion.digest.clone()))
+ .collect()
+ }
+
+ #[cfg(windows)]
+ pub(crate) async fn stop_outlook_companion_if_digest(&self, space_id: &str, digest: &str) {
+ let companion = {
+ let mut current = self.outlook_companions.lock().await;
+ if current
+ .get(space_id)
+ .is_some_and(|value| value.digest == digest)
+ {
+ current.remove(space_id)
+ } else {
+ None
+ }
+ };
+ if let Some(companion) = companion {
+ companion.stop().await;
+ }
}
pub(crate) async fn connected(&self, target_id: &str) -> bool {
@@ -1207,6 +1289,8 @@ impl AppState {
}
pub(crate) async fn close_all(&self) {
+ #[cfg(windows)]
+ self.stop_all_outlook_companions().await;
{
// Terminal processes may hold worker IPC (notably the bundled TUI).
// Revoke their document authority and tear down their process trees
diff --git a/apps/desktop/src/api.ts b/apps/desktop/src/api.ts
index 41c780272..db6e9bab3 100644
--- a/apps/desktop/src/api.ts
+++ b/apps/desktop/src/api.ts
@@ -1,6 +1,10 @@
import { Channel, invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
-import type { PluginInventory, PluginRequest } from "./plugins";
+import type {
+ OutlookCompanionStatus,
+ PluginInventory,
+ PluginRequest,
+} from "./plugins";
import { readNativeDialogAppearance } from "./theme/appearance";
import type { ProviderPreset, ProviderCatalogModel } from "./providerCatalog";
import type { GitStatus, GitCommitPage, GitCommitDetails } from "./git";
@@ -157,6 +161,17 @@ export function discoverManagedProviderModels(
export function getPluginInventory(targetId: string): Promise {
return call("get_plugin_inventory", { targetId });
}
+export function getOutlookCompanionStatus(
+ spaceId: string,
+): Promise {
+ return call("outlook_companion_status", { spaceId });
+}
+export function configureOutlookCompanion(
+ spaceId: string,
+ enabled: boolean,
+): Promise {
+ return call("configure_outlook_companion", { spaceId, enabled });
+}
export function resolvePluginSelection(
targetId: string,
prompt: string,
diff --git a/apps/desktop/src/components/OutlookCompanionControls.tsx b/apps/desktop/src/components/OutlookCompanionControls.tsx
new file mode 100644
index 000000000..1211e48c8
--- /dev/null
+++ b/apps/desktop/src/components/OutlookCompanionControls.tsx
@@ -0,0 +1,125 @@
+import { useEffect, useState } from "react";
+import {
+ configureOutlookCompanion,
+ diagnoseManagedMcpServer,
+ getOutlookCompanionStatus,
+} from "../api";
+import type { OutlookCompanionStatus } from "../plugins";
+
+export function OutlookCompanionControls({
+ spaceId,
+ pluginActive,
+ onChanged,
+}: {
+ spaceId: string;
+ pluginActive: boolean;
+ onChanged?: (() => void) | undefined;
+}) {
+ const [status, setStatus] = useState(null);
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState("");
+ const [check, setCheck] = useState("");
+
+ useEffect(() => {
+ let current = true;
+ void getOutlookCompanionStatus(spaceId)
+ .then((value) => {
+ if (current) setStatus(value);
+ })
+ .catch((reason: unknown) => {
+ if (current)
+ setError(
+ reason instanceof Error
+ ? reason.message
+ : "Could not read Outlook status.",
+ );
+ });
+ return () => {
+ current = false;
+ };
+ }, [spaceId]);
+
+ if (!status) return error ? {error}
: null;
+ if (!status.supported) return null;
+
+ async function toggle() {
+ if (!status || busy) return;
+ setBusy(true);
+ setError("");
+ setCheck("");
+ try {
+ const result = await configureOutlookCompanion(spaceId, !status.enabled);
+ setStatus(result);
+ onChanged?.();
+ } catch (reason) {
+ setError(
+ reason instanceof Error
+ ? reason.message
+ : "Could not change the Outlook session connection.",
+ );
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function testConnection() {
+ if (!status?.activeDigest || busy) return;
+ setBusy(true);
+ setError("");
+ setCheck("");
+ try {
+ const result = await diagnoseManagedMcpServer(spaceId, "outlook-session");
+ setCheck(
+ result.healthy
+ ? `${result.tools.length} allowlisted Outlook tools discovered.`
+ : (result.message ?? "Outlook session connection failed."),
+ );
+ } catch (reason) {
+ setError(
+ reason instanceof Error
+ ? reason.message
+ : "Could not test Outlook connection.",
+ );
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+
+ Classic Outlook session
+
+ Run the signed Outlook plugin in your Windows sign-in session. This
+ connection can read and change mail through its exact listed tools;
+ Colossus still reviews and audits tool calls.
+
+ void toggle()}
+ >
+ {status.enabled
+ ? "Disconnect Outlook session"
+ : "Connect Outlook session"}
+
+ void testConnection()}
+ >
+ Test connection
+
+ {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({
void setEnabled(!enabled)}
>
{enabled ? "Disable connection" : "Enable all plugin tools"}
@@ -127,6 +129,12 @@ export function PluginMcpControls({
Activate this plugin digest before configuring its connection.
+ ) : sessionRequired ? (
+
+ Classic Outlook needs the Windows session connection below. Its
+ sandboxed stdio connection cannot attach to the running Outlook
+ process.
+
) : !enabled ? (
A new connection permits every tool from this plugin, including tools
diff --git a/apps/desktop/src/plugins.ts b/apps/desktop/src/plugins.ts
index 73859e5fc..153ddd3d8 100644
--- a/apps/desktop/src/plugins.ts
+++ b/apps/desktop/src/plugins.ts
@@ -45,6 +45,12 @@ export interface PluginEntry {
detail: string;
}[];
}
+export interface OutlookCompanionStatus {
+ supported: boolean;
+ enabled: boolean;
+ activeDigest: string | null;
+}
+
export interface PluginInventory {
plugins: PluginEntry[];
managementAvailable: boolean;
diff --git a/docs/desktop/capabilities.md b/docs/desktop/capabilities.md
index da5b46478..e22e462b0 100644
--- a/docs/desktop/capabilities.md
+++ b/docs/desktop/capabilities.md
@@ -28,6 +28,11 @@ On Managed Local, the plugin page can install, verify, export, disable, and mana
Plugin-provided MCP servers are listed in the plugin details. Enable each one explicitly in the workspace's [plugin settings](settings.md#connections-search-and-telemetry); storing a credential alone does not enable it. Standalone MCP server definitions live in Global and Workspace settings. [MCP guide](../use/mcp-servers.md)
+Classic Outlook is a special case on Windows: its live COM connection needs the
+**Connect Outlook session** control in the signed plugin's details. The older published
+alpha.3 package has only a sandboxed stdio server and cannot attach to the running
+Outlook session. [Plugin installation and Outlook requirements](../extend/plugins.md#validate-package-and-install)
+
## Other navigation destinations
- **Library** lists released artifacts and their safe metadata for the current Desktop session. Open a thread's **Resources** or **Artifacts** pane for its specific outputs.
diff --git a/docs/extend/plugins.md b/docs/extend/plugins.md
index 30b13f54d..28eb2758d 100644
--- a/docs/extend/plugins.md
+++ b/docs/extend/plugins.md
@@ -167,8 +167,21 @@ reported digest before activation. A different registry needs a configured exact
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; the managed user-session companion tracked in [issue #246](https://github.com/obscuritylabs/Colossus/issues/246)
-is required for that connection.
+usable there. A signed package with the user-session companion is required.
+
+When a newer signed Outlook Classic package includes companion support, 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
+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.
```bash
colossus plugins validate ./example-plugin
From 16c6270f545fbdb337fd23586c0fa33c5463fc33 Mon Sep 17 00:00:00 2001
From: Alexander Rymdeko-Harvey
<8761706+killswitch-GUI@users.noreply.github.com>
Date: Fri, 2 Oct 2026 10:06:07 -0400
Subject: [PATCH 03/11] fix(desktop): preserve plugin connection authority and
companion lifecycle
---
.../src-tauri/src/managed_configuration.rs | 134 ++++++++++++++++--
.../src-tauri/src/outlook_companion.rs | 59 ++++----
apps/desktop/src-tauri/src/plugin_commands.rs | 126 ++++++++++++++--
.../components/OutlookCompanionControls.tsx | 4 +-
apps/desktop/src/pluginConnection.test.ts | 38 ++++-
apps/desktop/src/pluginConnection.ts | 6 +-
docs/desktop/settings.md | 2 +-
docs/extend/plugins.md | 4 +
8 files changed, 317 insertions(+), 56 deletions(-)
diff --git a/apps/desktop/src-tauri/src/managed_configuration.rs b/apps/desktop/src-tauri/src/managed_configuration.rs
index ea321f4f3..f6e7416f7 100644
--- a/apps/desktop/src-tauri/src/managed_configuration.rs
+++ b/apps/desktop/src-tauri/src/managed_configuration.rs
@@ -346,14 +346,7 @@ pub(crate) fn resolve_space_configuration(
.defaults
.revision(space.configuration.accepted_global_revision)
.ok_or_else(configuration_error)?;
- let mut field_overrides = defaults
- .field_overrides
- .iter()
- .map(|field| (field.field_id.clone(), field.value.clone()))
- .collect::>();
- for field in &space.configuration.field_overrides {
- field_overrides.insert(field.field_id.clone(), field.value.clone());
- }
+ let field_overrides = merge_field_overrides(defaults, &space.configuration)?;
let mut providers = resolve_catalog_values(
&global.providers,
&space.configuration.catalog_revisions,
@@ -441,6 +434,58 @@ pub(crate) fn resolve_space_configuration(
})
}
+fn merge_field_overrides(
+ defaults: &DefaultOverridesSetting,
+ space: &SpaceConfigurationSetting,
+) -> Result, CommandErrorDto> {
+ let mut fields = defaults
+ .field_overrides
+ .iter()
+ .map(|field| (field.field_id.clone(), field.value.clone()))
+ .collect::>();
+ for field in &space.field_overrides {
+ let value = if field.field_id == "plugins.mcpServers"
+ && field.value.get(PLUGIN_SERVER_PATCH_MARKER) == Some(&Value::Bool(true))
+ {
+ merge_plugin_server_overrides(fields.get(&field.field_id), &field.value)?
+ } else {
+ field.value.clone()
+ };
+ fields.insert(field.field_id.clone(), value);
+ }
+ Ok(fields)
+}
+
+pub(crate) const PLUGIN_SERVER_PATCH_MARKER: &str = "$colossusPatchV1";
+
+fn merge_plugin_server_overrides(
+ inherited: Option<&Value>,
+ local: &Value,
+) -> Result {
+ let mut servers = match inherited {
+ Some(value) => value.as_object().cloned().ok_or_else(configuration_error)?,
+ None => serde_json::Map::new(),
+ };
+ let local = local.as_object().ok_or_else(configuration_error)?;
+ for (name, override_value) in local {
+ if name == PLUGIN_SERVER_PATCH_MARKER {
+ continue;
+ }
+ let mut server = match servers.get(name) {
+ Some(value) => value.as_object().cloned().ok_or_else(configuration_error)?,
+ None => serde_json::Map::new(),
+ };
+ server.extend(
+ override_value
+ .as_object()
+ .ok_or_else(configuration_error)?
+ .clone(),
+ );
+ servers.insert(name.clone(), Value::Object(server));
+ }
+ Ok(Value::Object(servers))
+}
+
fn resolve_optional_catalog_value(
entries: &[CatalogEntrySetting],
references: &BTreeMap,
@@ -1223,6 +1268,79 @@ mod tests {
);
}
+ #[test]
+ fn plugin_server_override_keeps_unrelated_inherited_servers() {
+ let mut spaces = vec![space("one", provider("https://example.test/v1"))];
+ let mut global = GlobalConfigurationSetting::default();
+ initialize_catalog(&mut global, &mut spaces);
+ global.defaults.revisions[0]
+ .value
+ .field_overrides
+ .push(FieldOverrideSetting {
+ field_id: "plugins.mcpServers".into(),
+ value: serde_json::json!({
+ "example/mail": {"enabled": false, "allowedTools": ["list_mail"]},
+ "other/docs": {"enabled": true, "allowedTools": ["search"]}
+ }),
+ });
+ spaces[0]
+ .configuration
+ .field_overrides
+ .push(FieldOverrideSetting {
+ field_id: "plugins.mcpServers".into(),
+ value: serde_json::json!({
+ "$colossusPatchV1": true,
+ "example/mail": {"enabled": true}
+ }),
+ });
+ let resolved = resolve_space_configuration(&global, &spaces[0]).expect("resolved");
+ let servers = &resolved
+ .field_overrides
+ .iter()
+ .find(|field| field.field_id == "plugins.mcpServers")
+ .expect("plugin servers")
+ .value;
+ assert_eq!(servers["example/mail"]["enabled"], true);
+ assert_eq!(
+ servers["example/mail"]["allowedTools"],
+ serde_json::json!(["list_mail"])
+ );
+ assert_eq!(
+ servers["other/docs"]["allowedTools"],
+ serde_json::json!(["search"])
+ );
+ }
+
+ #[test]
+ fn legacy_plugin_server_override_still_replaces_global_servers() {
+ let mut spaces = vec![space("one", provider("https://example.test/v1"))];
+ let mut global = GlobalConfigurationSetting::default();
+ initialize_catalog(&mut global, &mut spaces);
+ global.defaults.revisions[0]
+ .value
+ .field_overrides
+ .push(FieldOverrideSetting {
+ field_id: "plugins.mcpServers".into(),
+ value: serde_json::json!({"global/mail": {"enabled": true}}),
+ });
+ spaces[0]
+ .configuration
+ .field_overrides
+ .push(FieldOverrideSetting {
+ field_id: "plugins.mcpServers".into(),
+ value: serde_json::json!({"local/docs": {"enabled": true}}),
+ });
+ let resolved = resolve_space_configuration(&global, &spaces[0]).expect("resolved");
+ let servers = &resolved
+ .field_overrides
+ .iter()
+ .find(|field| field.field_id == "plugins.mcpServers")
+ .expect("plugin servers")
+ .value;
+ assert!(servers.get("global/mail").is_none());
+ assert_eq!(servers["local/docs"]["enabled"], true);
+ }
+
#[test]
fn accepted_global_revision_controls_effective_defaults() {
let mut global = GlobalConfigurationSetting {
diff --git a/apps/desktop/src-tauri/src/outlook_companion.rs b/apps/desktop/src-tauri/src/outlook_companion.rs
index 8097b5b86..9a35afa78 100644
--- a/apps/desktop/src-tauri/src/outlook_companion.rs
+++ b/apps/desktop/src-tauri/src/outlook_companion.rs
@@ -171,29 +171,20 @@ pub(crate) fn start_watchdog(app: AppHandle) {
loop {
interval.tick().await;
let state = app.state::();
- let snapshots = state.outlook_companion_snapshots().await;
- if snapshots.is_empty() {
- continue;
- }
- let Ok(store) = crate::desktop_commands::settings_store() else {
- for (space_id, digest) in snapshots {
- state
- .stop_outlook_companion_if_digest(&space_id, &digest)
- .await;
- }
- continue;
- };
- let home = if let Ok(home) = store.home_root() {
- home.to_path_buf()
- } else {
- for (space_id, digest) in snapshots {
- state
- .stop_outlook_companion_if_digest(&space_id, &digest)
- .await;
- }
- continue;
- };
- let active = tokio::task::spawn_blocking(move || {
+ reconcile_active_digest(&state).await;
+ }
+ });
+}
+
+pub(crate) async fn reconcile_active_digest(state: &crate::state::AppState) {
+ let snapshots = state.outlook_companion_snapshots().await;
+ if snapshots.is_empty() {
+ return;
+ }
+ let active = if let Ok(store) = crate::desktop_commands::settings_store() {
+ if let Ok(home) = store.home_root() {
+ let home = home.to_path_buf();
+ tokio::task::spawn_blocking(move || {
PluginStore::new(home)
.and_then(|store| store.active(PLUGIN_NAME))
.ok()
@@ -202,16 +193,20 @@ pub(crate) fn start_watchdog(app: AppHandle) {
})
.await
.ok()
- .flatten();
- for (space_id, digest) in snapshots {
- if active.as_deref() != Some(&digest) {
- state
- .stop_outlook_companion_if_digest(&space_id, &digest)
- .await;
- }
- }
+ .flatten()
+ } else {
+ None
}
- });
+ } else {
+ None
+ };
+ for (space_id, digest) in snapshots {
+ if active.as_deref() != Some(&digest) {
+ state
+ .stop_outlook_companion_if_digest(&space_id, &digest)
+ .await;
+ }
+ }
}
#[cfg(test)]
diff --git a/apps/desktop/src-tauri/src/plugin_commands.rs b/apps/desktop/src-tauri/src/plugin_commands.rs
index 54ce6e288..0a165f064 100644
--- a/apps/desktop/src-tauri/src/plugin_commands.rs
+++ b/apps/desktop/src-tauri/src/plugin_commands.rs
@@ -6,6 +6,8 @@ use serde_json::{Value, json};
use tauri::{AppHandle, State};
use tauri_plugin_dialog::{DialogExt as _, MessageDialogButtons, MessageDialogKind};
+#[cfg(windows)]
+use crate::managed_configuration::FieldOverrideSetting;
use crate::plugin_adapter::{self, PluginInventoryDto, PluginPreviewInput, PluginPreviewKind};
use crate::{
commands::{target, unary_slot},
@@ -75,9 +77,7 @@ pub(crate) async fn configure_outlook_companion(
}
let previous = settings.clone();
let space = settings
- .spaces
- .iter_mut()
- .find(|space| space.id == space_id)
+ .space(&space_id)
.ok_or_else(|| CommandErrorDto::invalid("spaceId", "The Workspace is unknown."))?;
if space.archived {
return Err(CommandErrorDto::invalid(
@@ -85,7 +85,14 @@ pub(crate) async fn configure_outlook_companion(
"Restore this Workspace first.",
));
}
- if space.outlook_companion_enabled != enabled {
+ let space = settings
+ .spaces
+ .iter_mut()
+ .find(|space| space.id == space_id)
+ .expect("space checked above");
+ let portable_changed =
+ disable_portable_outlook_connection(&mut space.configuration.field_overrides)?;
+ if space.outlook_companion_enabled != enabled || portable_changed {
if enabled {
// Verify both the active signed digest and its companion handshake
// before persisting a setting that would fail every future startup.
@@ -108,6 +115,45 @@ pub(crate) async fn configure_outlook_companion(
}
}
+#[cfg(windows)]
+fn disable_portable_outlook_connection(
+ overrides: &mut Vec,
+) -> Result {
+ let mut servers = overrides
+ .iter()
+ .find(|field| field.field_id == "plugins.mcpServers")
+ .map_or_else(
+ || json!({ crate::managed_configuration::PLUGIN_SERVER_PATCH_MARKER: true }),
+ |field| field.value.clone(),
+ );
+ let servers = servers.as_object_mut().ok_or_else(|| {
+ CommandErrorDto::invalid("plugins.mcpServers", "The plugin connections are invalid.")
+ })?;
+ let server = servers
+ .entry("outlook-classic/mail")
+ .or_insert_with(|| json!({}));
+ let server = server.as_object_mut().ok_or_else(|| {
+ CommandErrorDto::invalid("plugins.mcpServers", "The Outlook connection is invalid.")
+ })?;
+ server.insert("enabled".into(), Value::Bool(false));
+ let value = Value::Object(servers.clone());
+ if let Some(existing) = overrides
+ .iter_mut()
+ .find(|field| field.field_id == "plugins.mcpServers")
+ {
+ if existing.value == value {
+ return Ok(false);
+ }
+ existing.value = value;
+ } else {
+ overrides.push(FieldOverrideSetting {
+ field_id: "plugins.mcpServers".into(),
+ value,
+ });
+ }
+ Ok(true)
+}
+
#[tauri::command(rename_all = "camelCase")]
pub(crate) async fn get_plugin_inventory(
state: State<'_, AppState>,
@@ -259,6 +305,18 @@ pub(crate) async fn manage_plugin(
| PluginManagementRequest::Uninstall { name, .. }
if name == "outlook-classic"
);
+ #[cfg(windows)]
+ let force_revoke_on_success = match &request {
+ PluginManagementRequest::Disable { name } if name == "outlook-classic" => true,
+ PluginManagementRequest::Uninstall { name, digest, .. } if name == "outlook-classic" => {
+ state
+ .outlook_companion_snapshots()
+ .await
+ .iter()
+ .any(|(_, active_digest)| active_digest == digest)
+ }
+ _ => false,
+ };
let result = plugin_adapter::manage(&worker, request, cancellation, |prompt| {
let app = app.clone();
let target_id = target_id.clone();
@@ -271,14 +329,30 @@ pub(crate) async fn manage_plugin(
.buttons(MessageDialogButtons::OkCancelCustom(approve.clone(), deny)).blocking_show()).await.ok()?;
accepted.then_some(prompt.choices[0].clone())
}
- }).await.map_err(operation_error)?;
+ }).await;
#[cfg(windows)]
- if revokes_outlook && result.get("cancelled").and_then(Value::as_bool) != Some(true) {
- // The store's immutable-content lease protects an in-flight process, but a
- // lifecycle change must revoke the old session token and endpoint now.
- state.stop_all_outlook_companions().await;
+ if revokes_outlook {
+ match &result {
+ Ok(value)
+ if force_revoke_on_success
+ && value.get("cancelled").and_then(Value::as_bool) != Some(true) =>
+ {
+ // A disable, or removal of the running digest, invalidates its
+ // session even if another process re-enables it immediately.
+ state.stop_all_outlook_companions().await;
+ }
+ Ok(_) | Err(colossus_worker_protocol::WorkerControlError::Remote(_)) => {
+ // Updates can leave the old digest active, and a failed operation
+ // can still have changed it. Keep a healthy helper when it matches.
+ crate::outlook_companion::reconcile_active_digest(&state).await;
+ }
+ Err(_) => {
+ // A lost worker response leaves the operation outcome unknown.
+ state.stop_all_outlook_companions().await;
+ }
+ }
}
- Ok(result)
+ result.map_err(operation_error)
}
#[tauri::command(rename_all = "camelCase")]
@@ -359,3 +433,35 @@ fn operation_error(error: colossus_worker_protocol::WorkerControlError) -> Comma
_ => CommandErrorDto { code: "plugin_operation_unknown".into(), message: "The plugin operation lost contact with the runtime. Refresh the inventory before retrying; a change may have committed.".into(), retryable: false, outcome_unknown: true, violations: Vec::new() },
}
}
+
+#[cfg(all(test, windows))]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn disconnect_persists_portable_stdio_disabled_without_copying_other_servers() {
+ let mut fields = vec![FieldOverrideSetting {
+ field_id: "plugins.mcpServers".into(),
+ value: json!({
+ "outlook-classic/mail": {"enabled": true, "allowedTools": ["get_status"]},
+ "local/docs": {"enabled": true}
+ }),
+ }];
+ assert!(disable_portable_outlook_connection(&mut fields).expect("disable"));
+ assert_eq!(fields[0].value["outlook-classic/mail"]["enabled"], false);
+ assert_eq!(
+ fields[0].value["outlook-classic/mail"]["allowedTools"],
+ json!(["get_status"])
+ );
+ assert_eq!(fields[0].value["local/docs"]["enabled"], true);
+ assert!(!disable_portable_outlook_connection(&mut fields).expect("idempotent"));
+ }
+
+ #[test]
+ fn disconnect_without_local_override_uses_a_server_patch() {
+ let mut fields = Vec::new();
+ assert!(disable_portable_outlook_connection(&mut fields).expect("disable"));
+ assert_eq!(fields[0].value["$colossusPatchV1"], true);
+ assert_eq!(fields[0].value["outlook-classic/mail"]["enabled"], false);
+ }
+}
diff --git a/apps/desktop/src/components/OutlookCompanionControls.tsx b/apps/desktop/src/components/OutlookCompanionControls.tsx
index 1211e48c8..bf577d230 100644
--- a/apps/desktop/src/components/OutlookCompanionControls.tsx
+++ b/apps/desktop/src/components/OutlookCompanionControls.tsx
@@ -71,7 +71,7 @@ export function OutlookCompanionControls({
const result = await diagnoseManagedMcpServer(spaceId, "outlook-session");
setCheck(
result.healthy
- ? `${result.tools.length} allowlisted Outlook tools discovered.`
+ ? `${result.tools.length} allowlisted Outlook tools discovered. Outlook attachment is checked when a tool runs.`
: (result.message ?? "Outlook session connection failed."),
);
} catch (reason) {
@@ -109,7 +109,7 @@ export function OutlookCompanionControls({
disabled={busy || !status.activeDigest}
onClick={() => void testConnection()}
>
- Test connection
+ Discover tools
{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;