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
160 changes: 158 additions & 2 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -7528,6 +7528,42 @@
"privateTrustEnabled": {
"type": "boolean"
},
"commandAuthorization": {
"type": "object",
"properties": {
"default": {
"type": "array",
"items": {
"type": "string",
"enum": [
"maintainer",
"collaborator",
"pr_author",
"confirmed_miner"
]
}
},
"commands": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string",
"enum": [
"maintainer",
"collaborator",
"pr_author",
"confirmed_miner"
]
}
}
}
},
"required": [
"default",
"commands"
]
},
"createdAt": {
"type": "string",
"nullable": true
Expand All @@ -7550,7 +7586,8 @@
"includeMaintainerAuthors",
"requireLinkedIssue",
"backfillEnabled",
"privateTrustEnabled"
"privateTrustEnabled",
"commandAuthorization"
]
},
"InstallationRepair": {
Expand Down Expand Up @@ -7994,6 +8031,54 @@
},
"requireLinkedIssue": {
"type": "boolean"
},
"commandAuthorization": {
"type": "object",
"properties": {
"defaultAllowed": {
"type": "array",
"items": {
"type": "string",
"enum": [
"maintainer",
"collaborator",
"pr_author",
"confirmed_miner"
]
}
},
"commandOverrides": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "string"
},
"allowedRoles": {
"type": "array",
"items": {
"type": "string",
"enum": [
"maintainer",
"collaborator",
"pr_author",
"confirmed_miner"
]
}
}
},
"required": [
"command",
"allowedRoles"
]
}
}
},
"required": [
"defaultAllowed",
"commandOverrides"
]
}
},
"required": [
Expand All @@ -8006,7 +8091,77 @@
"gittensorLabel",
"createMissingLabel",
"includeMaintainerAuthors",
"requireLinkedIssue"
"requireLinkedIssue",
"commandAuthorization"
]
},
"commandAuthorizationPreview": {
"type": "object",
"properties": {
"commandName": {
"type": "string"
},
"commenterLogin": {
"type": "string"
},
"commenterAssociation": {
"type": "string"
},
"decision": {
"type": "object",
"properties": {
"authorized": {
"type": "boolean"
},
"reason": {
"type": "string"
},
"actorKind": {
"type": "string",
"enum": [
"maintainer",
"author",
"none"
]
},
"matchedRole": {
"type": "string",
"nullable": true,
"enum": [
"maintainer",
"collaborator",
"pr_author",
"confirmed_miner",
null
]
},
"allowedRoles": {
"type": "array",
"items": {
"type": "string",
"enum": [
"maintainer",
"collaborator",
"pr_author",
"confirmed_miner"
]
}
}
},
"required": [
"authorized",
"reason",
"actorKind",
"matchedRole",
"allowedRoles"
]
}
},
"required": [
"commandName",
"commenterLogin",
"commenterAssociation",
"decision"
]
},
"installation": {
Expand Down Expand Up @@ -8397,6 +8552,7 @@
"repoFullName",
"generatedAt",
"settings",
"commandAuthorizationPreview",
"installation",
"sample",
"decision",
Expand Down
1 change: 1 addition & 0 deletions migrations/0020_command_authorization_policy.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE repository_settings ADD COLUMN command_authorization_json TEXT NOT NULL DEFAULT '{}';
11 changes: 11 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
type AuthIdentity,
} from "../auth/security";
import { normalizeGittBountySnapshot } from "../bounties/ingest";
import { DEFAULT_COMMAND_AUTHORIZATION_POLICY, normalizeCommandAuthorizationPolicy } from "../settings/command-authorization";
import {
countOpenIssues,
countOpenPullRequests,
Expand Down Expand Up @@ -432,6 +433,12 @@ const repositorySettingsSchema = z.object({
requireLinkedIssue: z.boolean().default(false),
backfillEnabled: z.boolean().default(true),
privateTrustEnabled: z.boolean().default(true),
commandAuthorization: z
.object({
default: z.array(z.enum(["maintainer", "collaborator", "pr_author", "confirmed_miner"])).max(4).optional(),
commands: z.record(z.string().trim().min(1).max(64), z.array(z.enum(["maintainer", "collaborator", "pr_author", "confirmed_miner"])).max(4)).optional(),
})
.default(DEFAULT_COMMAND_AUTHORIZATION_POLICY),
});

const settingsPreviewSchema = z.object({
Expand All @@ -445,6 +452,9 @@ const settingsPreviewSchema = z.object({
body: z.string().max(10000).nullable().optional(),
labels: z.array(z.string().max(100)).max(50).optional(),
linkedIssues: z.array(z.number().int().positive()).max(50).optional(),
commandName: z.string().trim().min(1).max(64).optional(),
commenterLogin: z.string().trim().min(1).max(100).optional(),
commenterAssociation: z.enum(["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIMER", "FIRST_TIME_CONTRIBUTOR", "MANNEQUIN", "NONE"]).optional(),
})
.optional(),
});
Expand Down Expand Up @@ -2147,6 +2157,7 @@ export function createApp() {
requireLinkedIssue: parsed.data.requireLinkedIssue,
backfillEnabled: parsed.data.backfillEnabled,
privateTrustEnabled: parsed.data.privateTrustEnabled,
commandAuthorization: normalizeCommandAuthorizationPolicy(parsed.data.commandAuthorization).policy,
}),
);
});
Expand Down
10 changes: 10 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import type {
} from "../types";
import type { GittensorContributorSnapshot, OfficialGittensorMinerDetection } from "../gittensor/api";
import { classifyMcpClientVersion, LATEST_RECOMMENDED_MCP_VERSION, MINIMUM_SUPPORTED_MCP_VERSION } from "../services/mcp-compatibility";
import { DEFAULT_COMMAND_AUTHORIZATION_POLICY, normalizeCommandAuthorizationPolicy } from "../settings/command-authorization";
import { sha256Hex } from "../utils/crypto";
import { jsonString, nowIso, parseJson, repoParts } from "../utils/json";

