Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .semgrep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,21 @@ rules:
- "**/tests/**"
- "**/docs/**"

- id: no-legacy-defk-handler-signatures
languages: [generic]
severity: ERROR
message: >
HYP-197: production Hy effect handlers must use defhandler. Do not write
legacy defk handlers with effect, eff, or k parameters because doeff-hy
rejects those signatures at import time.
pattern-regex: '(?m)^\s*\(defk\s+[^\s\[]+\s+\[[^\]\n]*(?:\beffect\b|\beff\b|\bk\b)[^\]\n]*\]'
paths:
include:
- "/packages/**/src/**/*.hy"
exclude:
- "**/tests/**"
- "**/examples/**"

- id: cache-handler-no-result-duck-typing
languages: [python]
severity: ERROR
Expand Down
13 changes: 13 additions & 0 deletions Hypha/Evidence/HYP-197/pr-explanation.imagegen-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Tool: imagegen

Prompt:
Use case: infographic-diagram
Asset type: GitHub PR説明図, 16:9 PNG
Primary request: HYP-197 の実装内容を説明する、霞ヶ関スタイルの情報密度が高い日本語インフォグラフィックを作成する。
Visual style: 白地に濃紺と深緑の罫線、行政資料のような整然とした区画、細い矢印、チェック印、コードファイルの小さなピクトグラム、盾の検査アイコン。装飾は控えめ、文字は読みやすい太めのゴシック体。
Layout: 上部に大見出し「HYP-197: 旧 handler 署名を defhandler へ移行」。左から右へ 3 カラム。
Left column heading: 「問題」。内容: 「production Hy source に旧 defk [effect k] が残存」「doeff-hy macro guard が import / pytest collection を停止」。
Middle column heading: 「変更」。内容: 「doeff-docker: Dockerfile / Docker build / push を defhandler 化」「doeff-ml-nexus: file / docker / rsync / resolve を defhandler 化」「Dockerfile 生成 Program に契約を追加」。中央に大きな矢印「defk 署名禁止 → defhandler 節」。
Right column heading: 「確認」。内容: 「handler module import 通過」「Dockerfile 生成 4 tests 通過」「package tests 27 tests 通過」「Semgrep guard: no-legacy-defk-handler-signatures」。
Bottom band heading: 「レビュアーが見る点」。内容: 「未対応 effect は defhandler の既定転送」「Tell / Ask / slog の effect delegation を保持」「旧署名の再混入は architecture test と Semgrep が検出」。
Constraints: すべての見出し、ラベル、説明文は日本語。技術識別子は必要なものだけそのまま使う。Program.resolve や lazy_ask や resolved["tdnet"] のような、このPRに存在しない疑似APIや処理は一切描かない。Markdown、HTML、SVG、Mermaid風ではなく、生成されたビットマップの完成図にする。
Binary file added Hypha/Evidence/HYP-197/pr-explanation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 15 additions & 19 deletions packages/doeff-docker/src/doeff_docker/handlers/docker.hy
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
;;; Handles DockerBuild, ImagePush effects via shell commands.
;;; DockerRun is application-specific (depends on runner module) — not included here.

(require doeff_hy.macros [defk <-])
(require doeff_hy.macros [<- defhandler])
(import doeff [do :as _doeff-do])
(import doeff [Resume Pass])
(import doeff_core_effects [slog])

(import subprocess)
Expand All @@ -14,6 +13,7 @@
(import doeff_docker.effects [DockerBuild ImagePush])


;; Plain callable: shared synchronous subprocess boundary used by handler clauses.
(defn run-cmd [args * [host "localhost"] [stdin-data None]]
"Run a command as list of args, optionally on a remote host via SSH.
Returns CompletedProcess."
Expand All @@ -27,24 +27,20 @@
proc)


(defk docker-build-handler [effect k]
(defhandler docker-build-handler
"Handle DockerBuild: build Docker image from Dockerfile string."
(if (isinstance effect DockerBuild)
(do
(<- (slog :msg f"docker build: {effect.tag} on {effect.host}"))
(setv args ["docker" "build" "-t" effect.tag "-f" "-" (str effect.context-path)])
(run-cmd args :host effect.host
:stdin-data (.encode effect.dockerfile "utf-8"))
(yield (Resume k effect.tag)))
(yield (Pass effect k))))
(DockerBuild [dockerfile tag context-path host]
(<- (slog :msg f"docker build: {tag} on {host}"))
(setv args ["docker" "build" "-t" tag "-f" "-" (str context-path)])
(run-cmd args :host host
:stdin-data (.encode dockerfile "utf-8"))
(resume tag)))


