Skip to content

Commit c879511

Browse files
Kabuki94claude
andcommitted
feat(self-replication): close the git push -> build -> bootc switch loop
The defining MiOS feature -- live `/` is a git working tree, Forgejo at localhost:3000 hosts the bare repo, the OS rebuilds itself -- was structurally incomplete. This commit lands every missing piece so the loop closes end-to-end with no manual operator steps after firstboot. Self-replication loop: - new etc/containers/systemd/mios-forgejo-runner.container Quadlet (Privileged=true, documented architectural exception alongside mios-ceph + mios-k3s; shares /var/lib/containers/storage with host) - new .forgejo/workflows/build-mios.yml (podman build, bootc-label verification, sentinel-file handoff to the host watcher) - usr/libexec/mios/forge-firstboot.sh: extends admin bootstrap to also create the initial <admin>/mios repo via /api/v1/user/repos and mint a Forgejo Runner registration token (CLI first, admin API fallback) into /etc/mios/forge/runner-token (mode 0600) - new usr/libexec/mios/bootc-switch-from-build.sh + matching mios-bootc-switch.{service,path}: host-side watcher that reads the runner's sentinel and runs `bootc switch --transport containers-storage <ref>`. Privilege boundary stops at the file -- runner doesn't touch bootc, host doesn't touch the build. - new usr/lib/tmpfiles.d/mios-forge-runner.conf for the state dirs - .gitignore: whitelist .forgejo/, mios-*.{path,socket}; exclude runtime state (forge-runner/, bootc-switch-history.tsv, firstboot sentinels) so it never round-trips through `git push` Build hardening / attack-surface reduction: - usr/share/mios/PACKAGES.md: split build toolchain (gcc, g++, make, cmake, golang, selinux-policy-devel, binutils, pkgconf-pkg-config) out of packages-containers into a new packages-build-toolchain block; a deployed runtime no longer carries compilers - automation/12-virt.sh installs the new block early; new automation/91-strip-build-toolchain.sh removes it after SBOM and before cleanup, with a verification pass that no compiler binary remains in PATH - automation/build.sh marks 91-strip-build-toolchain.sh non-fatal - mios-bootstrap/build-mios.sh adds packages-build-toolchain to the FHS-install exclude list (deployed FHS hosts never get it) Quadlet parser fix (was disabling all Quadlets, including Forgejo): - etc/containers/systemd/mios-forge.container: remove inline `# ...` comments after PublishPort= (systemd unit syntax doesn't allow trailing comments; the malformed value broke podman-system-generator and prevented every Quadlet from rendering) Boot-log noise + race fixes: - usr/lib/systemd/system/dbus-daemon-wsl.service: Type=notify + NotifyAccess=main, closes the startup race that surfaced as "Failed to connect to system scope bus via local transport: Connection refused" at user login on WSL2 - new usr/lib/systemd/system/coreos-warn-invalid-mounts.service.d/ 10-mios-wsl2.conf: ConditionVirtualization=!wsl, suppresses the composefs/read-only MOTD warnings that are structurally impossible on WSL2 External repo resilience: - automation/05-enable-external-repos.sh: every external scurl + dnf copr op now uses a try_fetch wrapper with warn-on-failure + partial-file cleanup, so a single transient HTTP 22 from any one external mirror no longer tanks the whole build - VSCodium RPM repo block removed -- applications ship as Flatpaks per the project invariant (VM | Container | Flatpak only) Configurator HTML + Containerfile cleanup: - usr/share/mios/configurator/index.html: full Flatpak catalog (40 apps, 8 pre-selected as the MiOS minimal default, 32 opt-in across Gaming / GNOME core / Alternatives); parser fix for multi-line arrays; emit improved for long arrays; schema 1.2.0 - Containerfile: drop the always-empty /usr/lib/extensions/source sysext-pack RUN (no automation populates it; tools/mios-sysext- pack.sh stays in tree for future use; commented recipe shows how to re-enable when sysext sources are actually staged) - automation/37-aichat.sh: header flags the open Distrobox migration; packages-ai block emptied (aichat ships as tarball, not RPM) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a24f7e1 commit c879511

