fix: fail-close permission gate

This commit is contained in:
Alpha Nerd 2026-05-30 10:56:09 +02:00
parent 9e1e054f05
commit b4d8fd75d2
Signed by: alpha-nerd
SSH key fingerprint: SHA256:QkkAgVoYi9TQ0UKPkiKSfnerZy2h4qhi3SVPXJmBN+M

View file

@ -196,8 +196,10 @@ async function createAuthConfig(): Promise<string> {
try {
await createAuthConfig()
// Strip Forgejo write credentials from opencode's env so its bash tool cannot reach them.
const STRIP_FROM_AGENT_ENV = new Set(["FORGEJO_TOKEN", "FORGEJO_PUSH_TOKEN", "GITHUB_TOKEN"])
// Strip credentials from opencode's env so its bash tool cannot reach them.
// NOMYO_API_KEY is handed to the server via OPENCODE_AUTH_CONTENT, so the raw
// env var is not needed by the agent and is removed to limit exfiltration.
const STRIP_FROM_AGENT_ENV = new Set(["FORGEJO_TOKEN", "FORGEJO_PUSH_TOKEN", "GITHUB_TOKEN", "NOMYO_API_KEY"])
const agentEnv: NodeJS.ProcessEnv = {}
for (const [k, v] of Object.entries(process.env)) {
if (!STRIP_FROM_AGENT_ENV.has(k)) agentEnv[k] = v
@ -216,9 +218,11 @@ try {
forgejoHost = new URL(forgejoApiUrl).hostname
accessToken = forgejoToken
// Gate on permissions before doing any work (fetching prompt images, etc.).
await assertPermissions()
const { userPrompt, promptFiles } = await getUserPrompt()
await configureGitIdentity()
await assertPermissions()
const comment = await createComment()
commentId = comment.id
@ -655,23 +659,37 @@ async function assertPermissions() {
console.log(`Asserting permissions for user ${actor}...`)
try {
// Forgejo: check if user is a collaborator/member
await forgejoFetch<any>(
forgejoApiUrl("repos", context.repo.owner, context.repo.repo, "collaborators", actor),
)
console.log(" permission: write (collaborator)")
} catch (error: any) {
// If not a collaborator, check if actor is the repo owner (via GITHUB_ACTOR)
if (actor === context.repo.owner || actor === `${context.repo.owner}[bot]`) {
console.log(" permission: admin (owner)")
return
}
console.error(`Failed to check permissions: ${error.message}`)
// In Actions context, if we can write to the repo, we have write access
// We'll assume write access since the workflow has the right permissions
console.log(" permission: write (assumed from workflow permissions)")
// The repo owner (and its bot account) is always allowed.
if (actor === context.repo.owner || actor === `${context.repo.owner}[bot]`) {
console.log(" permission: admin (owner)")
return
}
// Otherwise the actor must have write access, i.e. be a collaborator/member.
// Forgejo: GET .../collaborators/{user} returns 204 if a collaborator, 404 if not.
// We use a raw fetch (not forgejoFetch) because the 204 response has no JSON body.
const { forgejoToken } = getForgejoConfig()
const url = forgejoApiUrl("repos", context.repo.owner, context.repo.repo, "collaborators", actor)
let res: Response
try {
res = await fetch(url, { headers: { Authorization: `token ${forgejoToken}` } })
} catch (error: any) {
// Fail closed: if we cannot verify permissions, deny.
throw new Error(`Could not verify permissions for "${actor}": ${error.message}`)
}
if (res.status === 204 || res.ok) {
console.log(" permission: write (collaborator)")
return
}
if (res.status === 404) {
throw new Error(
`User "${actor}" is not authorized to trigger this action ` +
`(requires write access to ${context.repo.owner}/${context.repo.repo}).`,
)
}
const text = await res.text().catch(() => "")
throw new Error(`Could not verify permissions for "${actor}": ${res.status} ${res.statusText} ${text}`)
}
// ─── Git operations ─────────────────────────────────────────────────────────