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..135af6b34 100644
--- a/apps/desktop/src/components/PluginMcpControls.tsx
+++ b/apps/desktop/src/components/PluginMcpControls.tsx
@@ -5,22 +5,30 @@ 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,
+ sessionRequired = false,
+ onChanged,
}: {
- targetId: string;
+ spaceId: string;
server: string;
enabled: boolean;
+ pluginActive: boolean;
http: boolean;
+ sessionRequired?: boolean;
+ onChanged?: (() => void) | undefined;
}) {
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
@@ -32,7 +40,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 +55,51 @@ export function PluginMcpControls({
setBusy(false);
}
}
+ async function setEnabled(next: boolean) {
+ if (busy) return;
+ setBusy(true);
+ setError("");
+ setMessage("");
+ try {
+ const snapshot = await getManagedConfiguration();
+ const request = pluginConnectionRequest(snapshot, spaceId, server, next);
+ await saveSpaceConfiguration(request);
+ setDiagnostic(null);
+ setStatus(null);
+ setMessage(
+ next
+ ? "Connection enabled. Test it after the runtime restarts."
+ : "Connection disabled for this workspace.",
+ );
+ onChanged?.();
+ } catch (error) {
+ setError(
+ error instanceof Error
+ ? error.message
+ : "Could not update this plugin connection.",
+ );
+ } finally {
+ setBusy(false);
+ }
+ }
return (
+
- {!enabled && (
+ {!pluginActive ? (
- Enable and apply this server explicitly in plugin settings first.
+ Activate this plugin digest before configuring its connection.
- )}
- {busy &&
Checking MCP connection…
}
+ ) : 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
+ added by a later update. Configure an exact tool list in plugin
+ settings for narrower access.
+
+ ) : null}
+ {busy &&
- 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..aa5fd297b
--- /dev/null
+++ b/apps/desktop/src/pluginConnection.test.ts
@@ -0,0 +1,96 @@
+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: {
+ $colossusPatchV1: true,
+ "example/mail": { enabled: true, allowedTools: ["*"] },
+ },
+ },
+ ]);
+ });
+
+ 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
new file mode 100644
index 000000000..76e7bcdf1
--- /dev/null
+++ b/apps/desktop/src/pluginConnection.ts
@@ -0,0 +1,58 @@
+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.configuration.fieldOverrides.find(
+ (entry) => entry.fieldId === "plugins.mcpServers",
+ );
+ const servers = {
+ ...record(current?.value ?? { $colossusPatchV1: true }),
+ };
+ 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/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/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..a16be66f6
--- /dev/null
+++ b/crates/colossus-cli/tests/plugin_mcp_smoke.rs
@@ -0,0 +1,186 @@
+//! 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 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");
+ 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-plugins/src/store.rs b/crates/colossus-plugins/src/store.rs
index 655097f83..68d627298 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 & 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());
+}
diff --git a/crates/colossus-runtime/src/composition.rs b/crates/colossus-runtime/src/composition.rs
index 12f82948c..06a4a0e70 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 37ec94f98..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,13 +368,79 @@ 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: 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)]
@@ -1584,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
@@ -1700,7 +1768,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..c390bcba2 100644
--- a/crates/colossus-runtime/src/plugin_registry_tests.rs
+++ b/crates/colossus-runtime/src/plugin_registry_tests.rs
@@ -7,6 +7,134 @@ 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()
+ );
+}
+
+#[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/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/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/desktop/settings.md b/docs/desktop/settings.md
index 8327068a5..d71012e9c 100644
--- a/docs/desktop/settings.md
+++ b/docs/desktop/settings.md
@@ -64,7 +64,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 d84d49735..22ee730e2 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,36 @@ 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.4.ci.7.1-windows-amd64
+colossus plugins list
+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.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
+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. 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
+prove that classic Outlook is open.
+
```bash
colossus plugins validate ./example-plugin
colossus plugins package ./example-plugin --output ./example-plugin.oci
@@ -176,6 +215,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 +241,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: {}
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.