Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smart-crews-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@caplets/core": patch
---

Centralize Caplets exposure projection rendering across attach, native, remote, and MCP surfaces.
6 changes: 6 additions & 0 deletions CONCEPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ Shared domain vocabulary for this project -- entities, named processes, and stat

A configured capability surface that exposes a backend to agents through a stable handle, progressive wrapper tools, or direct tool operations.

### Caplets Exposure Projection

The shared adapter-neutral runtime view of which local and remote Caplets are exposed as Code Mode handles, progressive tools, direct downstream operations, or direct MCP surfaces, including non-callable diagnostic breadcrumbs for hidden Caplets.

Caplets Exposure Projection is the source of truth for exposure identity and availability. MCP, native, and attach host adapters render it differently; they do not re-own exposure policy or execution behavior.

### Prebuilt Caplets Catalog

The repo-owned collection of installable Caplet files under `caplets/`.
Expand Down
4 changes: 4 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ _Avoid_: Caplets utility namespace, custom helper surface
The first compatibility-global set for Code Mode: URL and query handling, text encoding, base64 and Buffer-compatible byte handling, web binary payload objects, safe crypto primitives, timing, scheduling, and structured cloning.
_Avoid_: Minimal encoding helpers, full Node compatibility

**Caplets exposure projection**:
A shared adapter-neutral runtime view of which local and remote Caplets are exposed as Code Mode handles, progressive tools, direct downstream operations, or direct MCP surfaces, including non-callable diagnostic breadcrumbs for hidden Caplets. MCP, native, and attach host adapters render it differently; they do not own exposure policy or execution behavior.
_Avoid_: Tool registration list, exposure wrapper map, transport-specific surface

**Code Mode bridge value**:
A value passed between a Code Mode script and a Caplets backend call, including JSON-compatible data and supported standard-library binary payload objects.
_Avoid_: JSON-only payload, host object leak
Expand Down
4 changes: 3 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Project sources override user/global sources. Source-aware inspection reports wh

Backend managers provide a common shape for listing tools, searching tools, describing exact tools, calling tools, and checking readiness. MCP-backed Caplets additionally support resources, resource templates, prompts, and completion.

### Exposure Policy
### Exposure Policy And Projection

`packages/core/src/exposure/policy.ts` resolves one exposure value into three booleans:

Expand All @@ -38,6 +38,8 @@ Backend managers provide a common shape for listing tools, searching tools, desc

The global default is `code_mode`. Per-Caplet config may choose `direct`, `progressive`, `code_mode`, `direct_and_code_mode`, or `progressive_and_code_mode`.

`packages/core/src/exposure/projection.ts` turns resolved discovery snapshots and attach manifests into the Caplets exposure projection: the adapter-neutral view of Code Mode handles, progressive tools, direct downstream operations, direct MCP surfaces, route descriptors, hidden diagnostic breadcrumbs, and local/remote merge outcomes. MCP serving, native integrations, and attach/remote clients render this projection; they do not re-own exposure identity, namespace shadowing, or hidden-Caplet policy.

### MCP Server

`packages/core/src/serve/session.ts` registers the user-facing MCP surface.
Expand Down
406 changes: 406 additions & 0 deletions docs/plans/2026-07-01-001-refactor-caplets-exposure-projection-plan.md

Large diffs are not rendered by default.

270 changes: 139 additions & 131 deletions packages/core/src/attach/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ import {
decodeDirectResourceUri,
directResourceUriMatchesTemplate,
} from "../exposure/direct-names";
import type {
CallableCaplet,
DirectPromptRegistration,
DirectResourceRegistration,
DirectResourceTemplateRegistration,
DirectToolRegistration,
ExposureSnapshot,
} from "../exposure/discovery";
import { generatedToolInputJsonSchemaForCaplet } from "../generated-tool-input-schema";
import {
buildExposureProjection,
type ExposureProjectionEntry,
type ExposureProjectionHiddenCaplet,
} from "../exposure/projection";
import type { NativeCapletsService } from "../native/service";