19 files changed

Lines changed: 992 additions & 132 deletions

File tree

.forgejo/workflows/build-mios.yml

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# .forgejo/workflows/build-mios.yml
2+
# Self-replication loop: triggered by `git push http://localhost:3000/<admin>/mios`,
3+
# this workflow runs in the mios-forgejo-runner Quadlet, builds a new OCI
4+
# image via `podman build` against /Containerfile, and signals the host's
5+
# mios-bootc-switch.path watcher to stage the new image for next boot.
6+
#
7+
# Privilege boundary:
8+
# - Runner container (Privileged=true, documented exception): does
9+
# `podman build` and writes the build-output sentinel.
10+
# - Host (mios-bootc-switch.service triggered by mios-bootc-switch.path):
11+
# reads the sentinel, runs `bootc switch --transport containers-storage`.
12+
# - The runner has NO access to /usr/bin/bootc; the split is intentional.
13+
#
14+
# Storage: the runner shares /var/lib/containers/storage with the host
15+
# (Volume= in the Quadlet). After `podman build` produces
16+
# `localhost/mios:latest`, the image is visible to host bootc directly.
17+
18+
name: build-mios
19+
on:
20+
push:
21+
branches: [main]
22+
workflow_dispatch: {}
23+
24+
jobs:
25+
build:
26+
runs-on: mios-self-hosted
27+
steps:
28+
- name: Checkout (live working tree of /)
29+
uses: actions/checkout@v4
30+
with:
31+
fetch-depth: 1
32+
# The repo IS the deployed root; checkout fetches the freshly
33+
# pushed state into the runner's job workspace. The
34+
# /Containerfile in that workspace is what podman build consumes.
35+
36+
- name: Resolve image tag from VERSION + commit
37+
id: tag
38+
run: |
39+
VER="$(cat VERSION 2>/dev/null || echo 'v0.0.0')"
40+
SHA="$(git rev-parse --short=12 HEAD)"
41+
TS="$(date -u +%Y%m%d-%H%M%S)"
42+
TAG="${VER#v}-${TS}-${SHA}"
43+
echo "image_tag=${TAG}" >> "$GITHUB_OUTPUT"
44+
echo "Building tag: ${TAG}"
45+
46+
- name: Build OCI image (localhost/mios:latest + tagged)
47+
run: |
48+
set -euo pipefail
49+
# Build args sourced from /etc/mios/install.env if present
50+
# (host-mounted by the runner Quadlet for read-only access).
51+
BUILD_ARGS=()
52+
if [[ -r /etc/mios/install.env ]]; then
53+
# shellcheck source=/dev/null
54+
source /etc/mios/install.env
55+
[[ -n "${MIOS_LINUX_USER:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_USER=${MIOS_LINUX_USER}")
56+
[[ -n "${MIOS_HOSTNAME:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_HOSTNAME=${MIOS_HOSTNAME}")
57+
[[ -n "${MIOS_AI_MODEL:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_AI_MODEL=${MIOS_AI_MODEL}")
58+
[[ -n "${MIOS_AI_EMBED_MODEL:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_AI_EMBED_MODEL=${MIOS_AI_EMBED_MODEL}")
59+
[[ -n "${MIOS_OLLAMA_BAKE_MODELS:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_OLLAMA_BAKE_MODELS=${MIOS_OLLAMA_BAKE_MODELS}")
60+
fi
61+
podman build \
62+
"${BUILD_ARGS[@]}" \
63+
-f Containerfile \
64+
-t "localhost/mios:latest" \
65+
-t "localhost/mios:${{ steps.tag.outputs.image_tag }}" \
66+
.
67+
68+
- name: Verify bootc lint passes (image must be bootc-switchable)
69+
run: |
70+
# Containerfile already runs `bootc container lint` as the final
71+
# build step (Architectural Law 4). This is a belt-and-suspenders
72+
# check: confirm the freshly-built image still carries the
73+
# required bootc labels before signaling the host watcher.
74+
podman image inspect localhost/mios:latest \
75+
--format '{{ index .Config.Labels "containers.bootc" }}' \
76+
| grep -qx '1' || {
77+
echo "ERROR: built image missing containers.bootc=1 label" >&2
78+
exit 1
79+
}
80+
podman image inspect localhost/mios:latest \
81+
--format '{{ index .Config.Labels "ostree.bootable" }}' \
82+
| grep -qx '1' || {
83+
echo "ERROR: built image missing ostree.bootable=1 label" >&2
84+
exit 1
85+
}
86+
87+
- name: Signal host -> bootc switch on next boot
88+
run: |
89+
# The host's mios-bootc-switch.path watches this file. Writing
90+
# the timestamp + image ref triggers mios-bootc-switch.service,
91+
# which validates the image exists in containers-storage and
92+
# runs `bootc switch --transport containers-storage <ref>`.
93+
# No reboot here; operator decides when to apply.
94+
install -d -m 0755 /var/lib/mios/forge-runner
95+
printf '%s %s\n' \
96+
"$(date -u +%FT%TZ)" \
97+
"localhost/mios:latest" \
98+
> /var/lib/mios/forge-runner/last-build.txt
99+
chmod 0644 /var/lib/mios/forge-runner/last-build.txt
100+
echo "Build signaled. Host will stage on next boot."
101+
echo "Apply now: sudo bootc upgrade --apply"
102+
103+
- name: Optional - mirror to GHCR
104+
if: ${{ env.GHCR_TOKEN != '' }}
105+
env:
106+
GHCR_USER: ${{ secrets.GHCR_USER }}
107+
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}
108+
run: |
109+
set -euo pipefail
110+
podman login -u "${GHCR_USER}" -p "${GHCR_TOKEN}" ghcr.io
111+
podman tag localhost/mios:latest "ghcr.io/mios-dev/mios:latest"
112+
podman tag localhost/mios:latest "ghcr.io/mios-dev/mios:${{ steps.tag.outputs.image_tag }}"
113+
podman push "ghcr.io/mios-dev/mios:latest"
114+
podman push "ghcr.io/mios-dev/mios:${{ steps.tag.outputs.image_tag }}"

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
!/.env.mios
2626
!/.github/
2727
!/.github/**
28+
# Forgejo Actions workflows (consumed by mios-forgejo-runner Quadlet
29+
# inside the closed self-replication loop). Forgejo Runner accepts
30+
# both .forgejo/workflows/ and .github/workflows/; the .forgejo/ path
31+
# is canonical for self-hosted-only workflows that shouldn't leak to
32+
# GHCR-side CI.
33+
!/.forgejo/
34+
!/.forgejo/**
2835
!/AGENTS.md
2936
!/AGREEMENTS.md
3037
!/AI.md
@@ -304,6 +311,8 @@ usr/lib/systemd/system/*
304311
!/usr/lib/systemd/system/mios-*.service
305312
!/usr/lib/systemd/system/mios-*.timer
306313
!/usr/lib/systemd/system/mios-*.target
314+
!/usr/lib/systemd/system/mios-*.path
315+
!/usr/lib/systemd/system/mios-*.socket
307316
!/usr/lib/systemd/system/var-*.mount
308317
!/usr/lib/systemd/system/*.service.d/
309318
!/usr/lib/systemd/system/*.service.d/**
@@ -379,6 +388,18 @@ opt/mios/*
379388
!/opt/mios/prompts/
380389
!/opt/mios/prompts/**
381390

391+
# ─────────────────────────────────────────────────────────────────────────────
392+
# 9f. /var/lib/mios -- exclude runtime state written by services after boot.
393+
# These files are operationally produced (firstboot sentinels, build-output
394+
# pointers, switch history) and MUST NOT round-trip through `git push`.
395+
# Whitelisted directories are still tracked; only specific runtime files
396+
# are excluded.
397+
# ─────────────────────────────────────────────────────────────────────────────
398+
var/lib/mios/forge/.firstboot-done
399+
var/lib/mios/forge-runner/
400+
var/lib/mios/bootc-switch-history.tsv
401+
var/lib/mios/.wsl-firstboot-done
402+
382403
# ─────────────────────────────────────────────────────────────────────────────
383404
# 10. SAFETY OVERRIDES -- sensitive, volatile, or OS-managed files
384405
# These exclusions always win regardless of section 9 allows above.

Containerfile

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,19 @@ RUN --mount=type=bind,from=ctx,source=/ctx,target=/ctx,ro \
7070
find /run -mindepth 1 -maxdepth 1 ! -name "secrets" -exec rm -rf {} + 2>/dev/null || true
7171

7272
RUN bootc completion bash > /etc/bash_completion.d/bootc
73-
RUN --mount=type=bind,from=ctx,source=/ctx/tools,target=/ctx/tools,ro \
74-
install -d -m 0755 /usr/lib/extensions/source && \
75-
bash /ctx/tools/mios-sysext-pack.sh /usr/lib/extensions/source || true
73+
# System-extension pack step: intentionally a no-op when no sysext source
74+
# trees are staged in the image. The pack tool at tools/mios-sysext-pack.sh
75+
# consolidates one-or-more `/usr/lib/extensions/source-*` trees into a single
76+
# monolithic SquashFS sysext (mitigation for the overlayfs stacking-depth
77+
# limit on bootc systems). The current build is FHS-overlay-only and stages
78+
# no sysext sources, so this step skips silently. To start packing sysexts:
79+
# 1. Have an earlier automation/*.sh phase populate
80+
# /usr/lib/extensions/source-<name>/{usr,etc,...} with the files to pack.
81+
# 2. Re-enable the RUN below (un-comment), passing every populated source
82+
# dir as a positional argument to mios-sysext-pack.sh.
83+
#
84+
# RUN --mount=type=bind,from=ctx,source=/ctx/tools,target=/ctx/tools,ro \
85+
# bash /ctx/tools/mios-sysext-pack.sh /usr/lib/extensions/source-*
7686
RUN ostree container commit
7787
# bootc container lint MUST be the final instruction (ARCHITECTURAL LAW 4).
7888
RUN bootc container lint

automation/05-enable-external-repos.sh

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,37 +25,41 @@ source "$(dirname "$0")/lib/common.sh"
2525

2626
REPO_DIR=/etc/yum.repos.d
2727

28+
# Best-effort policy. Every external repo enable here is non-critical:
29+
# downstream installs already pass --skip-unavailable, so a missing repo
30+
# silently drops its packages rather than failing the build. Wrap every
31+
# external fetch / dnf op in warn-on-failure + clean partials, and keep
32+
# the script's overall exit at 0 (warnings are tracked separately by the
33+
# build chain summary).
34+
35+
# Try a curl fetch into a target file. On failure: warn, delete partial,
36+
# return 1 so callers can short-circuit downstream key import / sed.
37+
try_fetch() {
38+
local url="$1" out="$2" label="$3"
39+
if scurl -fsSL --connect-timeout 20 --max-time 60 "$url" -o "$out" 2>/dev/null; then
40+
return 0
41+
fi
42+
warn "${label}: fetch failed (${url}) -- skipping"
43+
rm -f "$out"
44+
return 1
45+
}
46+
2847
# --- 1. Terra (fyralabs) ----------------------------------------------------
2948
# Patched WINE/Mesa/miscellaneous packages missing from Fedora + RPM Fusion.
3049
if [[ ! -f "${REPO_DIR}/terra.repo" ]]; then
3150
log "enabling Terra repo (fyralabs)"
32-
if ! scurl -fsSL --connect-timeout 20 --max-time 60 \
33-
https://github.com/terrapkg/subatomic-repos/raw/main/terra.repo \
34-
-o "${REPO_DIR}/terra.repo" 2>/dev/null; then
35-
warn "Terra repo download failed (github.com unreachable?) -- skipping Terra"
36-
fi
51+
try_fetch "https://github.com/terrapkg/subatomic-repos/raw/main/terra.repo" \
52+
"${REPO_DIR}/terra.repo" "Terra repo" || true
3753
else
3854
log "Terra repo already present -- skipping"
3955
fi
4056

41-
# --- 2. VSCodium (FOSS) ------------------------------------------------------
42-
if [[ ! -f "${REPO_DIR}/vscodium.repo" ]]; then
43-
log "enabling VSCodium repo (FOSS)"
44-
scurl -fsSL https://gitlab.com/paulcarroty/vscodium-deb-rpm-repo/raw/master/pub.gpg -o /tmp/vscodium.gpg
45-
rpm --import /tmp/vscodium.gpg && rm -f /tmp/vscodium.gpg
46-
cat > "${REPO_DIR}/vscodium.repo" <<'EOF'
47-
[vscodium]
48-
name=VSCodium
49-
baseurl=https://download.vscodium.com/rpms/
50-
enabled=1
51-
autorefresh=1
52-
type=rpm-md
53-
gpgcheck=1
54-
gpgkey=https://gitlab.com/paulcarroty/vscodium-deb-rpm-repo/raw/master/pub.gpg
55-
EOF
56-
else
57-
log "VSCodium repo already present -- skipping"
58-
fi
57+
# --- 2. (removed) VSCodium ---------------------------------------------------
58+
# PROJECT INVARIANT: applications ship as Flatpaks, only system dependencies
59+
# ship as RPMs. VSCodium is an application -- it ships from Flathub via
60+
# MIOS_FLATPAKS / the Flatpak install path, never from an RPM repo. The
61+
# previous VSCodium .repo / gpg-import block was removed for this reason.
62+
# If any other app RPM repo creeps into this script, delete it the same way.
5963

6064
# --- 7. Kubernetes stable v1.32 (kubectl) -----------------------------------
6165
# kubectl is NOT in standard Fedora repos -- must come from the Kubernetes
@@ -84,12 +88,12 @@ fi
8488
# Using Fedora 44 repo endpoint; COPR auto-publishes new packages as they land.
8589
if [[ ! -f "${REPO_DIR}/ublue-os-packages.repo" ]]; then
8690
log "enabling ublue-os/packages COPR (uupd + greenboot)"
87-
scurl -fsSL \
88-
"https://copr.fedorainfracloud.org/coprs/ublue-os/packages/repo/fedora-44/ublue-os-packages-fedora-44.repo" \
89-
-o "${REPO_DIR}/ublue-os-packages.repo"
90-
# Lower priority than Fedora base so Fedora wins on conflicting packages.
91-
if ! grep -q '^priority=' "${REPO_DIR}/ublue-os-packages.repo"; then
92-
sed -i '/^\[/a priority=75' "${REPO_DIR}/ublue-os-packages.repo"
91+
if try_fetch "https://copr.fedorainfracloud.org/coprs/ublue-os/packages/repo/fedora-44/ublue-os-packages-fedora-44.repo" \
92+
"${REPO_DIR}/ublue-os-packages.repo" "ublue-os/packages COPR"; then
93+
# Lower priority than Fedora base so Fedora wins on conflicting packages.
94+
if ! grep -q '^priority=' "${REPO_DIR}/ublue-os-packages.repo"; then
95+
sed -i '/^\[/a priority=75' "${REPO_DIR}/ublue-os-packages.repo"
96+
fi
9397
fi
9498
else
9599
log "ublue-os/packages COPR already present -- skipping"
@@ -98,7 +102,9 @@ fi
98102
# ── Waydroid (Aleasto) ───────────────────────────────────────────────────
99103
if ! [ -f /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:aleasto:waydroid.repo ]; then
100104
log "enabling aleasto/waydroid COPR (GNOME 50 fix)"
101-
$DNF_BIN "${DNF_SETOPT[@]}" copr enable -y aleasto/waydroid
105+
if ! $DNF_BIN "${DNF_SETOPT[@]}" copr enable -y aleasto/waydroid 2>/dev/null; then
106+
warn "aleasto/waydroid COPR enable failed -- skipping (GNOME 50 fix unavailable)"
107+
fi
102108
else
103109
log "aleasto/waydroid COPR already present -- skipping"
104110
fi
@@ -108,8 +114,8 @@ fi
108114
# Tailscale repo keeps it at the latest stable regardless of ucore cadence.
109115
if [[ ! -f "${REPO_DIR}/tailscale.repo" ]]; then
110116
log "enabling Tailscale official repo"
111-
scurl -fsSL https://pkgs.tailscale.com/stable/fedora/tailscale.repo \
112-
-o "${REPO_DIR}/tailscale.repo"
117+
try_fetch "https://pkgs.tailscale.com/stable/fedora/tailscale.repo" \
118+
"${REPO_DIR}/tailscale.repo" "Tailscale repo" || true
113119
else
114120
log "Tailscale repo already present -- skipping"
115121
fi
@@ -124,13 +130,18 @@ if [[ ! -f "${REPO_DIR}/crowdsec.repo" ]]; then
124130
# The 'dist' query parameter pins the packagecloud distro release;
125131
# crowdsec ships a single fedora repo across releases (the value is
126132
# only used for substituting $releasever in baseurl).
127-
scurl -fsSL "https://packagecloud.io/crowdsec/crowdsec/config_file.repo?os=fedora&dist=44&source=script" \
128-
-o "${REPO_DIR}/crowdsec.repo"
133+
try_fetch "https://packagecloud.io/crowdsec/crowdsec/config_file.repo?os=fedora&dist=44&source=script" \
134+
"${REPO_DIR}/crowdsec.repo" "CrowdSec repo" || true
129135
else
130136
log "CrowdSec repo already present -- skipping"
131137
fi
132138

133139
log "external repos enabled; refreshing metadata"
134-
$DNF_BIN "${DNF_SETOPT[@]}" makecache -y
140+
# makecache is best-effort: if a single repo's metadata is unreachable
141+
# the next dnf install in this build will retry it. Hard-failing here
142+
# wastes the entire build for a transient mirror hiccup.
143+
if ! $DNF_BIN "${DNF_SETOPT[@]}" makecache -y 2>&1 | tail -20; then
144+
warn "dnf makecache returned non-zero -- continuing (downstream installs will retry per-repo)"
145+
fi
135146

136147
log "05-enable-external-repos.sh complete"

automation/12-virt.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ install_packages "containers"
2626
# Extra self-build tools (image-rechunking, etc. - may be repo-dependent)
2727
install_packages "self-build"
2828

29+
# ── Build toolchain (image-build only; stripped by 91-strip-build-toolchain.sh) ──
30+
# Required by 19-k3s-selinux.sh (SELinux module compile) and
31+
# 53-bake-lookingglass-client.sh (Looking Glass B7 from source). Removed
32+
# before image commit so the runtime carries no compilers.
33+
echo "[12-virt] Installing build toolchain (image-build only; will be stripped)..."
34+
install_packages "build-toolchain"
35+
2936
# ── Cockpit Web Management ──────────────────────────────────────────────────
3037
echo "[12-virt] Installing Cockpit..."
3138
install_packages_strict "cockpit"

automation/37-aichat.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
#!/bin/bash
22
# 37-aichat: Install AIChat and AIChat-NG Rust CLI tools
3+
#
4+
# OPEN TASK (project invariant: VM | Container | Flatpak only):
5+
# These are user-facing AI CLI applications. Per the project-wide
6+
# delivery rule, they should run inside a Distrobox container, not
7+
# directly on the host. The current /usr/bin install is transitional;
8+
# the migration is to package them as a Distrobox container with
9+
# wrapper scripts in /usr/bin that exec into the container. See
10+
# usr/share/mios/PACKAGES.md > "AI Tools" section for context.
311
set -euo pipefail
412
# shellcheck source=lib/common.sh
513
source "$(dirname "$0")/lib/common.sh"
614
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
715
source "${SCRIPT_DIR}/lib/packages.sh"
816

9-
echo "[37-aichat] Installing AI-related packages (redis, sqlite)..."
17+
echo "[37-aichat] Resolving 'ai' package block (currently empty -- aichat/aichat-ng ship as tarballs, not RPMs)..."
1018
install_packages "ai"
1119

1220
echo "[37-aichat] Installing AIChat and AIChat-NG binaries..."

0 commit comments

Comments
 (0)