diff --git a/apps/nest-dashboard/README.md b/apps/nest-dashboard/README.md
index abc011a0..0b78127d 100644
--- a/apps/nest-dashboard/README.md
+++ b/apps/nest-dashboard/README.md
@@ -31,9 +31,10 @@ npm run typecheck
```
The tests exercise the real API handler, form action and rendered page with
-external persistence, request context and fetch responses replaced by test
-doubles. They require no database, credentials or live endpoint. They do not
-prove database transactions, browser hydration or a deployed Next server.
+external persistence and request context replaced by test doubles. Unexpected
+network access is blocked. They require no database, credentials or live
+endpoint. They do not prove database transactions, browser hydration or a
+deployed Next server.
The form lifecycle tests use the real React action queue and a DOM environment,
with only the server action replaced. They cover repeated submit events, pending
@@ -43,10 +44,10 @@ HTTP retries or concurrent server requests. Server idempotency needs a separate
contract with an explicit operation key and atomic persistence. A time-window
lookup before insertion is not that guarantee.
-URL validation checks source syntax only. API submissions do not probe links
-and keep `reachable: null`; the page labels those links as not checked.
-The existing form's best-effort probe is unchanged. These observations are not
-capability, endorsement or permission to execute a submitted skill.
+URL validation checks source syntax only. API and form submissions do not probe
+links and keep `reachable: null`; the page labels those links as not checked.
+A saved catalog record is not evidence that a link is live or safe, an
+endorsement, or permission to execute a submitted skill.
## Learn More
diff --git a/apps/nest-dashboard/src/app/docs/page.tsx b/apps/nest-dashboard/src/app/docs/page.tsx
index e9304ab1..840d3d95 100644
--- a/apps/nest-dashboard/src/app/docs/page.tsx
+++ b/apps/nest-dashboard/src/app/docs/page.tsx
@@ -505,8 +505,11 @@ export default function DocsPage() {
Skills page
- . Agents can discover your service there and call it in sandbox
- runs today, and in the live town once it is running.
+ . Registration only makes the catalog entry discoverable. It
+ does not verify, install, authorize, endorse, or execute the
+ submitted skill, agent, or service. For new URL and GitHub
+ entries, the catalog stores the source link without fetching or
+ hosting the linked content, agent, or service.
diff --git a/apps/nest-dashboard/src/app/llms.txt/route.ts b/apps/nest-dashboard/src/app/llms.txt/route.ts
index bedb1401..cf646eb1 100644
--- a/apps/nest-dashboard/src/app/llms.txt/route.ts
+++ b/apps/nest-dashboard/src/app/llms.txt/route.ts
@@ -15,7 +15,7 @@ Key facts:
- The simulator is deterministic: same seed, same trace. Scenarios are plain YAML; validators check protocol invariants on every run.
- Twelve protocol layers cover identity, trust, payments, registry, comms, memory, coordination, and more; each layer accepts community plugins via a pull request to the repo.
- Community contributions arrive as pull requests to https://github.com/projnanda/nandatown and appear in the PR gallery, organized by layer.
-- The SkillMD registry hosts Markdown skill files that teach agents how to call live APIs; browse or submit at ${SITE_URL}/skills (JSON API at /api/skills).
+- The SkillMD registry catalogs submissions at ${SITE_URL}/skills (JSON API at /api/skills). Registration only makes the catalog entry discoverable. It does not verify, install, authorize, endorse, or execute the submitted skill, agent, or service. For new URL and GitHub entries, the catalog stores the source link without fetching or hosting the linked content, agent, or service.
- Agents can also run in the cloud instead of locally: Maritime (https://maritime.sh) is the preferred partner for hosting agents (CLI: \`npm install -g maritime-cli\`; SDK: \`pip install maritime\` / \`npm install maritime-sdk\`). Nanda Town builders get 20 agents free with promo code NANDATOWN.
- Nanda Town hosts hackathons (NandaHacks): the NandaHack x HCLTech skills event and the Agentic Commerce Hackathon (Prava payments track).
diff --git a/apps/nest-dashboard/src/app/skills/actions.ts b/apps/nest-dashboard/src/app/skills/actions.ts
index 452945a1..d13441ce 100644
--- a/apps/nest-dashboard/src/app/skills/actions.ts
+++ b/apps/nest-dashboard/src/app/skills/actions.ts
@@ -43,26 +43,6 @@ async function clientIp(): Promise {
return h.get("x-real-ip");
}
-/**
- * Best-effort check that a submitted URL actually answers. Never throws — a
- * failed or slow request just means we record `false`, we don't block the save.
- */
-async function checkReachable(url: string): Promise {
- try {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 5000);
- const res = await fetch(url, {
- signal: controller.signal,
- redirect: "follow",
- headers: { "user-agent": "NandaTown-SkillMD-Checker" },
- });
- clearTimeout(timer);
- return res.ok;
- } catch {
- return false;
- }
-}
-
export async function submitSkill(
_prev: SubmitState,
formData: FormData,
@@ -107,12 +87,6 @@ export async function submitSkill(
};
}
- // --- Best-effort reachability check for hosted links --------------------
- let reachable: boolean | null = null;
- if (sourceType === "url" || sourceType === "github") {
- reachable = await checkReachable(sourceUrl);
- }
-
// --- Save ---------------------------------------------------------------
try {
const submitterIp = await clientIp();
@@ -125,7 +99,7 @@ export async function submitSkill(
content: sourceType === "content" ? content : null,
endpoints: endpoints || null,
tags: tags || null,
- reachable,
+ reachable: null,
email: email || null,
github_username: githubUsername || null,
submitter_ip: submitterIp,
diff --git a/apps/nest-dashboard/tests/catalog-discovery-copy.test.tsx b/apps/nest-dashboard/tests/catalog-discovery-copy.test.tsx
new file mode 100644
index 00000000..d4a5d072
--- /dev/null
+++ b/apps/nest-dashboard/tests/catalog-discovery-copy.test.tsx
@@ -0,0 +1,43 @@
+import { renderToStaticMarkup } from "react-dom/server";
+import { expect, test } from "vitest";
+import DocsPage from "../src/app/docs/page";
+import { GET as getLlmsTxt } from "../src/app/llms.txt/route";
+
+function expectDiscoveryOnlyCopy(copy: string): void {
+ expect(copy).toContain("only makes the catalog entry discoverable");
+ expect(copy).toContain(
+ "does not verify, install, authorize, endorse, or execute",
+ );
+ expect(copy).toContain(
+ "For new URL and GitHub entries, the catalog stores the source link without fetching or hosting the linked content, agent, or service",
+ );
+ expect(copy).not.toMatch(
+ /\b(?:registry|registration|catalog(?:ing)?|entry)\s+(?:installs?|authorizes?|endorses?|verifies?|hosts?|executes?)\b/i,
+ );
+ expect(copy).not.toMatch(/\bagents can .*call it in sandbox/i);
+}
+
+test("public docs describe SkillMD registration as discovery only", () => {
+ const markup = renderToStaticMarkup();
+ const start = markup.indexOf('", start);
+
+ expect(start).toBeGreaterThanOrEqual(0);
+ expect(end).toBeGreaterThan(start);
+
+ const sectionText = markup
+ .slice(start, end)
+ .replace(/<[^>]+>/g, " ")
+ .replace(/\s+/g, " ");
+ expectDiscoveryOnlyCopy(sectionText);
+});
+
+test("llms.txt describes the SkillMD registry as discovery only", async () => {
+ const response = getLlmsTxt();
+ const registryLine = (await response.text())
+ .split("\n")
+ .find((line) => line.startsWith("- The SkillMD registry"));
+
+ expect(registryLine).toBeTypeOf("string");
+ expectDiscoveryOnlyCopy(registryLine ?? "");
+});
diff --git a/apps/nest-dashboard/tests/skill-catalog.test.tsx b/apps/nest-dashboard/tests/skill-catalog.test.tsx
index 2964ad41..31c7ef57 100644
--- a/apps/nest-dashboard/tests/skill-catalog.test.tsx
+++ b/apps/nest-dashboard/tests/skill-catalog.test.tsx
@@ -104,17 +104,16 @@ for (const sourceType of ["url", "github"]) {
expect(fetch).not.toHaveBeenCalled();
});
- test(`form retains its existing successful probe for ${sourceType} ${sourceUrl}`, async () => {
- vi.mocked(fetch).mockResolvedValueOnce(new Response("ok"));
+ test(`form saves ${sourceType} ${sourceUrl} without claiming a probe`, async () => {
const result = await submitSkill(initialSubmitState, form({
name: skill.name, source_type: sourceType, source_url: ` ${sourceUrl} `,
}));
expect(result.ok).toBe(true);
expect(result.createdId).toBe(skill.id);
expect(external.createSkill).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
- source_type: sourceType, source_url: sourceUrl, reachable: true,
+ source_type: sourceType, source_url: sourceUrl, reachable: null,
}));
- expect(fetch).toHaveBeenCalledExactlyOnceWith(sourceUrl, expect.any(Object));
+ expect(fetch).not.toHaveBeenCalled();
});
}
}