mirror of
https://github.com/logseq/logseq.git
synced 2026-05-24 12:44:22 +00:00
GitHub app installation token
This commit is contained in:
18
deps/workers/README.md
vendored
18
deps/workers/README.md
vendored
@@ -180,7 +180,10 @@ Cloudflare runtime flow:
|
||||
| CLOUDFLARE_REPO_CLONE_COMMAND | Optional repo clone command template for Cloudflare sandbox |
|
||||
| CLOUDFLARE_HEALTH_RETRIES | Cloudflare sandbox health check retry count |
|
||||
| CLOUDFLARE_HEALTH_INTERVAL_MS | Cloudflare sandbox health check retry interval (ms) |
|
||||
| GITHUB_TOKEN | Fallback token used for both git push and PR API calls |
|
||||
| GITHUB_APP_ID | GitHub App ID used to mint installation tokens |
|
||||
| GITHUB_APP_INSTALLATION_ID | Optional fixed installation ID (if omitted, resolved from repo) |
|
||||
| GITHUB_APP_PRIVATE_KEY | GitHub App private key PEM used for JWT signing |
|
||||
| GITHUB_APP_SLUG | Optional app slug used to build install URL in setup prompts |
|
||||
| GITHUB_API_BASE | Optional GitHub API base URL override (default `https://api.github.com`) |
|
||||
| OPENAI_API_KEY | Passed into Cloudflare sandbox runtime env (if set) |
|
||||
| ANTHROPIC_API_KEY | Passed into Cloudflare sandbox runtime env (if set) |
|
||||
@@ -197,15 +200,24 @@ This endpoint is available to any authenticated collaborator and supports:
|
||||
- push only (`{"create-pr": false}`)
|
||||
- push + PR (`{"create-pr": true}` or omitted)
|
||||
|
||||
When creating a session, the worker verifies the GitHub App is installed on the target repo.
|
||||
If not installed, `POST /sessions` returns `412` with an install prompt message.
|
||||
|
||||
Agent chat messages can also trigger the same publish flow:
|
||||
- `push`
|
||||
- `submit PR`
|
||||
- natural language requests like `please push this branch` or `can you submit a pull request?`
|
||||
|
||||
Response `status` values:
|
||||
- `pushed`
|
||||
- `pr-created`
|
||||
- `manual-pr-required`
|
||||
|
||||
If PR credentials are missing or PR API creation fails after a successful push, response includes `manual-pr-url`.
|
||||
If PR API creation fails after a successful push, response includes `manual-pr-url`.
|
||||
|
||||
For Cloudflare deploys, store tokens as agents worker secrets:
|
||||
- `wrangler secret put GITHUB_TOKEN -c worker/wrangler.agents.toml --env <staging|prod>`
|
||||
- `wrangler secret put GITHUB_APP_PRIVATE_KEY -c worker/wrangler.agents.toml --env <staging|prod>`
|
||||
- set `GITHUB_APP_ID` and optional `GITHUB_APP_INSTALLATION_ID` in worker vars
|
||||
|
||||
## Notes
|
||||
- Protocol definitions live in `docs/agent-guide/db-sync/protocol.md`.
|
||||
|
||||
103
deps/workers/src/logseq/agents/do.cljs
vendored
103
deps/workers/src/logseq/agents/do.cljs
vendored
@@ -267,21 +267,17 @@
|
||||
[]
|
||||
"main")
|
||||
|
||||
(defn- github-default-branch-token
|
||||
[^js env]
|
||||
(or (source-control/push-token env)
|
||||
(source-control/pr-token env)))
|
||||
|
||||
(defn- github-branches-token
|
||||
[^js env]
|
||||
(or (source-control/push-token env)
|
||||
(source-control/pr-token env)))
|
||||
|
||||
(defn- task-requested-base-branch
|
||||
[task]
|
||||
(or (some-> (get-in task [:project :base-branch]) source-control/sanitize-branch-name)
|
||||
(some-> (get-in task [:project :branch]) source-control/sanitize-branch-name)))
|
||||
|
||||
(defn- github-install-required-message
|
||||
[install-url]
|
||||
(if (string? install-url)
|
||||
(str "GitHub App is not installed for this repository. Install it and retry: " install-url)
|
||||
"GitHub App is not installed for this repository. Install it and retry."))
|
||||
|
||||
(defn- <ensure-task-base-branch!
|
||||
[^js env task]
|
||||
(let [repo-url (some-> (get-in task [:project :repo-url]) str string/trim not-empty)
|
||||
@@ -301,7 +297,7 @@
|
||||
|
||||
:else
|
||||
(p/let [detected-base (source-control/<default-branch! env
|
||||
(github-default-branch-token env)
|
||||
nil
|
||||
repo-url)
|
||||
detected-base (source-control/sanitize-branch-name detected-base)
|
||||
fallback-base (source-control/sanitize-branch-name (default-base-branch))
|
||||
@@ -567,24 +563,36 @@
|
||||
(http/forbidden)
|
||||
|
||||
:else
|
||||
(p/let [task (<ensure-task-base-branch! (.-env self) task)]
|
||||
(let [session (session/initial-session task audit now)
|
||||
[session events _event] (session/append-event session [] {:type "session.created"
|
||||
:data {:requested-by user-id
|
||||
:project (:project task)
|
||||
:agent (:agent task)}
|
||||
:ts now})]
|
||||
(p/let [_ (<put-session! self session)
|
||||
_ (<put-events! self events)
|
||||
_ (<provision-runtime! self task task-id)
|
||||
updated-session (<get-session self)]
|
||||
(http/json-response :sessions/create
|
||||
{:session-id task-id
|
||||
:status (or (:status updated-session)
|
||||
(:status session))
|
||||
:runtime-provider (session-runtime-provider updated-session)
|
||||
:terminal-enabled (session-terminal-enabled? updated-session)
|
||||
:stream-url (stream-url request task-id)}))))))))))))
|
||||
(p/let [repo-url (some-> (get-in task [:project :repo-url]) str string/trim not-empty)
|
||||
install-status (-> (if (string? repo-url)
|
||||
(source-control/<repo-installation-status! (.-env self) repo-url)
|
||||
(p/resolved {:installed? true}))
|
||||
(p/catch (fn [error]
|
||||
{:error error})))]
|
||||
(if (:error install-status)
|
||||
(http/error-response "failed to verify GitHub App installation" 500)
|
||||
(if (and (map? install-status)
|
||||
(false? (:installed? install-status)))
|
||||
(http/error-response (github-install-required-message (:install-url install-status)) 412)
|
||||
(p/let [task (<ensure-task-base-branch! (.-env self) task)]
|
||||
(let [session (session/initial-session task audit now)
|
||||
[session events _event]
|
||||
(session/append-event session [] {:type "session.created"
|
||||
:data {:requested-by user-id
|
||||
:project (:project task)
|
||||
:agent (:agent task)}
|
||||
:ts now})]
|
||||
(p/let [_ (<put-session! self session)
|
||||
_ (<put-events! self events)
|
||||
_ (<provision-runtime! self task task-id)
|
||||
updated-session (<get-session self)]
|
||||
(http/json-response :sessions/create
|
||||
{:session-id task-id
|
||||
:status (or (:status updated-session)
|
||||
(:status session))
|
||||
:runtime-provider (session-runtime-provider updated-session)
|
||||
:terminal-enabled (session-terminal-enabled? updated-session)
|
||||
:stream-url (stream-url request task-id)})))))))))))))))
|
||||
|
||||
(defn- handle-status [^js self _request]
|
||||
(p/let [session (<get-session self)]
|
||||
@@ -741,12 +749,11 @@
|
||||
|
||||
(defn- <handle-pr-after-push!
|
||||
[^js self current-session body user-id repo-url head-branch force? create-pr?]
|
||||
(p/let [pr-token (source-control/pr-token (.-env self))
|
||||
requested-base-branch (source-control/sanitize-branch-name (:base-branch body))
|
||||
(p/let [requested-base-branch (source-control/sanitize-branch-name (:base-branch body))
|
||||
default-base (source-control/sanitize-branch-name (default-base-branch))
|
||||
detected-base-branch (when (nil? requested-base-branch)
|
||||
(source-control/<default-branch! (.-env self)
|
||||
pr-token
|
||||
nil
|
||||
repo-url))
|
||||
detected-base-branch (source-control/sanitize-branch-name detected-base-branch)
|
||||
base-branch (or requested-base-branch
|
||||
@@ -774,21 +781,13 @@
|
||||
(http/forbidden)
|
||||
|
||||
:else
|
||||
(let [description (or (some-> (:body body) str string/trim not-empty)
|
||||
"Automated changes from agent session.")
|
||||
title (or (some-> (:title body) str string/trim not-empty)
|
||||
(description->pr-title description)
|
||||
(str "Agent updates for session " (:id current-session)))
|
||||
manual-url (source-control/manual-pr-url repo-url head-branch base-branch)]
|
||||
(if-not (string? pr-token)
|
||||
(<manual-pr-required-response! self
|
||||
{:user-id user-id
|
||||
:head-branch head-branch
|
||||
:base-branch base-branch
|
||||
:manual-url manual-url
|
||||
:reason "missing-token"
|
||||
:force? force?
|
||||
:message "branch pushed; create pull request manually"})
|
||||
(p/let [pr-token (source-control/<pr-token! (.-env self) repo-url)]
|
||||
(let [description (or (some-> (:body body) str string/trim not-empty)
|
||||
"Automated changes from agent session.")
|
||||
title (or (some-> (:title body) str string/trim not-empty)
|
||||
(description->pr-title description)
|
||||
(str "Agent updates for session " (:id current-session)))
|
||||
manual-url (source-control/manual-pr-url repo-url head-branch base-branch)]
|
||||
(<create-pr-response! self
|
||||
{:user-id user-id
|
||||
:repo-url repo-url
|
||||
@@ -803,13 +802,14 @@
|
||||
(defn- <perform-pr-push!
|
||||
[^js self current-session body user-id repo-url runtime force? create-pr? push-branch-fn]
|
||||
(let [provider (runtime-provider/resolve-provider (.-env self) runtime)
|
||||
push-token (source-control/push-token (.-env self))
|
||||
env (.-env self)
|
||||
commit-message (some-> (:commit-message body) str string/trim not-empty)
|
||||
head-branch (source-control/resolve-head-branch (:head-branch body)
|
||||
(generated-head-branch current-session body))]
|
||||
(if-not (string? head-branch)
|
||||
(http/bad-request "invalid head branch")
|
||||
(-> (p/let [_ (<append-publish-event! self "git.push.started"
|
||||
(-> (p/let [push-token (source-control/<push-token! env repo-url)
|
||||
_ (<append-publish-event! self "git.push.started"
|
||||
{:by user-id
|
||||
:head-branch head-branch
|
||||
:force force?})
|
||||
@@ -1084,8 +1084,9 @@
|
||||
(repo-url-from-session session))]
|
||||
(if-not (string? repo-url)
|
||||
(http/bad-request "missing repo url")
|
||||
(p/let [branches (source-control/<list-branches! env
|
||||
(github-branches-token env)
|
||||
(p/let [token (source-control/<push-token! env repo-url)
|
||||
branches (source-control/<list-branches! env
|
||||
token
|
||||
repo-url)]
|
||||
(http/json-response :sessions/branches
|
||||
{:branches (->> branches
|
||||
|
||||
358
deps/workers/src/logseq/agents/source_control.cljs
vendored
358
deps/workers/src/logseq/agents/source_control.cljs
vendored
@@ -98,21 +98,154 @@
|
||||
(or (some-> (aget env "GITHUB_USER_AGENT") non-empty-str)
|
||||
"logseq-worker/1.0"))
|
||||
|
||||
(defn pr-token
|
||||
[^js env]
|
||||
(some-> (aget env "GITHUB_TOKEN") non-empty-str))
|
||||
(def ^:private github-api-version "2022-11-28")
|
||||
(def ^:private token-refresh-skew-ms 60000)
|
||||
(defonce ^:private installation-token-cache (atom {}))
|
||||
|
||||
(defn push-token
|
||||
[^js env]
|
||||
(some-> (aget env "GITHUB_TOKEN") non-empty-str))
|
||||
(defn- now-ms []
|
||||
(.now js/Date))
|
||||
|
||||
(defn push-remote-url
|
||||
[repo-url token]
|
||||
(when-let [{:keys [provider owner name]} (repo-ref repo-url)]
|
||||
(when (= "github" provider)
|
||||
(if (string? token)
|
||||
(str "https://x-access-token:" (js/encodeURIComponent token) "@github.com/" owner "/" name ".git")
|
||||
(str "https://github.com/" owner "/" name ".git")))))
|
||||
(defn- parse-int-safe
|
||||
[value]
|
||||
(let [num (some-> value js/parseInt)]
|
||||
(when (and (number? num)
|
||||
(not (js/isNaN num)))
|
||||
num)))
|
||||
|
||||
(defn- parse-time-ms
|
||||
[value]
|
||||
(let [ms (some-> value js/Date.parse)]
|
||||
(when (and (number? ms)
|
||||
(not (js/isNaN ms)))
|
||||
ms)))
|
||||
|
||||
(defn- base64url
|
||||
[value]
|
||||
(-> (or value "")
|
||||
(string/replace #"\+" "-")
|
||||
(string/replace #"/" "_")
|
||||
(string/replace #"=+$" "")))
|
||||
|
||||
(defn- uint8->base64url
|
||||
[payload]
|
||||
(base64url (js/btoa (apply str (map char payload)))))
|
||||
|
||||
(defn- text->base64url
|
||||
[text]
|
||||
(-> (.encode (js/TextEncoder.) (or text ""))
|
||||
uint8->base64url))
|
||||
|
||||
(defn- json->base64url
|
||||
[payload]
|
||||
(text->base64url (js/JSON.stringify (clj->js payload))))
|
||||
|
||||
(defn- github-app-id
|
||||
[^js env]
|
||||
(some-> (aget env "GITHUB_APP_ID") non-empty-str))
|
||||
|
||||
(defn- github-app-installation-id
|
||||
[^js env]
|
||||
(some-> (aget env "GITHUB_APP_INSTALLATION_ID")
|
||||
non-empty-str
|
||||
parse-int-safe))
|
||||
|
||||
(defn- github-app-private-key
|
||||
[^js env]
|
||||
(some-> (aget env "GITHUB_APP_PRIVATE_KEY")
|
||||
non-empty-str
|
||||
(string/replace #"\\n" "\n")))
|
||||
|
||||
(defn- github-app-slug
|
||||
[^js env]
|
||||
(or (some-> (aget env "GITHUB_APP_SLUG") non-empty-str)
|
||||
(some-> (aget env "GITHUB_APP_NAME") non-empty-str)))
|
||||
|
||||
(defn- node-subtle
|
||||
[]
|
||||
(let [require-fn (aget js/globalThis "require")]
|
||||
(when (fn? require-fn)
|
||||
(let [crypto-module (.call require-fn nil "node:crypto")
|
||||
webcrypto (when crypto-module
|
||||
(aget crypto-module "webcrypto"))]
|
||||
(when webcrypto
|
||||
(aget webcrypto "subtle"))))))
|
||||
|
||||
(defn- subtle-crypto
|
||||
[]
|
||||
(or (some-> (aget js/globalThis "crypto")
|
||||
.-subtle)
|
||||
(node-subtle)))
|
||||
|
||||
(defn- pem->array-buffer
|
||||
[pem]
|
||||
(let [body (-> (or pem "")
|
||||
(string/replace #"-+BEGIN[^-]+-+" "")
|
||||
(string/replace #"-+END[^-]+-+" "")
|
||||
(string/replace #"\s+" ""))
|
||||
binary (js/atob body)
|
||||
length (.-length binary)
|
||||
payload (js/Uint8Array. length)]
|
||||
(dotimes [idx length]
|
||||
(aset payload idx (.charCodeAt binary idx)))
|
||||
(.-buffer payload)))
|
||||
|
||||
(defn- <import-private-key!
|
||||
[private-key]
|
||||
(if-let [subtle (subtle-crypto)]
|
||||
(.importKey subtle
|
||||
"pkcs8"
|
||||
(pem->array-buffer private-key)
|
||||
#js {:name "RSASSA-PKCS1-v1_5"
|
||||
:hash #js {:name "SHA-256"}}
|
||||
false
|
||||
#js ["sign"])
|
||||
(p/rejected (ex-info "webcrypto subtle unavailable"
|
||||
{:reason :crypto-unavailable}))))
|
||||
|
||||
(defn- <github-app-jwt!
|
||||
[^js env]
|
||||
(let [app-id-raw (github-app-id env)
|
||||
app-id (or (parse-int-safe app-id-raw) app-id-raw)
|
||||
private-key (github-app-private-key env)]
|
||||
(cond
|
||||
(nil? app-id)
|
||||
(p/rejected (ex-info "missing GITHUB_APP_ID"
|
||||
{:reason :missing-github-app-auth}))
|
||||
|
||||
(not (string? private-key))
|
||||
(p/rejected (ex-info "missing GITHUB_APP_PRIVATE_KEY"
|
||||
{:reason :missing-github-app-auth}))
|
||||
|
||||
:else
|
||||
(let [issued-at (js/Math.floor (/ (now-ms) 1000))
|
||||
header (json->base64url {:alg "RS256"
|
||||
:typ "JWT"})
|
||||
claims (json->base64url {:iat (- issued-at 60)
|
||||
:exp (+ issued-at 540)
|
||||
:iss app-id})
|
||||
unsigned-token (str header "." claims)
|
||||
encoded-token (.encode (js/TextEncoder.) unsigned-token)]
|
||||
(p/let [private-key-obj (<import-private-key! private-key)
|
||||
subtle (subtle-crypto)
|
||||
signature (.sign subtle
|
||||
#js {:name "RSASSA-PKCS1-v1_5"}
|
||||
private-key-obj
|
||||
encoded-token)
|
||||
signature-token (-> (js/Uint8Array. signature)
|
||||
uint8->base64url)]
|
||||
(str unsigned-token "." signature-token))))))
|
||||
|
||||
(defn- request-headers
|
||||
[^js env {:keys [token content-type]}]
|
||||
(let [headers (js/Headers.)]
|
||||
(.set headers "accept" "application/vnd.github+json")
|
||||
(.set headers "user-agent" (user-agent env))
|
||||
(.set headers "x-github-api-version" github-api-version)
|
||||
(when-let [auth-token (non-empty-str token)]
|
||||
(.set headers "authorization" (str "Bearer " auth-token)))
|
||||
(when-let [value (non-empty-str content-type)]
|
||||
(.set headers "content-type" value))
|
||||
headers))
|
||||
|
||||
(defn- parse-json-safe
|
||||
[text]
|
||||
@@ -144,18 +277,182 @@
|
||||
{}
|
||||
github-debug-response-headers))
|
||||
|
||||
(defn- fetch-json-safe!
|
||||
[url request]
|
||||
(p/let [resp (js/fetch url request)
|
||||
status (.-status resp)
|
||||
text (.text resp)
|
||||
payload (parse-json-safe text)
|
||||
response-headers (select-response-headers resp)]
|
||||
{:status status
|
||||
:ok? (<= 200 status 299)
|
||||
:payload payload
|
||||
:raw-body text
|
||||
:response-headers response-headers}))
|
||||
|
||||
(defn- token-cache-key
|
||||
[^js env repo-url installation-id]
|
||||
(str (api-base-url env) "|" (or installation-id repo-url)))
|
||||
|
||||
(defn- cached-installation-token
|
||||
[cache-key]
|
||||
(let [{:keys [token expires-at-ms]} (get @installation-token-cache cache-key)]
|
||||
(when (and (string? token)
|
||||
(number? expires-at-ms)
|
||||
(> (- expires-at-ms (now-ms)) token-refresh-skew-ms))
|
||||
token)))
|
||||
|
||||
(defn- <resolve-installation-id!
|
||||
[^js env app-jwt repo-url]
|
||||
(if-let [installation-id (github-app-installation-id env)]
|
||||
(p/resolved installation-id)
|
||||
(if-let [{:keys [provider owner name]} (repo-ref repo-url)]
|
||||
(if-not (= "github" provider)
|
||||
(p/rejected (ex-info "unsupported scm provider"
|
||||
{:reason :unsupported-provider
|
||||
:provider provider}))
|
||||
(let [url (str (api-base-url env) "/repos/" owner "/" name "/installation")
|
||||
headers (request-headers env {:token app-jwt})]
|
||||
(p/let [{:keys [ok? status payload raw-body response-headers]}
|
||||
(fetch-json-safe! url #js {:method "GET" :headers headers})
|
||||
installation-id (some-> (:id payload) parse-int-safe)]
|
||||
(if (and ok? (number? installation-id))
|
||||
installation-id
|
||||
(p/rejected (ex-info "resolve installation failed"
|
||||
{:reason :installation-resolution-failed
|
||||
:status status
|
||||
:body payload
|
||||
:raw-body raw-body
|
||||
:response-headers response-headers}))))))
|
||||
(p/rejected (ex-info "invalid repo url"
|
||||
{:reason :invalid-repo-url
|
||||
:repo-url repo-url})))))
|
||||
|
||||
(defn <installation-token!
|
||||
[^js env repo-url]
|
||||
(if-not (map? (repo-ref repo-url))
|
||||
(p/rejected (ex-info "invalid repo url"
|
||||
{:reason :invalid-repo-url
|
||||
:repo-url repo-url}))
|
||||
(let [configured-installation-id (github-app-installation-id env)
|
||||
initial-cache-key (token-cache-key env repo-url configured-installation-id)
|
||||
cached-token (cached-installation-token initial-cache-key)]
|
||||
(if (string? cached-token)
|
||||
(p/resolved cached-token)
|
||||
(p/let [app-jwt (<github-app-jwt! env)
|
||||
installation-id (<resolve-installation-id! env app-jwt repo-url)
|
||||
cache-key (token-cache-key env repo-url installation-id)
|
||||
cached-token (cached-installation-token cache-key)]
|
||||
(if (string? cached-token)
|
||||
cached-token
|
||||
(let [url (str (api-base-url env) "/app/installations/" installation-id "/access_tokens")
|
||||
headers (request-headers env {:token app-jwt
|
||||
:content-type "application/json"})]
|
||||
(p/let [{:keys [ok? status payload raw-body response-headers]}
|
||||
(fetch-json-safe! url
|
||||
#js {:method "POST"
|
||||
:headers headers
|
||||
:body "{}"})
|
||||
token (:token payload)
|
||||
expires-at-ms (some-> (:expires_at payload) parse-time-ms)]
|
||||
(if (and ok?
|
||||
(string? token)
|
||||
(number? expires-at-ms))
|
||||
(do
|
||||
(swap! installation-token-cache
|
||||
assoc
|
||||
cache-key
|
||||
{:token token
|
||||
:expires-at-ms expires-at-ms})
|
||||
token)
|
||||
(p/rejected (ex-info "create installation token failed"
|
||||
{:reason :token-mint-failed
|
||||
:status status
|
||||
:body payload
|
||||
:raw-body raw-body
|
||||
:response-headers response-headers})))))))))))
|
||||
|
||||
(defn app-install-url
|
||||
[^js env repo-url]
|
||||
(when-let [{:keys [provider]} (repo-ref repo-url)]
|
||||
(when (= "github" provider)
|
||||
(some-> (github-app-slug env)
|
||||
(#(str "https://github.com/apps/" % "/installations/new"))))))
|
||||
|
||||
(defn <repo-installation-status!
|
||||
[^js env repo-url]
|
||||
(if-let [{:keys [provider owner name]} (repo-ref repo-url)]
|
||||
(if-not (= "github" provider)
|
||||
(p/resolved {:provider provider
|
||||
:installed? true
|
||||
:check-required? false})
|
||||
(p/let [app-jwt (<github-app-jwt! env)
|
||||
url (str (api-base-url env) "/repos/" owner "/" name "/installation")
|
||||
headers (request-headers env {:token app-jwt})
|
||||
{:keys [ok? status payload raw-body response-headers]}
|
||||
(fetch-json-safe! url #js {:method "GET" :headers headers})
|
||||
installation-id (some-> (:id payload) parse-int-safe)]
|
||||
(cond
|
||||
(and ok? (number? installation-id))
|
||||
{:provider provider
|
||||
:installed? true
|
||||
:check-required? true
|
||||
:installation-id installation-id}
|
||||
|
||||
(= status 404)
|
||||
{:provider provider
|
||||
:installed? false
|
||||
:check-required? true
|
||||
:reason :not-installed
|
||||
:install-url (app-install-url env repo-url)}
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "check github app installation failed"
|
||||
{:reason :installation-check-failed
|
||||
:status status
|
||||
:body payload
|
||||
:raw-body raw-body
|
||||
:response-headers response-headers})))))
|
||||
(p/resolved {:provider nil
|
||||
:installed? true
|
||||
:check-required? false})))
|
||||
|
||||
(defn pr-token
|
||||
[_env]
|
||||
nil)
|
||||
|
||||
(defn push-token
|
||||
[_env]
|
||||
nil)
|
||||
|
||||
(defn <pr-token!
|
||||
[^js env repo-url]
|
||||
(<installation-token! env repo-url))
|
||||
|
||||
(defn <push-token!
|
||||
[^js env repo-url]
|
||||
(<installation-token! env repo-url))
|
||||
|
||||
(defn push-remote-url
|
||||
[repo-url token]
|
||||
(when-let [{:keys [provider owner name]} (repo-ref repo-url)]
|
||||
(when (and (= "github" provider)
|
||||
(string? token))
|
||||
(str "https://x-access-token:"
|
||||
(js/encodeURIComponent token)
|
||||
"@github.com/"
|
||||
owner
|
||||
"/"
|
||||
name
|
||||
".git"))))
|
||||
|
||||
(defn <default-branch!
|
||||
[^js env token repo-url]
|
||||
(if-let [{:keys [provider owner name]} (repo-ref repo-url)]
|
||||
(if-not (= "github" provider)
|
||||
(p/resolved nil)
|
||||
(let [url (str (api-base-url env) "/repos/" owner "/" name)
|
||||
headers (doto (js/Headers.)
|
||||
(.set "accept" "application/vnd.github+json")
|
||||
(.set "user-agent" (user-agent env))
|
||||
(.set "x-github-api-version" "2022-11-28"))
|
||||
_ (when (string? token)
|
||||
(.set headers "authorization" (str "Bearer " token)))]
|
||||
headers (request-headers env {:token token})]
|
||||
(p/let [resp (js/fetch url #js {:method "GET" :headers headers})
|
||||
status (.-status resp)
|
||||
text (.text resp)
|
||||
@@ -171,12 +468,7 @@
|
||||
(if-not (= "github" provider)
|
||||
(p/resolved [])
|
||||
(let [url (str (api-base-url env) "/repos/" owner "/" name "/branches?per_page=100")
|
||||
headers (doto (js/Headers.)
|
||||
(.set "accept" "application/vnd.github+json")
|
||||
(.set "user-agent" (user-agent env))
|
||||
(.set "x-github-api-version" "2022-11-28"))
|
||||
_ (when (string? token)
|
||||
(.set headers "authorization" (str "Bearer " token)))]
|
||||
headers (request-headers env {:token token})]
|
||||
(p/let [resp (js/fetch url #js {:method "GET" :headers headers})
|
||||
status (.-status resp)
|
||||
text (.text resp)
|
||||
@@ -195,7 +487,7 @@
|
||||
[^js env token repo-url {:keys [title body head-branch base-branch draft]}]
|
||||
(cond
|
||||
(not (string? token))
|
||||
(p/rejected (ex-info "missing github token" {:reason :missing-token}))
|
||||
(p/rejected (ex-info "missing github app installation token" {:reason :missing-token}))
|
||||
|
||||
(not (string? title))
|
||||
(p/rejected (ex-info "missing pull request title" {:reason :invalid-request}))
|
||||
@@ -214,12 +506,8 @@
|
||||
:head (or head-branch "")
|
||||
:base (or base-branch "main")}
|
||||
(true? draft) (assoc :draft true))
|
||||
headers (doto (js/Headers.)
|
||||
(.set "accept" "application/vnd.github+json")
|
||||
(.set "content-type" "application/json")
|
||||
(.set "authorization" (str "Bearer " token))
|
||||
(.set "user-agent" (user-agent env))
|
||||
(.set "x-github-api-version" "2022-11-28"))]
|
||||
headers (request-headers env {:token token
|
||||
:content-type "application/json"})]
|
||||
(p/let [resp (js/fetch url #js {:method "POST"
|
||||
:headers headers
|
||||
:body (js/JSON.stringify (clj->js payload))})
|
||||
@@ -257,11 +545,7 @@
|
||||
(string? base-branch)
|
||||
(str "&base=" (js/encodeURIComponent base-branch)))
|
||||
url (str (api-base-url env) "/repos/" owner "/" name "/pulls?" query)
|
||||
headers (doto (js/Headers.)
|
||||
(.set "accept" "application/vnd.github+json")
|
||||
(.set "authorization" (str "Bearer " token))
|
||||
(.set "user-agent" (user-agent env))
|
||||
(.set "x-github-api-version" "2022-11-28"))]
|
||||
headers (request-headers env {:token token})]
|
||||
(p/let [resp (js/fetch url #js {:method "GET" :headers headers})
|
||||
status (.-status resp)
|
||||
text (.text resp)
|
||||
|
||||
@@ -44,7 +44,10 @@
|
||||
:cloudflare-repo-clone-command (env-value env "CLOUDFLARE_REPO_CLONE_COMMAND")
|
||||
:cloudflare-health-retries (env-value env "CLOUDFLARE_HEALTH_RETRIES")
|
||||
:cloudflare-health-interval-ms (env-value env "CLOUDFLARE_HEALTH_INTERVAL_MS")
|
||||
:github-token (env-value env "GITHUB_TOKEN")
|
||||
:github-app-id (env-value env "GITHUB_APP_ID")
|
||||
:github-app-installation-id (env-value env "GITHUB_APP_INSTALLATION_ID")
|
||||
:github-app-private-key (env-value env "GITHUB_APP_PRIVATE_KEY")
|
||||
:github-app-slug (env-value env "GITHUB_APP_SLUG")
|
||||
:github-api-base (env-value env "GITHUB_API_BASE")
|
||||
:openai-api-key (env-value env "OPENAI_API_KEY")
|
||||
:anthropic-api-key (env-value env "ANTHROPIC_API_KEY")
|
||||
@@ -66,7 +69,7 @@
|
||||
:cloudflare-sandbox-name-prefix :cloudflare-sandbox-agent-port
|
||||
:cloudflare-bootstrap-command :cloudflare-repo-clone-command
|
||||
:cloudflare-health-retries :cloudflare-health-interval-ms
|
||||
:github-token :github-api-base
|
||||
:github-app-id :github-app-installation-id :github-app-private-key :github-app-slug :github-api-base
|
||||
:openai-api-key :anthropic-api-key :openai-base-url :anthropic-base-url
|
||||
:log-level :cognito-issuer :cognito-client-id :cognito-jwks-url])
|
||||
|
||||
|
||||
@@ -53,7 +53,10 @@
|
||||
(aset "CLOUDFLARE_REPO_CLONE_COMMAND" (:cloudflare-repo-clone-command cfg))
|
||||
(aset "CLOUDFLARE_HEALTH_RETRIES" (:cloudflare-health-retries cfg))
|
||||
(aset "CLOUDFLARE_HEALTH_INTERVAL_MS" (:cloudflare-health-interval-ms cfg))
|
||||
(aset "GITHUB_TOKEN" (:github-token cfg))
|
||||
(aset "GITHUB_APP_ID" (:github-app-id cfg))
|
||||
(aset "GITHUB_APP_INSTALLATION_ID" (:github-app-installation-id cfg))
|
||||
(aset "GITHUB_APP_PRIVATE_KEY" (:github-app-private-key cfg))
|
||||
(aset "GITHUB_APP_SLUG" (:github-app-slug cfg))
|
||||
(aset "GITHUB_API_BASE" (:github-api-base cfg))
|
||||
(aset "OPENAI_API_KEY" (:openai-api-key cfg))
|
||||
(aset "ANTHROPIC_API_KEY" (:anthropic-api-key cfg))
|
||||
|
||||
2
deps/workers/test/logseq/agents/do_test.cljs
vendored
2
deps/workers/test/logseq/agents/do_test.cljs
vendored
@@ -844,8 +844,6 @@
|
||||
:repo-url (:repo-url opts)
|
||||
:force (:force opts)
|
||||
:remote "origin"}))
|
||||
source-control/pr-token
|
||||
(fn [_env] "token-1")
|
||||
source-control/<default-branch!
|
||||
(fn [_env _token _repo-url]
|
||||
(js/Promise.resolve same-branch))]
|
||||
|
||||
5
deps/workers/worker/env_example
vendored
5
deps/workers/worker/env_example
vendored
@@ -1,4 +1,7 @@
|
||||
AGENT_RUNTIME_PROVIDER=cloudflare
|
||||
SANDBOX_AGENT_TOKEN=${SANDBOX_AGENT_TOKEN}
|
||||
SPRITE_TOKEN=${SPRITE_TOKEN}
|
||||
GITHUB_TOKEN=${GITHUB_TOKEN}
|
||||
GITHUB_APP_ID=${GITHUB_APP_ID}
|
||||
GITHUB_APP_INSTALLATION_ID=${GITHUB_APP_INSTALLATION_ID}
|
||||
GITHUB_APP_PRIVATE_KEY=${GITHUB_APP_PRIVATE_KEY}
|
||||
GITHUB_APP_SLUG=${GITHUB_APP_SLUG}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.shui.ui :as shui]
|
||||
[logseq.sync.malli-schema :as db-sync-schema]
|
||||
[promesa.core :as p]))
|
||||
|
||||
@@ -114,6 +115,55 @@
|
||||
(re-matches #"^ssh://git@github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$"))]
|
||||
{:owner owner :name repo}))))
|
||||
|
||||
(def ^:private github-install-required-notification-uid :agent/github-install-required)
|
||||
|
||||
(defn- first-url-in-text
|
||||
[text]
|
||||
(some-> (when (string? text)
|
||||
(re-find #"https?://[^\s)\]}]+" text))
|
||||
(string/replace #"[,.;:!?]+$" "")))
|
||||
|
||||
(defn- start-session-error-message
|
||||
[error]
|
||||
(or (some-> (ex-data error) :body :error)
|
||||
(some-> error ex-message)
|
||||
"Failed to start agent session."))
|
||||
|
||||
(defn- start-session-install-url
|
||||
[error]
|
||||
(first-url-in-text (some-> (ex-data error) :body :error)))
|
||||
|
||||
(defn- open-external-url!
|
||||
[url]
|
||||
(when (string? url)
|
||||
(if (util/electron?)
|
||||
(js/window.apis.openExternal url)
|
||||
(js/window.open url "_blank" "noopener,noreferrer"))))
|
||||
|
||||
(defn- show-github-install-required-notification!
|
||||
[error retry-fn]
|
||||
(let [message (start-session-error-message error)
|
||||
install-url (start-session-install-url error)
|
||||
can-retry? (fn? retry-fn)]
|
||||
(notification/show!
|
||||
[:div.space-y-2
|
||||
[:div.whitespace-pre-line message]
|
||||
[:div.flex.flex-wrap.gap-2
|
||||
(when (string? install-url)
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:on-click (fn []
|
||||
(open-external-url! install-url))}
|
||||
"Install GitHub App"))
|
||||
(when can-retry?
|
||||
(shui/button
|
||||
{:variant :outline
|
||||
:size :sm
|
||||
:on-click (fn []
|
||||
(retry-fn))}
|
||||
"retry"))]]
|
||||
:warning false github-install-required-notification-uid)))
|
||||
|
||||
(defn- normalize-branches
|
||||
[branches]
|
||||
(->> branches
|
||||
@@ -536,32 +586,42 @@
|
||||
(do
|
||||
(notification/show! "Invalid agent session payload." :error false)
|
||||
nil)
|
||||
(p/let [resp (db-sync/fetch-json (str base "/sessions")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify (clj->js body))}
|
||||
{:response-schema :sessions/create})
|
||||
session-id (:session-id resp)
|
||||
status (:status resp)
|
||||
stream-url (:stream-url resp)
|
||||
block-uuid (:block/uuid block)
|
||||
_ (when-let [raw-message (message-body (:content raw-body))]
|
||||
(let [coerced (coerce-http-request :sessions/message raw-message)
|
||||
msg-body (if (map? coerced) coerced raw-message)]
|
||||
(db-sync/fetch-json (str base "/sessions/" session-id "/messages")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify (clj->js msg-body))}
|
||||
{:response-schema :sessions/message})))]
|
||||
(update-session-state! block-uuid {:session-id session-id
|
||||
:status status
|
||||
:runtime-provider (:runtime-provider resp)
|
||||
:terminal-enabled (true? (:terminal-enabled resp))
|
||||
:stream-url stream-url
|
||||
:started-at (util/time-ms)})
|
||||
(mark-task-session-created! block-uuid)
|
||||
(<connect-session-stream! block-uuid stream-url)
|
||||
resp)))))))
|
||||
(-> (p/let [resp (db-sync/fetch-json (str base "/sessions")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify (clj->js body))}
|
||||
{:response-schema :sessions/create})
|
||||
session-id (:session-id resp)
|
||||
status (:status resp)
|
||||
stream-url (:stream-url resp)
|
||||
block-uuid (:block/uuid block)
|
||||
_ (when-let [raw-message (message-body (:content raw-body))]
|
||||
(let [coerced (coerce-http-request :sessions/message raw-message)
|
||||
msg-body (if (map? coerced) coerced raw-message)]
|
||||
(db-sync/fetch-json (str base "/sessions/" session-id "/messages")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify (clj->js msg-body))}
|
||||
{:response-schema :sessions/message})))]
|
||||
(notification/clear! github-install-required-notification-uid)
|
||||
(update-session-state! block-uuid {:session-id session-id
|
||||
:status status
|
||||
:runtime-provider (:runtime-provider resp)
|
||||
:terminal-enabled (true? (:terminal-enabled resp))
|
||||
:stream-url stream-url
|
||||
:started-at (util/time-ms)})
|
||||
(mark-task-session-created! block-uuid)
|
||||
(<connect-session-stream! block-uuid stream-url)
|
||||
resp)
|
||||
(p/catch (fn [error]
|
||||
(if (= 412 (:status (ex-data error)))
|
||||
(show-github-install-required-notification!
|
||||
error
|
||||
(fn []
|
||||
(-> (<start-session! block opts)
|
||||
(p/catch (fn [_] nil)))))
|
||||
(notification/show! (start-session-error-message error) :error false))
|
||||
nil)))))))))
|
||||
|
||||
(defn- publish-request-body
|
||||
[{:keys [title body commit-message head-branch base-branch create-pr? force?]}]
|
||||
|
||||
Reference in New Issue
Block a user