export const CAPLETS_ATTACH_SESSION_HEADER = "caplets-attach-session-id";
Expand Down Expand Up @@ -130,10 +126,6 @@ type AttachRoute =
| { kind: "prompt"; capletId: string; downstreamName: string }
| { kind: "completion"; capletId: string };

type AttachCapletWithShadowing = {
shadowing?: CapletShadowingPolicy | undefined;
};

export type AttachProjection = {
manifest: AttachManifest;
routes: Map<string, AttachRoute>;
Expand All @@ -151,16 +143,16 @@ type AttachManifestProjectionInput = {
};

export async function buildAttachProjection(engine: CapletsEngine): Promise<AttachProjection> {
const snapshot = await engine.exposureSnapshot();
const projection = buildExposureProjection(await engine.exposureSnapshot());
const partial = sortAttachProjectionInput({
caplets: snapshot.progressiveCaplets.map(progressiveCapletExport),
tools: snapshot.directTools.map(toolExport),
resources: snapshot.directResources.map(resourceExport),
resourceTemplates: snapshot.directResourceTemplates.map(resourceTemplateExport),
prompts: snapshot.directPrompts.map(promptExport),
completions: completionExports(snapshot),
codeModeCaplets: snapshot.codeModeCaplets.map(codeModeCapletExport),
diagnostics: snapshot.hiddenCaplets.map(attachDiagnosticForHiddenCaplet),
caplets: projection.entries.flatMap(progressiveCapletExport),
tools: projection.entries.flatMap(toolExport),
resources: projection.entries.flatMap(resourceExport),
resourceTemplates: projection.entries.flatMap(resourceTemplateExport),
prompts: projection.entries.flatMap(promptExport),
completions: projection.entries.flatMap(completionExport),
codeModeCaplets: projection.entries.flatMap(codeModeCapletExport),
diagnostics: projection.hiddenCaplets.map(attachDiagnosticForHiddenCaplet),
});
const revision = revisionFor(partial);
const manifest: AttachManifest = {
Expand All @@ -175,7 +167,7 @@ export async function buildAttachProjection(engine: CapletsEngine): Promise<Atta
};
}

function attachDiagnosticForHiddenCaplet(hidden: ExposureSnapshot["hiddenCaplets"][number]) {
function attachDiagnosticForHiddenCaplet(hidden: ExposureProjectionHiddenCaplet) {
return {
code: `ATTACH_CAPLET_${hidden.reason.toUpperCase()}`,
message: `Caplet ${hidden.capletId} is not exported: ${hidden.reason}.`,
Expand All @@ -185,12 +177,12 @@ function attachDiagnosticForHiddenCaplet(hidden: ExposureSnapshot["hiddenCaplets
}

function hiddenDiagnosticDetails(
hidden: ExposureSnapshot["hiddenCaplets"][number],
hidden: ExposureProjectionHiddenCaplet,
): Pick<AttachDiagnostic, "details"> {
if (!hidden.error) return {};
const details = hidden.error.details;
if (!hidden.diagnostic) return {};
const details = hidden.diagnostic.details;
if (!hidden.reason.startsWith("project_binding_")) {
return { details: hidden.error };
return { details: hidden.diagnostic };
}
const existing =
isRecord(details) && isRecord(details.projectBinding) ? details.projectBinding : {};
Expand Down Expand Up @@ -412,126 +404,142 @@ export function attachErrorResponse(error: unknown): {
}

function progressiveCapletExport(
entry: CallableCaplet,
): Omit<AttachProgressiveCapletExport, "exportId"> {
const inputSchema = generatedToolInputJsonSchemaForCaplet(entry.caplet);
return {
stableId: `progressive:${entry.caplet.server}`,
kind: "caplet",
name: entry.caplet.server,
title: entry.caplet.name,
description: entry.caplet.description,
inputSchema,
schemaHash: schemaHash(inputSchema),
capletId: entry.caplet.server,
shadowing: shadowingPolicy(entry.caplet),
};
entry: ExposureProjectionEntry,
): Array<Omit<AttachProgressiveCapletExport, "exportId">> {
if (entry.kind !== "progressive-caplet") return [];
return [
{
stableId: `progressive:${entry.capletId}`,
kind: "caplet",
name: entry.id,
title: entry.title,
description: entry.description,
inputSchema: entry.inputSchema,
schemaHash: schemaHash(entry.inputSchema ?? null),
capletId: entry.capletId,
shadowing: entry.shadowing,
},
];
}

function codeModeCapletExport(entry: CallableCaplet): Omit<AttachCodeModeCaplet, "exportId"> {
return {
stableId: `code_mode:${entry.caplet.server}`,
kind: "caplet",
name: entry.caplet.name,
title: entry.caplet.name,
description: entry.caplet.description,
schemaHash: null,
capletId: entry.caplet.server,
shadowing: shadowingPolicy(entry.caplet),
};
function codeModeCapletExport(
entry: ExposureProjectionEntry,
): Array<Omit<AttachCodeModeCaplet, "exportId">> {
if (entry.kind !== "code-mode-caplet") return [];
return [
{
stableId: `code_mode:${entry.capletId}`,
kind: "caplet",
name: entry.title ?? entry.id,
title: entry.title,
description: entry.description,
schemaHash: null,
capletId: entry.capletId,
shadowing: entry.shadowing,
},
];
}

function toolExport(entry: DirectToolRegistration): Omit<AttachToolExport, "exportId"> {
return {
stableId: `tool:${entry.caplet.server}:${entry.downstreamName}`,
kind: "tool",
name: entry.name,
downstreamName: entry.downstreamName,
title: entry.tool.name,
description: entry.tool.description,
inputSchema: entry.tool.inputSchema,
outputSchema: entry.tool.outputSchema,
annotations: entry.tool.annotations,
schemaHash: schemaHash({ input: entry.tool.inputSchema, output: entry.tool.outputSchema }),
capletId: entry.caplet.server,
shadowing: shadowingPolicy(entry.caplet),
};
function toolExport(entry: ExposureProjectionEntry): Array<Omit<AttachToolExport, "exportId">> {
if (entry.kind !== "direct-tool" || entry.route.kind !== "direct-tool") return [];
return [
{
stableId: `tool:${entry.capletId}:${entry.route.downstreamName}`,
kind: "tool",
name: entry.id,
downstreamName: entry.route.downstreamName,
title: entry.title,
description: entry.description,
inputSchema: entry.inputSchema,
outputSchema: entry.outputSchema,
annotations: entry.annotations,
schemaHash: schemaHash({ input: entry.inputSchema, output: entry.outputSchema }),
capletId: entry.capletId,
shadowing: entry.shadowing,
},
];
}

function resourceExport(entry: DirectResourceRegistration): Omit<AttachResourceExport, "exportId"> {
return {
stableId: `resource:${entry.caplet.server}:${entry.downstreamUri}`,
kind: "resource",
uri: entry.uri,
downstreamUri: entry.downstreamUri,
title: entry.resource.name,
description: entry.resource.description,
...(entry.resource.mimeType ? { mimeType: entry.resource.mimeType } : {}),
...(typeof entry.resource.size === "number" ? { size: entry.resource.size } : {}),
schemaHash: null,
capletId: entry.caplet.server,
shadowing: shadowingPolicy(entry.caplet),
};
function resourceExport(
entry: ExposureProjectionEntry,
): Array<Omit<AttachResourceExport, "exportId">> {
if (entry.kind !== "direct-resource" || entry.route.kind !== "direct-resource") return [];
return [
{
stableId: `resource:${entry.capletId}:${entry.route.downstreamUri}`,
kind: "resource",
uri: entry.id,
downstreamUri: entry.route.downstreamUri,
title: entry.title,
description: entry.description,
...(entry.mimeType ? { mimeType: entry.mimeType } : {}),
...(typeof entry.size === "number" ? { size: entry.size } : {}),
schemaHash: null,
capletId: entry.capletId,
shadowing: entry.shadowing,
},
];
}

function resourceTemplateExport(
entry: DirectResourceTemplateRegistration,
): Omit<AttachResourceTemplateExport, "exportId"> {
return {
stableId: `resourceTemplate:${entry.caplet.server}:${entry.downstreamUriTemplate}`,
kind: "resourceTemplate",
uriTemplate: entry.uriTemplate,
downstreamUriTemplate: entry.downstreamUriTemplate,
title: entry.resourceTemplate.name,
description: entry.resourceTemplate.description,
...(entry.resourceTemplate.mimeType ? { mimeType: entry.resourceTemplate.mimeType } : {}),
schemaHash: null,
capletId: entry.caplet.server,
shadowing: shadowingPolicy(entry.caplet),
};
entry: ExposureProjectionEntry,
): Array<Omit<AttachResourceTemplateExport, "exportId">> {
if (
entry.kind !== "direct-resource-template" ||
entry.route.kind !== "direct-resource-template"
) {
return [];
}
return [
{
stableId: `resourceTemplate:${entry.capletId}:${entry.route.downstreamUriTemplate}`,
kind: "resourceTemplate",
uriTemplate: entry.id,
downstreamUriTemplate: entry.route.downstreamUriTemplate,
title: entry.title,
description: entry.description,
...(entry.mimeType ? { mimeType: entry.mimeType } : {}),
schemaHash: null,
capletId: entry.capletId,
shadowing: entry.shadowing,
},
];
}

function promptExport(entry: DirectPromptRegistration): Omit<AttachPromptExport, "exportId"> {
const inputSchema = { arguments: entry.prompt.arguments ?? [] };
return {
stableId: `prompt:${entry.caplet.server}:${entry.downstreamName}`,
kind: "prompt",
name: entry.name,
downstreamName: entry.downstreamName,
title: entry.prompt.name,
description: entry.prompt.description,
inputSchema,
schemaHash: schemaHash(inputSchema),
capletId: entry.caplet.server,
shadowing: shadowingPolicy(entry.caplet),
};
function promptExport(entry: ExposureProjectionEntry): Array<Omit<AttachPromptExport, "exportId">> {
if (entry.kind !== "direct-prompt" || entry.route.kind !== "direct-prompt") return [];
return [
{
stableId: `prompt:${entry.capletId}:${entry.route.downstreamName}`,
kind: "prompt",
name: entry.id,
downstreamName: entry.route.downstreamName,
title: entry.title,
description: entry.description,
inputSchema: entry.inputSchema,
schemaHash: schemaHash(entry.inputSchema ?? null),
capletId: entry.capletId,
shadowing: entry.shadowing,
},
];
}

function completionExports(
snapshot: ExposureSnapshot,
function completionExport(
entry: ExposureProjectionEntry,
): Array<Omit<AttachCompletionExport, "exportId">> {
const caplets = new Map(
[...snapshot.directPrompts, ...snapshot.directResourceTemplates].map((entry) => [
entry.caplet.server,
entry.caplet,
]),
);
return [...caplets.entries()]
.sort(([left], [right]) => left.localeCompare(right))
.map(([capletId, caplet]) => ({
stableId: `completion:${capletId}`,
if (entry.kind !== "completion") return [];
return [
{
stableId: `completion:${entry.capletId}`,
kind: "completion",
name: `${capletId}:complete`,
title: "Complete",
description: `MCP completion for ${capletId}.`,
name: entry.id,
title: entry.title,
description: entry.description,
schemaHash: null,
capletId,
shadowing: shadowingPolicy(caplet),
}));
}

function shadowingPolicy(caplet: AttachCapletWithShadowing): CapletShadowingPolicy {
return caplet.shadowing ?? "forbid";
capletId: entry.capletId,
shadowing: entry.shadowing,
},
];
}

function sortAttachProjectionInput(
Expand Down
Loading