@farming-labs/docs 0.2.120 → 0.2.122
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/README.md +8 -0
- package/dist/access.d.mts +13 -0
- package/dist/access.mjs +56 -0
- package/dist/agent-conformance.d.mts +60 -0
- package/dist/agent-conformance.mjs +538 -0
- package/dist/agent-contract.d.mts +47 -0
- package/dist/agent-contract.mjs +645 -0
- package/dist/agent-detection.d.mts +6 -0
- package/dist/agent-detection.mjs +83 -0
- package/dist/agent-evals.d.mts +224 -0
- package/dist/{agent-evals-BLNLvXbG.mjs → agent-evals.mjs} +7 -565
- package/dist/agent-feedback-evaluations-node.mjs +38 -0
- package/dist/agent-feedback-evaluations.d.mts +91 -0
- package/dist/{agent-feedback-evaluations-CcMGlgsO.mjs → agent-feedback-evaluations.mjs} +2 -2
- package/dist/agent-feedback-loop.d.mts +45 -0
- package/dist/{agent-feedback-loop-CcZL_sb1.mjs → agent-feedback-loop.mjs} +1 -1
- package/dist/agent-maintenance.d.mts +71 -0
- package/dist/{agent-maintenance-CuZ7iAKq.mjs → agent-maintenance.mjs} +1 -1
- package/dist/agent-provenance.d.mts +23 -0
- package/dist/agent-provenance.mjs +77 -0
- package/dist/agent-scope.mjs +321 -0
- package/dist/agent-skills-archive.mjs +86 -0
- package/dist/agent-skills-bundle.d.mts +1 -1
- package/dist/{agent-skills-progressive-disclosure-PjPtTYL-.mjs → agent-skills-progressive-disclosure.mjs} +2 -2
- package/dist/{agent-skills-server-D5IuvwM7.d.mts → agent-skills-server.d.mts} +3 -3
- package/dist/{agent-skills-server-BC7md56k.mjs → agent-skills-server.mjs} +2 -2
- package/dist/agent-skills-vite.d.mts +3 -3
- package/dist/agent-skills-vite.mjs +1 -2
- package/dist/agent-surface-drift.mjs +236 -0
- package/dist/{golden-evaluations-CLUViycJ.mjs → agent-usefulness.mjs} +5 -290
- package/dist/{content-change-hydration-wqXIZHp6.d.mts → agent.d.mts} +10 -411
- package/dist/agent.mjs +2729 -0
- package/dist/{analytics-w3FDOksr.d.mts → analytics.d.mts} +2 -2
- package/dist/{analytics-p8Vhhu2P.mjs → analytics.mjs} +2 -2
- package/dist/{sitemap-server-BUdPBcGs.d.mts → api-reference.d.mts} +2 -6
- package/dist/api-reference.mjs +1481 -0
- package/dist/{audience-exposure-DBrFWULA.d.mts → audience-exposure.d.mts} +1 -1
- package/dist/audience-exposure.mjs +8 -0
- package/dist/audience.d.mts +44 -0
- package/dist/audience.mjs +1654 -0
- package/dist/authoring-mcp.d.mts +44 -0
- package/dist/{authoring-mcp-CiCUQe3o.mjs → authoring-mcp.mjs} +3 -3
- package/dist/browser.d.mts +6 -4
- package/dist/browser.mjs +6 -7
- package/dist/build.d.mts +8 -0
- package/dist/build.mjs +8 -0
- package/dist/changelog.d.mts +20 -0
- package/dist/changelog.mjs +39 -0
- package/dist/{agent-export-CA6IUQ7T.mjs → cli/agent-export.mjs} +19 -45
- package/dist/{agent-CYk2DxY7.mjs → cli/agent.mjs} +6 -35
- package/dist/{agents-BUiVmj8i.mjs → cli/agents.mjs} +5 -18
- package/dist/cli/api-doctor.mjs +206 -0
- package/dist/{authoring-C2ymvYWl.mjs → cli/authoring.mjs} +2 -8
- package/dist/{cloud-6eKrlmKd.mjs → cli/cloud.mjs} +3 -3
- package/dist/{codeblocks-CHpgAwY-.mjs → cli/codeblocks.mjs} +2 -2
- package/dist/cli/config.d.mts +11 -0
- package/dist/{config-BZnKAj_Y.mjs → cli/config.mjs} +1 -1
- package/dist/cli/dev-banner.mjs +58 -0
- package/dist/{dev-Bnn-NUNa.mjs → cli/dev.mjs} +3 -57
- package/dist/{doctor-Bfkrkzad.mjs → cli/doctor.mjs} +41 -226
- package/dist/{downgrade-CHMta85e.mjs → cli/downgrade.mjs} +2 -2
- package/dist/cli/errors.d.mts +4 -0
- package/dist/{errors-BtnVLhNF.mjs → cli/errors.mjs} +1 -1
- package/dist/{feedback-evals-DMilg9pY.mjs → cli/feedback-evals.mjs} +3 -36
- package/dist/{feedback-DL4T5oIO.mjs → cli/feedback.mjs} +1 -1
- package/dist/cli/git-dates.mjs +35 -0
- package/dist/cli/golden-evaluations.mjs +52 -0
- package/dist/cli/index.d.mts +2 -3
- package/dist/cli/index.mjs +38 -38
- package/dist/{init-K0LmXDZ5.mjs → cli/init.mjs} +3 -3
- package/dist/{mcp-iLCmnPJI.mjs → cli/mcp.mjs} +7 -19
- package/dist/{openapi-diff-HcpUNjA-.mjs → cli/openapi-diff.mjs} +2 -7
- package/dist/{openapi-sdk-BQKC6H-_.mjs → cli/openapi-sdk.mjs} +2 -7
- package/dist/{openapi-source-DK6ts_Bk.mjs → cli/openapi-source.mjs} +2 -2
- package/dist/{package-version-HJhYeq4M.mjs → cli/package-version.mjs} +2 -2
- package/dist/{propose-JmUv_BTt.mjs → cli/propose.mjs} +1 -1
- package/dist/{review-C6EmbV0l.mjs → cli/review.mjs} +16 -15
- package/dist/{robots-DteZB8vl.mjs → cli/robots.mjs} +3 -7
- package/dist/{search-Bhuv_YsL.mjs → cli/search.mjs} +7 -20
- package/dist/{sitemap-DW5LDKDa.mjs → cli/sitemap.mjs} +7 -42
- package/dist/{skills-BAPQLSCm.mjs → cli/skills.mjs} +5 -19
- package/dist/{templates-BuQ8YsGv.mjs → cli/templates.mjs} +1 -1
- package/dist/{upgrade-DlQMOFV1.mjs → cli/upgrade.mjs} +2 -2
- package/dist/{utils-C7KDrPpA.mjs → cli/utils.mjs} +1 -1
- package/dist/client/react.d.mts +2 -18
- package/dist/client/react.mjs +1 -157
- package/dist/{cloud-analytics-D14HplI7.mjs → cloud-analytics.mjs} +1 -1
- package/dist/{cloud-ask-ai-BrsTXQf6.d.mts → cloud-ask-ai.d.mts} +2 -2
- package/dist/{cloud-ask-ai-BqKhjpS5.mjs → cloud-ask-ai.mjs} +3 -3
- package/dist/code-blocks.d.mts +51 -0
- package/dist/{code-blocks-GYdvS3BH.mjs → code-blocks.mjs} +2 -2
- package/dist/code-group-mdx.d.mts +11 -0
- package/dist/code-group-mdx.mjs +57 -0
- package/dist/content-change-hydration.d.mts +87 -0
- package/dist/{openapi-mcp-BIOTLiA7.mjs → content-change-hydration.mjs} +5 -210
- package/dist/content-changes.d.mts +47 -0
- package/dist/content-changes.mjs +216 -0
- package/dist/create-theme.d.mts +48 -0
- package/dist/create-theme.mjs +57 -0
- package/dist/date.d.mts +5 -0
- package/dist/date.mjs +17 -0
- package/dist/define-docs.d.mts +9 -0
- package/dist/define-docs.mjs +46 -0
- package/dist/docs-cloud-client.d.mts +18 -0
- package/dist/docs-cloud-client.mjs +161 -0
- package/dist/docs-cloud-server.d.mts +2 -2
- package/dist/docs-cloud-server.mjs +2 -3
- package/dist/heading-toc.d.mts +10 -0
- package/dist/heading-toc.mjs +23 -0
- package/dist/http-body.d.mts +14 -0
- package/dist/{http-body-DkAPk6D5.mjs → http-body.mjs} +1 -1
- package/dist/http-cache.d.mts +25 -0
- package/dist/http-cache.mjs +140 -0
- package/dist/http-link.mjs +81 -0
- package/dist/i18n.d.mts +18 -0
- package/dist/{i18n-B1gJ9EW0.mjs → i18n.mjs} +1 -1
- package/dist/index.d.mts +42 -674
- package/dist/index.mjs +40 -14
- package/dist/markdown-rendering.mjs +1 -1
- package/dist/markdown-section-cache.mjs +49 -0
- package/dist/markdown-sections.d.mts +47 -0
- package/dist/{markdown-sections-9Oxr33b-.mjs → markdown-sections.mjs} +29 -703
- package/dist/mcp-auth.d.mts +14 -0
- package/dist/mcp-auth.mjs +99 -0
- package/dist/mcp.d.mts +5 -3
- package/dist/mcp.mjs +21 -9
- package/dist/mdx.d.mts +6 -0
- package/dist/mdx.mjs +7 -0
- package/dist/metadata.d.mts +60 -0
- package/dist/{metadata-BOHn0Y5K.mjs → metadata.mjs} +2 -2
- package/dist/native-api-reference.mjs +968 -0
- package/dist/okf.d.mts +42 -0
- package/dist/okf.mjs +145 -0
- package/dist/openapi-contract.mjs +154 -0
- package/dist/{openapi-mcp-node-C4jUpZG6.mjs → openapi-mcp-node.mjs} +1 -1
- package/dist/openapi-mcp.d.mts +26 -0
- package/dist/openapi-mcp.mjs +132 -0
- package/dist/openapi-operations.d.mts +71 -0
- package/dist/{openapi-operations-DFpaqOqA.mjs → openapi-operations.mjs} +1 -1
- package/dist/openapi-overlays.mjs +221 -0
- package/dist/openapi-references.mjs +233 -0
- package/dist/pagination.d.mts +7 -0
- package/dist/pagination.mjs +100 -0
- package/dist/prompt-references.d.mts +12 -0
- package/dist/prompt-references.mjs +132 -0
- package/dist/prompt-utils.d.mts +36 -0
- package/dist/{prompt-utils-B5g-bv7o.mjs → prompt-utils.mjs} +1 -57
- package/dist/reading-time.d.mts +23 -0
- package/dist/reading-time.mjs +58 -0
- package/dist/related.d.mts +7 -0
- package/dist/related.mjs +50 -0
- package/dist/retrieval-digest.d.mts +16 -0
- package/dist/{retrieval-digest-CT-eIEcp.mjs → retrieval-digest.mjs} +1 -1
- package/dist/review.d.mts +69 -0
- package/dist/review.mjs +562 -0
- package/dist/robots.d.mts +54 -0
- package/dist/{robots-BYJAVeI6.mjs → robots.mjs} +5 -3
- package/dist/runtime.d.mts +3 -3
- package/dist/runtime.mjs +3 -9
- package/dist/search.d.mts +210 -0
- package/dist/search.mjs +3401 -0
- package/dist/server.d.mts +22 -198
- package/dist/server.mjs +21 -19
- package/dist/sidebar.d.mts +15 -0
- package/dist/sidebar.mjs +73 -0
- package/dist/sitemap-server.d.mts +7 -0
- package/dist/{sitemap-server-Lg3YhUx4.mjs → sitemap-server.mjs} +2 -2
- package/dist/{sitemap-Cl8vrixd.d.mts → sitemap.d.mts} +3 -3
- package/dist/sitemap.mjs +254 -0
- package/dist/{standards-discovery-BHPc7YIX.d.mts → standards-discovery.d.mts} +2 -2
- package/dist/{standards-discovery-sSEojU80.mjs → standards-discovery.mjs} +3 -139
- package/dist/telemetry-runtime.d.mts +42 -0
- package/dist/telemetry-runtime.mjs +238 -0
- package/dist/telemetry.d.mts +15 -0
- package/dist/telemetry.mjs +117 -0
- package/dist/{types-BJFeMXWZ.d.mts → types.d.mts} +3 -2
- package/dist/urls.d.mts +7 -0
- package/dist/urls.mjs +21 -0
- package/dist/utils.d.mts +8 -0
- package/dist/utils.mjs +22 -0
- package/package.json +21 -3
- package/dist/agent-DkAidqtV.mjs +0 -9026
- package/dist/api-reference-BYee4zMo.mjs +0 -3046
- package/dist/prompt-references-DMgbu_dz.mjs +0 -1082
- package/dist/retrieval-digest-AOx2-LbC.d.mts +0 -367
- package/dist/sidebar-CFHBj29f.mjs +0 -421
- package/dist/sidebar-CMPbVfGP.d.mts +0 -63
- /package/dist/{openapi-CfCzX_a1.mjs → cli/openapi.mjs} +0 -0
package/README.md
CHANGED
|
@@ -96,6 +96,14 @@ Routing is file-based, so a page such as `app/docs/guides/deployment/page.mdx` b
|
|
|
96
96
|
`/docs/guides/deployment` in Next.js. The CLI scaffolds the correct directory layout for every
|
|
97
97
|
supported framework.
|
|
98
98
|
|
|
99
|
+
### MDX compiler integration
|
|
100
|
+
|
|
101
|
+
Custom MDX integrations can import `remarkCodeGroup`, `applyDocsMarkdownHeadingAnchors`,
|
|
102
|
+
`encodeDocsHeadingTocUrls`, `isolateDocsMarkdownPromptReferences`, and
|
|
103
|
+
`withDocsMarkdownRenderableHeadings` from `@farming-labs/docs/mdx`. This entry point exposes the
|
|
104
|
+
same helpers without loading the unrelated core/server runtime exports. Existing imports from
|
|
105
|
+
the root and server entry points remain supported.
|
|
106
|
+
|
|
99
107
|
## Documentation for agents
|
|
100
108
|
|
|
101
109
|
Farming Labs can expose the same docs in machine-readable forms, including:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DocsAccessPrincipal, DocsPageAccessPolicy, PageAgentFrontmatter } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/access.d.ts
|
|
4
|
+
declare function normalizeDocsPageAccessPolicy(value: unknown): DocsPageAccessPolicy | undefined;
|
|
5
|
+
declare function isDocsPageAccessAllowed(policy: DocsPageAccessPolicy | undefined, principal?: DocsAccessPrincipal): boolean;
|
|
6
|
+
declare function isDocsAgentPageAccessible(page: {
|
|
7
|
+
agent?: PageAgentFrontmatter;
|
|
8
|
+
}, principal?: DocsAccessPrincipal): boolean;
|
|
9
|
+
declare function filterDocsPagesByAccess<T extends {
|
|
10
|
+
agent?: PageAgentFrontmatter;
|
|
11
|
+
}>(pages: readonly T[], principal?: DocsAccessPrincipal): T[];
|
|
12
|
+
//#endregion
|
|
13
|
+
export { filterDocsPagesByAccess, isDocsAgentPageAccessible, isDocsPageAccessAllowed, normalizeDocsPageAccessPolicy };
|
package/dist/access.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/access.ts
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function normalizeStrings(value) {
|
|
6
|
+
if (!Array.isArray(value)) return void 0;
|
|
7
|
+
const strings = Array.from(new Set(value.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : [])));
|
|
8
|
+
return strings.length > 0 ? strings : void 0;
|
|
9
|
+
}
|
|
10
|
+
function normalizeClaimValue(value) {
|
|
11
|
+
if (typeof value === "string") return value.trim() || void 0;
|
|
12
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
13
|
+
if (typeof value === "boolean") return value;
|
|
14
|
+
return normalizeStrings(value);
|
|
15
|
+
}
|
|
16
|
+
function normalizeDocsPageAccessPolicy(value) {
|
|
17
|
+
if (!isRecord(value)) return void 0;
|
|
18
|
+
const visibility = value.visibility === "public" || value.visibility === "authenticated" ? value.visibility : void 0;
|
|
19
|
+
const scopes = normalizeStrings(value.scopes);
|
|
20
|
+
const rawClaims = isRecord(value.claims) ? value.claims : void 0;
|
|
21
|
+
const claims = rawClaims ? Object.fromEntries(Object.entries(rawClaims).flatMap(([name, rawValue]) => {
|
|
22
|
+
const normalizedName = name.trim();
|
|
23
|
+
const normalizedValue = normalizeClaimValue(rawValue);
|
|
24
|
+
return normalizedName && normalizedValue !== void 0 ? [[normalizedName, normalizedValue]] : [];
|
|
25
|
+
})) : void 0;
|
|
26
|
+
const normalizedClaims = claims && Object.keys(claims).length > 0 ? claims : void 0;
|
|
27
|
+
if (!visibility && !scopes && !normalizedClaims) return void 0;
|
|
28
|
+
return {
|
|
29
|
+
...visibility ? { visibility } : {},
|
|
30
|
+
...scopes ? { scopes } : {},
|
|
31
|
+
...normalizedClaims ? { claims: normalizedClaims } : {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function claimMatches(expected, actual) {
|
|
35
|
+
const expectedValues = Array.isArray(expected) ? expected : [expected];
|
|
36
|
+
const actualValues = Array.isArray(actual) ? actual : [actual];
|
|
37
|
+
return expectedValues.some((expectedValue) => actualValues.some((actualValue) => actualValue === expectedValue));
|
|
38
|
+
}
|
|
39
|
+
function isDocsPageAccessAllowed(policy, principal) {
|
|
40
|
+
if (!policy) return true;
|
|
41
|
+
if (!(policy.visibility !== "public" || Boolean(policy.scopes?.length) || Boolean(policy.claims && Object.keys(policy.claims).length > 0))) return true;
|
|
42
|
+
if (!principal) return false;
|
|
43
|
+
const scopes = new Set(principal.scopes ?? []);
|
|
44
|
+
if (policy.scopes?.some((scope) => !scopes.has(scope))) return false;
|
|
45
|
+
for (const [name, expected] of Object.entries(policy.claims ?? {})) if (!claimMatches(expected, principal.claims?.[name])) return false;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function isDocsAgentPageAccessible(page, principal) {
|
|
49
|
+
return isDocsPageAccessAllowed(page.agent?.access, principal);
|
|
50
|
+
}
|
|
51
|
+
function filterDocsPagesByAccess(pages, principal) {
|
|
52
|
+
return pages.filter((page) => isDocsAgentPageAccessible(page, principal));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { filterDocsPagesByAccess, isDocsAgentPageAccessible, isDocsPageAccessAllowed, normalizeDocsPageAccessPolicy };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/agent-conformance.d.ts
|
|
2
|
+
declare const DOCS_AGENT_CONTRACT_VERSION = "1.3";
|
|
3
|
+
type DocsAgentAdapter = "next" | "tanstack-start" | "farmjs" | "sveltekit" | "astro" | "nuxt";
|
|
4
|
+
type DocsAgentContractSurface = "discovery" | "api-catalog" | "api-catalog-head" | "agent-skills-index" | "agent-skills-index-head" | "agent-skill" | "agent-skill-head" | "config" | "diagnostics" | "feedback-schema" | "markdown" | "markdown-accept" | "markdown-locale" | "markdown-missing" | "llms" | "llms-full" | "agents" | "skill" | "sitemap-xml" | "sitemap-markdown" | "robots" | "mcp";
|
|
5
|
+
interface DocsAgentContractRequest {
|
|
6
|
+
url: string;
|
|
7
|
+
init?: RequestInit;
|
|
8
|
+
}
|
|
9
|
+
interface DocsAgentContractExpectation {
|
|
10
|
+
statuses: readonly number[];
|
|
11
|
+
contentTypes: readonly string[];
|
|
12
|
+
bodyIncludes?: readonly string[];
|
|
13
|
+
bodyEmpty?: boolean;
|
|
14
|
+
headerIncludes?: Readonly<Record<string, readonly string[]>>;
|
|
15
|
+
linkRelations?: readonly {
|
|
16
|
+
href: string;
|
|
17
|
+
rel: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
interface DocsAgentContractCase {
|
|
22
|
+
surface: DocsAgentContractSurface;
|
|
23
|
+
request: DocsAgentContractRequest;
|
|
24
|
+
expect: DocsAgentContractExpectation;
|
|
25
|
+
}
|
|
26
|
+
interface DocsAgentConformanceCaseResult {
|
|
27
|
+
surface: DocsAgentContractSurface;
|
|
28
|
+
passed: boolean;
|
|
29
|
+
status?: number;
|
|
30
|
+
contentType?: string;
|
|
31
|
+
issues: string[];
|
|
32
|
+
}
|
|
33
|
+
interface DocsAgentConformanceReport {
|
|
34
|
+
adapter: DocsAgentAdapter;
|
|
35
|
+
contractVersion: typeof DOCS_AGENT_CONTRACT_VERSION;
|
|
36
|
+
passed: boolean;
|
|
37
|
+
cases: DocsAgentConformanceCaseResult[];
|
|
38
|
+
}
|
|
39
|
+
interface RunDocsAgentConformanceOptions {
|
|
40
|
+
adapter: DocsAgentAdapter;
|
|
41
|
+
/** Origin used to construct requests. Defaults to http://localhost. */
|
|
42
|
+
origin?: string;
|
|
43
|
+
/** Public docs entry. Defaults to docs. */
|
|
44
|
+
entry?: string;
|
|
45
|
+
/** Locale fixture expected to render the word "Bonjour". Set false to skip. */
|
|
46
|
+
locale?: string | false;
|
|
47
|
+
handle(request: Request, surface: DocsAgentContractSurface): Response | Promise<Response>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The framework-neutral public surface every first-party adapter must implement.
|
|
51
|
+
* Adapter tests consume this list so new routes and invariants are added once.
|
|
52
|
+
*/
|
|
53
|
+
declare function createDocsAgentContractCases(options?: {
|
|
54
|
+
origin?: string;
|
|
55
|
+
entry?: string;
|
|
56
|
+
locale?: string | false;
|
|
57
|
+
}): DocsAgentContractCase[];
|
|
58
|
+
declare function runDocsAgentConformance(options: RunDocsAgentConformanceOptions): Promise<DocsAgentConformanceReport>;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { DOCS_AGENT_CONTRACT_VERSION, DocsAgentAdapter, DocsAgentConformanceCaseResult, DocsAgentConformanceReport, DocsAgentContractCase, DocsAgentContractExpectation, DocsAgentContractRequest, DocsAgentContractSurface, RunDocsAgentConformanceOptions, createDocsAgentContractCases, runDocsAgentConformance };
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { DEFAULT_MCP_WELL_KNOWN_ROUTE } from "./mcp-auth.mjs";
|
|
2
|
+
import { isDocsAgentSkillName, validateDocsAgentSkillFrontmatter } from "./agent-skills-spec.mjs";
|
|
3
|
+
import { AGENT_SKILLS_DISCOVERY_SCHEMA_URI, API_CATALOG_MEDIA_TYPE, API_CATALOG_PROFILE_URI, DEFAULT_AGENT_SKILLS_INDEX_ROUTE, DEFAULT_AGENT_SKILLS_ROUTE_PREFIX, DEFAULT_API_CATALOG_ROUTE, DOCS_AGENT_MANIFEST_FORMAT, DOCS_AGENT_MANIFEST_SCHEMA_MEDIA_TYPE, DOCS_AGENT_MANIFEST_SCHEMA_URI, DOCS_AGENT_MANIFEST_VERSION } from "./standards-discovery.mjs";
|
|
4
|
+
import { DEFAULT_SITEMAP_MD_ROUTE, DEFAULT_SITEMAP_XML_ROUTE } from "./sitemap.mjs";
|
|
5
|
+
import { DEFAULT_AGENTS_MD_ROUTE, DEFAULT_AGENT_FEEDBACK_ROUTE, DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE, DEFAULT_DOCS_CONFIG_ROUTE, DEFAULT_DOCS_DIAGNOSTICS_ROUTE, DEFAULT_LLMS_FULL_TXT_ROUTE, DEFAULT_LLMS_TXT_ROUTE, DEFAULT_SKILL_MD_ROUTE } from "./agent.mjs";
|
|
6
|
+
import { AGENT_SKILL_ARCHIVE_MAX_UNCOMPRESSED_BYTES, readAgentSkillDocumentFromTar } from "./agent-skills-archive.mjs";
|
|
7
|
+
import { httpLinkMatchesExpectation, parseHttpLinkHeader, splitHttpList, unquoteHttpValue } from "./http-link.mjs";
|
|
8
|
+
import { DEFAULT_ROBOTS_TXT_ROUTE } from "./robots.mjs";
|
|
9
|
+
|
|
10
|
+
//#region src/agent-conformance.ts
|
|
11
|
+
const DOCS_AGENT_CONTRACT_VERSION = "1.3";
|
|
12
|
+
function normalizeOrigin(value) {
|
|
13
|
+
return value.replace(/\/+$/, "");
|
|
14
|
+
}
|
|
15
|
+
function normalizeEntry(value) {
|
|
16
|
+
return value.replace(/^\/+|\/+$/g, "") || "docs";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The framework-neutral public surface every first-party adapter must implement.
|
|
20
|
+
* Adapter tests consume this list so new routes and invariants are added once.
|
|
21
|
+
*/
|
|
22
|
+
function createDocsAgentContractCases(options = {}) {
|
|
23
|
+
const origin = normalizeOrigin(options.origin ?? "http://localhost");
|
|
24
|
+
const entry = normalizeEntry(options.entry ?? "docs");
|
|
25
|
+
const locale = options.locale === void 0 ? "fr" : options.locale;
|
|
26
|
+
const url = (route) => `${origin}${route.startsWith("/") ? route : `/${route}`}`;
|
|
27
|
+
const markdown = ["text/markdown"];
|
|
28
|
+
const json = ["application/json"];
|
|
29
|
+
const cases = [
|
|
30
|
+
{
|
|
31
|
+
surface: "discovery",
|
|
32
|
+
request: { url: url(DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE) },
|
|
33
|
+
expect: {
|
|
34
|
+
statuses: [200],
|
|
35
|
+
contentTypes: json,
|
|
36
|
+
linkRelations: [
|
|
37
|
+
{
|
|
38
|
+
href: DEFAULT_API_CATALOG_ROUTE,
|
|
39
|
+
rel: "api-catalog"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
href: DEFAULT_AGENT_SKILLS_INDEX_ROUTE,
|
|
43
|
+
rel: "service-meta"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
href: DOCS_AGENT_MANIFEST_SCHEMA_URI,
|
|
47
|
+
rel: "describedby",
|
|
48
|
+
type: DOCS_AGENT_MANIFEST_SCHEMA_MEDIA_TYPE
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
surface: "api-catalog",
|
|
55
|
+
request: { url: url(DEFAULT_API_CATALOG_ROUTE) },
|
|
56
|
+
expect: {
|
|
57
|
+
statuses: [200],
|
|
58
|
+
contentTypes: [`${API_CATALOG_MEDIA_TYPE}; profile="${API_CATALOG_PROFILE_URI}"`],
|
|
59
|
+
bodyIncludes: [
|
|
60
|
+
"linkset",
|
|
61
|
+
DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE,
|
|
62
|
+
DEFAULT_AGENT_SKILLS_INDEX_ROUTE
|
|
63
|
+
],
|
|
64
|
+
linkRelations: [{
|
|
65
|
+
href: DEFAULT_API_CATALOG_ROUTE,
|
|
66
|
+
rel: "api-catalog"
|
|
67
|
+
}]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
surface: "api-catalog-head",
|
|
72
|
+
request: {
|
|
73
|
+
url: url(DEFAULT_API_CATALOG_ROUTE),
|
|
74
|
+
init: { method: "HEAD" }
|
|
75
|
+
},
|
|
76
|
+
expect: {
|
|
77
|
+
statuses: [200],
|
|
78
|
+
contentTypes: [`${API_CATALOG_MEDIA_TYPE}; profile="${API_CATALOG_PROFILE_URI}"`],
|
|
79
|
+
bodyEmpty: true,
|
|
80
|
+
linkRelations: [{
|
|
81
|
+
href: DEFAULT_API_CATALOG_ROUTE,
|
|
82
|
+
rel: "api-catalog"
|
|
83
|
+
}]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
surface: "agent-skills-index",
|
|
88
|
+
request: { url: url(DEFAULT_AGENT_SKILLS_INDEX_ROUTE) },
|
|
89
|
+
expect: {
|
|
90
|
+
statuses: [200],
|
|
91
|
+
contentTypes: json,
|
|
92
|
+
bodyIncludes: [
|
|
93
|
+
"https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
|
94
|
+
`${DEFAULT_AGENT_SKILLS_ROUTE_PREFIX}/docs/SKILL.md`,
|
|
95
|
+
"sha256:"
|
|
96
|
+
],
|
|
97
|
+
linkRelations: [{
|
|
98
|
+
href: `${DEFAULT_AGENT_SKILLS_ROUTE_PREFIX}/docs/SKILL.md`,
|
|
99
|
+
rel: "item"
|
|
100
|
+
}, {
|
|
101
|
+
href: DEFAULT_API_CATALOG_ROUTE,
|
|
102
|
+
rel: "api-catalog"
|
|
103
|
+
}]
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
surface: "agent-skills-index-head",
|
|
108
|
+
request: {
|
|
109
|
+
url: url(DEFAULT_AGENT_SKILLS_INDEX_ROUTE),
|
|
110
|
+
init: { method: "HEAD" }
|
|
111
|
+
},
|
|
112
|
+
expect: {
|
|
113
|
+
statuses: [200],
|
|
114
|
+
contentTypes: json,
|
|
115
|
+
bodyEmpty: true,
|
|
116
|
+
linkRelations: [{
|
|
117
|
+
href: DEFAULT_API_CATALOG_ROUTE,
|
|
118
|
+
rel: "api-catalog"
|
|
119
|
+
}]
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
surface: "agent-skill",
|
|
124
|
+
request: { url: url(`${DEFAULT_AGENT_SKILLS_ROUTE_PREFIX}/docs/SKILL.md`) },
|
|
125
|
+
expect: {
|
|
126
|
+
statuses: [200],
|
|
127
|
+
contentTypes: markdown,
|
|
128
|
+
bodyIncludes: ["name: docs"],
|
|
129
|
+
linkRelations: [{
|
|
130
|
+
href: DEFAULT_AGENT_SKILLS_INDEX_ROUTE,
|
|
131
|
+
rel: "collection"
|
|
132
|
+
}, {
|
|
133
|
+
href: DEFAULT_API_CATALOG_ROUTE,
|
|
134
|
+
rel: "api-catalog"
|
|
135
|
+
}]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
surface: "agent-skill-head",
|
|
140
|
+
request: {
|
|
141
|
+
url: url(`${DEFAULT_AGENT_SKILLS_ROUTE_PREFIX}/docs/SKILL.md`),
|
|
142
|
+
init: { method: "HEAD" }
|
|
143
|
+
},
|
|
144
|
+
expect: {
|
|
145
|
+
statuses: [200],
|
|
146
|
+
contentTypes: markdown,
|
|
147
|
+
bodyEmpty: true,
|
|
148
|
+
linkRelations: [{
|
|
149
|
+
href: DEFAULT_AGENT_SKILLS_INDEX_ROUTE,
|
|
150
|
+
rel: "collection"
|
|
151
|
+
}, {
|
|
152
|
+
href: DEFAULT_API_CATALOG_ROUTE,
|
|
153
|
+
rel: "api-catalog"
|
|
154
|
+
}]
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
surface: "config",
|
|
159
|
+
request: { url: url(DEFAULT_DOCS_CONFIG_ROUTE) },
|
|
160
|
+
expect: {
|
|
161
|
+
statuses: [200],
|
|
162
|
+
contentTypes: json,
|
|
163
|
+
bodyIncludes: ["docs-config-map.v1"]
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
surface: "diagnostics",
|
|
168
|
+
request: { url: url(DEFAULT_DOCS_DIAGNOSTICS_ROUTE) },
|
|
169
|
+
expect: {
|
|
170
|
+
statuses: [200],
|
|
171
|
+
contentTypes: json,
|
|
172
|
+
bodyIncludes: ["docs-diagnostics.v1"]
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
surface: "feedback-schema",
|
|
177
|
+
request: { url: url(`${DEFAULT_AGENT_FEEDBACK_ROUTE}/schema`) },
|
|
178
|
+
expect: {
|
|
179
|
+
statuses: [200],
|
|
180
|
+
contentTypes: ["application/schema+json"]
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
surface: "markdown",
|
|
185
|
+
request: { url: url(`/${entry}.md`) },
|
|
186
|
+
expect: {
|
|
187
|
+
statuses: [200],
|
|
188
|
+
contentTypes: markdown,
|
|
189
|
+
bodyIncludes: ["Introduction"]
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
surface: "markdown-accept",
|
|
194
|
+
request: {
|
|
195
|
+
url: url(`/${entry}`),
|
|
196
|
+
init: { headers: { Accept: "text/markdown" } }
|
|
197
|
+
},
|
|
198
|
+
expect: {
|
|
199
|
+
statuses: [200],
|
|
200
|
+
contentTypes: markdown,
|
|
201
|
+
bodyIncludes: ["Introduction"]
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
surface: "markdown-missing",
|
|
206
|
+
request: { url: url(`/${entry}/missing.md`) },
|
|
207
|
+
expect: {
|
|
208
|
+
statuses: [200, 404],
|
|
209
|
+
contentTypes: markdown,
|
|
210
|
+
bodyIncludes: ["not found"]
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
surface: "llms",
|
|
215
|
+
request: { url: url(DEFAULT_LLMS_TXT_ROUTE) },
|
|
216
|
+
expect: {
|
|
217
|
+
statuses: [200],
|
|
218
|
+
contentTypes: ["text/plain"],
|
|
219
|
+
bodyIncludes: ["Introduction"]
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
surface: "llms-full",
|
|
224
|
+
request: { url: url(DEFAULT_LLMS_FULL_TXT_ROUTE) },
|
|
225
|
+
expect: {
|
|
226
|
+
statuses: [200],
|
|
227
|
+
contentTypes: ["text/plain"],
|
|
228
|
+
bodyIncludes: ["Introduction"]
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
surface: "agents",
|
|
233
|
+
request: { url: url(DEFAULT_AGENTS_MD_ROUTE) },
|
|
234
|
+
expect: {
|
|
235
|
+
statuses: [200],
|
|
236
|
+
contentTypes: markdown,
|
|
237
|
+
bodyIncludes: ["AGENTS.md"]
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
surface: "skill",
|
|
242
|
+
request: { url: url(DEFAULT_SKILL_MD_ROUTE) },
|
|
243
|
+
expect: {
|
|
244
|
+
statuses: [200],
|
|
245
|
+
contentTypes: markdown,
|
|
246
|
+
bodyIncludes: ["name:"]
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
surface: "sitemap-xml",
|
|
251
|
+
request: { url: url(DEFAULT_SITEMAP_XML_ROUTE) },
|
|
252
|
+
expect: {
|
|
253
|
+
statuses: [200],
|
|
254
|
+
contentTypes: ["application/xml", "text/xml"],
|
|
255
|
+
bodyIncludes: ["<urlset"]
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
surface: "sitemap-markdown",
|
|
260
|
+
request: { url: url(DEFAULT_SITEMAP_MD_ROUTE) },
|
|
261
|
+
expect: {
|
|
262
|
+
statuses: [200],
|
|
263
|
+
contentTypes: markdown,
|
|
264
|
+
bodyIncludes: ["Introduction"]
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
surface: "robots",
|
|
269
|
+
request: { url: url(DEFAULT_ROBOTS_TXT_ROUTE) },
|
|
270
|
+
expect: {
|
|
271
|
+
statuses: [200],
|
|
272
|
+
contentTypes: ["text/plain"],
|
|
273
|
+
bodyIncludes: ["User-agent:"]
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
surface: "mcp",
|
|
278
|
+
request: {
|
|
279
|
+
url: url(DEFAULT_MCP_WELL_KNOWN_ROUTE),
|
|
280
|
+
init: {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: {
|
|
283
|
+
Accept: "application/json, text/event-stream",
|
|
284
|
+
"Content-Type": "application/json",
|
|
285
|
+
"MCP-Protocol-Version": "2025-11-25"
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify({
|
|
288
|
+
jsonrpc: "2.0",
|
|
289
|
+
id: 1,
|
|
290
|
+
method: "initialize",
|
|
291
|
+
params: {
|
|
292
|
+
protocolVersion: "2025-11-25",
|
|
293
|
+
capabilities: {},
|
|
294
|
+
clientInfo: {
|
|
295
|
+
name: "agent-conformance",
|
|
296
|
+
version: DOCS_AGENT_CONTRACT_VERSION
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
expect: {
|
|
303
|
+
statuses: [200],
|
|
304
|
+
contentTypes: ["application/json", "text/event-stream"],
|
|
305
|
+
bodyIncludes: ["serverInfo", "protocolVersion"]
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
];
|
|
309
|
+
if (locale) {
|
|
310
|
+
const markdownMissingIndex = cases.findIndex((contractCase) => contractCase.surface === "markdown-missing");
|
|
311
|
+
cases.splice(markdownMissingIndex, 0, {
|
|
312
|
+
surface: "markdown-locale",
|
|
313
|
+
request: { url: url(`/${entry}.md?lang=${encodeURIComponent(locale)}`) },
|
|
314
|
+
expect: {
|
|
315
|
+
statuses: [200],
|
|
316
|
+
contentTypes: markdown,
|
|
317
|
+
bodyIncludes: ["Bonjour"]
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return cases;
|
|
322
|
+
}
|
|
323
|
+
function parseParameterizedValue(value) {
|
|
324
|
+
const [rawValue, ...rawParameters] = splitHttpList(value, ";");
|
|
325
|
+
const normalizedValue = rawValue?.trim().toLowerCase();
|
|
326
|
+
if (!normalizedValue) return null;
|
|
327
|
+
const parameters = /* @__PURE__ */ new Map();
|
|
328
|
+
for (const rawParameter of rawParameters) {
|
|
329
|
+
const separator = rawParameter.indexOf("=");
|
|
330
|
+
if (separator < 0) continue;
|
|
331
|
+
const name = rawParameter.slice(0, separator).trim().toLowerCase();
|
|
332
|
+
if (!name) continue;
|
|
333
|
+
parameters.set(name, unquoteHttpValue(rawParameter.slice(separator + 1)));
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
value: normalizedValue,
|
|
337
|
+
parameters
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function matchesContentType(actual, expected) {
|
|
341
|
+
const parsedActual = parseParameterizedValue(actual);
|
|
342
|
+
if (!parsedActual) return false;
|
|
343
|
+
return expected.some((value) => {
|
|
344
|
+
const parsedExpected = parseParameterizedValue(value);
|
|
345
|
+
if (!parsedExpected || parsedExpected.value !== parsedActual.value) return false;
|
|
346
|
+
return [...parsedExpected.parameters].every(([name, expectedValue]) => parsedActual.parameters.get(name) === expectedValue);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function asRecord(value) {
|
|
350
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
351
|
+
}
|
|
352
|
+
function isNonEmptyString(value) {
|
|
353
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
354
|
+
}
|
|
355
|
+
function validateAgentManifest(content) {
|
|
356
|
+
let parsed;
|
|
357
|
+
try {
|
|
358
|
+
parsed = JSON.parse(content);
|
|
359
|
+
} catch {
|
|
360
|
+
return ["Agent manifest did not contain valid JSON"];
|
|
361
|
+
}
|
|
362
|
+
const root = asRecord(parsed);
|
|
363
|
+
const issues = [];
|
|
364
|
+
if (root?.$schema !== DOCS_AGENT_MANIFEST_SCHEMA_URI) issues.push(`Agent manifest did not declare top-level $schema ${DOCS_AGENT_MANIFEST_SCHEMA_URI}`);
|
|
365
|
+
if (root?.format !== DOCS_AGENT_MANIFEST_FORMAT) issues.push(`Agent manifest did not declare top-level format ${DOCS_AGENT_MANIFEST_FORMAT}`);
|
|
366
|
+
if (root?.version !== DOCS_AGENT_MANIFEST_VERSION) issues.push(`Agent manifest did not declare top-level version ${DOCS_AGENT_MANIFEST_VERSION}`);
|
|
367
|
+
return issues;
|
|
368
|
+
}
|
|
369
|
+
function httpLinkMatchesContractExpectation(header, expectation, responseUrl) {
|
|
370
|
+
if (!httpLinkMatchesExpectation(parseHttpLinkHeader(header), expectation, responseUrl)) return false;
|
|
371
|
+
if (!expectation.type) return true;
|
|
372
|
+
let expectedUrl;
|
|
373
|
+
try {
|
|
374
|
+
expectedUrl = new URL(expectation.href, responseUrl).toString();
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
for (const value of splitHttpList(header ?? "", ",")) {
|
|
379
|
+
const target = value.match(/^\s*<([^>]*)>/);
|
|
380
|
+
if (!target) continue;
|
|
381
|
+
let actualUrl;
|
|
382
|
+
try {
|
|
383
|
+
actualUrl = new URL(target[1], responseUrl).toString();
|
|
384
|
+
} catch {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (actualUrl !== expectedUrl) continue;
|
|
388
|
+
const relations = [];
|
|
389
|
+
let type;
|
|
390
|
+
for (const rawParameter of splitHttpList(value.slice(target[0].length), ";")) {
|
|
391
|
+
const separator = rawParameter.indexOf("=");
|
|
392
|
+
if (separator < 0) continue;
|
|
393
|
+
const name = rawParameter.slice(0, separator).trim().toLowerCase();
|
|
394
|
+
const parameterValue = unquoteHttpValue(rawParameter.slice(separator + 1));
|
|
395
|
+
if (name === "rel") relations.push(...parameterValue.toLowerCase().split(/\s+/).filter(Boolean));
|
|
396
|
+
else if (name === "type") type = parameterValue.toLowerCase();
|
|
397
|
+
}
|
|
398
|
+
if (relations.includes(expectation.rel.toLowerCase()) && type === expectation.type.toLowerCase()) return true;
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
async function sha256Content(content) {
|
|
403
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", content);
|
|
404
|
+
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
405
|
+
}
|
|
406
|
+
async function decompressAgentSkillArchive(content) {
|
|
407
|
+
if (typeof DecompressionStream !== "function") throw new Error("gzip decompression is unavailable in this runtime");
|
|
408
|
+
const stream = new Blob([content]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
409
|
+
const chunks = [];
|
|
410
|
+
let totalBytes = 0;
|
|
411
|
+
for await (const chunk of stream) {
|
|
412
|
+
totalBytes += chunk.byteLength;
|
|
413
|
+
if (totalBytes > AGENT_SKILL_ARCHIVE_MAX_UNCOMPRESSED_BYTES) throw new Error(`archive exceeds ${AGENT_SKILL_ARCHIVE_MAX_UNCOMPRESSED_BYTES} uncompressed bytes`);
|
|
414
|
+
chunks.push(chunk);
|
|
415
|
+
}
|
|
416
|
+
const archive = new Uint8Array(new ArrayBuffer(totalBytes));
|
|
417
|
+
let offset = 0;
|
|
418
|
+
for (const chunk of chunks) {
|
|
419
|
+
archive.set(chunk, offset);
|
|
420
|
+
offset += chunk.byteLength;
|
|
421
|
+
}
|
|
422
|
+
return archive;
|
|
423
|
+
}
|
|
424
|
+
async function validateAgentSkillsIndex(content, indexUrl, handle) {
|
|
425
|
+
const issues = [];
|
|
426
|
+
let parsed;
|
|
427
|
+
try {
|
|
428
|
+
parsed = JSON.parse(content);
|
|
429
|
+
} catch {
|
|
430
|
+
return ["Agent Skills index did not contain valid JSON"];
|
|
431
|
+
}
|
|
432
|
+
const root = asRecord(parsed);
|
|
433
|
+
if (root?.$schema !== AGENT_SKILLS_DISCOVERY_SCHEMA_URI) issues.push(`Agent Skills index did not declare ${AGENT_SKILLS_DISCOVERY_SCHEMA_URI}`);
|
|
434
|
+
if (!Array.isArray(root?.skills) || root.skills.length === 0) {
|
|
435
|
+
issues.push("Agent Skills index did not declare any skills");
|
|
436
|
+
return issues;
|
|
437
|
+
}
|
|
438
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
439
|
+
const origin = new URL(indexUrl).origin;
|
|
440
|
+
for (const rawSkill of root.skills) {
|
|
441
|
+
const skill = asRecord(rawSkill);
|
|
442
|
+
const name = skill?.name;
|
|
443
|
+
const description = skill?.description;
|
|
444
|
+
const artifactRoute = skill?.url;
|
|
445
|
+
const digest = skill?.digest;
|
|
446
|
+
if (!isNonEmptyString(name) || !isDocsAgentSkillName(name) || skill?.type !== "skill-md" && skill?.type !== "archive" || !isNonEmptyString(description) || Array.from(description).length > 1024 || !isNonEmptyString(artifactRoute) || typeof digest !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(digest)) {
|
|
447
|
+
issues.push("Agent Skills index contained an invalid skill entry");
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (seenNames.has(name)) {
|
|
451
|
+
issues.push(`Agent Skills index declared duplicate skill ${JSON.stringify(name)}`);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
seenNames.add(name);
|
|
455
|
+
let artifactUrl;
|
|
456
|
+
try {
|
|
457
|
+
artifactUrl = new URL(artifactRoute, indexUrl);
|
|
458
|
+
} catch {
|
|
459
|
+
issues.push(`Agent Skills artifact URL for ${JSON.stringify(name)} was invalid`);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const expectedPath = skill.type === "archive" ? `${DEFAULT_AGENT_SKILLS_ROUTE_PREFIX}/${encodeURIComponent(name)}.tar.gz` : `${DEFAULT_AGENT_SKILLS_ROUTE_PREFIX}/${encodeURIComponent(name)}/SKILL.md`;
|
|
463
|
+
if (artifactUrl.origin !== origin || !artifactUrl.pathname.endsWith(expectedPath)) {
|
|
464
|
+
issues.push(`Agent Skills artifact URL for ${JSON.stringify(name)} did not resolve to same-origin ${expectedPath}`);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
let skillDocument = null;
|
|
468
|
+
try {
|
|
469
|
+
const response = await handle(new Request(artifactUrl), "agent-skill");
|
|
470
|
+
const artifact = new Uint8Array(await response.arrayBuffer());
|
|
471
|
+
if (response.status !== 200) issues.push(`Agent Skills artifact ${JSON.stringify(artifactRoute)} returned status ${response.status}`);
|
|
472
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
473
|
+
if (!matchesContentType(contentType, [skill.type === "archive" ? "application/gzip" : "text/markdown"])) issues.push(`Agent Skills artifact ${JSON.stringify(artifactRoute)} returned content-type ${contentType || "<missing>"}`);
|
|
474
|
+
const actualDigest = `sha256:${await sha256Content(artifact)}`;
|
|
475
|
+
if (actualDigest !== digest) issues.push(`Agent Skills artifact ${JSON.stringify(artifactRoute)} digest ${actualDigest} did not match ${digest}`);
|
|
476
|
+
if (skill.type === "skill-md") skillDocument = new TextDecoder("utf-8", { fatal: true }).decode(artifact);
|
|
477
|
+
else skillDocument = readAgentSkillDocumentFromTar(await decompressAgentSkillArchive(artifact));
|
|
478
|
+
} catch (error) {
|
|
479
|
+
issues.push(`Agent Skills artifact ${JSON.stringify(artifactRoute)} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
480
|
+
}
|
|
481
|
+
if (skillDocument !== null) {
|
|
482
|
+
const validation = validateDocsAgentSkillFrontmatter(skillDocument, { directoryName: name });
|
|
483
|
+
if (!validation.valid) issues.push(`Agent Skill ${JSON.stringify(name)} has invalid SKILL.md frontmatter: ${validation.issues.map((issue) => issue.message).join("; ")}`);
|
|
484
|
+
else if (validation.data.description !== description) issues.push(`Agent Skill ${JSON.stringify(name)} SKILL.md description does not match the discovery index`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return issues;
|
|
488
|
+
}
|
|
489
|
+
async function runDocsAgentConformance(options) {
|
|
490
|
+
const cases = createDocsAgentContractCases(options);
|
|
491
|
+
const results = [];
|
|
492
|
+
for (const contractCase of cases) {
|
|
493
|
+
const issues = [];
|
|
494
|
+
try {
|
|
495
|
+
const response = await options.handle(new Request(contractCase.request.url, contractCase.request.init), contractCase.surface);
|
|
496
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
497
|
+
const body = await response.text();
|
|
498
|
+
if (!contractCase.expect.statuses.includes(response.status)) issues.push(`expected status ${contractCase.expect.statuses.join(" or ")}, received ${response.status}`);
|
|
499
|
+
if (!matchesContentType(contentType, contractCase.expect.contentTypes)) issues.push(`expected content-type ${contractCase.expect.contentTypes.join(" or ")}, received ${contentType || "<missing>"}`);
|
|
500
|
+
for (const requiredText of contractCase.expect.bodyIncludes ?? []) if (!body.toLowerCase().includes(requiredText.toLowerCase())) issues.push(`response body did not include ${JSON.stringify(requiredText)}`);
|
|
501
|
+
if (contractCase.expect.bodyEmpty && body !== "") issues.push(`expected an empty response body, received ${body.length} characters`);
|
|
502
|
+
if (contractCase.surface === "discovery") issues.push(...validateAgentManifest(body));
|
|
503
|
+
if (contractCase.surface === "agent-skills-index") issues.push(...await validateAgentSkillsIndex(body, contractCase.request.url, options.handle));
|
|
504
|
+
for (const [header, requiredValues] of Object.entries(contractCase.expect.headerIncludes ?? {})) {
|
|
505
|
+
const actual = response.headers.get(header) ?? "";
|
|
506
|
+
for (const requiredValue of requiredValues) if (!actual.toLowerCase().includes(requiredValue.toLowerCase())) issues.push(`response header ${JSON.stringify(header)} did not include ${JSON.stringify(requiredValue)}`);
|
|
507
|
+
}
|
|
508
|
+
const linkHeader = response.headers.get("link");
|
|
509
|
+
for (const expectation of contractCase.expect.linkRelations ?? []) if (!httpLinkMatchesContractExpectation(linkHeader, expectation, contractCase.request.url)) {
|
|
510
|
+
const typeRequirement = expectation.type ? ` and type=${JSON.stringify(expectation.type)}` : "";
|
|
511
|
+
issues.push(`response Link header did not include ${JSON.stringify(expectation.href)} with rel=${JSON.stringify(expectation.rel)}${typeRequirement} in the same link-value`);
|
|
512
|
+
}
|
|
513
|
+
results.push({
|
|
514
|
+
surface: contractCase.surface,
|
|
515
|
+
passed: issues.length === 0,
|
|
516
|
+
status: response.status,
|
|
517
|
+
contentType,
|
|
518
|
+
issues
|
|
519
|
+
});
|
|
520
|
+
} catch (error) {
|
|
521
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
522
|
+
results.push({
|
|
523
|
+
surface: contractCase.surface,
|
|
524
|
+
passed: false,
|
|
525
|
+
issues
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
adapter: options.adapter,
|
|
531
|
+
contractVersion: DOCS_AGENT_CONTRACT_VERSION,
|
|
532
|
+
passed: results.every((result) => result.passed),
|
|
533
|
+
cases: results
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
//#endregion
|
|
538
|
+
export { DOCS_AGENT_CONTRACT_VERSION, createDocsAgentContractCases, runDocsAgentConformance };
|