Files
PaperClipAI/server/src/services/github-operation-credentials.ts
T
Devin FoleyandPaperclip 0d73505098 perf(git): narrow operation identity snapshot reads (#17567)
Select only the run context used by GitHub operation credential checks. Preserve the default complete identity capture for Muse and other consumers, current trust checks, grants, and saved run evidence.

Validation: 72 focused GitHub, identity, Slack, chat-project, and project-route tests; full build and typecheck; exact-head Greptile 5/5 (medium), all CI checks green, and zero unresolved comments. Real PostgreSQL tests prove current revocation and response-size bounds. No live external GitHub push was performed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 00:31:41 -07:00

195 lines
5.9 KiB
TypeScript

import { and, eq } from "drizzle-orm";
import { isUuidLike } from "@paperclipai/shared";
import {
agents,
heartbeatRuns,
issues,
projects,
runIdentityContexts,
type Db,
} from "@paperclipai/db";
import { forbidden } from "../errors.js";
import { captureRunIdentity } from "./run-identity.js";
import {
buildGitAuthInvocation,
resolveManagedGitHubCredential,
} from "./git-credentials.js";
import { secretService } from "./secrets.js";
import { resolveCoreTrustPreset } from "./trust-preset-resolver.js";
import { isLowTrustQuarantined } from "./source-trust.js";
export type GitHubCredentialSummary = {
status: "available" | "absent" | "unavailable";
source?: "personal" | "dedicated";
login?: string;
reason?: string;
connectionId?: string;
grantId?: string;
authenticationMode?: "managed" | "host" | "anonymous";
};
/** A raw GitHub token cannot enforce the low-trust read-only tool boundary. */
async function allowsGitHubCredentialExport(
db: Db,
run: typeof heartbeatRuns.$inferSelect,
) {
const issueId =
run.contextSnapshot?.issueId ??
run.contextSnapshot?.taskId ??
run.nativeIssueId;
if (
issueId !== undefined &&
issueId !== null &&
(typeof issueId !== "string" || !isUuidLike(issueId))
)
return false;
const [agent] = await db
.select({ companyId: agents.companyId, permissions: agents.permissions })
.from(agents)
.where(
and(eq(agents.id, run.agentId), eq(agents.companyId, run.companyId)),
);
if (!agent) return false;
const [issue] =
typeof issueId === "string"
? await db
.select({
companyId: issues.companyId,
projectId: issues.projectId,
executionPolicy: issues.executionPolicy,
sourceTrust: issues.sourceTrust,
})
.from(issues)
.where(
and(eq(issues.id, issueId), eq(issues.companyId, run.companyId)),
)
: [];
if (issueId !== undefined && issueId !== null && !issue) return false;
if (isLowTrustQuarantined(issue?.sourceTrust)) return false;
const projectId = issue?.projectId ?? run.contextSnapshot?.projectId;
if (
projectId !== undefined &&
projectId !== null &&
(typeof projectId !== "string" || !isUuidLike(projectId))
)
return false;
const [project] =
typeof projectId === "string"
? await db
.select({
companyId: projects.companyId,
executionWorkspacePolicy: projects.executionWorkspacePolicy,
})
.from(projects)
.where(
and(
eq(projects.id, projectId),
eq(projects.companyId, run.companyId),
),
)
: [];
if (projectId !== undefined && projectId !== null && !project) return false;
return (
resolveCoreTrustPreset({
companyId: run.companyId,
agent,
project,
issue,
run: {
companyId: run.companyId,
executionPolicy: run.contextSnapshot?.executionPolicy,
},
}).kind === "standard"
);
}
/** No company secrets or ambient credentials are consulted by this path. */
export async function resolveGitHubOperationCredentials(
db: Db,
input: {
companyId: string;
agentId: string;
runId: string;
},
) {
const { run, context } = await captureRunIdentity(db, input, { readScope: "github_credentials" });
if (!context) throw forbidden("This run predates managed GitHub credentials");
let summary: GitHubCredentialSummary;
let env: Record<string, string> = {};
// A sponsored guest's responsible person is an internal accountability field,
// not authorization to export that person's (or a dedicated bot's) token.
// Re-read every policy source for each operation, including a run's retained
// boundary after task policy edits. Deny before touching the credential store.
if (!(await allowsGitHubCredentialExport(db, run))) {
summary = {
status: "unavailable",
reason:
"GitHub credentials are not available to low-trust or unverified executions; use authorized read-only tools.",
};
await db
.update(runIdentityContexts)
.set({ github: summary })
.where(eq(runIdentityContexts.id, context.id));
return {
identityContextId: context.id,
revision: context.revision,
...summary,
env,
};
}
try {
const resolved = await resolveManagedGitHubCredential(
db,
secretService(db),
input.companyId,
{
agentId: input.agentId,
heartbeatRunId: input.runId,
allowStandingDelegation: false,
responsibleUserId:
context?.cause === "company_default"
? null
: (context?.responsibleUserId ?? null),
issueId:
typeof run.contextSnapshot?.issueId === "string"
? run.contextSnapshot.issueId
: null,
},
);
if (resolved.credential) {
summary = {
status: "available",
source: resolved.credential.identitySource,
login: resolved.credential.githubIdentity?.login,
connectionId: resolved.credential.connectionId,
grantId: resolved.credential.grantId,
authenticationMode: "managed",
};
env = buildGitAuthInvocation(resolved.credential).env;
} else {
summary = {
status: resolved.configured ? "unavailable" : "absent",
source: resolved.identitySource ?? "personal",
reason: resolved.error ?? "No GitHub identity connected",
};
}
} catch {
// Provider/secret errors can contain sensitive response bodies. Never persist them.
summary = {
status: "unavailable",
reason: "GitHub credentials are temporarily unavailable",
};
}
if (context)
await db
.update(runIdentityContexts)
.set({ github: summary })
.where(eq(runIdentityContexts.id, context.id));
return {
identityContextId: context?.id ?? null,
revision: context?.revision ?? null,
...summary,
env,
};
}