diff --git a/index.ts b/index.ts index 29b19f1..c495df3 100644 --- a/index.ts +++ b/index.ts @@ -196,8 +196,10 @@ async function createAuthConfig(): Promise { 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( - 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 ─────────────────────────────────────────────────────────