(defk image-push-handler [effect k]
(defhandler image-push-handler
"Handle ImagePush: tag and push image to registry."
(if (isinstance effect ImagePush)
(do
(<- (slog :msg f"docker push: {effect.local-tag} -> {effect.remote-tag}"))
(run-cmd ["docker" "tag" effect.local-tag effect.remote-tag])
(run-cmd ["docker" "push" effect.remote-tag])
(yield (Resume k effect.remote-tag)))
(yield (Pass effect k))))
(ImagePush [local-tag remote-tag]
(<- (slog :msg f"docker push: {local-tag} -> {remote-tag}"))
(run-cmd ["docker" "tag" local-tag remote-tag])
(run-cmd ["docker" "push" remote-tag])
(resume remote-tag)))
57 changes: 29 additions & 28 deletions packages/doeff-docker/src/doeff_docker/handlers/dockerfile.hy
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,63 @@
;;; Collects Dockerfile instruction effects (From, Run, Copy, etc.)
;;; via WithObserve + Tell, then retrieves via state.

(require doeff_hy.macros [defk <-])
(require doeff_hy.macros [defk <- defhandler])
(import doeff [do :as _doeff-do])
(import doeff [Resume Pass WithObserve WithHandler])
(import doeff [Program WithObserve])
(import doeff_core_effects [Tell WriterTellEffect])

(import doeff_docker.effects [From Run Copy Workdir SetEnv Expose])


(defk dockerfile-collector-handler [effect k]
(defhandler dockerfile-collector-handler
"Handler that converts typed Dockerfile effects (From, Run, etc.)
into Tell messages, then Resumes with None.
Pair with WithObserve or Listen to collect the messages."
(cond
(isinstance effect From)
(do (<- (Tell f"FROM {effect.image}"))
(yield (Resume k None)))
(From [image]
(<- (Tell f"FROM {image}"))
(resume None))

(isinstance effect Run)
(do (<- (Tell f"RUN {effect.command}"))
(yield (Resume k None)))
(Run [command]
(<- (Tell f"RUN {command}"))
(resume None))

(isinstance effect Copy)
(do (<- (Tell f"COPY {effect.src} {effect.dst}"))
(yield (Resume k None)))
(Copy [src dst]
(<- (Tell f"COPY {src} {dst}"))
(resume None))

(isinstance effect Workdir)
(do (<- (Tell f"WORKDIR {effect.path}"))
(yield (Resume k None)))
(Workdir [path]
(<- (Tell f"WORKDIR {path}"))
(resume None))

(isinstance effect SetEnv)
(do (<- (Tell f"ENV {effect.key}={effect.value}"))
(yield (Resume k None)))
(SetEnv [key value]
(<- (Tell f"ENV {key}={value}"))
(resume None))

(isinstance effect Expose)
(do (<- (Tell f"EXPOSE {effect.port}"))
(yield (Resume k None)))
(Expose [port]
(<- (Tell f"EXPOSE {port}"))
(resume None)))

True
(yield (Pass effect k))))


(defn render-dockerfile [messages]
(defk render-dockerfile [messages]
"Convert collected WriterTellEffect list to Dockerfile string. Pure function."
{:pre [(: messages list)]
:post [(: % str)]}
(.join "\n" (lfor m messages m.msg)))


(defk collect-dockerfile [image-program]
"Run an image-definition Program, collect Dockerfile instructions.
Uses WithObserve to capture Tell effects emitted by dockerfile-collector-handler.
Returns Dockerfile string."
{:pre [(: image-program Program)]
:post [(: % str)]}
(setv collected [])

(defn _observer [effect]
(when (isinstance effect WriterTellEffect)
(.append collected effect)))

(<- (WithObserve _observer
(WithHandler dockerfile-collector-handler image-program)))
(render-dockerfile collected))
(dockerfile-collector-handler image-program)))
(<- dockerfile (render-dockerfile collected))
dockerfile)
46 changes: 40 additions & 6 deletions packages/doeff-ml-nexus/src/doeff_ml_nexus/docker.hy
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,45 @@

