feat: seperate read/write token for enhanced security
This commit is contained in:
parent
fcfea07f01
commit
958b2ba9a9
2 changed files with 40 additions and 31 deletions
|
|
@ -27,7 +27,11 @@ inputs:
|
|||
default: "https://bitfreedom.net/code/"
|
||||
|
||||
forgejo_token:
|
||||
description: "Forgejo PAT with repo scope (contents, pull-requests, issues)"
|
||||
description: "Forgejo PAT used by the outer action for read operations (clone, fetch, read APIs). Recommended scopes: read:repository, read:issue, read:user. Never exposed to the opencode subprocess."
|
||||
required: false
|
||||
|
||||
forgejo_push_token:
|
||||
description: "Optional separate Forgejo PAT used only for write operations (git push, create/update comments, create MR). Recommended scopes: write:repository, write:issue. Falls back to forgejo_token if unset."
|
||||
required: false
|
||||
|
||||
mentions:
|
||||
|
|
@ -85,6 +89,7 @@ runs:
|
|||
PROMPT: ${{ inputs.prompt }}
|
||||
FORGEJO_API_URL: ${{ inputs.forgejo_api_url }}
|
||||
FORGEJO_TOKEN: ${{ inputs.forgejo_token }}
|
||||
FORGEJO_PUSH_TOKEN: ${{ inputs.forgejo_push_token }}
|
||||
MENTIONS: ${{ inputs.mentions }}
|
||||
VARIANT: ${{ inputs.variant }}
|
||||
run: |
|
||||
|
|
|
|||
64
index.ts
64
index.ts
|
|
@ -121,7 +121,6 @@ const SERVER_URL = `http://${HOST}:${PORT}`
|
|||
let proc: ReturnType<typeof spawn> | undefined
|
||||
let accessToken: string
|
||||
let commentId: number
|
||||
let gitConfig: string
|
||||
let session: { id: string; title: string; version: string }
|
||||
let shareId: string | undefined
|
||||
let exitCode = 0
|
||||
|
|
@ -190,8 +189,16 @@ 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"])
|
||||
const agentEnv: NodeJS.ProcessEnv = {}
|
||||
for (const [k, v] of Object.entries(process.env)) {
|
||||
if (!STRIP_FROM_AGENT_ENV.has(k)) agentEnv[k] = v
|
||||
}
|
||||
|
||||
proc = spawn(`opencode`, [`serve`, `--hostname=${HOST}`, `--port=${PORT}`], {
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
env: agentEnv,
|
||||
})
|
||||
assertContextEvent("issue_comment", "pull_request_review_comment", "pull_request_review")
|
||||
assertPayloadKeyword()
|
||||
|
|
@ -203,7 +210,7 @@ try {
|
|||
accessToken = forgejoToken
|
||||
|
||||
const { userPrompt, promptFiles } = await getUserPrompt()
|
||||
await configureGit(accessToken)
|
||||
await configureGitIdentity()
|
||||
await assertPermissions()
|
||||
|
||||
const comment = await createComment()
|
||||
|
|
@ -295,7 +302,6 @@ try {
|
|||
if (proc) {
|
||||
proc.kill()
|
||||
}
|
||||
await restoreGitConfig()
|
||||
}
|
||||
process.exit(exitCode)
|
||||
|
||||
|
|
@ -311,7 +317,14 @@ function getForgejoConfig() {
|
|||
if (!token) {
|
||||
throw new Error(`Environment variable "FORGEJO_TOKEN" is not set`)
|
||||
}
|
||||
return { forgejoApiUrl: apiUrl, forgejoToken: token }
|
||||
const pushToken = process.env["FORGEJO_PUSH_TOKEN"] || token
|
||||
return { forgejoApiUrl: apiUrl, forgejoToken: token, forgejoPushToken: pushToken }
|
||||
}
|
||||
|
||||
async function authedGit(token: string, args: string[]) {
|
||||
const credential = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64")
|
||||
const headerCfg = `http.https://${forgejoHost}/.extraheader=AUTHORIZATION: basic ${credential}`
|
||||
return await $`git -c ${headerCfg} ${args}`
|
||||
}
|
||||
|
||||
function forgejoApiUrl(...pathParts: string[]): string {
|
||||
|
|
@ -322,11 +335,14 @@ function forgejoApiUrl(...pathParts: string[]): string {
|
|||
}
|
||||
|
||||
async function forgejoFetch<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const { forgejoToken } = getForgejoConfig()
|
||||
const { forgejoToken, forgejoPushToken } = getForgejoConfig()
|
||||
const method = (options?.method || "GET").toUpperCase()
|
||||
const isWrite = method !== "GET" && method !== "HEAD"
|
||||
const token = isWrite ? forgejoPushToken : forgejoToken
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `token ${forgejoToken}`,
|
||||
Authorization: `token ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
...(options?.headers || {}),
|
||||
},
|
||||
|
|
@ -653,29 +669,12 @@ async function assertPermissions() {
|
|||
|
||||
// ─── Git operations ─────────────────────────────────────────────────────────
|
||||
|
||||
async function configureGit(appToken: string) {
|
||||
console.log("Configuring git...")
|
||||
const config = "http.https://github.com/.extraheader"
|
||||
const ret = await $`git config --local --get ${config}`.catch(() => ({ stdout: "" }))
|
||||
gitConfig = (ret.stdout as string)?.toString().trim() || ""
|
||||
|
||||
// Use Forgejo host for git credentials
|
||||
const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64")
|
||||
const gitUrl = `http.https://${forgejoHost}/.extraheader`
|
||||
|
||||
await $`git config --local --unset-all ${config}`.catch(() => {})
|
||||
await $`git config --local ${gitUrl} "AUTHORIZATION: basic ${newCredentials}"`
|
||||
async function configureGitIdentity() {
|
||||
console.log("Configuring git identity...")
|
||||
await $`git config --global user.name "opencode-agent[bot]"`
|
||||
await $`git config --global user.email "opencode-agent[bot]@users.noreply.${forgejoHost}"`
|
||||
}
|
||||
|
||||
async function restoreGitConfig() {
|
||||
if (gitConfig === undefined) return
|
||||
console.log("Restoring git config...")
|
||||
const config = "http.https://github.com/.extraheader"
|
||||
await $`git config --local ${config} "${gitConfig}"`.catch(() => {})
|
||||
}
|
||||
|
||||
async function checkoutNewBranch() {
|
||||
console.log("Checking out new branch...")
|
||||
const branch = generateBranchName("issue")
|
||||
|
|
@ -686,8 +685,9 @@ async function checkoutNewBranch() {
|
|||
async function checkoutLocalBranch(pr: ForgejoPullRequest) {
|
||||
console.log("Checking out local branch...")
|
||||
const branch = pr.head.ref
|
||||
const { forgejoToken } = getForgejoConfig()
|
||||
|
||||
await $`git fetch origin --depth=100 ${branch}`
|
||||
await authedGit(forgejoToken, ["fetch", "origin", "--depth=100", branch])
|
||||
await $`git checkout ${branch}`
|
||||
}
|
||||
|
||||
|
|
@ -695,10 +695,11 @@ async function checkoutForkBranch(pr: ForgejoPullRequest) {
|
|||
console.log("Checking out fork branch...")
|
||||
const remoteBranch = pr.head.ref
|
||||
const localBranch = generateBranchName("pr")
|
||||
const { forgejoToken } = getForgejoConfig()
|
||||
|
||||
const forkRemote = `https://${forgejoHost}/${pr.head.repo?.full_name}.git`
|
||||
await $`git remote add fork ${forkRemote}`
|
||||
await $`git fetch fork --depth=100 ${remoteBranch}`
|
||||
await authedGit(forgejoToken, ["fetch", "fork", "--depth=100", remoteBranch])
|
||||
await $`git checkout -b ${localBranch} fork/${remoteBranch}`
|
||||
}
|
||||
|
||||
|
|
@ -715,33 +716,36 @@ function generateBranchName(type: "issue" | "pr") {
|
|||
async function pushToNewBranch(summary: string, branch: string) {
|
||||
console.log("Pushing to new branch...")
|
||||
const actor = useContext().actor
|
||||
const { forgejoPushToken } = getForgejoConfig()
|
||||
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.${forgejoHost}>"`
|
||||
await $`git push -u origin ${branch}`
|
||||
await authedGit(forgejoPushToken, ["push", "-u", "origin", branch])
|
||||
}
|
||||
|
||||
async function pushToLocalBranch(summary: string) {
|
||||
console.log("Pushing to local branch...")
|
||||
const actor = useContext().actor
|
||||
const { forgejoPushToken } = getForgejoConfig()
|
||||
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.${forgejoHost}>"`
|
||||
await $`git push`
|
||||
await authedGit(forgejoPushToken, ["push"])
|
||||
}
|
||||
|
||||
async function pushToForkBranch(summary: string, pr: ForgejoPullRequest) {
|
||||
console.log("Pushing to fork branch...")
|
||||
const { forgejoPushToken } = getForgejoConfig()
|
||||
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${useContext().actor} <${useContext().actor}@users.noreply.${forgejoHost}>"`
|
||||
await $`git push fork HEAD:${pr.head.ref}`
|
||||
await authedGit(forgejoPushToken, ["push", "fork", `HEAD:${pr.head.ref}`])
|
||||
}
|
||||
|
||||
async function branchIsDirty() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue