vercel sandbox with snapshot support

This commit is contained in:
Tienson Qin
2026-03-02 11:05:11 +08:00
parent 1f4f0e8a5e
commit bf188febb4
14 changed files with 974 additions and 28 deletions

View File

@@ -124,6 +124,7 @@ The control plane forwards each task message through sandbox-agent
Agent runtime is selected by `AGENT_RUNTIME_PROVIDER`:
- `sprites`: provisions a Sprite and runs `sandbox-agent` inside it.
- `local-dev`: uses `SANDBOX_AGENT_URL` directly.
- `vercel`: provisions/manages Vercel sandboxes via `@vercel/sandbox`, then runs `sandbox-agent` inside the sandbox.
- `cloudflare`: provisions a sandbox first, then connects to the sandbox-hosted `sandbox-agent`.
- Agents worker default is `cloudflare` (set in `worker/wrangler.agents.toml`).
@@ -151,7 +152,7 @@ Cloudflare runtime flow:
| DB_SYNC_STATIC_USER_ID | Static user id for local dev |
| DB_SYNC_STATIC_EMAIL | Static user email for local dev |
| DB_SYNC_STATIC_USERNAME | Static username for local dev |
| AGENT_RUNTIME_PROVIDER | Runtime backend (`sprites`, `local-dev`, `cloudflare`) |
| AGENT_RUNTIME_PROVIDER | Runtime backend (`sprites`, `local-dev`, `vercel`, `cloudflare`) |
| SENTRY_DSN | Sentry DSN |
| SENTRY_RELEASE | Release identifier for Sentry events and sourcemaps |
| SENTRY_ENVIRONMENT | Sentry environment name (prod, staging, etc.) |
@@ -180,6 +181,16 @@ 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) |
| VERCEL_TEAM_ID | Vercel team ID for Sandbox SDK (required with `VERCEL_TOKEN`) |
| VERCEL_PROJECT_ID | Vercel project ID for Sandbox SDK (required with `VERCEL_TOKEN`) |
| VERCEL_TOKEN | Vercel API token for Sandbox SDK |
| VERCEL_REPO_CLONE_COMMAND | Optional repo clone command template for Vercel runtime |
| VERCEL_SANDBOX_AGENT_PORT | sandbox-agent port inside Vercel sandbox (default `2468`) |
| VERCEL_SANDBOX_TIMEOUT_MS | Vercel sandbox timeout in milliseconds (default `1800000`) |
| VERCEL_SANDBOX_RUNTIME | Vercel sandbox runtime image (default `node24`) |
| VERCEL_SANDBOX_VCPUS | Optional Vercel sandbox vCPU count |
| VERCEL_HEALTH_RETRIES | Vercel sandbox health check retry count |
| VERCEL_HEALTH_INTERVAL_MS | Vercel sandbox health check retry interval (ms) |
| 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 |

View File

@@ -22,3 +22,4 @@ Milestones are tracked as separate files in this folder:
- `16-m16-cloudflare-sandbox-backup-restore.md`
- `17-m17-separate-agents-worker-from-db-sync.md`
- `18-m18-github-app-installation-tokens.md`
- `19-m19-vercel-sandbox-runtime-and-snapshot.md`

View File

@@ -0,0 +1,70 @@
# M19: Vercel Sandbox Runtime and Snapshot Support
Status: Proposed
Target: Add a first-class `vercel` runtime provider with snapshot create/restore support for agent sessions.
## Goal
Support `AGENT_RUNTIME_PROVIDER=vercel` end-to-end so sessions can run in Vercel Sandbox and use snapshots for fast recovery and branch/task handoff.
## Why M19
- Runtime providers currently cover `sprites`, `cloudflare`, and `local-dev`, but not Vercel Sandbox.
- Snapshot behavior is becoming a core workflow primitive for long-running tasks and recovery from sandbox restarts.
- A Vercel provider reduces platform lock-in and gives teams another hosted runtime option with the same session API.
## Scope
1) Add `VercelProvider` in `src/logseq/agents/runtime_provider.cljs`.
2) Implement runtime lifecycle methods for Vercel:
- `<provision-runtime!`
- `<open-events-stream!`
- `<send-message!`
- `<open-terminal!` (if supported by Vercel sandbox APIs)
- `<terminate-runtime!`
3) Implement snapshot parity for Vercel provider:
- `<snapshot-runtime!` create snapshot
- restore-from-latest-snapshot during provision when repo/branch key matches
- keep one active snapshot handle per `repo/branch` key in provider cache policy (same behavior as Cloudflare flow)
4) Preserve current control-plane/session API contract:
- no route changes required for `/sessions/:id/snapshot`
- no behavior regression for non-Vercel providers
5) Add env/config wiring for Vercel credentials and runtime options.
## Out of Scope
- Cross-cloud snapshot migration between providers.
- Snapshot browsing/history UI.
- Replacing existing default runtime provider.
## Workstreams
### WS1: Provider Implementation
- Add `vercel` to provider-kind resolution and `create-provider` dispatch.
- Implement sandbox provisioning and sandbox-agent bootstrap for Vercel runtime.
- Persist runtime metadata needed for event stream, terminal, and teardown.
### WS2: Snapshot and Restore Integration
- Implement Vercel snapshot create adapter in runtime provider.
- Reuse repo/branch snapshot keying policy for restore selection.
- Restore snapshot on provision when available, otherwise clone repo.
- Keep auto-backup-on-terminate disabled.
### WS3: Config and Secrets
- Define required env vars/secrets in node config and worker docs.
- Wire runtime provider selection (`AGENT_RUNTIME_PROVIDER=vercel`) and provider-specific options.
- Add staging/prod/local guidance and failure-mode diagnostics.
### WS4: Tests
- Add runtime provider tests for Vercel provision/message/terminate behavior.
- Add snapshot tests for create, restore-on-provision, and fallback-to-clone.
- Add failure-path tests (invalid credentials, snapshot unavailable, restore failure).
- Verify existing provider tests remain green.
### WS5: Rollout
- Add staging smoke test checklist for Vercel sessions.
- Add rollback plan to switch provider back to `cloudflare` or `sprites` quickly.
- Document operational metrics/log keys for Vercel runtime and snapshot events.
## Exit Criteria
1) `AGENT_RUNTIME_PROVIDER=vercel` can create and run sessions end-to-end.
2) `/sessions/:id/snapshot` succeeds for Vercel sessions when provider config is valid.
3) Re-provision restores from matching snapshot key, else clones repo.
4) Existing providers (`sprites`, `cloudflare`, `local-dev`) behave unchanged.
5) Tests cover Vercel runtime lifecycle and snapshot create/restore behavior.

View File

@@ -29,6 +29,7 @@
"@fly/sprites": "^0.0.1",
"@sentry/cloudflare": "^10.38.0",
"@sentry/node": "^10.38.0",
"@vercel/sandbox": "1.7.1",
"better-sqlite3": "^12.6.2",
"shadow-cljs": "^3.3.4",
"ws": "^8.18.3"

View File

