diff --git a/deps/workers/src/logseq/agents/runtime_provider.cljs b/deps/workers/src/logseq/agents/runtime_provider.cljs index 4e083888c7..c5789aaf55 100644 --- a/deps/workers/src/logseq/agents/runtime_provider.cljs +++ b/deps/workers/src/logseq/agents/runtime_provider.cljs @@ -398,7 +398,7 @@ (defn- e2b-template-name [task] (when-let [graph-id (task-graph-id task)] - (sanitize-name (str "logseq-" graph-id "-" (task-repo-name task))))) + (sanitize-name (str graph-id "-" (task-repo-name task))))) (defn- e2b-agent-token [^js env runtime] @@ -470,6 +470,8 @@ {:reason :missing-e2b-get-host}))) (sandbox/normalize-base-url (.call get-host sandbox port)))) +(declare promise (.call create sandbox-class template params)) (->promise (.call create sandbox-class params))))) +(defn- e2b-template-not-found-error? + [error] + (let [message (or (some-> error (aget "message")) + (some-> error str))] + (boolean (and (string? message) + (string/includes? message "404:") + (string/includes? message "template") + (string/includes? message "not found"))))) + +(defn- ( error str)] - (boolean (and (string? message) - (string/starts-with? message "Error: 404:"))))) - -(defn- (->promise (.call get-tags template-fn template-name (clj->js (e2b-api-opts env)))) - (p/then (fn [_tags] template-name)) - (p/catch (fn [error] - (if (e2b-template-missing-error? error) - nil - (throw error))))))) + (when-not (fn? exists) + (throw (ex-info "e2b sdk missing Template.exists" + {:reason :missing-e2b-template-exists}))) + (-> (->promise (.call exists template-fn template-name (clj->js (e2b-api-opts env)))) + (p/then true?)))) + +(defn- (template-fn) (js-method "fromDockerfile")) + template-builder (when (fn? template-builder-fn) + (.call template-builder-fn (template-fn) docker-file)) + set-start-cmd (js-method template-builder "setStartCmd") + template-builder (if (and (some? template-builder) + (fn? set-start-cmd) + (fn? wait-for-timeout)) + (.call set-start-cmd + template-builder + "bash -lc 'while true; do sleep 3600; done'" + (.call wait-for-timeout nil 5000)) + template-builder) + build-opts (cond-> (merge (e2b-api-opts env) + {:cpuCount 1 + :memoryMB 2048 + :skipCache false}) + (fn? default-build-logger) + (assoc :onBuildLogs (.call default-build-logger nil)))] + (when-not (some? template-builder) + (throw (ex-info "e2b sdk missing Template().fromDockerfile" + {:reason :missing-e2b-from-dockerfile}))) + (p/let [_build-info (->promise (.call build + template-fn + template-builder + template-name + (clj->js build-opts)))] + template-name)))))) (defn- clj opts :keywordize-keys true)}) - (js/Promise.resolve #js []))) + (js/Promise.resolve false))) + (aset template-fn "build" + (fn [builder name opts] + (swap! calls conj {:type :build + :builder (= builder template-builder) + :name name + :opts (js->clj opts :keywordize-keys true)}) + (js/Promise.resolve #js {:templateId "tpl-from-docker"}))) (aset e2b-ns "Template" template-fn) + (aset e2b-ns "waitForTimeout" + (fn [timeout-ms] + (swap! calls conj {:type :wait-for-timeout + :timeout-ms timeout-ms}) + #js {:timeoutMs timeout-ms})) + (aset e2b-ns "defaultBuildLogger" + (fn [] + (swap! calls conj {:type :default-build-logger}) + (fn [_entry] nil))) (aset sandbox-class "create" (fn [& args] (let [template (first args) @@ -201,7 +234,22 @@ (restore!) (is (= "e2b" (:provider runtime))) (is (= "logseq-graph-123-agent-test" (:template runtime))) - (is (some #(and (= :get-tags (:type %)) + (is (some #(= {:type :from-dockerfile + :docker-file "FROM node:20\nRUN corepack enable"} %) + @calls)) + (is (some #(and (= :set-start-cmd (:type %)) + (= "bash -lc 'while true; do sleep 3600; done'" (:command %))) + @calls)) + (is (some #(= {:type :wait-for-timeout + :timeout-ms 5000} %) + @calls)) + (is (some #(= {:type :default-build-logger} %) + @calls)) + (is (some #(and (= :exists (:type %)) + (= "logseq-graph-123-agent-test" (:name %))) + @calls)) + (is (some #(and (= :build (:type %)) + (:builder %) (= "logseq-graph-123-agent-test" (:name %)) (= "e2b-key" (get-in % [:opts :apiKey]))) @calls)) @@ -214,12 +262,11 @@ (is false (str "unexpected error: " error)) (done))))))) -(deftest e2b-provider-provision-falls-back-to-default-sandbox-when-template-missing-test +(deftest e2b-provider-provision-reuses-existing-template-before-building-test (async done (let [calls (atom []) env #js {"E2B_API_KEY" "e2b-key" - "SANDBOX_AGENT_TOKEN" "agent-token" - "E2B_TEMPLATE" "fallback-template"} + "SANDBOX_AGENT_TOKEN" "agent-token"} provider (runtime-provider/create-provider env "e2b") task {:agent {:provider "codex"} :project {:repo-url "https://github.com/logseq/agent-test" @@ -235,29 +282,34 @@ (aset sandbox-class "create" original-create) (set! js/fetch original-fetch)) template-fn (fn [] #js {})] - (aset template-fn "getTags" - (fn [_name _opts] - (js/Promise.reject (js/Error. "404: Not Found")))) + (aset template-fn "exists" + (fn [name _opts] + (swap! calls conj {:type :exists + :name name}) + (js/Promise.resolve true))) + (aset template-fn "build" + (fn [& _args] + (swap! calls conj {:type :build}) + (js/Promise.resolve #js {:templateId "tpl-should-not-build"}))) (aset e2b-ns "Template" template-fn) (aset sandbox-class "create" (fn [& args] - (swap! calls conj {:type :create - :argc (count args) - :first-arg (first args) - :last-arg (js->clj (last args) :keywordize-keys true)}) - (js/Promise.resolve - #js {:sandboxId "e2b-sbx-docker-default" - :getHost (fn [_port] - "https://e2b-agent.local") - :commands - #js {:run (fn [cmd _opts] - (if (string/includes? cmd "/v1/health") - (js/Promise.resolve #js {:stdout "__HEALTH_OK__" - :stderr "" - :exitCode 0}) - (js/Promise.resolve #js {:stdout "" - :stderr "" - :exitCode 0})))}}))) + (let [template (first args)] + (swap! calls conj {:type :create + :template template}) + (js/Promise.resolve + #js {:sandboxId "e2b-sbx-existing-template" + :getHost (fn [_port] + "https://e2b-agent.local") + :commands + #js {:run (fn [cmd _opts] + (if (string/includes? cmd "/v1/health") + (js/Promise.resolve #js {:stdout "__HEALTH_OK__" + :stderr "" + :exitCode 0}) + (js/Promise.resolve #js {:stdout "" + :stderr "" + :exitCode 0})))}})))) (set! js/fetch (fn [_request] (js/Promise.resolve @@ -265,16 +317,186 @@ (js/JSON.stringify #js {:ok true}) #js {:status 200 :headers #js {"content-type" "application/json"}})))) - (-> (runtime-provider/ (runtime-provider/clj (last args) :keywordize-keys true)}) + (if (= 2 (count args)) + (js/Promise.reject + #js {:name "SandboxError" + :message "404: template '78fq0upgdyots2idb7fv' not found"}) + (js/Promise.resolve + #js {:sandboxId "e2b-sbx-fallback-template" + :getHost (fn [_port] + "https://e2b-agent.local") + :commands + #js {:run (fn [cmd _opts] + (if (string/includes? cmd "/v1/health") + (js/Promise.resolve #js {:stdout "__HEALTH_OK__" + :stderr "" + :exitCode 0}) + (js/Promise.resolve #js {:stdout "" + :stderr "" + :exitCode 0})))}})))) + (set! js/fetch + (fn [_request] + (js/Promise.resolve + (js/Response. + (js/JSON.stringify #js {:ok true}) + #js {:status 200 + :headers #js {"content-type" "application/json"}})))) + (-> (runtime-provider/ (runtime-provider/ /etc/apt/sources.list.d/github-cli.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends gh \ && rm -rf /var/lib/apt/lists/* -# Install Node.js 22 -# The base image has Node 20, we need to replace it with Node 22 -# Using direct binary download for reliability -ENV NODE_VERSION=22.20.0 -RUN ARCH="$(dpkg --print-architecture)" \ - && case "${ARCH}" in \ - amd64) NODE_ARCH="x64" ;; \ - arm64) NODE_ARCH="arm64" ;; \ - *) echo "Unsupported architecture: ${ARCH}" >&2; exit 1 ;; \ - esac \ - && apt-get update && apt-get install -y xz-utils ca-certificates rclone tmux vim \ - && curl -fsSLk https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz -o /tmp/node.tar.xz \ - && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \ - && rm /tmp/node.tar.xz \ - && node --version \ - && npm --version - -# GitHub CLI -RUN mkdir -p /etc/apt/keyrings \ - && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \ - && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ - > /etc/apt/sources.list.d/github-cli.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends gh \ - && gh --version \ - && rm -rf /var/lib/apt/lists/* - -# Prefer corepack over "npm i -g yarn" RUN corepack enable -# Optionally pin yarn (pick a version you want) -# RUN corepack prepare yarn@4.6.0 --activate -# Clojure -RUN curl -fsSL -o /tmp/linux-install.sh \ - https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh \ - && chmod +x /tmp/linux-install.sh \ - && /tmp/linux-install.sh \ - && rm -f /tmp/linux-install.sh +RUN curl -fsSL \ + https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh \ + -o /tmp/clojure-install.sh \ + && chmod +x /tmp/clojure-install.sh \ + && /tmp/clojure-install.sh \ + && rm -f /tmp/clojure-install.sh -# Babashka (actually run installer) RUN curl -fsSL https://raw.githubusercontent.com/babashka/babashka/master/install | bash -WORKDIR /workspace - -# sandbox-agent + codex agent RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.1.5/install.sh | sh \ - && sandbox-agent install-agent codex + && /usr/local/bin/sandbox-agent install-agent codex + +WORKDIR /home/user/workspace + +RUN git clone https://github.com/logseq/logseq + +WORKDIR /home/user/workspace/logseq + +RUN yarn install +RUN clojure -M:test compile test EXPOSE 2468 + +CMD ["/bin/bash"]