Skip to content
Open
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
30 changes: 26 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@
# For maximum integrity, set this to an immutable digest in CI/CD.
ARG SWIPL_IMAGE=docker.io/library/swipl:10.0.2

# Only plugin requirements reach the install step, so editing plugin code keeps its cache.
FROM ${SWIPL_IMAGE} AS plugin-requirements-collector

COPY plugins /plugins
RUN mkdir -p /plugin-requirements \
&& for declared in /plugins/*/requirements.txt; do \
[ -f "$declared" ] || continue; \
plugin="$(basename "$(dirname "$declared")")"; \
mkdir -p "/plugin-requirements/$plugin"; \
cp -p "$declared" "/plugin-requirements/$plugin/requirements.txt"; \
done

FROM scratch AS plugin-requirements

COPY --from=plugin-requirements-collector /plugin-requirements /

FROM ${SWIPL_IMAGE} AS builder

SHELL ["/bin/bash", "-o", "pipefail", "-c"]
Expand Down Expand Up @@ -52,12 +68,18 @@ RUN sh build.sh
RUN mkdir -p /PeTTa/repos \
&& git clone --depth 1 --branch "${CHROMADB_REF}" "${CHROMADB_REPO}" /PeTTa/repos/petta_lib_chromadb

COPY ./requirements.txt /tmp/requirements.txt
RUN python3 -m pip install --no-cache-dir --break-system-packages \
# Torch comes from the CPU wheel index; its version is read from requirements.txt, not repeated here.
RUN --mount=type=bind,source=requirements.txt,target=/tmp/omega/requirements.txt \
python3 -m pip install --no-cache-dir --break-system-packages \
--index-url https://download.pytorch.org/whl/cpu \
--extra-index-url https://pypi.org/simple/ \
torch==2.12.1 \
&& python3 -m pip install --no-cache-dir --break-system-packages -r /tmp/requirements.txt
"$(grep -E '^torch(\[|[=<>!~]|$)' /tmp/omega/requirements.txt)"

# Core's requirements and every plugin's, resolved together so a conflict fails the build.
RUN --mount=type=bind,source=requirements.txt,target=/tmp/omega/requirements.txt \
--mount=type=bind,from=plugin-requirements,target=/tmp/omega/plugins \
--mount=type=bind,source=scripts/install_dependencies.sh,target=/tmp/omega/scripts/install_dependencies.sh \
/tmp/omega/scripts/install_dependencies.sh --no-cache-dir --break-system-packages

# Pre-download the sentence-transformers model so runtime does not need network access.
RUN mkdir -p "${HF_HOME}" "${SENTENCE_TRANSFORMERS_HOME}" \
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,13 @@ source ./.venv/bin/activate

If you have CPU only machine or don't want calculate embeddings on GPU:
```
python3 -m pip install --index-url https://download.pytorch.org/whl/cpu torch
python3 -m pip install --index-url https://download.pytorch.org/whl/cpu \
"$(grep -E '^torch(\[|[=<>!~]|$)' ./repos/Omega/requirements.txt)"
```

Install Python dependencies:
Install Python dependencies — core's, and those of any plugin that declares its own:
```
python3 -m pip install -r ./repos/Omega/requirements.txt
./repos/Omega/scripts/install_dependencies.sh
```
---

Expand Down
28 changes: 28 additions & 0 deletions docs/reference-plugin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,34 @@ module. The plugin record has the following fields:
`name` module is located. Can include `{REPO}` placeholder to designate the
root folder of the Omega source repository.

## Plugin dependencies

A plugin that imports a third-party package declares it in a `requirements.txt`
in the plugin's own directory. `scripts/install_dependencies.sh` installs those
alongside Omega's own, and both the image build and the source install in
[README.md](/README.md#installation) call it. A plugin that imports nothing
outside the standard library and Omega ships no such file and needs no entry
anywhere.

Everything is installed by a single pip invocation, so Omega's own pins win. They
are exact, and a plugin asking for a different version of a package Omega pins
fails the install with a conflict rather than replacing it. Declare ranges wide
enough to include Omega's pin — `openai>=1.0.0` rather than `openai==2.1.0` —
and list only what the plugin actually imports.

The file holds requirements and nothing else. pip reads an option written inside
a requirements file — `--index-url` and `--extra-index-url` above all — as an
instruction for the whole invocation rather than for the file carrying it, which
would let one plugin choose where Omega's own packages are fetched from. Sharing
one invocation is what makes Omega's pins win, so there is nowhere to scope such
a line to, and a plugin that carries one stops the install with the line quoted.

Two limits are worth knowing. A plugin that pins a package Omega depends on
*indirectly* can change it without any error, because nothing is violated: keep
the list short for this reason. And a plugin mounted into an already built image
gets no installation at all, since the packages are baked in at build time — that
route needs an image built with the plugin present.

As an example of a MeTTa plugin one can look at the code of the [workflow
plugin](/plugins/workflow/workflow.metta). As an example of a Python plugin
one can look at the code of the [IRC communication channel](/channels/irc.py).
Expand Down
1 change: 1 addition & 0 deletions plugins/openclaw/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests
50 changes: 50 additions & 0 deletions scripts/install_dependencies.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/sh
# Install core's Python dependencies, and those of every plugin that declares any.
#
# A plugin is a directory under plugins/. One that ships a requirements.txt gets
# it installed without core naming the plugin; one that ships none costs nothing.
# That file lists requirements and nothing else — see refuse_pip_options.
#
# Everything reaches a single pip invocation. Separate invocations resolve
# separately, so a plugin pinning a version core also pins would replace core's
# copy in silence — core is not a pip distribution, so nothing records what it
# needed and no warning fires. Resolved together, a real conflict fails here
# rather than at import time, and pip check catches what resolution let through.
#
# Arguments are passed through to pip, so an image build can add its own:
#
# ./scripts/install_dependencies.sh
# ./scripts/install_dependencies.sh --no-cache-dir --break-system-packages
set -eu

repo="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"

# Stop on a plugin file that carries pip options instead of requirements.
#
# pip applies an option it reads inside a requirements file to the whole
# invocation rather than to the file carrying it, and it overrides the same
# option given on the command line. One plugin shipping an --index-url line
# therefore decides where every package is fetched from, core's own pinned ones
# included, and the install still reports success. Sharing one invocation is
# what makes core's pins win, so there is nowhere to scope such an option to,
# and the file is refused instead.
refuse_pip_options() {
options="$(grep -n '^[[:space:]]*-' "$1" || :)"
if [ -n "$options" ]; then
echo "$1: a plugin lists requirements here, and nothing else." >&2
echo "pip would apply these to the whole install, core's own packages included:" >&2
echo "$options" >&2
exit 1
fi
}

set -- "$@" -r "$repo/requirements.txt"
for plugin_requirements in "$repo"/plugins/*/requirements.txt; do
if [ -f "$plugin_requirements" ]; then
refuse_pip_options "$plugin_requirements"
set -- "$@" -r "$plugin_requirements"
fi
done

python3 -m pip install "$@"
python3 -m pip check
Loading
Loading