@@ -1,5 +1,6 @@
(ns logseq.agents.runtime-provider
(:require ["@cloudflare/sandbox" :as cf-sandbox]
["@vercel/sandbox" :as vercel-sandbox]
[clojure.string :as string]
[lambdaisland.glogi :as log]
[logseq.agents.sandbox :as sandbox]
@@ -34,7 +35,7 @@
(when-not (string/blank? normalized) normalized))))
(def ^:private supported-provider-kinds
#{"sprites" "local-dev" "cloudflare"})
#{"sprites" "local-dev" "cloudflare" "vercel"})
(defn- known-provider-kind [value]
(let [provider (normalize-provider value)]
@@ -120,14 +121,20 @@
(str "http://127.0.0.1:" port path))
(def ^:private default-repo-base-dir "/workspace")
(def ^:private vercel-repo-base-dir "/vercel/sandbox")
(def ^:private cloudflare-local-host "http://localhost")
(def ^:private cloudflare-snapshot-ttl-seconds (* 7 24 60 60))
(defonce ^:private cloudflare-backup-cache (atom {}))
(defonce ^:private vercel-snapshot-cache (atom {}))
(defn clear-cloudflare-backup-cache!
[]
(reset! cloudflare-backup-cache {}))
(defn clear-vercel-snapshot-cache!
[]
(reset! vercel-snapshot-cache {}))
(defn- cloudflare-agent-port [^js env]
(parse-int (env-str env "CLOUDFLARE_SANDBOX_AGENT_PORT") 2468))
@@ -345,15 +352,42 @@
"printf '%s\\n' '" (escape-shell-single credentials-url) "' > ~/.git-credentials; "
"chmod 600 ~/.git-credentials; "))))
(defn- get-repo-dir [session-id]
(let [session-id (some-> session-id str)]
(when (string? session-id)
(str default-repo-base-dir "/" (sanitize-name session-id)))))
(declare task-repo-url)
(defn- task-repo-name
[task]
(let [repo-url (task-repo-url task)
from-ref (some-> repo-url source-control/repo-ref :name sanitize-name)
from-url (some-> repo-url
(string/replace #"/+$" "")
(string/split #"/")
last
(string/replace #"\.git$" "")
sanitize-name)]
(or from-ref from-url "repo")))
(defn- repo-base-dir
[provider]
(if (= "vercel" (normalize-provider provider))
vercel-repo-base-dir
default-repo-base-dir))
(defn- get-repo-dir
([session-id]
(get-repo-dir session-id nil nil))
([session-id task provider]
(if (= "vercel" (normalize-provider provider))
(str vercel-repo-base-dir "/" (task-repo-name task))
(let [session-id (some-> session-id str)]
(when (string? session-id)
(str default-repo-base-dir "/" (sanitize-name session-id)))))))
(defn- repo-cd-command
[session-id]
(when-let [dir (get-repo-dir session-id)]
(str "cd '" (escape-shell-single dir) "'")))
([session-id]
(repo-cd-command session-id nil nil))
([session-id task provider]
(when-let [dir (get-repo-dir session-id task provider)]
(str "cd '" (escape-shell-single dir) "'"))))
;; FIXME: sandbox-agent 2.x.x changes session routes to opencode/session
(defn- sandbox-agent-version
@@ -507,6 +541,45 @@
(when (string? backup-key)
(swap! cloudflare-backup-cache dissoc backup-key)))
(defn- prune-vercel-snapshot-cache!
[]
(let [now (js/Date.now)]
(swap! vercel-snapshot-cache
(fn [entries]
(reduce-kv (fn [acc k v]
(let [expires-at-ms (:expires-at-ms v)
snapshot-id (:id v)]
(if (and (string? k)
(string? snapshot-id)
(number? expires-at-ms)
(> expires-at-ms now))
(assoc acc k v)
acc)))
{}
entries)))))
(defn- vercel-snapshot-entry
[backup-key]
(prune-vercel-snapshot-cache!)
(when (string? backup-key)
(get @vercel-snapshot-cache backup-key)))
(defn- remember-vercel-snapshot!
[backup-key snapshot-id source-dir]
(when (and (string? backup-key) (string? snapshot-id))
(let [now (js/Date.now)
ttl-ms (* cloudflare-snapshot-ttl-seconds 1000)]
(swap! vercel-snapshot-cache assoc backup-key {:id snapshot-id
:dir source-dir
:ttl-seconds cloudflare-snapshot-ttl-seconds
:expires-at-ms (+ now ttl-ms)
:updated-at-ms now}))))
(defn- forget-vercel-snapshot!
[backup-key]
(when (string? backup-key)
(swap! vercel-snapshot-cache dissoc backup-key)))
(defn- sanitize-backup-name
[value]
(let [raw (or (some-> value str string/lower-case) "snapshot")
@@ -542,9 +615,11 @@
([^js env session-id task provider]
(let [repo-url (task-repo-url task)
session-id (some-> session-id str)
repo-dir (get-repo-dir session-id)
override-key (if (= "cloudflare" provider)
"CLOUDFLARE_REPO_CLONE_COMMAND"
repo-dir (get-repo-dir session-id task provider)
base-dir (repo-base-dir provider)
override-key (case provider
"cloudflare" "CLOUDFLARE_REPO_CLONE_COMMAND"
"vercel" "VERCEL_REPO_CLONE_COMMAND"
"SPRITES_REPO_CLONE_COMMAND")
override (env-str env override-key)]
(when (and (string? repo-url) (string? session-id) (string? repo-dir))
@@ -552,19 +627,21 @@
(fill-repo-template override {:repo-url repo-url
:session-id session-id
:repo-dir repo-dir})
(str "mkdir -p " default-repo-base-dir
" && cd " default-repo-base-dir
(str "mkdir -p '" (escape-shell-single base-dir) "'"
" && cd '" (escape-shell-single base-dir) "'"
" && git clone --depth 1 --single-branch --no-tags '"
(escape-shell-single repo-url) "' '" (escape-shell-single repo-dir) "'"
" && chmod -R u+rw '" (escape-shell-single repo-dir) "'"))))))
(defn- project-init-setup-command
[session-id task]
(let [setup-script (task-project-init-setup task)
session-id (some-> session-id str)
repo-dir (get-repo-dir session-id)]
(when (and (string? setup-script) (string? repo-dir))
(str "set -e; cd '" (escape-shell-single repo-dir) "'; " setup-script))))
([session-id task]
(project-init-setup-command session-id task "sprites"))
([session-id task provider]
(let [setup-script (task-project-init-setup task)
session-id (some-> session-id str)
repo-dir (get-repo-dir session-id task provider)]
(when (and (string? setup-script) (string? repo-dir))
(str "set -e; cd '" (escape-shell-single repo-dir) "'; " setup-script)))))
(defn- classify-push-error
[error]
@@ -766,6 +843,54 @@
(or (:agent-token runtime)
(env-str env "SANDBOX_AGENT_TOKEN")))
(defn- vercel-agent-token [^js env runtime]
(or (:agent-token runtime)
(env-str env "SANDBOX_AGENT_TOKEN")))
(defn- vercel-agent-port [^js env runtime]
(or (:sandbox-port runtime)
(parse-int (env-str env "VERCEL_SANDBOX_AGENT_PORT") 2468)))
(defn- vercel-health-retries [^js env]
(parse-int (env-str env "VERCEL_HEALTH_RETRIES") 30))
(defn- vercel-health-interval-ms [^js env]
(parse-int (env-str env "VERCEL_HEALTH_INTERVAL_MS") 300))
(defn- vercel-sandbox-timeout-ms [^js env]
(parse-int (env-str env "VERCEL_SANDBOX_TIMEOUT_MS") (* 30 60 1000)))
(defn- vercel-sandbox-runtime [^js env]
(or (env-str env "VERCEL_SANDBOX_RUNTIME")
"node24"))
(defn- vercel-sandbox-vcpus [^js env]
(let [v (parse-int (env-str env "VERCEL_SANDBOX_VCPUS") 0)]
(when (pos? v) v)))
(defn- vercel-sdk-credentials [^js env]
(let [team-id (env-str env "VERCEL_TEAM_ID")
project-id (env-str env "VERCEL_PROJECT_ID")
token (env-str env "VERCEL_TOKEN")
has-explicit? (or (string? team-id) (string? project-id) (string? token))]
(cond
(and (string? team-id) (string? project-id) (string? token))
{:teamId team-id
:projectId project-id
:token token}
has-explicit?
(throw (ex-info "missing VERCEL_TEAM_ID/VERCEL_PROJECT_ID/VERCEL_TOKEN for vercel runtime provider"
{:reason :missing-vercel-credentials
:team-id? (boolean (string? team-id))
:project-id? (boolean (string? project-id))
:token? (boolean (string? token))}))
:else
(throw (ex-info "missing vercel sdk credentials"
{:reason :missing-vercel-credentials
:required ["VERCEL_TEAM_ID" "VERCEL_PROJECT_ID" "VERCEL_TOKEN"]})))))
(defn- cloudflare-sandbox-namespace [^js env]
(let [sandbox-ns (aget env "Sandbox")]
(when-not sandbox-ns
@@ -1190,6 +1315,299 @@
:session-id session-id})))
(->promise (.call terminal session request (clj->js opts))))))))
(defn vercel-sandbox-class
[]
(let [sandbox-class (aget vercel-sandbox "Sandbox")]
(when-not sandbox-class
(throw (ex-info "missing @vercel/sandbox Sandbox export"
{:reason :missing-vercel-sdk})))
sandbox-class))
(defn- vercel-create-params
[^js env source]
(let [port (vercel-agent-port env nil)
timeout-ms (vercel-sandbox-timeout-ms env)
runtime (vercel-sandbox-runtime env)
vcpus (vercel-sandbox-vcpus env)
creds (vercel-sdk-credentials env)]
(cond-> (merge creds
{:ports [port]
:timeout timeout-ms
:runtime runtime}
(when (map? source)
{:source source}))
(number? vcpus) (assoc :resources {:vcpus vcpus}))))
(defn <vercel-create-sandbox!
[^js env source]
(let [sandbox-class (vercel-sandbox-class)
create (js-method sandbox-class "create")]
(when-not (fn? create)
(throw (ex-info "vercel sdk missing Sandbox.create" {:reason :missing-vercel-create})))
(->promise (.call create sandbox-class (clj->js (vercel-create-params env source))))))
(defn <vercel-get-sandbox!
[^js env sandbox-id]
(let [sandbox-class (vercel-sandbox-class)
get-sandbox (js-method sandbox-class "get")]
(when-not (string? sandbox-id)
(throw (ex-info "missing sandbox-id on runtime"
{:reason :missing-sandbox-id})))
(when-not (fn? get-sandbox)
(throw (ex-info "vercel sdk missing Sandbox.get" {:reason :missing-vercel-get})))
(->promise (.call get-sandbox sandbox-class
(clj->js (assoc (vercel-sdk-credentials env)
:sandboxId sandbox-id))))))
(defn vercel-sandbox-domain
[sandbox port]
(let [domain (js-method sandbox "domain")]
(when-not (fn? domain)
(throw (ex-info "vercel sandbox missing domain method"
{:reason :missing-vercel-domain})))
(.call domain sandbox port)))
(defn <vercel-stop-sandbox!
[sandbox]
(let [stop (js-method sandbox "stop")]
(if (fn? stop)
(->promise (.call stop sandbox #js {:blocking false}))
(p/resolved nil))))
(defn- <vercel-command-output!
[command]
(let [stdout-fn (js-method command "stdout")
stderr-fn (js-method command "stderr")]
(p/let [stdout (if (fn? stdout-fn)
(->promise (.call stdout-fn command))
"")
stderr (if (fn? stderr-fn)
(->promise (.call stderr-fn command))
"")]
{:stdout (or stdout "")
:stderr (or stderr "")
:exit-code (aget command "exitCode")})))
(defn <vercel-run-shell!
[sandbox command & [opts]]
(let [run-command (js-method sandbox "runCommand")
params (cond-> {:cmd "bash"
:args ["-lc" command]}
(map? (:env opts)) (assoc :env (:env opts))
(true? (:detached opts)) (assoc :detached true))]
(when-not (fn? run-command)
(throw (ex-info "vercel sandbox missing runCommand method"
{:reason :missing-vercel-run-command})))
(if (true? (:detached opts))
(->promise (.call run-command sandbox (clj->js params)))
(p/let [result (->promise (.call run-command sandbox (clj->js params)))
{:keys [stdout stderr exit-code]} (<vercel-command-output! result)]
(when (and (number? exit-code) (not (zero? exit-code)))
(throw (ex-info "vercel sandbox command failed"
{:reason :vercel-command-failed
:command command
:exit-code exit-code
:stdout stdout
:stderr stderr})))
{:stdout stdout
:stderr stderr
:exit-code exit-code}))))
(defn- vercel-health-command [port agent-token]
(str "if curl -fsS "
(curl-auth-arg agent-token)
" "
cloudflare-local-host
":"
port
"/v1/health >/dev/null; then echo __HEALTH_OK__; else echo __HEALTH_FAIL__; fi"))
(defn- <vercel-health-once! [sandbox port agent-token]
(-> (<vercel-run-shell! sandbox (vercel-health-command port agent-token))
(p/then (fn [{:keys [stdout stderr]}]
(let [output (str stdout "\n" stderr)]
(string/includes? output "__HEALTH_OK__"))))
(p/catch (fn [_] false))))
(defn- <vercel-health!
[^js env sandbox port agent-token]
(let [retries (vercel-health-retries env)
interval-ms (vercel-health-interval-ms env)]
(letfn [(step [left]
(if (<= left 0)
(throw (ex-info "sandbox-agent health check timed out in vercel sandbox"
{:port port}))
(p/let [healthy? (<vercel-health-once! sandbox port agent-token)]
(if healthy?
true
(p/let [_ (p/delay interval-ms)]
(step (dec left)))))))]
(step retries))))
(defn- <vercel-agent-env-vars!
[^js env task]
(let [base (reduce (fn [acc k]
(if-let [v (env-str env k)]
(assoc acc k v)
acc))
{}
cloudflare-env-pass-through)
task-env (normalize-agent-env-map (get-in task [:agent :env]))
agent-id (:agent (session-payload task))
api-token (some-> (get-in task [:agent :api-token]) str string/trim not-empty)]
(p/let [github-token (<github-installation-token-for-task! env task)]
(cond-> (merge base task-env)
(and (string? api-token) (= "codex" agent-id)) (assoc "OPENAI_API_KEY" api-token)
(and (string? api-token) (= "claude" agent-id)) (assoc "ANTHROPIC_API_KEY" api-token)
(string? github-token) (assoc-github-installation-token github-token)))))
(defn- vercel-server-command
[^js env task session-id port agent-token env-vars]
(let [auth-json (get-in task [:agent :auth-json])
write-auth (or (auth-json-write-command auth-json) "")
repo-cd (or (repo-cd-command session-id task "vercel") "")
token-exports (shell-export-command env-vars)
github-auth-setup (github-auth-setup-command (get env-vars "GITHUB_TOKEN"))]
(str (sandbox-agent-install-command env)
token-exports
github-auth-setup
write-auth
(when (string? repo-cd) (str repo-cd "; "))
"nohup sandbox-agent server "
(if (string? agent-token)
(str "--token '" (escape-shell-single agent-token) "'")
"--no-token")
" --host 0.0.0.0 --port " port
" --no-telemetry >/tmp/sandbox-agent.log 2>&1 &")))
(defn- <vercel-clone-repo!
[^js env sandbox session-id task]
(when-let [cmd (repo-clone-command env session-id task "vercel")]
(<vercel-run-shell! sandbox cmd)))
(defn- <vercel-run-project-init-setup!
[sandbox session-id task]
(when-let [cmd (project-init-setup-command session-id task "vercel")]
(<vercel-run-shell! sandbox cmd)))
(defn- start-vercel-project-init-setup-background!
[sandbox session-id task sandbox-id]
(js/setTimeout
(fn []
(when-let [setup-promise (<vercel-run-project-init-setup! sandbox session-id task)]
(-> setup-promise
(p/catch (fn [error]
(log/error :agent/vercel-project-init-setup-failed
{:session-id session-id
:sandbox-id sandbox-id
:error (str error)}))))))
0)
nil)
(defn- <vercel-ensure-running!
[^js env sandbox session-id task port agent-token]
(p/let [env-vars (<vercel-agent-env-vars! env task)
bootstrap-cmd (vercel-server-command env task session-id port agent-token env-vars)
_ (<vercel-run-shell! sandbox bootstrap-cmd)
_ (<vercel-health! env sandbox port agent-token)]
true))
(defn- <vercel-create-sandbox-from-cache!
[^js env backup-key]
(let [entry (vercel-snapshot-entry backup-key)
snapshot-id (:id entry)
snapshot-dir (:dir entry)]
(if-not (string? snapshot-id)
(p/let [sandbox (<vercel-create-sandbox! env nil)]
{:sandbox sandbox
:snapshot-dir nil
:restored? false})
(-> (<vercel-create-sandbox! env {:type "snapshot"
:snapshotId snapshot-id})
(p/then (fn [sandbox]
(log/debug :agent/vercel-snapshot-restored
{:backup-key backup-key
:snapshot-id snapshot-id})
{:sandbox sandbox
:snapshot-dir snapshot-dir
:restored? true}))
(p/catch (fn [error]
(forget-vercel-snapshot! backup-key)
(log/error :agent/vercel-snapshot-restore-failed
{:backup-key backup-key
:snapshot-id snapshot-id
:error (str error)})
(p/let [sandbox (<vercel-create-sandbox! env nil)]
{:sandbox sandbox
:snapshot-dir nil
:restored? false})))))))
(defn- <vercel-runtime-base-url!
[^js env runtime]
(let [cached (:base-url runtime)
port (vercel-agent-port env runtime)
sandbox-id (:sandbox-id runtime)]
(if (string? cached)
(p/resolved cached)
(p/let [sandbox (<vercel-get-sandbox! env sandbox-id)]
(vercel-sandbox-domain sandbox port)))))
(defn- <vercel-restore-repo-dir!
[sandbox snapshot-dir repo-dir]
(cond
(not (string? repo-dir))
(p/resolved nil)
(or (not (string? snapshot-dir))
(= snapshot-dir repo-dir))
(p/resolved nil)
:else
(<vercel-run-shell! sandbox
(str "set -e; "
"if [ -d '" (escape-shell-single snapshot-dir) "' ]; then "
"rm -rf '" (escape-shell-single repo-dir) "'; "
"cp -a '" (escape-shell-single snapshot-dir) "' '" (escape-shell-single repo-dir) "'; "
"fi"))))
(defn- vercel-snapshot-expiration-ms
[]
(* cloudflare-snapshot-ttl-seconds 1000))
(defn <vercel-create-snapshot!
[sandbox source-dir snapshot-name]
(let [snapshot-fn (js-method sandbox "snapshot")]
(cond
(not (string? source-dir))
(p/rejected (ex-info "invalid snapshot source dir"
{:reason :invalid-snapshot-source-dir
:source-dir source-dir}))
(not (fn? snapshot-fn))
(p/rejected (ex-info "vercel runtime does not support snapshots"
{:reason :unsupported-snapshot
:provider "vercel"}))
:else
(-> (->promise (.call snapshot-fn sandbox
(clj->js {:expiration (vercel-snapshot-expiration-ms)})))
(p/then (fn [snapshot]
(let [snapshot-id (aget snapshot "snapshotId")]
(if (string? snapshot-id)
{:snapshot-id snapshot-id
:name snapshot-name
:dir source-dir}
(throw (ex-info "vercel snapshot create returned invalid id"
{:reason :invalid-snapshot-id
:snapshot snapshot}))))))
(p/catch (fn [error]
(p/rejected
(ex-info (or (some-> error ex-message str string/trim not-empty)
"vercel snapshot create failed")
{:reason :unsupported-snapshot
:provider "vercel"
:raw-error (str error)}))))))))
(defprotocol RuntimeProvider
(<provision-runtime! [this session-id task])
(<open-events-stream! [this runtime])
@@ -1392,6 +1810,92 @@
(sandbox/<terminate-session base-url agent-token session-id)
(fn [_] nil))))))
(defrecord VercelProvider [env]
RuntimeProvider
(<provision-runtime! [_ session-id task]
(let [agent-token (vercel-agent-token env nil)
port (vercel-agent-port env nil)
payload (session-payload task)
repo-dir (get-repo-dir session-id task "vercel")
backup-key (repo-backup-key task)]
(p/let [{:keys [sandbox snapshot-dir restored?]} (<vercel-create-sandbox-from-cache! env backup-key)
_ (<vercel-ensure-running! env sandbox session-id task port agent-token)
_ (when restored?
(<vercel-restore-repo-dir! sandbox snapshot-dir repo-dir))
_ (when-not restored?
(p/catch
(<vercel-clone-repo! env sandbox session-id task)
(fn [error]
(log/error :agent/vercel-repo-clone-failed
{:session-id session-id
:error (str error)})
nil)))
base-url (vercel-sandbox-domain sandbox port)
response (sandbox/<create-session base-url agent-token session-id payload)
sandbox-id (aget sandbox "sandboxId")]
;; (start-vercel-project-init-setup-background! sandbox session-id task sandbox-id)
{:provider "vercel"
:sandbox-id sandbox-id
:sandbox-name sandbox-id
:sandbox-port port
:base-url base-url
:agent-token agent-token
:session-id (:session-id response)
:backup-key backup-key
:backup-dir repo-dir})))
(<open-events-stream! [_ runtime]
(let [agent-token (vercel-agent-token env runtime)]
(p/let [base-url (<vercel-runtime-base-url! env runtime)]
(sandbox/<open-events-stream base-url agent-token (:session-id runtime)))))
(<send-message! [_ runtime message]
(let [agent-token (vercel-agent-token env runtime)]
(p/let [base-url (<vercel-runtime-base-url! env runtime)]
(sandbox/<send-message base-url agent-token (:session-id runtime) message))))
(<open-terminal! [_ _runtime _request _opts]
(p/rejected
(ex-info "vercel runtime provider does not support browser terminal"
{:reason :unsupported-terminal
:provider "vercel"})))
(<snapshot-runtime! [_ runtime opts]
(let [session-id (:session-id runtime)
sandbox-id (:sandbox-id runtime)
backup-dir (or (:backup-dir runtime)
(get-repo-dir session-id (:task opts) "vercel"))
task (:task opts)
backup-key (repo-backup-key task)
snapshot-name (snapshot-backup-name runtime task)]
(when-not (string? sandbox-id)
(throw (ex-info "missing sandbox-id on runtime"
{:reason :missing-sandbox-id
:runtime runtime})))
(p/let [sandbox (<vercel-get-sandbox! env sandbox-id)
result (<vercel-create-snapshot! sandbox backup-dir snapshot-name)
snapshot-id (:snapshot-id result)]
(when (and (string? backup-key) (string? snapshot-id))
(remember-vercel-snapshot! backup-key snapshot-id backup-dir))
result)))
(<push-branch! [_ _runtime _opts]
(p/rejected
(ex-info "vercel runtime provider does not support managed git push yet"
{:reason :unsupported
:provider "vercel"})))
(<terminate-runtime! [_ runtime]
(let [sandbox-id (:sandbox-id runtime)]
(if-not (string? sandbox-id)
(p/resolved nil)
(p/catch
(p/let [sandbox (<vercel-get-sandbox! env sandbox-id)]
(<vercel-stop-sandbox! sandbox)
nil)
(fn [_] nil))))))
(defrecord CloudflareProvider [env]
RuntimeProvider
@@ -1570,6 +2074,7 @@
(cond
(instance? SpritesProvider provider) "sprites"
(instance? LocalDevProvider provider) "local-dev"
(instance? VercelProvider provider) "vercel"
(instance? CloudflareProvider provider) "cloudflare"
:else nil))
@@ -1579,6 +2084,7 @@
:resolved (known-provider-kind kind)})
(case (known-provider-kind kind)
"local-dev" (->LocalDevProvider env)
"vercel" (->VercelProvider env)
"cloudflare" (->CloudflareProvider env)
(->SpritesProvider env)))

