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
2 changes: 1 addition & 1 deletion src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ export function createApp() {
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 }));
return c.json(buildSubnetInterfaceDescriptor({ origin, generatedAt: nowIso(), upstreamRepo: c.env.GITTENSOR_UPSTREAM_REPO }));
});

// Proof of Power (#1059): unauthenticated homepage stats counter — lifetime PRs handled / merged / closed,
Expand Down
16 changes: 12 additions & 4 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,11 @@ declare global {
* hasn't explicitly configured its own value. Unset/blank/anything else = off (the existing behavior). A
* repo's own `contributorCapCancelCi` (DB or `.gittensory.yml`) always takes precedence over this. */
CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT?: string;
GITHUB_WEBHOOK_SECRET: string;
/** The retired review App's webhook secret. Optional: that App has been fully deleted (its installation was
* suspended, then removed); GitHub can no longer deliver to POST /v1/github/webhook for it either way — the
* route already fails closed on a missing/invalid secret. Kept only so a self-hoster running their OWN App
* under this same name still works without code changes. */
GITHUB_WEBHOOK_SECRET?: string;
GITHUB_WEBHOOK_MAX_BODY_BYTES?: string;
/** Webhook secret for the central Gittensory Orb GitHub App (#1255) — distinct from the review app's
* GITHUB_WEBHOOK_SECRET. Verifies inbound POST /v1/orb/webhook deliveries. Inject as a wrangler secret. */
Expand All @@ -168,9 +172,13 @@ declare global {
/** Override the Orb broker base URL the self-host client calls (default https://gittensory-api.aethereal.dev);
* point at a private gittensory deployment if you self-host the broker too. */
ORB_BROKER_URL?: string;
GITHUB_APP_PRIVATE_KEY: string;
GITHUB_APP_ID: string;
GITHUB_APP_SLUG: string;
/** The retired review App's own credentials. Optional: that App has been fully deleted — cloud no longer
* mints tokens or verifies check-run ownership with it (review execution is self-host-only now, brokered
* through the Orb App above). Kept optional (not removed) so a self-hoster running their OWN App under
* these same names still works without code changes. */
GITHUB_APP_PRIVATE_KEY?: string;
GITHUB_APP_ID?: string;
GITHUB_APP_SLUG?: string;
GITHUB_OAUTH_CLIENT_ID?: string;
GITHUB_OAUTH_CLIENT_SECRET?: string;
GITTENSOR_UPSTREAM_REPO?: string;
Expand Down
27 changes: 17 additions & 10 deletions src/github/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@ async function mintInstallationToken(
}
}
const jwt = await createAppJwt(env);
// createAppJwt(env) above already threw if GITHUB_APP_ID were missing, so it's always set here — this const
// only exists to narrow the type for expireCachedAppJwt below (TS can't infer that guarantee across the call).
const appId = env.GITHUB_APP_ID;
/* v8 ignore next -- unreachable: createAppJwt(env) just succeeded, which requires GITHUB_APP_ID to be set. */
if (!appId) throw new Error("GitHub App credentials are not configured.");
let response = await requestInstallationTokenWithJwt(jwt, installationId);
if (response.status === 401) {
// The cached App JWT itself was rejected (a transient GitHub-side validation hiccup, a clock-skew edge case,
Expand All @@ -298,11 +303,11 @@ async function mintInstallationToken(
JSON.stringify({
level: "warn",
event: "github_app_jwt_rejected",
appId: env.GITHUB_APP_ID,
appId,
status: response.status,
}),
);
expireCachedAppJwt(env.GITHUB_APP_ID);
expireCachedAppJwt(appId);
const freshJwt = await createAppJwt(env);
response = await requestInstallationTokenWithJwt(freshJwt, installationId);
if (response.status === 401) {
Expand All @@ -312,7 +317,7 @@ async function mintInstallationToken(
// (flagged by the gate's own review of #2453). Evict again; the throw below still surfaces this failure to
// the caller, but the NEXT mint attempt (this one or any other installation's) gets a fresh JWT instead of
// replaying the poisoned one.
expireCachedAppJwt(env.GITHUB_APP_ID);
expireCachedAppJwt(appId);
}
}
if (!response.ok) {
Expand Down Expand Up @@ -667,25 +672,27 @@ function expireCachedAppJwt(appId: string): void {
* has since revoked (only a real API call would), but it catches the common "unset/invalid key" failure mode
* that otherwise leaves /ready reporting 200 while the review pipeline is completely dead. */
export async function createAppJwt(env: Env): Promise<string> {
if (!env.GITHUB_APP_PRIVATE_KEY) {
if (!env.GITHUB_APP_PRIVATE_KEY || !env.GITHUB_APP_ID) {
throw new Error("GitHub App credentials are not configured.");
}
const appId = env.GITHUB_APP_ID;
const privateKey = env.GITHUB_APP_PRIVATE_KEY;
const nowMs = Date.now();
const cached = appJwtCache.get(env.GITHUB_APP_ID);
if (cached && cached.privateKey === env.GITHUB_APP_PRIVATE_KEY && cached.expiresAtMs > nowMs) {
const cached = appJwtCache.get(appId);
if (cached && cached.privateKey === privateKey && cached.expiresAtMs > nowMs) {
return cached.jwt;
}
const now = Math.floor(nowMs / 1000);
const jwt = await signRs256Jwt(
{
iss: env.GITHUB_APP_ID,
iss: appId,
iat: now - 60,
exp: now + 540,
},
env.GITHUB_APP_PRIVATE_KEY,
privateKey,
);
appJwtCache.set(env.GITHUB_APP_ID, {
privateKey: env.GITHUB_APP_PRIVATE_KEY,
appJwtCache.set(appId, {
privateKey,
jwt,
expiresAtMs: nowMs + APP_JWT_REUSE_MS,
});
Expand Down
8 changes: 6 additions & 2 deletions src/github/backfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2514,7 +2514,9 @@ const GITHUB_ACTIONS_VALIDATE_AGGREGATE_PREREQUISITES = new Set([

function isOwnGitHubAppCheckRun(env: Env, run: { name: string; app?: { slug?: string | null } | null }): boolean {
const appSlug = typeof run.app?.slug === "string" ? run.app.slug.trim().toLowerCase() : "";
const ownSlug = env.GITHUB_APP_SLUG.trim().toLowerCase();
// GITHUB_APP_SLUG is optional (the retired review App was deleted; a self-hoster's own App name may be unset
// too) — never throw on a missing/blank value, just fail the match like an empty appSlug already would.
const ownSlug = (env.GITHUB_APP_SLUG ?? "").trim().toLowerCase();
return ownSlug.length > 0 && appSlug === ownSlug && BOT_OWNED_CHECK_NAMES.has(run.name);
}

Expand Down Expand Up @@ -3936,7 +3938,9 @@ function isTrustedScannerReviewThreadAuthor(env: Env, login: string | null | und
// other "is this our own bot" check in the codebase already does this (self-authored.ts, pr-actions.ts,
// comments.ts, processors.ts) -- so a self-hoster who renamed their App still recognizes its own comments.
export function isOwnReviewThreadAuthor(env: Env, login: string | null | undefined): boolean {
const slug = env.GITHUB_APP_SLUG.trim().toLowerCase();
// GITHUB_APP_SLUG is optional (see isOwnGitHubAppCheckRun above) — an unset value just means "no own-app
// login recognized," never a thrown error.
const slug = (env.GITHUB_APP_SLUG ?? "").trim().toLowerCase();
if (!slug) return false;
const escapedSlug = escapeRegExpForOwnAuthorSlug(slug);
return new RegExp(`^${escapedSlug}[-\\w]*\\[bot\\]$`, "i").test(login ?? "") || new RegExp(`^(${escapedSlug}|${escapedSlug}-orb)$`, "i").test(login ?? "");
Expand Down
11 changes: 8 additions & 3 deletions src/services/subnet-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { GITTENSORY_MCP_PACKAGE_NAME, LATEST_RECOMMENDED_MCP_VERSION, MINIMUM_SU
export const GITTENSOR_NETUID = 74;
const DEFAULT_GITTENSOR_UPSTREAM_REPO = "entrius/gittensor";
const SUBNET_INTERFACE_SCHEMA_VERSION = "1.0";
// The publicly installable GitHub App maintainers add to a gittensor-registered repo (#695's onboarding step
// below). Hardcoded like the other product-identity constants in this file (not env-driven): it's the same
// stable, real app across every deployment of this descriptor, independent of which credentials any one
// Worker instance happens to hold for its own operational purposes.
const PUBLIC_GITHUB_APP_SLUG = "gittensory-orb";

// 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).
Expand Down Expand Up @@ -51,7 +56,7 @@ export type SubnetInterfaceDescriptor = {
* 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 {
export function buildSubnetInterfaceDescriptor(args: { origin: string; generatedAt: string; upstreamRepo?: string | undefined }): SubnetInterfaceDescriptor {
const origin = args.origin.replace(/\/+$/, "");
return {
schemaVersion: SUBNET_INTERFACE_SCHEMA_VERSION,
Expand Down Expand Up @@ -80,8 +85,8 @@ export function buildSubnetInterfaceDescriptor(args: { origin: string; generated
},
githubApp: {
kind: "github_app",
slug: args.appSlug,
installUrl: `https://github.com/apps/${args.appSlug}`,
slug: PUBLIC_GITHUB_APP_SLUG,
installUrl: `https://github.com/apps/${PUBLIC_GITHUB_APP_SLUG}`,
},
},
onboarding: {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export async function sha256Hex(input: string): Promise<string> {
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

export async function verifyGitHubSignature(rawBody: string, signatureHeader: string | null, secret: string): Promise<boolean> {
export async function verifyGitHubSignature(rawBody: string, signatureHeader: string | null, secret: string | undefined): Promise<boolean> {
if (!signatureHeader?.startsWith("sha256=")) return false;
if (!secret) return false;

Expand Down
3 changes: 2 additions & 1 deletion test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6657,7 +6657,8 @@ describe("api routes", () => {
});
});

async function signWebhook(body: string, secret: string): Promise<string> {
async function signWebhook(body: string, secret: string | undefined): Promise<string> {
if (!secret) throw new Error("signWebhook requires a real test secret — createTestEnv() should always set one.");
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
"sign",
]);
Expand Down
8 changes: 5 additions & 3 deletions test/integration/subnet-interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 env = createTestEnv({ PUBLIC_API_ORIGIN: "https://gittensory-api.aethereal.dev" });

const response = await app.request("/v1/public/subnet-interface", {}, env);
expect(response.status).toBe(200);
Expand All @@ -16,14 +16,16 @@ describe("public subnet-interface descriptor route", () => {
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" },
// The publicly installable App slug is a stable hardcoded product identity now (see
// subnet-interface.ts) -- independent of the Worker's own GITHUB_APP_SLUG, which no longer exists.
githubApp: { slug: "gittensory-orb", installUrl: "https://github.com/apps/gittensory-orb" },
},
});
});

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

const response = await app.request("https://fallback.example/v1/public/subnet-interface", {}, env);
Expand Down
29 changes: 29 additions & 0 deletions test/unit/backfill-2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,28 @@ describe("GitHub backfill", () => {
expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "Gittensory Orb Review Agent", summary: "External gate failed" })]);
});

it("does not skip a same-slug bot-owned-named check-run when GITHUB_APP_SLUG is unset (no self-hoster crash)", async () => {
// GITHUB_APP_SLUG is optional now (the retired review App was deleted) -- isOwnGitHubAppCheckRun must
// degrade to "never matches" instead of throwing on `env.GITHUB_APP_SLUG.trim()` when it's absent.
const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" });
delete (env as Partial<Env>).GITHUB_APP_SLUG;
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("/check-runs?")) {
return Response.json({
check_runs: [{ name: "Gittensory Orb Review Agent", status: "completed", conclusion: "failure", output: { title: "Real gate failure" }, app: { slug: "gittensory" } }],
});
}
if (url.includes("/status?")) return Response.json({ statuses: [] });
return new Response("not found", { status: 404 });
});

const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", new Set(["Gittensory Orb Review Agent"]));

expect(aggregate.ciState).toBe("failed");
expect(aggregate.failingDetails).toEqual([expect.objectContaining({ name: "Gittensory Orb Review Agent", summary: "Real gate failure" })]);
});

it("does not ignore classic statuses named like the Gate", async () => {
const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" });
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
Expand Down Expand Up @@ -2561,5 +2583,12 @@ describe("isOwnReviewThreadAuthor", () => {
expect(isOwnReviewThreadAuthor(blank, "gittensory[bot]")).toBe(false);
expect(isOwnReviewThreadAuthor(blank, "")).toBe(false);
});

it("fails closed when GITHUB_APP_SLUG is unset (the retired review App was deleted)", () => {
const unset = createTestEnv();
delete (unset as Partial<Env>).GITHUB_APP_SLUG;
expect(isOwnReviewThreadAuthor(unset, "gittensory[bot]")).toBe(false);
expect(isOwnReviewThreadAuthor(unset, "")).toBe(false);
});
});

7 changes: 4 additions & 3 deletions test/unit/subnet-interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ describe("buildSubnetInterfaceDescriptor", () => {
const descriptor = buildSubnetInterfaceDescriptor({
origin: "https://gittensory-api.aethereal.dev/",
generatedAt: "2026-06-14T00:00:00.000Z",
appSlug: "gittensory",
upstreamRepo: "entrius/gittensor",
});

Expand All @@ -16,7 +15,9 @@ describe("buildSubnetInterfaceDescriptor", () => {
// 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");
// The publicly installable App slug is a stable hardcoded product identity, not derived from any Worker
// var (the old review App's GITHUB_APP_SLUG was removed; gittensory-orb is the real, current, installable App).
expect(descriptor.interfaces.githubApp).toMatchObject({ kind: "github_app", slug: "gittensory-orb", installUrl: "https://github.com/apps/gittensory-orb" });

const toolNames = descriptor.interfaces.mcp.tools.map((tool) => tool.name);
expect(toolNames).toContain("gittensory_get_decision_pack");
Expand All @@ -26,7 +27,7 @@ describe("buildSubnetInterfaceDescriptor", () => {
});

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" });
const descriptor = buildSubnetInterfaceDescriptor({ origin: "https://x.dev", generatedAt: "2026-06-14T00:00:00.000Z" });
expect(descriptor.subnet.upstreamRepo).toBe("entrius/gittensor");
expect(JSON.stringify(descriptor)).not.toMatch(/wallet|hotkey|reward|payout|earn|scoring|multiplier|trust score|scoreability|rank(?:ing)?/i);
});
Expand Down
3 changes: 2 additions & 1 deletion test/unit/webhook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,8 @@ describe("handleOrbRelay (brokered self-host relay receiver)", () => {
});
});

async function signWebhook(body: string, secret: string): Promise<string> {
async function signWebhook(body: string, secret: string | undefined): Promise<string> {
if (!secret) throw new Error("signWebhook requires a real test secret — createTestEnv() should always set one.");
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const signed = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body));
return `sha256=${[...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
Expand Down
6 changes: 1 addition & 5 deletions worker-configuration.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: 3699106af002eb177a4ca0f3fbbc2f7e)
// Generated by Wrangler by running `wrangler types` (hash: 902e76d8bfe7cb797b601690c9e44c8d)
// Runtime types generated with workerd@1.20260701.1 2026-05-28 nodejs_compat
interface __BaseEnv_Env {
DB: D1Database;
JOBS: Queue;
GITHUB_APP_ID: "3824093";
GITHUB_APP_SLUG: "gittensory";
GITHUB_OAUTH_CLIENT_ID: "Ov23lixYxDzyUKE070sm";
GITHUB_WEBHOOK_MAX_BODY_BYTES: "1048576";
GITTENSOR_UPSTREAM_REPO: "entrius/gittensor";
Expand Down Expand Up @@ -64,8 +62,6 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
declare namespace NodeJS {
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env,
| "ADMIN_GITHUB_LOGINS"
| "GITHUB_APP_ID"
| "GITHUB_APP_SLUG"
| "GITHUB_OAUTH_CLIENT_ID"
| "GITHUB_STATUS_ROLLUP_GRAPHQL"
| "GITHUB_WEBHOOK_MAX_BODY_BYTES"
Expand Down
6 changes: 4 additions & 2 deletions wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
},
},
"vars": {
"GITHUB_APP_ID": "3824093",
"GITHUB_APP_SLUG": "gittensory",
// GITHUB_APP_ID / GITHUB_APP_SLUG (the retired review App's identity) removed 2026-07-12: that App has
// been fully deleted. Cloud no longer holds its credentials either (GITHUB_APP_PRIVATE_KEY/
// GITHUB_WEBHOOK_SECRET Worker secrets removed in the same change) -- review execution is self-host-only,
// brokered through the separate Orb App (ORB_GITHUB_APP_ID, a Worker secret).
"GITHUB_OAUTH_CLIENT_ID": "Ov23lixYxDzyUKE070sm",
"GITHUB_WEBHOOK_MAX_BODY_BYTES": "1048576",
"GITTENSOR_UPSTREAM_REPO": "entrius/gittensor",
Expand Down