agent-trellis 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/mcpPlan.d.ts +1 -0
- package/dist/adapters/mcpPlan.js +14 -5
- package/dist/cli.js +30 -5
- package/dist/commands/mcp.d.ts +71 -0
- package/dist/commands/mcp.js +145 -4
- package/dist/commands/mcpAuth.d.ts +6 -0
- package/dist/commands/mcpAuth.js +38 -10
- package/dist/commands/mcpGateway.js +7 -7
- package/dist/commands/skill.d.ts +45 -1
- package/dist/commands/skill.js +83 -2
- package/dist/core/canonical.d.ts +1 -0
- package/dist/core/canonical.js +8 -3
- package/dist/core/types.d.ts +4 -1
- package/dist/core/types.js +3 -0
- package/dist/lib/agentBridgeProvider.d.ts +1 -1
- package/dist/lib/gatewayBackend.d.ts +11 -0
- package/dist/lib/gatewayBackend.js +23 -4
- package/dist/lib/mcpConnect.d.ts +6 -3
- package/dist/lib/mcpConnect.js +8 -6
- package/dist/lib/mcpRuntime.d.ts +12 -2
- package/dist/lib/mcpRuntime.js +70 -9
- package/dist/lib/mcpStatusProvider.d.ts +1 -1
- package/dist/lib/memoryProvider.d.ts +1 -1
- package/dist/lib/oauth/discovery.d.ts +1 -0
- package/dist/lib/oauth/discovery.js +2 -0
- package/dist/lib/oauth/flow.d.ts +4 -0
- package/dist/lib/oauth/flow.js +48 -11
- package/dist/lib/oauth/refresh.d.ts +1 -0
- package/dist/lib/oauth/refresh.js +3 -1
- package/dist/lib/oauth/store.d.ts +2 -0
- package/dist/lib/runtimeControlProvider.d.ts +1 -1
- package/dist/lib/scopeSelection.d.ts +36 -0
- package/dist/lib/scopeSelection.js +65 -0
- package/dist/lib/skillManifest.d.ts +66 -0
- package/dist/lib/skillManifest.js +203 -0
- package/dist/lib/skillProvider.d.ts +21 -1
- package/dist/lib/skillProvider.js +61 -1
- package/dist/lib/taskProvider.d.ts +1 -1
- package/dist/lib/upstreamResources.d.ts +23 -0
- package/dist/lib/upstreamResources.js +192 -0
- package/dist/pi-bridge/bundle.js +37906 -19932
- package/docs/architecture.md +14 -13
- package/docs/mcp-authorization-ownership.md +59 -0
- package/docs/skills-over-mcp.md +91 -0
- package/package.json +6 -3
|
@@ -5,6 +5,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
5
5
|
import { isInScope } from "../core/adapter.js";
|
|
6
6
|
import { loadCanonicalSource } from "../core/canonical.js";
|
|
7
7
|
import { resolveScope } from "../core/types.js";
|
|
8
|
+
import { buildSkillEntry, parseSkillUri, readSkillFileBytes, toResourceContent } from "./skillManifest.js";
|
|
8
9
|
const MAX_FILE_BYTES = 256 * 1024;
|
|
9
10
|
const SKILL_RESOURCE_PREFIX = "trellis://skills/";
|
|
10
11
|
const SEARCH_TOOL = {
|
|
@@ -99,7 +100,47 @@ function skillMetadata(skill, canonical, agentId, content) {
|
|
|
99
100
|
};
|
|
100
101
|
}
|
|
101
102
|
export class SkillProvider {
|
|
103
|
+
onDiagnostic;
|
|
102
104
|
id = "skills";
|
|
105
|
+
/** Each omitted skill is reported once per process, not on every request. */
|
|
106
|
+
reported = new Set();
|
|
107
|
+
constructor(onDiagnostic = (message) => console.error(message)) {
|
|
108
|
+
this.onDiagnostic = onDiagnostic;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The skills this agent may be served under `skill://`, each with a manifest
|
|
112
|
+
* computed from the bytes it would read (trellis-skills-over-mcp D2/D3). A
|
|
113
|
+
* skill that cannot be served validly is omitted and reported, never served
|
|
114
|
+
* under a URI the specification calls invalid.
|
|
115
|
+
*/
|
|
116
|
+
catalog(canonical, agentId) {
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const skill of canonical.skills) {
|
|
119
|
+
if (!hasSkillDocument(skill) || !isInScope(agentId, skill.scope, canonical.managedAgents))
|
|
120
|
+
continue;
|
|
121
|
+
const built = buildSkillEntry(skill.name, skill.dir);
|
|
122
|
+
if (built.ok) {
|
|
123
|
+
out.push({ skill, entry: built.entry });
|
|
124
|
+
}
|
|
125
|
+
else if (!this.reported.has(built.reason)) {
|
|
126
|
+
this.reported.add(built.reason);
|
|
127
|
+
this.onDiagnostic(`trellis-mcp-runtime: skill not served under skill://: ${built.reason}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** `skills/list` — one entry per servable, in-scope skill. */
|
|
133
|
+
listSkillEntries(context) {
|
|
134
|
+
return this.catalog(loadCanonicalSource(context.homeDir), context.agentId).map(({ entry }) => entry);
|
|
135
|
+
}
|
|
136
|
+
/** `skills/get` — lookup by URI, independent of listing. Out-of-scope and
|
|
137
|
+
* unknown URIs are indistinguishable by design. */
|
|
138
|
+
getSkillEntry(uri, context) {
|
|
139
|
+
const parsed = parseSkillUri(uri);
|
|
140
|
+
if (!parsed || parsed.path !== "SKILL.md")
|
|
141
|
+
return undefined;
|
|
142
|
+
return this.listSkillEntries(context).find((entry) => entry.uri === uri);
|
|
143
|
+
}
|
|
103
144
|
listTools() {
|
|
104
145
|
return [SEARCH_TOOL, READ_TOOL, READ_FILE_TOOL];
|
|
105
146
|
}
|
|
@@ -116,7 +157,7 @@ export class SkillProvider {
|
|
|
116
157
|
}
|
|
117
158
|
listResources(context) {
|
|
118
159
|
const canonical = loadCanonicalSource(context.homeDir);
|
|
119
|
-
|
|
160
|
+
const legacy = canonical.skills
|
|
120
161
|
.filter((skill) => hasSkillDocument(skill) && isInScope(context.agentId, skill.scope, canonical.managedAgents))
|
|
121
162
|
.map((skill) => ({
|
|
122
163
|
uri: skillUri(skill.name),
|
|
@@ -124,8 +165,15 @@ export class SkillProvider {
|
|
|
124
165
|
description: `Instructions for the ${skill.name} skill`,
|
|
125
166
|
mimeType: "text/markdown",
|
|
126
167
|
}));
|
|
168
|
+
// The convention's view of the same data, additive to the above
|
|
169
|
+
// (design.md D1): every manifest file, so a host that lists resources
|
|
170
|
+
// sees exactly what skills/list advertises.
|
|
171
|
+
const conventional = this.catalog(canonical, context.agentId).flatMap(({ entry }) => entry.resources.map((file) => ({ uri: file.uri, name: file.uri.slice("skill://".length), mimeType: file.uri.endsWith(".md") ? "text/markdown" : "text/plain", size: file.size })));
|
|
172
|
+
return [...legacy, ...conventional];
|
|
127
173
|
}
|
|
128
174
|
readResource(uri, context) {
|
|
175
|
+
if (uri.protocol === "skill:")
|
|
176
|
+
return this.readConventionalResource(uri.toString(), context);
|
|
129
177
|
if (uri.protocol !== "trellis:" || uri.hostname !== "skills") {
|
|
130
178
|
throw new Error(`unsupported skill resource URI "${uri.toString()}"`);
|
|
131
179
|
}
|
|
@@ -140,6 +188,18 @@ export class SkillProvider {
|
|
|
140
188
|
const content = this.readBoundedFile(resolve(skill.dir, "SKILL.md"), skill.dir);
|
|
141
189
|
return { contents: [{ uri: uri.toString(), mimeType: "text/markdown", text: content }] };
|
|
142
190
|
}
|
|
191
|
+
/** Serves only what the manifest advertises, from the same reader that
|
|
192
|
+
* computed its digests — the two cannot disagree. */
|
|
193
|
+
readConventionalResource(uri, context) {
|
|
194
|
+
const parsed = parseSkillUri(uri);
|
|
195
|
+
const canonical = loadCanonicalSource(context.homeDir);
|
|
196
|
+
const skill = parsed ? skillFor(canonical, context.agentId, parsed.name) : undefined;
|
|
197
|
+
const served = skill && parsed ? this.catalog(canonical, context.agentId).find(({ entry }) => entry.uri === `skill://${parsed.name}/SKILL.md`) : undefined;
|
|
198
|
+
if (!parsed || !skill || !served || !served.entry.resources.some((file) => file.uri === uri)) {
|
|
199
|
+
throw new Error(`skill resource "${uri}" is not available to ${context.agentId}`);
|
|
200
|
+
}
|
|
201
|
+
return { contents: [toResourceContent(uri, parsed.path, readSkillFileBytes(skill.dir, parsed.path))] };
|
|
202
|
+
}
|
|
143
203
|
search(canonical, agentId, input) {
|
|
144
204
|
const query = typeof input.query === "string" ? input.query.trim().toLowerCase() : "";
|
|
145
205
|
const limit = typeof input.limit === "number" ? Math.min(50, Math.max(1, Math.floor(input.limit))) : 20;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CallToolResult, Tool } from "@modelcontextprotocol/
|
|
1
|
+
import type { CallToolResult, Tool } from "@modelcontextprotocol/server";
|
|
2
2
|
import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
|
|
3
3
|
export declare class TaskProvider implements TrellisProvider {
|
|
4
4
|
readonly id = "tasks";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Client, Resource, ReadResourceResult } from "@modelcontextprotocol/client";
|
|
2
|
+
import { type SkillEntry } from "./skillManifest.js";
|
|
3
|
+
/** The outer response belongs to the gateway; upstream identity is provenance. */
|
|
4
|
+
export declare function withoutUpstreamServerInfo<T extends {
|
|
5
|
+
_meta?: Record<string, unknown>;
|
|
6
|
+
}>(result: T): T;
|
|
7
|
+
/** Retain path segments so relative supporting-file references remain valid. */
|
|
8
|
+
export declare function upstreamUri(source: string, uri: string): string;
|
|
9
|
+
export declare class UpstreamResources {
|
|
10
|
+
private readonly timeoutMs;
|
|
11
|
+
private readonly warn;
|
|
12
|
+
private readonly clients;
|
|
13
|
+
private readonly files;
|
|
14
|
+
constructor(timeoutMs: number, warn: (message: string) => void);
|
|
15
|
+
add(source: string, client: Client): void;
|
|
16
|
+
private skillsEnabled;
|
|
17
|
+
private isolated;
|
|
18
|
+
listResources(): Promise<Resource[]>;
|
|
19
|
+
private registerSkill;
|
|
20
|
+
listSkills(): Promise<SkillEntry[]>;
|
|
21
|
+
getSkill(uri: string): Promise<SkillEntry | undefined>;
|
|
22
|
+
readResource(uri: string): Promise<ReadResourceResult>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { digestOf, MAX_SKILL_BYTES, MAX_SKILL_FILES } from "./skillManifest.js";
|
|
5
|
+
import { withTimeout } from "./mcpConnect.js";
|
|
6
|
+
const EXTENSION = "io.modelcontextprotocol/skills";
|
|
7
|
+
const listSchema = z.looseObject({ skills: z.array(z.unknown()), nextCursor: z.string().optional() });
|
|
8
|
+
const getSchema = z.looseObject({ skill: z.unknown() });
|
|
9
|
+
const PREFIX = "trellis-upstream:";
|
|
10
|
+
/** The outer response belongs to the gateway; upstream identity is provenance. */
|
|
11
|
+
export function withoutUpstreamServerInfo(result) {
|
|
12
|
+
if (!result._meta || !("io.modelcontextprotocol/serverInfo" in result._meta))
|
|
13
|
+
return result;
|
|
14
|
+
const { "io.modelcontextprotocol/serverInfo": _server, ...meta } = result._meta;
|
|
15
|
+
return { ...result, _meta: meta };
|
|
16
|
+
}
|
|
17
|
+
/** Retain path segments so relative supporting-file references remain valid. */
|
|
18
|
+
export function upstreamUri(source, uri) {
|
|
19
|
+
const original = new URL(uri);
|
|
20
|
+
if (original.username || original.password || !original.pathname.startsWith("/"))
|
|
21
|
+
throw new Error("resource URI must be hierarchical and contain no credentials");
|
|
22
|
+
const label = `s${Buffer.from(source).toString("hex")}`;
|
|
23
|
+
const protocol = Buffer.from(original.protocol).toString("base64url");
|
|
24
|
+
return `${PREFIX}//${label}/${protocol}/${encodeURIComponent(original.host) || "~"}${original.pathname}${original.search}${original.hash}`;
|
|
25
|
+
}
|
|
26
|
+
function originalUri(uri) {
|
|
27
|
+
try {
|
|
28
|
+
const exposed = new URL(uri);
|
|
29
|
+
if (exposed.protocol !== PREFIX || !/^s(?:[0-9a-f]{2})+$/.test(exposed.hostname))
|
|
30
|
+
return undefined;
|
|
31
|
+
const match = /^\/([^/]+)\/([^/]+)(\/.*)$/.exec(exposed.pathname);
|
|
32
|
+
if (!match)
|
|
33
|
+
return undefined;
|
|
34
|
+
const protocol = Buffer.from(match[1], "base64url").toString();
|
|
35
|
+
if (!/^[a-z][a-z0-9+.-]*:$/.test(protocol))
|
|
36
|
+
return undefined;
|
|
37
|
+
const source = Buffer.from(exposed.hostname.slice(1), "hex").toString();
|
|
38
|
+
const original = `${protocol}//${match[2] === "~" ? "" : decodeURIComponent(match[2])}${match[3]}${exposed.search}${exposed.hash}`;
|
|
39
|
+
if (upstreamUri(source, original) !== uri)
|
|
40
|
+
return undefined;
|
|
41
|
+
return { source, original };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class UpstreamResources {
|
|
48
|
+
timeoutMs;
|
|
49
|
+
warn;
|
|
50
|
+
clients = new Map();
|
|
51
|
+
files = new Map();
|
|
52
|
+
constructor(timeoutMs, warn) {
|
|
53
|
+
this.timeoutMs = timeoutMs;
|
|
54
|
+
this.warn = warn;
|
|
55
|
+
}
|
|
56
|
+
add(source, client) { this.clients.set(source, client); }
|
|
57
|
+
skillsEnabled(client) {
|
|
58
|
+
return Boolean(client.getServerCapabilities()?.resources && client.getDiscoverResult()?.capabilities.extensions?.[EXTENSION]);
|
|
59
|
+
}
|
|
60
|
+
async isolated(source, work, fallback) {
|
|
61
|
+
try {
|
|
62
|
+
return await withTimeout(work(), this.timeoutMs, "upstream resource request timed out");
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
this.warn(`trellis-mcp-runtime: upstream "${source}" resources: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async listResources() {
|
|
70
|
+
const lists = await Promise.all([...this.clients].map(([source, client]) => this.isolated(source, async () => {
|
|
71
|
+
if (!client.getServerCapabilities()?.resources)
|
|
72
|
+
return [];
|
|
73
|
+
const { resources } = await client.listResources();
|
|
74
|
+
if (resources.length > 8192)
|
|
75
|
+
throw new Error("upstream resource catalog exceeds 8192 entries");
|
|
76
|
+
return resources.map((resource) => {
|
|
77
|
+
const uri = upstreamUri(source, resource.uri);
|
|
78
|
+
if (!this.files.get(uri)?.manifest)
|
|
79
|
+
this.files.set(uri, { source, original: resource.uri });
|
|
80
|
+
return { ...resource, uri, _meta: { ...resource._meta, "io.trellis/origin": { server: source, uri: resource.uri } } };
|
|
81
|
+
});
|
|
82
|
+
}, [])));
|
|
83
|
+
return lists.flat();
|
|
84
|
+
}
|
|
85
|
+
registerSkill(source, value) {
|
|
86
|
+
if (!value || typeof value !== "object")
|
|
87
|
+
throw new Error("invalid upstream skill entry");
|
|
88
|
+
const entry = value;
|
|
89
|
+
if (typeof entry.uri !== "string" || !entry.frontmatter || typeof entry.frontmatter.name !== "string" || typeof entry.frontmatter.description !== "string" || !entry.frontmatter.description.trim())
|
|
90
|
+
throw new Error("invalid upstream skill metadata");
|
|
91
|
+
if (!Array.isArray(entry.resources) || entry.resources.length === 0 || entry.resources.length > MAX_SKILL_FILES)
|
|
92
|
+
throw new Error("upstream skill needs a bounded, complete manifest");
|
|
93
|
+
const root = new URL(".", entry.uri).toString();
|
|
94
|
+
if (!new URL(entry.uri).pathname.endsWith("/SKILL.md") || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(entry.frontmatter.name) || entry.frontmatter.name.length > 64)
|
|
95
|
+
throw new Error("invalid upstream skill name or entry URI");
|
|
96
|
+
const directory = new URL(root);
|
|
97
|
+
const parent = directory.pathname.split("/").filter(Boolean).at(-1) ?? directory.hostname;
|
|
98
|
+
if (decodeURIComponent(parent) !== entry.frontmatter.name)
|
|
99
|
+
throw new Error("upstream skill name does not match its directory");
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
let total = 0;
|
|
102
|
+
for (const file of entry.resources) {
|
|
103
|
+
if (!file || typeof file.uri !== "string" || !file.uri.startsWith(root) || !/^sha256:[a-f0-9]{64}$/.test(file.digest) || !Number.isSafeInteger(file.size) || file.size < 0 || seen.has(file.uri))
|
|
104
|
+
throw new Error("invalid upstream skill manifest");
|
|
105
|
+
const parsed = new URL(file.uri);
|
|
106
|
+
if (!parsed.toString().startsWith(root))
|
|
107
|
+
throw new Error("skill file escaped its source directory");
|
|
108
|
+
upstreamUri(source, file.uri);
|
|
109
|
+
seen.add(file.uri);
|
|
110
|
+
total += file.size;
|
|
111
|
+
}
|
|
112
|
+
if (total > MAX_SKILL_BYTES || !seen.has(entry.uri))
|
|
113
|
+
throw new Error("upstream skill manifest exceeds limits or omits SKILL.md");
|
|
114
|
+
const uri = upstreamUri(source, entry.uri);
|
|
115
|
+
for (const [key, route] of this.files)
|
|
116
|
+
if (route.skill === uri)
|
|
117
|
+
this.files.delete(key);
|
|
118
|
+
const resources = entry.resources.map((file) => {
|
|
119
|
+
const exposed = upstreamUri(source, file.uri);
|
|
120
|
+
this.files.set(exposed, { source, original: file.uri, manifest: file, skill: uri, ...(file.uri === entry.uri ? { frontmatter: entry.frontmatter } : {}) });
|
|
121
|
+
return { ...file, uri: exposed };
|
|
122
|
+
});
|
|
123
|
+
return { ...entry, uri, resources, _meta: { "io.trellis/origin": { server: source, uri: entry.uri } } };
|
|
124
|
+
}
|
|
125
|
+
async listSkills() {
|
|
126
|
+
const lists = await Promise.all([...this.clients].map(([source, client]) => this.isolated(source, async () => {
|
|
127
|
+
if (!this.skillsEnabled(client))
|
|
128
|
+
return [];
|
|
129
|
+
const entries = [];
|
|
130
|
+
let cursor;
|
|
131
|
+
const cursors = new Set();
|
|
132
|
+
for (let page = 0; page < 64; page++) {
|
|
133
|
+
const result = await client.request({ method: "skills/list", params: cursor ? { cursor } : {} }, listSchema, { timeout: this.timeoutMs });
|
|
134
|
+
for (const entry of result.skills) {
|
|
135
|
+
if (entries.length >= 2048)
|
|
136
|
+
throw new Error("upstream skill catalog exceeds 2048 entries");
|
|
137
|
+
try {
|
|
138
|
+
entries.push(this.registerSkill(source, entry));
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
this.warn(`trellis-mcp-runtime: omitted skill from "${source}": ${err instanceof Error ? err.message : String(err)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!result.nextCursor)
|
|
145
|
+
return entries;
|
|
146
|
+
if (cursors.has(result.nextCursor))
|
|
147
|
+
throw new Error("repeated upstream skill cursor");
|
|
148
|
+
cursors.add(result.nextCursor);
|
|
149
|
+
cursor = result.nextCursor;
|
|
150
|
+
}
|
|
151
|
+
throw new Error("upstream skill pagination exceeded 64 pages");
|
|
152
|
+
}, [])));
|
|
153
|
+
return lists.flat();
|
|
154
|
+
}
|
|
155
|
+
async getSkill(uri) {
|
|
156
|
+
const decoded = originalUri(uri);
|
|
157
|
+
if (!decoded)
|
|
158
|
+
return undefined;
|
|
159
|
+
const client = this.clients.get(decoded.source);
|
|
160
|
+
if (!client || !this.skillsEnabled(client))
|
|
161
|
+
return undefined;
|
|
162
|
+
const result = await client.request({ method: "skills/get", params: { uri: decoded.original } }, getSchema, { timeout: this.timeoutMs });
|
|
163
|
+
if (result.skill?.uri !== decoded.original)
|
|
164
|
+
throw new Error("upstream returned another skill identity");
|
|
165
|
+
const entry = this.registerSkill(decoded.source, result.skill);
|
|
166
|
+
if (entry.uri !== uri)
|
|
167
|
+
throw new Error("upstream returned another skill identity");
|
|
168
|
+
return entry;
|
|
169
|
+
}
|
|
170
|
+
async readResource(uri) {
|
|
171
|
+
const route = this.files.get(uri);
|
|
172
|
+
const client = route && this.clients.get(route.source);
|
|
173
|
+
if (!route || !client)
|
|
174
|
+
throw new Error("unknown upstream resource");
|
|
175
|
+
const result = await client.readResource({ uri: route.original }, { timeout: this.timeoutMs });
|
|
176
|
+
if (result.contents.length !== 1 || result.contents[0].uri !== route.original)
|
|
177
|
+
throw new Error("upstream resource identity mismatch");
|
|
178
|
+
const content = result.contents[0];
|
|
179
|
+
if (route.manifest) {
|
|
180
|
+
const bytes = "blob" in content ? Buffer.from(content.blob, "base64") : Buffer.from(content.text, "utf8");
|
|
181
|
+
if (bytes.length !== route.manifest.size || digestOf(bytes) !== route.manifest.digest)
|
|
182
|
+
throw new Error("upstream skill file differs from its manifest; refresh the skill entry");
|
|
183
|
+
if (route.frontmatter) {
|
|
184
|
+
const text = new TextDecoder("utf8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
185
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
|
|
186
|
+
if (!match || !isDeepStrictEqual(parseYaml(match[1]), route.frontmatter))
|
|
187
|
+
throw new Error("upstream skill frontmatter differs from its entry");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { ...withoutUpstreamServerInfo(result), ttlMs: 0, cacheScope: "private", contents: [{ ...content, uri }] };
|
|
191
|
+
}
|
|
192
|
+
}
|