View File

@@ -24,6 +24,18 @@
(defn terminate-url [base session-id]
(str (session-url base session-id) "/terminate"))
(defn exec-command-url [base]
(str (normalize-base-url base) "/v1/commands/exec"))
(defn snapshots-base-url [base]
(str (normalize-base-url base) "/v1/snapshots"))
(defn snapshot-url [base snapshot-id]
(str (snapshots-base-url base) "/" snapshot-id))
(defn snapshot-restore-url [base snapshot-id]
(str (snapshot-url base snapshot-id) "/restore"))
(def ^:private agent-aliases
{"claude-code" "claude"
"claude_code" "claude"
@@ -125,3 +137,56 @@
{:status status
:session-id session-id})))
true)))
(defn <exec-command
[base token command]
(let [headers (js/Headers.)
_ (.set headers "content-type" "application/json")
_ (when (string? token) (.set headers "authorization" (str "Bearer " token)))
req (json-request (exec-command-url base) "POST" headers {:command command})]
(p/let [resp (js/fetch req)
status (.-status resp)
json (parse-json-or-default resp {})]
(if (<= 200 status 299)
json
(throw (ex-info "sandbox exec-command failed"
{:status status
:command command
:response json}))))))
(defn <create-snapshot
[base token {:keys [dir name ttl] :as opts}]
(let [headers (js/Headers.)
_ (.set headers "content-type" "application/json")
_ (when (string? token) (.set headers "authorization" (str "Bearer " token)))
body (cond-> {}
(string? dir) (assoc :dir dir)
(string? name) (assoc :name name)
(number? ttl) (assoc :ttl ttl))
req (json-request (snapshots-base-url base) "POST" headers body)]
(p/let [resp (js/fetch req)
status (.-status resp)
json (parse-json-or-default resp {})]
(if (<= 200 status 299)
(if (map? json) json opts)
(throw (ex-info "sandbox create-snapshot failed"
{:status status
:response json}))))))
(defn <restore-snapshot
[base token snapshot-id dir]
(let [headers (js/Headers.)
_ (.set headers "content-type" "application/json")
_ (when (string? token) (.set headers "authorization" (str "Bearer " token)))
body (cond-> {}
(string? dir) (assoc :dir dir))
req (json-request (snapshot-restore-url base snapshot-id) "POST" headers body)]
(p/let [resp (js/fetch req)
status (.-status resp)
json (parse-json-or-default resp {})]
(if (<= 200 status 299)
json
(throw (ex-info "sandbox restore-snapshot failed"
{:status status
:snapshot-id snapshot-id
:response json}))))))

