mirror of
https://github.com/logseq/logseq.git
synced 2026-05-23 20:24:15 +00:00
vercel sandbox with snapshot support
This commit is contained in:
13
deps/workers/README.md
vendored
13
deps/workers/README.md
vendored
@@ -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 |
|
||||
|
||||
@@ -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`
|
||||
|
||||
70
deps/workers/docs/milestones/agents/19-m19-vercel-sandbox-runtime-and-snapshot.md
vendored
Normal file
70
deps/workers/docs/milestones/agents/19-m19-vercel-sandbox-runtime-and-snapshot.md
vendored
Normal 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.
|
||||
1
deps/workers/package.json
vendored
1
deps/workers/package.json
vendored
@@ -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"
|
||||
|
||||
544
deps/workers/src/logseq/agents/runtime_provider.cljs
vendored
544
deps/workers/src/logseq/agents/runtime_provider.cljs
vendored
@@ -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)))
|
||||
|
||||
|
||||
65
deps/workers/src/logseq/agents/sandbox.cljs
vendored
65
deps/workers/src/logseq/agents/sandbox.cljs
vendored
@@ -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}))))))
|
||||
|
||||
13
deps/workers/src/logseq/sync/node/config.cljs
vendored
13
deps/workers/src/logseq/sync/node/config.cljs
vendored
@@ -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])
|
||||
|
||||
10
deps/workers/src/logseq/sync/node/server.cljs
vendored
10
deps/workers/src/logseq/sync/node/server.cljs
vendored
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")))
|
||||
|
||||
9
deps/workers/worker/env_example
vendored
9
deps/workers/worker/env_example
vendored
@@ -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}
|
||||
|
||||
8
deps/workers/worker/wrangler.agents.toml
vendored
8
deps/workers/worker/wrangler.agents.toml
vendored
@@ -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
120
deps/workers/yarn.lock
vendored
@@ -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==
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user