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
10 changes: 10 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ import {
} from "../services/mcp-compatibility";
import { buildOperatorDashboardPayload } from "../services/operator-dashboard";
import { buildSelfDogfoodRegistrationPack, resolveSelfDogfoodRepoFullName } from "../services/self-dogfood-registration-pack";
import { buildSubnetInterfaceDescriptor } from "../services/subnet-interface";
import {
buildWeeklyValueReport,
formatWeeklyValueReportMarkdown,
Expand Down Expand Up @@ -674,6 +675,14 @@ export function createApp() {
app.get("/openapi.json", (c) => c.json(buildOpenApiSpec()));
app.all("/mcp", handleMcpRequest);

// Public SN74 contribution-interface descriptor (#695): metagraphed (and any agent) fetches this to route
// gittensor discovery → Gittensory. Unauthenticated product metadata; excluded from requiresApiToken below.
app.get("/v1/public/subnet-interface", (c) => {
const origin = c.env.PUBLIC_API_ORIGIN ?? new URL(c.req.url).origin;
c.header("Cache-Control", "public, max-age=600, stale-while-revalidate=86400");
return c.json(buildSubnetInterfaceDescriptor({ origin, generatedAt: nowIso(), appSlug: c.env.GITHUB_APP_SLUG, upstreamRepo: c.env.GITTENSOR_UPSTREAM_REPO }));
});

app.get("/v1/public/github/repos/:owner/:repo/stats", async (c) => {
try {
const stats = await fetchPublicRepoStats(c.env, c.req.param("owner"), c.req.param("repo"));
Expand Down Expand Up @@ -4144,6 +4153,7 @@ function requiresApiToken(path: string): boolean {
if (path === "/health") return false;
if (path === "/v1/mcp/compatibility") return false;
if (/^\/v1\/public\/github\/repos\/[^/]+\/[^/]+\/stats$/.test(path)) return false;
if (path === "/v1/public/subnet-interface") return false;
if (path === "/openapi.json") return false;
if (path === "/mcp") return false;
if (path.startsWith("/v1/auth/")) return false;
Expand Down
91 changes: 91 additions & 0 deletions src/services/subnet-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { GITTENSOR_HOME_URL, GITTENSORY_SITE_URL } from "../github/footer";
import { GITTENSORY_MCP_PACKAGE_NAME, LATEST_RECOMMENDED_MCP_VERSION, MINIMUM_SUPPORTED_MCP_VERSION } from "./mcp-compatibility";

// Gittensor is Bittensor subnet 74 (the code subnet). Gittensory is its contribution interface.
export const GITTENSOR_NETUID = 74;
const DEFAULT_GITTENSOR_UPSTREAM_REPO = "entrius/gittensor";
const SUBNET_INTERFACE_SCHEMA_VERSION = "1.0";

// Curated, contribution-relevant MCP tools surfaced to agents/devs who discover gittensor via metagraphed.
// Names mirror src/mcp/server.ts registrations; the list is intentionally a miner-facing subset (not all 33).
const CONTRIBUTION_MCP_TOOLS: ReadonlyArray<{ name: string; summary: string }> = [
{ name: "gittensory_get_decision_pack", summary: "Rank high-fit, low-duplicate issues to contribute to across registered repos." },
{ name: "gittensory_check_before_start", summary: "Check whether an issue is already claimed or solved before writing code." },
{ name: "gittensory_validate_linked_issue", summary: "Confirm that linking an issue will earn the linked-issue scoring multiplier." },
{ name: "gittensory_preflight_pr", summary: "Preflight a planned PR for lane fit, duplicate risk, and review burden." },
{ name: "gittensory_monitor_open_prs", summary: "Track your open PRs and what to clean up first." },
{ name: "gittensory_list_notifications", summary: "See review feedback (e.g. changes requested) on your PRs." },
{ name: "gittensory_agent_plan_next_work", summary: "Deterministically rank your next gittensor contribution actions." },
];

const ONBOARDING_STEPS: ReadonlyArray<string> = [
"Maintainers: install the Gittensory GitHub App on a gittensor-registered repository.",
"Contributors (miners): connect the Gittensory MCP endpoint in your agent harness (Claude Code, Cursor, etc.).",
"Use gittensory_get_decision_pack to find high-fit, low-duplicate issues, then gittensory_check_before_start before writing code.",
"Preflight with gittensory_preflight_pr and open a focused PR linked to its issue.",
];

export type SubnetInterfaceDescriptor = {
schemaVersion: string;
generatedAt: string;
subnet: { netuid: number; name: string; home: string; upstreamRepo: string };
provider: { name: string; role: "contribution_interface"; site: string; summary: string };
interfaces: {
mcp: {
kind: "mcp";
transport: "http";
endpoint: string;
package: string;
minimumVersion: string;
recommendedVersion: string;
tools: Array<{ name: string; summary: string }>;
};
githubApp: { kind: "github_app"; slug: string; installUrl: string };
};
onboarding: { docs: string; steps: string[] };
};

/**
* Machine-readable descriptor declaring Gittensory as gittensor (subnet 74)'s contribution interface, so
* metagraphed (and any agent) can route discovery → contribution (#695). Pure product metadata (URLs, tool
* names) — no private/reward/score wording, so it never needs sanitization. Public + unauthenticated.
*/
export function buildSubnetInterfaceDescriptor(args: { origin: string; generatedAt: string; appSlug: string; upstreamRepo?: string | undefined }): SubnetInterfaceDescriptor {
const origin = args.origin.replace(/\/+$/, "");
return {
schemaVersion: SUBNET_INTERFACE_SCHEMA_VERSION,
generatedAt: args.generatedAt,
subnet: {
netuid: GITTENSOR_NETUID,
name: "gittensor",
home: GITTENSOR_HOME_URL,
upstreamRepo: args.upstreamRepo ?? DEFAULT_GITTENSOR_UPSTREAM_REPO,
},
provider: {
name: "Gittensory",
role: "contribution_interface",
site: GITTENSORY_SITE_URL,
summary: "Gittensor-native contribution quality & planning layer: deterministic signals for miners and a free anti-slop + AI second-opinion gate for maintainers.",
},
interfaces: {
mcp: {
kind: "mcp",
transport: "http",
endpoint: `${origin}/mcp`,
package: GITTENSORY_MCP_PACKAGE_NAME,
minimumVersion: MINIMUM_SUPPORTED_MCP_VERSION,
recommendedVersion: LATEST_RECOMMENDED_MCP_VERSION,
tools: CONTRIBUTION_MCP_TOOLS.map((tool) => ({ ...tool })),
},
githubApp: {
kind: "github_app",
slug: args.appSlug,
installUrl: `https://github.com/apps/${args.appSlug}`,
},
},
onboarding: {
docs: GITTENSORY_SITE_URL,
steps: [...ONBOARDING_STEPS],
},
};
}
35 changes: 35 additions & 0 deletions test/integration/subnet-interface.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { createApp } from "../../src/api/routes";
import { createTestEnv } from "../helpers/d1";

describe("public subnet-interface descriptor route", () => {
it("serves the SN74 contribution-interface descriptor without authentication", async () => {
const app = createApp();
const env = createTestEnv({ GITHUB_APP_SLUG: "gittensory", PUBLIC_API_ORIGIN: "https://gittensory-api.aethereal.dev" });

const response = await app.request("/v1/public/subnet-interface", {}, env);
expect(response.status).toBe(200);
expect(response.headers.get("cache-control")).toContain("max-age=600");
await expect(response.json()).resolves.toMatchObject({
schemaVersion: "1.0",
subnet: { netuid: 74, name: "gittensor" },
provider: { name: "Gittensory", role: "contribution_interface" },
interfaces: {
mcp: { endpoint: "https://gittensory-api.aethereal.dev/mcp", transport: "http" },
githubApp: { slug: "gittensory", installUrl: "https://github.com/apps/gittensory" },
},
});
});

it("falls back to the request origin when PUBLIC_API_ORIGIN is unset", async () => {
const app = createApp();
const env = createTestEnv({ GITHUB_APP_SLUG: "gittensory" });
delete (env as Partial<Env>).PUBLIC_API_ORIGIN;

const response = await app.request("https://fallback.example/v1/public/subnet-interface", {}, env);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
interfaces: { mcp: { endpoint: "https://fallback.example/mcp" } },
});
});
});
33 changes: 33 additions & 0 deletions test/unit/subnet-interface.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { buildSubnetInterfaceDescriptor, GITTENSOR_NETUID } from "../../src/services/subnet-interface";

describe("buildSubnetInterfaceDescriptor", () => {
it("declares Gittensory as gittensor SN74's contribution interface", () => {
const descriptor = buildSubnetInterfaceDescriptor({
origin: "https://gittensory-api.aethereal.dev/",
generatedAt: "2026-06-14T00:00:00.000Z",
appSlug: "gittensory",
upstreamRepo: "entrius/gittensor",
});

expect(GITTENSOR_NETUID).toBe(74);
expect(descriptor.subnet).toMatchObject({ netuid: 74, name: "gittensor", upstreamRepo: "entrius/gittensor" });
expect(descriptor.provider).toMatchObject({ name: "Gittensory", role: "contribution_interface" });
// Trailing slash on origin is normalized before appending /mcp.
expect(descriptor.interfaces.mcp.endpoint).toBe("https://gittensory-api.aethereal.dev/mcp");
expect(descriptor.interfaces.mcp.transport).toBe("http");
expect(descriptor.interfaces.githubApp.installUrl).toBe("https://github.com/apps/gittensory");

const toolNames = descriptor.interfaces.mcp.tools.map((tool) => tool.name);
expect(toolNames).toContain("gittensory_get_decision_pack");
expect(toolNames).toContain("gittensory_list_notifications");
expect(descriptor.interfaces.mcp.tools.every((tool) => tool.summary.length > 0)).toBe(true);
expect(descriptor.onboarding.steps.length).toBeGreaterThan(0);
});

it("defaults the upstream repo when not provided and contains no private/reward wording", () => {
const descriptor = buildSubnetInterfaceDescriptor({ origin: "https://x.dev", generatedAt: "2026-06-14T00:00:00.000Z", appSlug: "gittensory" });
expect(descriptor.subnet.upstreamRepo).toBe("entrius/gittensor");
expect(JSON.stringify(descriptor)).not.toMatch(/wallet|hotkey|reward|payout|trust score|scoreability|ranking/i);
});
});