View File

@@ -44,6 +44,16 @@
: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")
:vercel-team-id (env-value env "VERCEL_TEAM_ID")
:vercel-project-id (env-value env "VERCEL_PROJECT_ID")
:vercel-token (env-value env "VERCEL_TOKEN")
:vercel-repo-clone-command (env-value env "VERCEL_REPO_CLONE_COMMAND")
:vercel-sandbox-agent-port (env-value env "VERCEL_SANDBOX_AGENT_PORT")
:vercel-sandbox-timeout-ms (env-value env "VERCEL_SANDBOX_TIMEOUT_MS")
:vercel-sandbox-runtime (env-value env "VERCEL_SANDBOX_RUNTIME")
:vercel-sandbox-vcpus (env-value env "VERCEL_SANDBOX_VCPUS")
:vercel-health-retries (env-value env "VERCEL_HEALTH_RETRIES")
:vercel-health-interval-ms (env-value env "VERCEL_HEALTH_INTERVAL_MS")
: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")
@@ -69,6 +79,9 @@
:cloudflare-sandbox-name-prefix :cloudflare-sandbox-agent-port
:cloudflare-bootstrap-command :cloudflare-repo-clone-command
:cloudflare-health-retries :cloudflare-health-interval-ms
:vercel-team-id :vercel-project-id :vercel-token :vercel-repo-clone-command
:vercel-sandbox-agent-port :vercel-sandbox-timeout-ms :vercel-sandbox-runtime
:vercel-sandbox-vcpus :vercel-health-retries :vercel-health-interval-ms
: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])