Expand Down Expand Up @@ -367,6 +368,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
requireLinkedIssue: false,
backfillEnabled: true,
privateTrustEnabled: true,
commandAuthorization: normalizeCommandAuthorizationPolicy(DEFAULT_COMMAND_AUTHORIZATION_POLICY).policy,
};
}
return {
Expand All @@ -383,6 +385,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
requireLinkedIssue: row.requireLinkedIssue,
backfillEnabled: row.backfillEnabled,
privateTrustEnabled: row.privateTrustEnabled,
commandAuthorization: parseCommandAuthorizationPolicy(row.commandAuthorizationJson),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
Expand All @@ -403,6 +406,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
requireLinkedIssue: settings.requireLinkedIssue ?? false,
backfillEnabled: settings.backfillEnabled ?? true,
privateTrustEnabled: settings.privateTrustEnabled ?? true,
commandAuthorization: normalizeCommandAuthorizationPolicy(settings.commandAuthorization).policy,
};
const db = getDb(env.DB);
await db
Expand All @@ -421,6 +425,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
requireLinkedIssue: resolved.requireLinkedIssue,
backfillEnabled: resolved.backfillEnabled,
privateTrustEnabled: resolved.privateTrustEnabled,
commandAuthorizationJson: jsonString(resolved.commandAuthorization),
updatedAt: nowIso(),
})
.onConflictDoUpdate({
Expand All @@ -438,6 +443,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
requireLinkedIssue: resolved.requireLinkedIssue,
backfillEnabled: resolved.backfillEnabled,
privateTrustEnabled: resolved.privateTrustEnabled,
commandAuthorizationJson: jsonString(resolved.commandAuthorization),
updatedAt: nowIso(),
},
});
Expand Down Expand Up @@ -4171,6 +4177,10 @@ function parsePublicSurface(value: string): RepositorySettings["publicSurface"]
return "comment_and_label";
}

function parseCommandAuthorizationPolicy(value: string): RepositorySettings["commandAuthorization"] {
return normalizeCommandAuthorizationPolicy(parseJson<unknown>(value, null)).policy;
}