(defk copy-from [src dst build-context-path]
"Copy a file from outside build context: rsync to context, then COPY."
(<- (RsyncTo :src src :host "localhost"
:dst-path (str (/ build-context-path (Path src) .name))))
(<- (Copy :src (. (Path src) name) :dst dst)))
{:pre [(: src "str or Path")
(or (isinstance src str) (isinstance src Path))
(: dst str)
(: build-context-path Path)]
:post [(: % "None") (is % None)]}
(setv src-path (Path src))
(<- (RsyncTo :src src-path :host "localhost"
:dst-path (str (/ build-context-path src-path.name))))
(<- (Copy :src src-path.name :dst dst)))

(defk copy-resolved [source-id dst build-context-path]
"Resolve a source id to a path, rsync into context, then COPY."
{:pre [(: source-id str)
(: dst str)
(: build-context-path Path)]
:post [(: % "None") (is % None)]}
(<- src-path (Resolve :target source-id :kind Path))
(copy-from src-path dst build-context-path))
(<- (copy-from src-path dst build-context-path)))

(defk apt-install [#* packages]
"RUN apt-get install."
{:pre [(: packages tuple)]
:post [(: % "None") (is % None)]}
(<- (Run :command
(+ "apt-get update && apt-get install -y --no-install-recommends "
(.join " " packages)
" && rm -rf /var/lib/apt/lists/*"))))

(defk install-uv []
"Install uv into the image."
{:pre []
:post [(: % "None") (is % None)]}
(<- (Run :command "curl -LsSf https://astral.sh/uv/install.sh | sh"))
(<- (SetEnv :key "PATH" :value "/root/.local/bin:$PATH")))

(defk install-rust []
"Install Rust toolchain into the image."
{:pre []
:post [(: % "None") (is % None)]}
(<- (Run :command "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"))
(<- (SetEnv :key "PATH" :value "/root/.cargo/bin:$PATH")))

Expand All @@ -51,12 +67,16 @@
(defk copy-local-deps [deps]
"COPY top-level local deps into container at /deps/<name>.
Sub-deps (e.g. doeff-vm inside doeff) are included automatically."
{:pre [(: deps list)]
:post [(: % "None") (is % None)]}
(setv top-deps (_find-top-level-deps deps))
(for [dep top-deps]
(<- (Copy :src f"deps/{dep.name}" :dst f"/deps/{dep.name}"))))

(defn has-rust-extension [deps]
(defk has-rust-extension [deps]
"Check if any local dep requires Rust build (has Cargo.toml)."
{:pre [(: deps list)]
:post [(: % bool)]}
(any (lfor dep deps (.exists (/ dep.local-path "Cargo.toml")))))


Expand All @@ -78,6 +98,11 @@
2. Local deps COPY (if any)
3. Dependency layer (pyproject.toml + uv.lock only)
4. Full source + project install"
{:pre [(: base-image str)
(: project-root "Path or None")
(or (is project-root None) (isinstance project-root Path))
(: gpu bool)]
:post [(: % "None") (is % None)]}
(<- (From :image base-image))
(when gpu
(<- (SetEnv :key "NVIDIA_VISIBLE_DEVICES" :value "all"))
Expand All @@ -87,7 +112,8 @@

;; Check for local deps and rust extensions
(setv deps (if project-root (find-local-deps project-root) []))
(when (and deps (has-rust-extension deps))
(<- has-rust (has-rust-extension deps))
(when (and deps has-rust)
(<- (install-rust)))

;; Copy local deps if any
Expand All @@ -107,8 +133,16 @@

(defk uv-image [base-image * [project-root None]]
"Standard uv project Dockerfile."
{:pre [(: base-image str)
(: project-root "Path or None")
(or (is project-root None) (isinstance project-root Path))]
:post [(: % "None") (is % None)]}
(<- (uv-image-core base-image project-root)))

(defk uv-gpu-image [base-image * [project-root None]]
"uv project with NVIDIA GPU support."
{:pre [(: base-image str)
(: project-root "Path or None")
(or (is project-root None) (isinstance project-root Path))]
:post [(: % "None") (is % None)]}
(<- (uv-image-core base-image project-root :gpu True)))
79 changes: 38 additions & 41 deletions packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/docker.hy
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
;;; DockerBuild and ImagePush handlers are in doeff-docker.
;;; This module provides DockerRun handler with file-based cloudpickle exchange.

(require doeff_hy.macros [defk <-])
(require doeff_hy.macros [<- defhandler])
(import doeff [do :as _doeff-do])
(import doeff [Resume Pass])
(import doeff_core_effects [Ask slog])

(import uuid)
Expand All @@ -13,55 +12,53 @@
(import doeff_docker.handlers.docker [run-cmd])


(defk docker-run-handler [effect k]
(defhandler docker-run-handler
"Handle DockerRun: execute Program[T] in container via file-based cloudpickle.

1. Write pickled program to temp file on host
2. Docker run with volume mount, doeff run invokes p_run
3. Read pickled result from output file
4. Clean up"
(if (isinstance effect DockerRun)
(do
(<- (slog :msg f"docker run: {effect.image} on {effect.host} gpu={effect.gpu}"))
(<- serializer (Ask "serializer"))
(DockerRun [image program host gpu mounts env-vars]
(<- (slog :msg f"docker run: {image} on {host} gpu={gpu}"))
(<- serializer (Ask "serializer"))

;; Temp paths on host
(setv run-id (cut (. (uuid.uuid4) hex) 0 8))
(setv tmp-dir f"/tmp/doeff-run/{run-id}")
(setv container-exchange "/tmp/doeff-exchange")
;; Temp paths on host
(setv run-id (cut (. (uuid.uuid4) hex) 0 8))
(setv tmp-dir f"/tmp/doeff-run/{run-id}")
(setv container-exchange "/tmp/doeff-exchange")

;; Create tmp dir and write pickled program
(run-cmd ["mkdir" "-p" tmp-dir] :host effect.host)
(setv pickled (.dumps serializer effect.program))
(run-cmd ["tee" f"{tmp-dir}/program.pkl"] :host effect.host :stdin-data pickled)
;; Create tmp dir and write pickled program
(run-cmd ["mkdir" "-p" tmp-dir] :host host)
(setv pickled (.dumps serializer program))
(run-cmd ["tee" f"{tmp-dir}/program.pkl"] :host host :stdin-data pickled)

;; Docker run args
(setv parts ["docker" "run" "--rm"])
(when effect.gpu
(.extend parts ["--gpus" "all"]))
(.extend parts ["-v" f"{tmp-dir}:{container-exchange}"])
(for [m effect.mounts]
(.extend parts ["-v" m]))
(for [e effect.env-vars]
(.extend parts ["-e" e]))
(.extend parts ["-e" "DOEFF_DISABLE_PROFILE=1"
"-e" "DOEFF_DISABLE_RUNBOX=1"
"-e" f"DOEFF_INPUT={container-exchange}/program.pkl"
"-e" f"DOEFF_OUTPUT={container-exchange}/result.pkl"
effect.image
"uv" "run" "doeff" "run"
"--program" "doeff_ml_nexus.runner.p_run"
"--interpreter" "doeff_ml_nexus.runner.runner_interpreter"])
;; Docker run args
(setv parts ["docker" "run" "--rm"])
(when gpu
(.extend parts ["--gpus" "all"]))
(.extend parts ["-v" f"{tmp-dir}:{container-exchange}"])
(for [m mounts]
(.extend parts ["-v" m]))
(for [e env-vars]
(.extend parts ["-e" e]))
(.extend parts ["-e" "DOEFF_DISABLE_PROFILE=1"
"-e" "DOEFF_DISABLE_RUNBOX=1"
"-e" f"DOEFF_INPUT={container-exchange}/program.pkl"
"-e" f"DOEFF_OUTPUT={container-exchange}/result.pkl"
image
"uv" "run" "doeff" "run"
"--program" "doeff_ml_nexus.runner.p_run"
"--interpreter" "doeff_ml_nexus.runner.runner_interpreter"])

;; Execute
(run-cmd parts :host effect.host)
;; Execute
(run-cmd parts :host host)

;; Read result
(setv proc (run-cmd ["cat" f"{tmp-dir}/result.pkl"] :host effect.host))
(setv result (.loads serializer proc.stdout))
;; Read result
(setv proc (run-cmd ["cat" f"{tmp-dir}/result.pkl"] :host host))
(setv result (.loads serializer proc.stdout))

;; Cleanup
(run-cmd ["rm" "-rf" tmp-dir] :host effect.host)
;; Cleanup
(run-cmd ["rm" "-rf" tmp-dir] :host host)

(yield (Resume k result)))
(yield (Pass effect k))))
(resume result)))
Loading