View File

@@ -53,6 +53,16 @@
(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 "VERCEL_TEAM_ID" (:vercel-team-id cfg))
(aset "VERCEL_PROJECT_ID" (:vercel-project-id cfg))
(aset "VERCEL_TOKEN" (:vercel-token cfg))
(aset "VERCEL_REPO_CLONE_COMMAND" (:vercel-repo-clone-command cfg))
(aset "VERCEL_SANDBOX_AGENT_PORT" (:vercel-sandbox-agent-port cfg))
(aset "VERCEL_SANDBOX_TIMEOUT_MS" (:vercel-sandbox-timeout-ms cfg))
(aset "VERCEL_SANDBOX_RUNTIME" (:vercel-sandbox-runtime cfg))
(aset "VERCEL_SANDBOX_VCPUS" (:vercel-sandbox-vcpus cfg))
(aset "VERCEL_HEALTH_RETRIES" (:vercel-health-retries cfg))
(aset "VERCEL_HEALTH_INTERVAL_MS" (:vercel-health-interval-ms 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))

View File

@@ -1,6 +1,7 @@
(ns logseq.agents.runtime-provider-test
(:require [cljs.test :refer [async deftest is testing]]
[clojure.string :as string]
[logseq.agents.sandbox :as sandbox]
[logseq.agents.runtime-provider :as runtime-provider]))
(defn- fetch-url [request]
@@ -20,6 +21,7 @@
(is (= "sprites" (runtime-provider/provider-kind #js {})))
(is (= "sprites" (runtime-provider/provider-kind #js {"AGENT_RUNTIME_PROVIDER" "SPRITES"})))
(is (= "local-dev" (runtime-provider/provider-kind #js {"AGENT_RUNTIME_PROVIDER" "LOCAL-DEV"})))
(is (= "vercel" (runtime-provider/provider-kind #js {"AGENT_RUNTIME_PROVIDER" "VERCEL"})))
(is (= "cloudflare" (runtime-provider/provider-kind #js {"AGENT_RUNTIME_PROVIDER" "CLOUDFLARE"})))
(is (= "sprites" (runtime-provider/provider-kind #js {"AGENT_RUNTIME_PROVIDER" "unknown"})))))
@@ -37,6 +39,8 @@
(runtime-provider/runtime-provider-kind env {:provider "local-dev"})))
(is (= "sprites"
(runtime-provider/runtime-provider-kind env {:provider "sprites"})))
(is (= "vercel"
(runtime-provider/runtime-provider-kind env {:provider "vercel"})))
(is (= "cloudflare"
(runtime-provider/runtime-provider-kind env {:provider "cloudflare"})))
(is (= "cloudflare"
@@ -49,6 +53,8 @@
(runtime-provider/provider-id (runtime-provider/create-provider env "sprites"))))
(is (= "local-dev"
(runtime-provider/provider-id (runtime-provider/create-provider env "local-dev"))))
(is (= "vercel"
(runtime-provider/provider-id (runtime-provider/create-provider env "vercel"))))
(is (= "cloudflare"
(runtime-provider/provider-id (runtime-provider/create-provider env "cloudflare"))))
(is (= "sprites"
@@ -72,7 +78,7 @@
(let [env #js {}
task {:project {:repo-url "https://github.com/example/repo"}}
session-id "sess-1"]
(is (= "mkdir -p /workspace && cd /workspace && git clone --depth 1 --single-branch --no-tags 'https://github.com/example/repo' '/workspace/sess-1' && chmod -R u+rw '/workspace/sess-1'"
(is (= "mkdir -p '/workspace' && cd '/workspace' && git clone --depth 1 --single-branch --no-tags 'https://github.com/example/repo' '/workspace/sess-1' && chmod -R u+rw '/workspace/sess-1'"
(runtime-provider/repo-clone-command env session-id task)))))
(testing "fills override repo clone command template"
@@ -82,6 +88,20 @@
(is (= "echo https://github.com/example/repo sess-1 /workspace/sess-1"
(runtime-provider/repo-clone-command env session-id task)))))
(testing "fills vercel override repo clone command template"
(let [env #js {"VERCEL_REPO_CLONE_COMMAND" "echo vercel {repo_url} {session_id} {repo_dir}"}
task {:project {:repo-url "https://github.com/example/repo"}}
session-id "sess-1"]
(is (= "echo vercel https://github.com/example/repo sess-1 /vercel/sandbox/repo"
(runtime-provider/repo-clone-command env session-id task "vercel")))))
(testing "uses /vercel/sandbox/<repo-name> for vercel default clone path"
(let [env #js {}
task {:project {:repo-url "https://github.com/logseq/logseq.git"}}
session-id "sess-1"]
(is (= "mkdir -p '/vercel/sandbox' && cd '/vercel/sandbox' && git clone --depth 1 --single-branch --no-tags 'https://github.com/logseq/logseq.git' '/vercel/sandbox/logseq' && chmod -R u+rw '/vercel/sandbox/logseq'"
(runtime-provider/repo-clone-command env session-id task "vercel")))))
(testing "returns nil when repo url missing"
(is (nil? (runtime-provider/repo-clone-command #js {} "sess-1" {})))))
@@ -204,7 +224,93 @@
(.catch (fn [error]
(let [data (ex-data error)]
(is (= :unsupported (:reason data)))
(is (= "local-dev" (:provider data))))
(is (= "local-dev" (:provider data))))
(done)))))))
(deftest vercel-provider-snapshot-restore-flow-test
(async done
(runtime-provider/clear-vercel-snapshot-cache!)
(let [calls (atom {:clone 0
:sessions 0
:snapshots 0
:restores 0})
sandboxes (atom {})
next-id (atom 0)
make-sandbox (fn [sandbox-id]
#js {:sandboxId sandbox-id
:domain (fn [_port] "https://vercel-agent.local")})
env #js {"VERCEL_TEAM_ID" "team-1"
"VERCEL_PROJECT_ID" "project-1"
"VERCEL_TOKEN" "token-vercel"}
provider (runtime-provider/create-provider env "vercel")
task {:agent {:provider "codex"}
:project {:repo-url "https://github.com/example/repo"
:base-branch "main"}}]
(with-redefs [runtime-provider/<vercel-create-sandbox!
(fn [_env source]
(let [sandbox-id (str "vercel-sbx-" (swap! next-id inc))
sandbox (make-sandbox sandbox-id)]
(when (= "snapshot" (:type source))
(swap! calls update :restores inc))
(swap! sandboxes assoc sandbox-id sandbox)
(js/Promise.resolve sandbox)))
runtime-provider/<vercel-get-sandbox!
(fn [_env sandbox-id]
(js/Promise.resolve (get @sandboxes sandbox-id)))
runtime-provider/<vercel-run-shell!
(fn [_sandbox cmd & _]
(when (string/includes? cmd "git clone --depth 1 --single-branch --no-tags")
(swap! calls update :clone inc))
(js/Promise.resolve {:stdout "" :stderr "" :exit-code 0}))
sandbox/<create-session
(fn [_base-url _agent-token session-id _payload]
(swap! calls update :sessions inc)
(js/Promise.resolve {:session-id session-id}))
runtime-provider/<vercel-create-snapshot!
(fn [_sandbox _source-dir _snapshot-name]
(swap! calls update :snapshots inc)
(js/Promise.resolve {:snapshot-id "vercel-snap-1"}))]
(-> (runtime-provider/<provision-runtime! provider "sess-vercel-1" task)
(.then (fn [runtime-1]
(is (= "vercel" (:provider runtime-1)))
(is (= 1 (:clone @calls)))
(-> (runtime-provider/<snapshot-runtime! provider runtime-1 {:task task})
(.then (fn [snapshot-result]
(is (= "vercel-snap-1" (:snapshot-id snapshot-result)))
(is (= 1 (:snapshots @calls)))
(-> (runtime-provider/<provision-runtime! provider "sess-vercel-2" task)
(.then (fn [_runtime-2]
(is (= 1 (:clone @calls)))
(is (= 1 (:restores @calls)))
(is (= 2 (:sessions @calls)))
(done)))
(.catch (fn [error]
(is false (str "unexpected second provision error: " error))
(done))))))
(.catch (fn [error]
(is false (str "unexpected snapshot error: " error))
(done))))))
(.catch (fn [error]
(is false (str "unexpected first provision error: " error))
(done)))))))))
(deftest vercel-provider-open-terminal-unsupported-test
(async done
(let [env #js {}
provider (runtime-provider/create-provider env "vercel")
runtime {:provider "vercel"
:base-url "https://vercel-agent.local"
:session-id "sess-vercel-terminal"}
request (js/Request. "https://db-sync.local/sessions/sess-vercel-terminal/terminal"
#js {:method "GET"})]
(-> (runtime-provider/<open-terminal! provider runtime request {:cols 80 :rows 24})
(.then (fn [_]
(is false "expected vercel terminal to reject as unsupported")
(done)))
(.catch (fn [error]
(let [data (ex-data error)]
(is (= :unsupported-terminal (:reason data)))
(is (= "vercel" (:provider data))))
(done)))))))
(deftest sprites-provider-push-branch-command-test

View File

@@ -23,6 +23,18 @@
(is (= "https://sandbox.example/v1/sessions/sess-1/messages/stream"
(sandbox/messages-stream-url base session-id))))))
(deftest snapshot-endpoint-test
(testing "builds sandbox snapshot and exec endpoints"
(let [base "https://sandbox.example"]
(is (= "https://sandbox.example/v1/commands/exec"
(sandbox/exec-command-url base)))
(is (= "https://sandbox.example/v1/snapshots"
(sandbox/snapshots-base-url base)))
(is (= "https://sandbox.example/v1/snapshots/snap-1"
(sandbox/snapshot-url base "snap-1")))
(is (= "https://sandbox.example/v1/snapshots/snap-1/restore"
(sandbox/snapshot-restore-url base "snap-1"))))))
(deftest normalize-agent-id-test
(testing "normalizes known sandbox-agent aliases"
(is (= "claude" (sandbox/normalize-agent-id "claude-code")))

View File

@@ -1,4 +1,13 @@
SANDBOX_AGENT_TOKEN=${SANDBOX_AGENT_TOKEN}
VERCEL_TEAM_ID=${VERCEL_TEAM_ID}
VERCEL_PROJECT_ID=${VERCEL_PROJECT_ID}
VERCEL_TOKEN=${VERCEL_TOKEN}
VERCEL_SANDBOX_AGENT_PORT=${VERCEL_SANDBOX_AGENT_PORT}
VERCEL_SANDBOX_TIMEOUT_MS=${VERCEL_SANDBOX_TIMEOUT_MS}
VERCEL_SANDBOX_RUNTIME=${VERCEL_SANDBOX_RUNTIME}
VERCEL_SANDBOX_VCPUS=${VERCEL_SANDBOX_VCPUS}
VERCEL_HEALTH_RETRIES=${VERCEL_HEALTH_RETRIES}
VERCEL_HEALTH_INTERVAL_MS=${VERCEL_HEALTH_INTERVAL_MS}
SPRITE_TOKEN=${SPRITE_TOKEN}
GITHUB_APP_ID=${GITHUB_APP_ID}
GITHUB_APP_INSTALLATION_ID=${GITHUB_APP_INSTALLATION_ID}

View File

@@ -1,6 +1,6 @@
name = "logseq-agents"
main = "dist/agents/main.js"
compatibility_date = "2025-12-10"
compatibility_date = "2026-02-28"
compatibility_flags = [ "nodejs_compat" ]
[[containers]]
@@ -44,7 +44,7 @@ new_sqlite_classes = [ "Sandbox" ]
COGNITO_JWKS_URL = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8/.well-known/jwks.json"
COGNITO_ISSUER = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8"
COGNITO_CLIENT_ID = "69cs1lgme7p8kbgld8n5kseii6"
AGENT_RUNTIME_PROVIDER = "cloudflare"
AGENT_RUNTIME_PROVIDER = "vercel"
CLOUDFLARE_ACCOUNT_ID = "2553ea8236c11ea0f88de28fce1cbfee"
BACKUP_BUCKET_NAME = "logseq-sync-assets-prod"
@@ -61,7 +61,7 @@ max_instances = 100
COGNITO_JWKS_URL = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8/.well-known/jwks.json"
COGNITO_ISSUER = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8"
COGNITO_CLIENT_ID = "69cs1lgme7p8kbgld8n5kseii6"
AGENT_RUNTIME_PROVIDER = "cloudflare"
AGENT_RUNTIME_PROVIDER = "vercel"
CLOUDFLARE_ACCOUNT_ID = "2553ea8236c11ea0f88de28fce1cbfee"
BACKUP_BUCKET_NAME = "logseq-sync-assets-dev"
SENTRY_DSN = "https://dc09d27243b9492bbe15e0dd279ad7de@o416451.ingest.us.sentry.io/5311485"
@@ -100,7 +100,7 @@ name = "logseq-agents-prod"
COGNITO_JWKS_URL = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8/.well-known/jwks.json"
COGNITO_ISSUER = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8"
COGNITO_CLIENT_ID = "69cs1lgme7p8kbgld8n5kseii6"
AGENT_RUNTIME_PROVIDER = "cloudflare"
AGENT_RUNTIME_PROVIDER = "vercel"
CLOUDFLARE_ACCOUNT_ID = "2553ea8236c11ea0f88de28fce1cbfee"
BACKUP_BUCKET_NAME = "logseq-sync-assets-prod"
SENTRY_DSN = "https://dc09d27243b9492bbe15e0dd279ad7de@o416451.ingest.us.sentry.io/5311485"

120
deps/workers/yarn.lock vendored
View File

@@ -498,6 +498,26 @@
dependencies:
"@types/node" "*"
"@vercel/oidc@3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@vercel/oidc/-/oidc-3.2.0.tgz#5782a4d4904443f015808705b5537cf9c3b68528"
integrity sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==
"@vercel/sandbox@1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@vercel/sandbox/-/sandbox-1.7.1.tgz#f96450ba329e8f37de8191e9fed89a9be1c00165"
integrity sha512-TI9InUQe7sqyO4/TIiGXC/3RHA0hTt5PpFaTWeWunkbKZae26nuPVsd+p10W/WN2THUKE+NPtTJ21dhp1Yw48w==
dependencies:
"@vercel/oidc" "3.2.0"
async-retry "1.3.3"
jsonlines "0.1.1"
ms "2.1.3"
picocolors "^1.1.1"
tar-stream "3.1.7"
undici "^7.16.0"
xdg-app-paths "5.1.0"
zod "3.24.4"
acorn-import-attributes@^1.9.5:
version "1.9.5"
resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
@@ -508,16 +528,33 @@ acorn@^8.15.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
async-retry@1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280"
integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==
dependencies:
retry "0.13.1"
aws4fetch@^1.0.20:
version "1.0.20"
resolved "https://registry.yarnpkg.com/aws4fetch/-/aws4fetch-1.0.20.tgz#090d6c65e32c6df645dd5e5acf04cc56da575cbe"
integrity sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==
b4a@^1.6.4:
version "1.8.0"
resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.8.0.tgz#1ca3ba0edc9469aaabef5647e769a83d50180b1a"
integrity sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
bare-events@^2.7.0:
version "2.8.2"
resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.8.2.tgz#7b3e10bd8e1fc80daf38bb516921678f566ab89f"
integrity sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -616,11 +653,23 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
dependencies:
once "^1.4.0"
events-universal@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/events-universal/-/events-universal-1.0.1.tgz#b56a84fd611b6610e0a2d0f09f80fdf931e2dfe6"
integrity sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==
dependencies:
bare-events "^2.7.0"
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
fast-fifo@^1.2.0, fast-fifo@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -676,6 +725,11 @@ isexe@^3.1.1:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d"
integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==
jsonlines@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsonlines/-/jsonlines-0.1.1.tgz#4fcd246dc5d0e38691907c44ab002f782d1d94cc"
integrity sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
@@ -703,7 +757,7 @@ module-details-from-path@^1.0.3, module-details-from-path@^1.0.4:
resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.4.tgz#b662fdcd93f6c83d3f25289da0ce81c8d9685b94"
integrity sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==
ms@^2.1.3:
ms@2.1.3, ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -727,6 +781,11 @@ once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
os-paths@^4.0.1:
version "4.4.0"
resolved "https://registry.yarnpkg.com/os-paths/-/os-paths-4.4.0.tgz#2908b5bcb60cbfe3afb869292281a2a6b2f77ebe"
integrity sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==
pg-int8@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
@@ -748,6 +807,11 @@ pg-types@^2.2.0:
postgres-date "~1.0.4"
postgres-interval "^1.1.0"
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
postgres-array@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
@@ -843,6 +907,11 @@ require-in-the-middle@^8.0.0:
debug "^4.3.5"
module-details-from-path "^1.0.3"
retry@0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@@ -898,6 +967,15 @@ source-map@^0.6.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
streamx@^2.15.0:
version "2.23.0"
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.23.0.tgz#7d0f3d00d4a6c5de5728aecd6422b4008d66fd0b"
integrity sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==
dependencies:
events-universal "^1.0.0"
fast-fifo "^1.3.2"
text-decoder "^1.1.0"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -920,6 +998,15 @@ tar-fs@^2.0.0:
pump "^3.0.0"
tar-stream "^2.1.4"
tar-stream@3.1.7:
version "3.1.7"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b"
integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==
dependencies:
b4a "^1.6.4"
fast-fifo "^1.2.0"
streamx "^2.15.0"
tar-stream@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
@@ -931,6 +1018,13 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
text-decoder@^1.1.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.7.tgz#5d073a9a74b9c0a9d28dfadcab96b604af57d8ba"
integrity sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==
dependencies:
b4a "^1.6.4"
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -948,6 +1042,11 @@ undici@^6.22.0:
resolved "https://registry.yarnpkg.com/undici/-/undici-6.23.0.tgz#7953087744d9095a96f115de3140ca3828aff3a4"
integrity sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==
undici@^7.16.0:
version "7.22.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-7.22.0.tgz#7a82590a5908e504a47d85c60b0f89ca14240e60"
integrity sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -977,7 +1076,26 @@ ws@^8.18.1, ws@^8.18.3:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.19.0.tgz#ddc2bdfa5b9ad860204f5a72a4863a8895fd8c8b"
integrity sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==
xdg-app-paths@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/xdg-app-paths/-/xdg-app-paths-5.1.0.tgz#f52f724f91e88244148c085c09bcd396443d8cae"
integrity sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==
dependencies:
xdg-portable "^7.0.0"
xdg-portable@^7.0.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/xdg-portable/-/xdg-portable-7.3.0.tgz#c6b1610de806a2ca1fe65727d5f8402c295d2e96"
integrity sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==
dependencies:
os-paths "^4.0.1"
xtend@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
zod@3.24.4:
version "3.24.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.4.tgz#e2e2cca5faaa012d76e527d0d36622e0a90c315f"
integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==

View File

@@ -654,6 +654,7 @@
[loading-branches? set-loading-branches?!] (rum/use-state false)
[starting-session? set-starting-session?!] (rum/use-state false)
[publish-mode set-publish-mode!] (rum/use-state nil)
[snapshot-busy? set-snapshot-busy?!] (rum/use-state false)
[terminal-visible? set-terminal-visible!] (rum/use-state false)
[terminal-status set-terminal-status!] (rum/use-state :idle)
[terminal-error set-terminal-error!] (rum/use-state nil)
@@ -678,7 +679,11 @@
session-started?
(not (string? selected-start-branch))
(not (agent-handler/task-ready? block)))
publish-disabled? (or input-disabled? busy? publish-busy?)
publish-disabled? (or input-disabled? busy? publish-busy? snapshot-busy?)
snapshot-disabled? (or input-disabled?
busy?
publish-busy?
snapshot-busy?)
can-send? (and (not input-disabled?)
(not (string/blank? trimmed-draft))
(not busy?))
@@ -727,6 +732,12 @@
(-> (agent-handler/<publish-session! block opts)
(p/catch (fn [_] nil))
(p/finally (fn [] (set-publish-mode! nil)))))))
snapshot! (fn []
(when (and base session-id (not snapshot-disabled?))
(set-snapshot-busy?! true)
(-> (agent-handler/<snapshot-session! block)
(p/catch (fn [_] nil))
(p/finally (fn [] (set-snapshot-busy?! false))))))
open-terminal! (fn []
(when (and terminal-enabled? (not terminal-open-disabled?))
(set-terminal-visible! true)
@@ -1044,11 +1055,24 @@
[:div.flex.items-center.justify-between.gap-2
[:div.text-xs.opacity-60
(cond
snapshot-busy?
"Creating snapshot..."
publish-busy?
"Publishing changes..."
:else
"Publish session changes")]
[:div.flex.items-center.gap-2
(shui/button
{:size :sm
:variant :outline
:class "h-7 px-2 text-xs"
:disabled snapshot-disabled?
:on-click (fn [_]
(snapshot!))}
(if snapshot-busy?
"Snapshotting..."
"Snapshot"))
(shui/button
{:size :sm
:variant :outline