function parseSyncStatus(value: string): RepoSyncStateRecord["status"] {
if (
value === "running" ||
Expand Down
1 change: 1 addition & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const repositorySettings = sqliteTable("repository_settings", {
requireLinkedIssue: integer("require_linked_issue", { mode: "boolean" }).notNull().default(false),
backfillEnabled: integer("backfill_enabled", { mode: "boolean" }).notNull().default(true),
privateTrustEnabled: integer("private_trust_enabled", { mode: "boolean" }).notNull().default(true),
commandAuthorizationJson: text("command_authorization_json").notNull().default("{}"),
createdAt: text("created_at").notNull().default("CURRENT_TIMESTAMP"),
updatedAt: text("updated_at").notNull().default("CURRENT_TIMESTAMP"),
});
Expand Down
27 changes: 12 additions & 15 deletions src/github/commands.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { AGENT_COMMAND_COMMENT_MARKER } from "./comments";
import type { AgentRunBundle } from "../services/agent-orchestrator";
import type { GittensorContributorSnapshot, OfficialGittensorMinerDetection } from "../gittensor/api";
import type { AgentActionRecord } from "../types";
import type { AgentActionRecord, RepositoryCommandAuthorizationPolicy } from "../types";
import type { CheckSummaryRecord, GitHubIssuePayload, IssueRecord, PullRequestRecord, RecentMergedPullRequestRecord, RepositoryRecord } from "../types";
import { evaluateCommandAuthorization } from "../settings/command-authorization";
import { buildCollisionReport, buildQueueHealth, type CollisionCluster, type QueueHealth } from "../signals/engine";

const PUBLIC_MENTION_COMMAND_CATALOG = [
Expand Down Expand Up @@ -166,21 +167,17 @@ export function isAuthorizedCommandActor(args: {
commenterAssociation?: string | null | undefined;
pullRequestAuthorLogin?: string | null | undefined;
officialAuthorDetection?: OfficialGittensorMinerDetection | undefined;
commandAuthorizationPolicy?: RepositoryCommandAuthorizationPolicy | null | undefined;
}): { authorized: boolean; reason: string; actorKind: "maintainer" | "author" | "none" } {
if (isMaintainerAssociation(args.commenterAssociation)) return { authorized: true, reason: "maintainer_invocation", actorKind: "maintainer" };
if (args.commandName && isMaintainerOnlyCommand(args.commandName)) {
return { authorized: false, reason: "maintainer_command_requires_maintainer", actorKind: "none" };
}
if (!args.commenterLogin || !args.pullRequestAuthorLogin || args.commenterLogin.toLowerCase() !== args.pullRequestAuthorLogin.toLowerCase()) {
return { authorized: false, reason: "not_maintainer_or_pr_author", actorKind: "none" };
}
if (!args.officialAuthorDetection || args.officialAuthorDetection.status === "unavailable") {
return { authorized: false, reason: "miner_detection_unavailable", actorKind: "author" };
}
if (args.officialAuthorDetection.status !== "confirmed") {
return { authorized: false, reason: "pr_author_not_confirmed_miner", actorKind: "author" };
}
return { authorized: true, reason: "confirmed_miner_pr_author", actorKind: "author" };
const decision = evaluateCommandAuthorization({
policy: args.commandAuthorizationPolicy,
commandName: args.commandName ?? "preflight",
commenterLogin: args.commenterLogin,
commenterAssociation: args.commenterAssociation,
pullRequestAuthorLogin: args.pullRequestAuthorLogin,
minerStatus: args.officialAuthorDetection?.status,
});
return { authorized: decision.authorized, reason: decision.reason, actorKind: decision.actorKind };
}

export function buildPublicAgentCommandComment(args: {
Expand Down
25 changes: 25 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,10 @@ export const RepositorySettingsSchema = z
requireLinkedIssue: z.boolean(),
backfillEnabled: z.boolean(),
privateTrustEnabled: z.boolean(),
commandAuthorization: z.object({
default: z.array(z.enum(["maintainer", "collaborator", "pr_author", "confirmed_miner"])),
commands: z.record(z.string(), z.array(z.enum(["maintainer", "collaborator", "pr_author", "confirmed_miner"]))),
}),
createdAt: z.string().nullable().optional(),
updatedAt: z.string().nullable().optional(),
})
Expand All @@ -548,6 +552,27 @@ export const RepoSettingsPreviewSchema = z
createMissingLabel: z.boolean(),
includeMaintainerAuthors: z.boolean(),
requireLinkedIssue: z.boolean(),
commandAuthorization: z.object({
defaultAllowed: z.array(z.enum(["maintainer", "collaborator", "pr_author", "confirmed_miner"])),
commandOverrides: z.array(
z.object({
command: z.string(),
allowedRoles: z.array(z.enum(["maintainer", "collaborator", "pr_author", "confirmed_miner"])),
}),
),
}),
}),
commandAuthorizationPreview: z.object({
commandName: z.string(),
commenterLogin: z.string(),
commenterAssociation: z.string(),
decision: z.object({
authorized: z.boolean(),
reason: z.string(),
actorKind: z.enum(["maintainer", "author", "none"]),
matchedRole: z.enum(["maintainer", "collaborator", "pr_author", "confirmed_miner"]).nullable(),
allowedRoles: z.array(z.enum(["maintainer", "collaborator", "pr_author", "confirmed_miner"])),
}),
}),
installation: z
.object({
Expand Down
Loading