feat(#2050): set matrix avatar in the daemon over the live client
This commit is contained in:
parent
63e9e80232
commit
c99fa714d9
8 changed files with 118 additions and 210 deletions
|
|
@ -165,9 +165,8 @@ Tree-shape version:
|
|||
`systemd.globalEnvironment` in `meta.rs` for every container
|
||||
(manager included), so all token/state paths resolve through
|
||||
`$HYPERHIVE_STATE_DIR`. The harness-base shell scripts
|
||||
(tea-login, forge-avatar-sync, matrix-avatar-sync) simplified
|
||||
from glob+for loops to a direct `$HYPERHIVE_STATE_DIR/<token>`
|
||||
read.
|
||||
(tea-login, forge-avatar-sync) simplified from glob+for loops to a
|
||||
direct `$HYPERHIVE_STATE_DIR/<token>` read.
|
||||
|
||||
### E — prompt + tools
|
||||
|
||||
|
|
|
|||
|
|
@ -474,10 +474,10 @@ the state dir at provisioning time:
|
|||
- `forge-avatar-sync` — uploads `hyperhive.icon` SVG to the
|
||||
agent's Forgejo profile, so the icon shows up on commits / PRs /
|
||||
issue comments.
|
||||
- `matrix-avatar-sync` — same idea for matrix profile avatars
|
||||
(two-step `media upload` → `set avatar_url` dance — see
|
||||
`docs/persistence.md::matrix-avatar-sync` for the protocol
|
||||
detail).
|
||||
|
||||
(The matrix profile avatar is **not** a oneshot — `hive-matrix-daemon`
|
||||
sets it over its live authenticated Client; see
|
||||
`docs/persistence.md::matrix avatar`.)
|
||||
|
||||
Shape contract — every one of these:
|
||||
|
||||
|
|
@ -498,7 +498,7 @@ Shape contract — every one of these:
|
|||
5. **Re-runnable**: a second invocation produces the same final
|
||||
state (idempotent uploads, idempotent config rewrites). Used
|
||||
by the `.path` watchers that re-fire on token appearance (see
|
||||
`docs/persistence.md::matrix-avatar-sync`).
|
||||
`docs/persistence.md::Matrix per-agent daemon`).
|
||||
|
||||
The artefact lives under the agent user's home where applicable
|
||||
(`~/.config/tea/config.yml`) and is chown'd to that user, but the
|
||||
|
|
|
|||
|
|
@ -396,39 +396,35 @@ agent containers come up. Without the path-trigger sibling
|
|||
first time it ran and the MCP would have no backend until the next
|
||||
restart. The `.path` unit makes the appearance of the token re-fire
|
||||
the service so the daemon comes alive in the same boot cycle as
|
||||
provisioning. `matrix-avatar-sync.path` uses the same pattern for
|
||||
the icon-upload oneshot.
|
||||
provisioning. The same token watcher also drives avatar setting: on a
|
||||
restart the daemon re-runs each account's bring-up, which sets the
|
||||
avatar (see below).
|
||||
|
||||
### matrix-avatar-sync (two-step media + profile dance)
|
||||
### matrix avatar (set by the daemon over the live Client)
|
||||
|
||||
Mirrors the forge-avatar oneshot's shape (`docs/conventions.md::
|
||||
Best-effort oneshot services`) but differs in protocol: matrix
|
||||
avatars are a two-step `POST /media/r0/upload` → `PUT
|
||||
/profile/<user_id>/avatar_url` dance, both authenticated by the
|
||||
`access_token` written by `hive-c0re::matrix::ensure_user_for` to
|
||||
`<state>/matrix-token`.
|
||||
The agent icon (`hyperhive.icon`, an SVG) is published as each matrix
|
||||
account's profile avatar by `hive-matrix-daemon` itself
|
||||
(`hive-matrix-mcp::client::sync_avatar`), not a separate oneshot. After
|
||||
the daemon builds + restores an account's `Client` (authenticated,
|
||||
pointed at that account's resolved homeserver), it calls matrix-sdk's
|
||||
`account().upload_avatar()` — one call that uploads the media and sets
|
||||
`avatar_url`. Because it reuses the live Client, there is no hardcoded
|
||||
homeserver URL, no token re-read, and no token-file globbing: the daemon
|
||||
already iterates every configured + dashboard-discovered account in its
|
||||
bring-up loop, so the avatar is set for **every** account.
|
||||
|
||||
The oneshot syncs the avatar for **every** account, not just the
|
||||
hive-internal one: a `sync_one` shell function runs the upload + set
|
||||
dance for the bare `matrix-token` (`main`, against `localhost:8008`)
|
||||
and then for each dashboard-provisioned `matrix-token-<name>`, reading
|
||||
that account's homeserver from its `matrix-account-<name>.json` sidecar
|
||||
(mirrors `accounts::discover_token_accounts`). Idempotency is
|
||||
**per-account**: a `matrix-avatar-icon-hash[-<name>]` file each, because
|
||||
the `mxc://` URI is homeserver-scoped so every account uploads to its own
|
||||
homeserver independently.
|
||||
Nix rasterizes the SVG to a 512x512 PNG at build time (`iconPng`, via
|
||||
librsvg) and forwards its store path as `HIVE_ICON_PNG` on the daemon
|
||||
unit, gated on `hyperhive.icon != null`. No icon configured → the env is
|
||||
unset → `sync_avatar` returns early and no avatar is set.
|
||||
|
||||
Triggered by EITHER boot (`wantedBy = multi-user.target`) OR the
|
||||
sibling `matrix-avatar-sync.path` firing on token appearance — its glob
|
||||
covers both `matrix-token` and `matrix-token-*`, so an account logged in
|
||||
after boot gets its avatar without a restart. Both paths re-run the
|
||||
oneshot idempotently — running the avatar set twice is harmless.
|
||||
|
||||
Critically: **`RemainAfterExit = false`** (not the more common
|
||||
`true` for oneshots). systemd treats `RemainAfterExit = true`
|
||||
oneshots as "still running" after the first exit, so the second
|
||||
trigger from the `.path` watcher becomes a no-op. Setting it to
|
||||
`false` lets re-fires actually re-execute. The trade-off is the
|
||||
service unit shows `inactive (dead)` between fires — visible in
|
||||
`journalctl` but harmless; the `.path` unit drives the lifecycle.
|
||||
Idempotency is **per-account**: an `avatar-icon-hash` file in each
|
||||
account's matrix-sdk `state_dir`. The daemon hashes the PNG bytes and
|
||||
skips the upload when unchanged, because every upload mints a fresh
|
||||
`mxc://` URI that emits a profile state event in every joined room —
|
||||
re-uploading identical bytes is timeline spam. A dashboard-provisioned
|
||||
account gets its avatar when the `systemd.paths.hive-matrix-daemon` token
|
||||
watcher restarts the daemon (which re-runs the per-account bring-up), so
|
||||
no separate avatar trigger is needed. Avatar failures are swallowed
|
||||
(logged, non-fatal) so they never break account bring-up or sync.
|
||||
|
||||
|
|
|
|||
|
|
@ -317,9 +317,9 @@ hyperhive.icon = ./icon.svg; # default: null (falls back to shared hyperhive lo
|
|||
Path to an SVG file used as this agent's visual identity — shown in
|
||||
the per-agent page header, as the page favicon, and uploaded to the
|
||||
agent's Forgejo profile avatar (via the `forge-avatar-sync` boot
|
||||
unit) and Matrix profile avatar (via `matrix-avatar-sync`). Commit
|
||||
the SVG next to `agent.nix` in the config repo and reference it as a
|
||||
relative path.
|
||||
unit) and Matrix profile avatar (set by `hive-matrix-daemon` over its
|
||||
live Client). Commit the SVG next to `agent.nix` in the config repo
|
||||
and reference it as a relative path.
|
||||
|
||||
When `null` (the default), the agent falls back to the shared
|
||||
hyperhive branding mark. The harness serves whichever icon is active
|
||||
|
|
|
|||
|
|
@ -1011,7 +1011,7 @@ where
|
|||
# `environment.variables` only writes /etc/environment (login
|
||||
# shells); `systemd.globalEnvironment` is the analogue for
|
||||
# systemd units so tea-login / forge-avatar-sync /
|
||||
# matrix-avatar-sync etc. can read `$HYPERHIVE_STATE_DIR`
|
||||
# hive-matrix-daemon etc. can read `$HYPERHIVE_STATE_DIR`
|
||||
# without each service having to redeclare it.
|
||||
environment.variables = {
|
||||
HIVE_LABEL = name;
|
||||
|
|
|
|||
|
|
@ -184,3 +184,57 @@ async fn whoami(homeserver: &str, token: &str) -> Result<(OwnedUserId, OwnedDevi
|
|||
let device_id: OwnedDeviceId = device_id_raw.into();
|
||||
Ok((user_id, device_id))
|
||||
}
|
||||
|
||||
/// Best-effort: set this account's matrix avatar from the rasterized icon
|
||||
/// PNG. The path comes from `HIVE_ICON_PNG` (a nix-built derivation the
|
||||
/// harness forwards into the daemon env; absent when no icon resolves).
|
||||
/// Idempotent via a per-account `avatar-icon-hash` file in the account's
|
||||
/// sdk `state_dir` — a re-upload only happens when the icon bytes change,
|
||||
/// so daemon restarts don't re-spam the homeserver.
|
||||
///
|
||||
/// This replaces the old `matrix-avatar-sync` systemd curl oneshot: the
|
||||
/// daemon already holds an authenticated `Client` pointed at the correct
|
||||
/// homeserver, so it uploads over the live connection — no hardcoded URL,
|
||||
/// no token re-read, no token-file globbing. Any failure is logged and
|
||||
/// swallowed: avatar trouble must never break account bring-up or sync.
|
||||
pub async fn sync_avatar(client: &Client, state_dir: &Path, account: &str) {
|
||||
let Ok(png_path) = std::env::var("HIVE_ICON_PNG") else {
|
||||
return; // no icon forwarded → nothing to sync
|
||||
};
|
||||
let bytes = match fs::read(&png_path).await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
tracing::warn!(account, path = %png_path, error = %e, "matrix avatar: icon PNG unreadable; skipping");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// A non-crypto content hash is enough to answer "did the icon change?".
|
||||
let hash = {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
bytes.hash(&mut h);
|
||||
format!("{:016x}", h.finish())
|
||||
};
|
||||
let hash_file = state_dir.join("avatar-icon-hash");
|
||||
if let Ok(prev) = fs::read_to_string(&hash_file).await
|
||||
&& prev.trim() == hash
|
||||
{
|
||||
tracing::debug!(account, "matrix avatar: icon unchanged; skipping upload");
|
||||
return;
|
||||
}
|
||||
match client
|
||||
.account()
|
||||
.upload_avatar(&mime::IMAGE_PNG, bytes)
|
||||
.await
|
||||
{
|
||||
Ok(mxc) => {
|
||||
tracing::info!(account, mxc = %mxc, "matrix avatar set");
|
||||
if let Err(e) = fs::write(&hash_file, &hash).await {
|
||||
tracing::warn!(account, error = %e, "matrix avatar: set ok but hash write failed (re-uploads next start)");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(account, error = %format!("{e:#}"), "matrix avatar: upload failed; skipping (non-fatal)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,6 +196,10 @@ async fn bring_up_account(
|
|||
client::build_and_restore(&homeserver, &cfg.token_file, &cfg.state_dir, is_primary)
|
||||
.await
|
||||
.with_context(|| format!("build matrix client for account {}", cfg.name))?;
|
||||
// Best-effort: sync the agent icon to this account's matrix avatar over
|
||||
// the live (authenticated, correct-homeserver) Client. Replaces the old
|
||||
// curl oneshot; failures are swallowed inside sync_avatar.
|
||||
client::sync_avatar(&client, &cfg.state_dir, &cfg.name).await;
|
||||
timeline::install_message_handler(&client, hyperhive_socket.to_path_buf(), tag.clone());
|
||||
|
||||
let sync_client = client.clone();
|
||||
|
|
|
|||
|
|
@ -61,6 +61,15 @@ let
|
|||
# cannot drift). Matches the daemon's own built-in default
|
||||
# (`paths::DEFAULT_HOMESERVER`).
|
||||
matrixUrlDefault = "http://localhost:8008";
|
||||
# Rasterize the operator-set agent icon (`hyperhive.icon`, an SVG) to a
|
||||
# 512x512 PNG so the matrix daemon can upload it as each account's avatar
|
||||
# over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar).
|
||||
# Replaces the old `matrix-avatar-sync` curl oneshot. Only forced when an
|
||||
# icon is configured — the `HIVE_ICON_PNG` daemon-env entry is gated on
|
||||
# `hyperhive.icon != null`, so this binding stays lazy when no icon is set.
|
||||
iconPng = pkgs.runCommand "hive-agent-icon.png" { nativeBuildInputs = [ pkgs.librsvg ]; } ''
|
||||
rsvg-convert -f png -w 512 -h 512 ${config.hyperhive.icon} -o $out
|
||||
'';
|
||||
in
|
||||
{
|
||||
# Shared scaffolding for every hyperhive harness container.
|
||||
|
|
@ -406,7 +415,7 @@ in
|
|||
daemon auto-skips when `<state>/matrix-token` is missing,
|
||||
and a `systemd.paths` watcher restarts it the moment
|
||||
hive-c0re provisions the token (same path-trigger shape
|
||||
as `matrix-avatar-sync`).
|
||||
as `forge-avatar-sync`).
|
||||
- exposes the matrix tool surface (send_message, send_dm,
|
||||
send_reaction, send_reply, mark_read, list_rooms,
|
||||
list_room_members, read_room) to claude via an auto-injected
|
||||
|
|
@ -1433,8 +1442,8 @@ in
|
|||
};
|
||||
|
||||
# Path-trigger sibling: re-fires forge-avatar-sync the moment
|
||||
# `<state>/forge-token` appears. Mirrors the matrix-avatar-sync
|
||||
# pattern — on first agent deployment the container boots before
|
||||
# `<state>/forge-token` appears. Mirrors the hive-matrix-daemon
|
||||
# token-watcher pattern — on first agent deployment the container boots before
|
||||
# hive-c0re has provisioned the forge-token, so the service fires
|
||||
# too early and exits with "no forge-token found". Without this path
|
||||
# unit, RemainAfterExit=true would prevent systemd from ever
|
||||
|
|
@ -1553,6 +1562,12 @@ in
|
|||
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }
|
||||
) config.hyperhive.matrixAccounts
|
||||
);
|
||||
}
|
||||
# Rasterized agent icon path for the daemon's avatar sync. Only set
|
||||
# when an icon is configured; absent → the daemon skips avatar setting
|
||||
# (hive-matrix-mcp::client::sync_avatar returns early on unset env).
|
||||
// lib.optionalAttrs (config.hyperhive.icon != null) {
|
||||
HIVE_ICON_PNG = "${iconPng}";
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon";
|
||||
|
|
@ -1642,166 +1657,6 @@ in
|
|||
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token*";
|
||||
};
|
||||
|
||||
# Path-trigger sibling: re-fires matrix-avatar-sync the moment a
|
||||
# token appears - both the hive-internal `matrix-token` and any
|
||||
# dashboard-provisioned extra `matrix-token-<name>` (so an external
|
||||
# account logged in after boot gets its avatar without a restart).
|
||||
# Same first-boot-ordering pattern as hive-matrix-daemon above.
|
||||
systemd.paths.matrix-avatar-sync = {
|
||||
description = "trigger matrix-avatar-sync when a matrix token appears";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
# Single `matrix-token*` glob (same as hive-matrix-daemon above) so
|
||||
# both the hive-internal `matrix-token` and any dashboard-provisioned
|
||||
# `matrix-token-<name>` re-fire the sync. A list value here is
|
||||
# unverified for `systemd.paths.*.pathConfig`, and the wildcard
|
||||
# covers both cases anyway.
|
||||
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token*";
|
||||
};
|
||||
|
||||
# One-shot: hyperhive.icon → matrix profile avatar (two-step media
|
||||
# upload + set avatar_url). Shape contract:
|
||||
# docs/conventions.md::Best-effort oneshot services. Protocol +
|
||||
# why RemainAfterExit = false:
|
||||
# docs/persistence.md::matrix-avatar-sync.
|
||||
systemd.services.matrix-avatar-sync = {
|
||||
description = "sync agent icon to matrix profile avatar (best-effort)";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
# No `after = [ "tea-login.service" ]` — matrix has no
|
||||
# equivalent prerequisite; we just need the homeserver up.
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
# RemainAfterExit = false so the .path trigger can re-fire
|
||||
# the unit (see docs/persistence.md::matrix-avatar-sync).
|
||||
RemainAfterExit = false;
|
||||
# Pin the journal identity (else it's the `script` store-path wrapper).
|
||||
SyslogIdentifier = "matrix-avatar-sync";
|
||||
};
|
||||
path = [
|
||||
pkgs.curl
|
||||
pkgs.coreutils
|
||||
pkgs.jq
|
||||
pkgs.librsvg
|
||||
];
|
||||
script = ''
|
||||
ICON=/etc/hyperhive/icon.svg
|
||||
if [ ! -f "$ICON" ]; then
|
||||
echo "matrix-avatar-sync: no icon configured; skipping"
|
||||
exit 0
|
||||
fi
|
||||
# Hash-based idempotency: skip the upload if the icon hasn't
|
||||
# changed since the last successful sync. Every upload mints a
|
||||
# new mxc:// URI which triggers a profile state event in every
|
||||
# joined room — uploading the same bytes again produces timeline
|
||||
# spam without changing the visible avatar. Hash files live in
|
||||
# $HYPERHIVE_STATE_DIR (survives restart, wiped on purge so
|
||||
# purge + re-provision gets a fresh upload). One hash file per
|
||||
# account (the mxc:// URI is homeserver-scoped, so each account
|
||||
# uploads to its own homeserver independently). Delete to force
|
||||
# a re-upload.
|
||||
CURRENT_HASH=$(sha256sum "$ICON" | cut -d' ' -f1)
|
||||
|
||||
# Rasterize SVG → PNG ONCE; the same bytes are reused for every
|
||||
# account (matrix media accepts any image type but we already
|
||||
# standardise on PNG for the forge sync).
|
||||
PNG=$(mktemp --suffix=.png)
|
||||
if ! rsvg-convert -f png -w 512 -h 512 "$ICON" -o "$PNG" 2>/dev/null; then
|
||||
echo "matrix-avatar-sync: rsvg-convert failed; skipping"
|
||||
rm -f "$PNG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# sync_one <token-file> <matrix-url> <hash-suffix> <label>
|
||||
# Two-step media upload + avatar_url set for a single account.
|
||||
# Best-effort: every failure path logs + returns 0 so one bad
|
||||
# account never aborts the others.
|
||||
sync_one() {
|
||||
token_file=$1
|
||||
matrix_url=$2
|
||||
hash_suffix=$3
|
||||
label=$4
|
||||
if [ ! -f "$token_file" ]; then
|
||||
return 0
|
||||
fi
|
||||
hash_file="$HYPERHIVE_STATE_DIR/matrix-avatar-icon-hash$hash_suffix"
|
||||
if [ -f "$hash_file" ] && [ "$(cat "$hash_file" 2>/dev/null)" = "$CURRENT_HASH" ]; then
|
||||
echo "matrix-avatar-sync[$label]: icon unchanged (hash matches); skipping"
|
||||
return 0
|
||||
fi
|
||||
token=$(cat "$token_file")
|
||||
# whoami → user_id. Needed to scope the avatar set call.
|
||||
# Tolerant of the homeserver being unreachable (`-f` makes
|
||||
# curl fail on 4xx/5xx; `|| true` swallows the exit).
|
||||
user_id=$(curl -sf --max-time 5 \
|
||||
-H "Authorization: Bearer $token" \
|
||||
"$matrix_url/_matrix/client/v3/account/whoami" 2>/dev/null \
|
||||
| jq -r '.user_id // empty' || true)
|
||||
if [ -z "$user_id" ]; then
|
||||
echo "matrix-avatar-sync[$label]: whoami failed or homeserver unreachable; skipping"
|
||||
return 0
|
||||
fi
|
||||
# Step 1: upload bytes → mxc:// URI.
|
||||
mxc=$(curl -sf --max-time 10 \
|
||||
-X POST "$matrix_url/_matrix/media/v3/upload" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: image/png" \
|
||||
--data-binary "@$PNG" 2>/dev/null \
|
||||
| jq -r '.content_uri // empty' || true)
|
||||
if [ -z "$mxc" ]; then
|
||||
echo "matrix-avatar-sync[$label]: media upload failed; skipping"
|
||||
return 0
|
||||
fi
|
||||
# Step 2: set avatar_url on the profile.
|
||||
payload=$(jq -n --arg url "$mxc" '{avatar_url:$url}')
|
||||
code=$(curl -s --max-time 10 \
|
||||
-X PUT "$matrix_url/_matrix/client/v3/profile/$user_id/avatar_url" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
-o /dev/null -w "%{http_code}" 2>/dev/null || true)
|
||||
if [ "$code" = "200" ]; then
|
||||
echo "matrix-avatar-sync[$label]: avatar set on $user_id"
|
||||
# Persist hash so subsequent runs skip the upload when the
|
||||
# icon hasn't changed.
|
||||
echo "$CURRENT_HASH" > "$hash_file"
|
||||
else
|
||||
echo "matrix-avatar-sync[$label]: avatar PUT returned HTTP $code — skipping (non-fatal)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Hive-internal `main` account: bare `matrix-token`, written by
|
||||
# `hive-c0re::matrix::ensure_user_for` to the agent's
|
||||
# bind-mounted state dir. $HYPERHIVE_STATE_DIR is set system-wide
|
||||
# by the meta flake (systemd.globalEnvironment) to
|
||||
# `/agents/<name>/state`. Local tuwunel reachable on the shared
|
||||
# host netns at the default matrix-spec port.
|
||||
sync_one "$HYPERHIVE_STATE_DIR/matrix-token" "http://localhost:8008" "" "main"
|
||||
|
||||
# Dashboard-provisioned extra accounts: each is a
|
||||
# `matrix-token-<name>` file plus a `matrix-account-<name>.json`
|
||||
# sidecar carrying its (possibly external) homeserver. Mirrors
|
||||
# hive-matrix-mcp::accounts::discover_token_accounts — a token
|
||||
# without a sidecar is skipped because the homeserver is unknown.
|
||||
for token_file in "$HYPERHIVE_STATE_DIR"/matrix-token-*; do
|
||||
[ -f "$token_file" ] || continue
|
||||
name=''${token_file##*/matrix-token-}
|
||||
[ -n "$name" ] || continue
|
||||
sidecar="$HYPERHIVE_STATE_DIR/matrix-account-$name.json"
|
||||
if [ ! -f "$sidecar" ]; then
|
||||
echo "matrix-avatar-sync[$name]: no homeserver sidecar; skipping"
|
||||
continue
|
||||
fi
|
||||
homeserver=$(jq -r '.homeserver // empty' "$sidecar" 2>/dev/null || true)
|
||||
if [ -z "$homeserver" ]; then
|
||||
echo "matrix-avatar-sync[$name]: empty homeserver in sidecar; skipping"
|
||||
continue
|
||||
fi
|
||||
sync_one "$token_file" "$homeserver" "-$name" "$name"
|
||||
done
|
||||
|
||||
rm -f "$PNG"
|
||||
'';
|
||||
};
|
||||
|
||||
# Write declared dashboardLinks to the state dir so hive-c0re can
|
||||
# read them without accessing the container's /etc/ from the host.
|
||||
# Best-effort oneshot (always exit 0):
|
||||
|
|
|
|||
Loading…
Reference in a new issue