Compare commits
5 Commits
fabf1274aa
...
v1.8.14
| Author | SHA1 | Date | |
|---|---|---|---|
| 361babd4fd | |||
| 70e675afee | |||
| 601fc98a49 | |||
| 7e0e66997d | |||
| 6bd8b79d3a |
@@ -87,6 +87,35 @@ SSH_KEY_PATH=~/.ssh
|
||||
# MEMPALACE_PI_REMOTE_PATH=/data/feed
|
||||
# MEMPALACE_PI_DEVICE=
|
||||
|
||||
# ── Mailbox notification: MUST BE NAMED, auto-detect CANNOT work here ──
|
||||
# The mempalace extension polls the logstream for fleet asks addressed to this
|
||||
# device and queues them into the next turn. That part needs no config. The
|
||||
# NOTIFICATION that tells the human it happened does, and unset means SILENT
|
||||
# outside the pi TUI.
|
||||
#
|
||||
# Why there is no working default: terminal identity lives in env vars set by
|
||||
# the emulator (KITTY_WINDOW_ID, TERM_PROGRAM) and `docker exec` does NOT
|
||||
# forward them — inside the container pi sees only TERM=xterm-256color no matter
|
||||
# what is rendering it. So "desktop" auto-detection always falls through to
|
||||
# OSC 777, which Kitty does not implement, and the notification silently does
|
||||
# nothing: the worst outcome for a feature whose only job is to break a silence.
|
||||
# Naming the protocol is what makes it fire.
|
||||
#
|
||||
# kitty OSC 99 desktop notification (correct for Kitty, incl. over SSH)
|
||||
# osc777 OSC 777 (tmux/iTerm2/foot and others)
|
||||
# desktop OSC 99 if KITTY_WINDOW_ID is visible, else OSC 777 — inside a
|
||||
# container that means effectively always OSC 777, so prefer naming
|
||||
# 0 / off suppress entirely (in-TUI notify still shows)
|
||||
# MEMPALACE_MAILBOX_NOTIFY=kitty
|
||||
#
|
||||
# Cadence, if the delivery ever feels late: the poll is coupled to session
|
||||
# activity (it runs when the agent settles), NOT to a wall clock.
|
||||
# MEMPALACE_MAILBOX_POLL_MS is therefore a FLOOR BETWEEN POLLS (default 300000),
|
||||
# not a promise of one every 5 minutes — an idle session polls zero times, and
|
||||
# session start does the first look.
|
||||
# MEMPALACE_MAILBOX_POLL_MS=300000
|
||||
# MEMPALACE_MAILBOX_RESURFACE_MS=3600000
|
||||
|
||||
# ── LAN access from the container (host-OS-agnostic) ─────────────────
|
||||
# On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't
|
||||
# reach the host's directly-attached LAN peers by default. The entrypoint
|
||||
|
||||
@@ -157,7 +157,41 @@ jobs:
|
||||
# buildcache silently reuses the layer from whatever pi version was
|
||||
# current when the cache was first populated. Same class of bug as
|
||||
# pi-devbox v0.74.0..v0.75.5 (fixed in v0.75.5b 2026-05-23).
|
||||
# ── release gate ──────────────────────────────────────────────
|
||||
# Refuse to spend a base build on a tree whose own shell scripts do not lint.
|
||||
#
|
||||
# v1.8.14's first attempt is why this exists. smoke and smoke-studio both failed
|
||||
# at scripts/smoke-test.sh:770 AFTER build-base had already spent ~46 minutes,
|
||||
# on a defect shellcheck had flagged as SC2289 (severity error) a day earlier:
|
||||
# the lint workflow went red on the very push that introduced it (run 186) and
|
||||
# stayed red for runs 187 and 188, unread.
|
||||
#
|
||||
# lint.yml deliberately does not run on tag pushes, and its reasoning is sound
|
||||
# (the tagged tree was already linted on main; a tag-ref lint run sorts above
|
||||
# the publish run and makes a release look finished before anything ships). The
|
||||
# missing invariant was never "lint the tag" -- it was "do not RELEASE a tree
|
||||
# whose lint failed", and only a job inside THIS workflow can enforce that.
|
||||
#
|
||||
# ~40 s, ahead of everything expensive, and it runs scripts/lint-shell.sh --
|
||||
# the same file lint.yml calls, not a second copy that drifts.
|
||||
lint-gate:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install shellcheck
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends shellcheck
|
||||
|
||||
- name: "Shellcheck + syntax-check repository scripts (severity: error)"
|
||||
run: bash scripts/lint-shell.sh
|
||||
|
||||
resolve-versions:
|
||||
# Gated: a defective tree must not reach a 46-minute base build.
|
||||
needs: [lint-gate]
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
|
||||
@@ -75,31 +75,11 @@ jobs:
|
||||
# are shell scripts with no extension. -print0/mapfile -d '' so a path
|
||||
# with a space cannot silently split, and the file count is asserted
|
||||
# non-zero — a green tick over an empty file set is not a check.
|
||||
run: |
|
||||
# Union of two signals, because either alone misses a real case:
|
||||
# a shebang scan misses a sourced fragment with no shebang, and a
|
||||
# *.sh glob misses the extensionless tools in rootfs/usr/local/bin/.
|
||||
# Silent skipping is precisely the failure mode this gate exists to
|
||||
# prevent, so err toward over-collecting.
|
||||
mapfile -d '' -t all_files < <(find . -not -path './.git/*' -type f -print0)
|
||||
sh_files=()
|
||||
for f in "${all_files[@]}"; do
|
||||
case "$f" in *.sh) sh_files+=("$f"); continue;; esac
|
||||
if head -n1 "$f" 2>/dev/null | grep -qE '^#!.*\b(bash|sh)\b'; then
|
||||
sh_files+=("$f")
|
||||
fi
|
||||
done
|
||||
echo "Checking ${#sh_files[@]} shell file(s)"
|
||||
if [ "${#sh_files[@]}" -eq 0 ]; then
|
||||
echo "::error::no shell files found — the shebang scan or the checkout is wrong"
|
||||
exit 1
|
||||
fi
|
||||
shellcheck -S error -f gcc "${sh_files[@]}"
|
||||
rc=0
|
||||
for f in "${sh_files[@]}"; do
|
||||
bash -n "$f" || { echo "::error file=$f::bash -n failed"; rc=1; }
|
||||
done
|
||||
exit "$rc"
|
||||
#
|
||||
# The implementation moved to scripts/lint-shell.sh on 2026-09-08 so the
|
||||
# release gate in docker-publish.yml runs the SAME code rather than a
|
||||
# second copy that drifts. Edit the script, not a copy of it.
|
||||
run: bash scripts/lint-shell.sh
|
||||
|
||||
- name: Gitea shell guard (catches the actionlint blind spot)
|
||||
# actionlint models GitHub Actions, where the default run shell is
|
||||
|
||||
+115
-1
@@ -11,7 +11,44 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
||||
|
||||
---
|
||||
|
||||
## Unreleased
|
||||
## v1.8.14 — 2026-09-08
|
||||
|
||||
> **First release attempt failed; fixed in this same entry.** The `smoke` and
|
||||
> `smoke-studio` jobs both failed at `scripts/smoke-test.sh:770` with
|
||||
> `agent-browser: command not found`, after `build-base` had already succeeded
|
||||
> (~46 min spent). Root cause was in the agent-browser execution guard added the
|
||||
> day before: the explanatory comment inside the **single-quoted** `exec_test`
|
||||
> body contained an apostrophe (`the fleet\'s`). Inside `'...'` bash treats a
|
||||
> backslash literally, so `\'` does not escape — it **closes the string**. The
|
||||
> body silently truncated (measured: `exec_test` received **12** arguments
|
||||
> instead of 2), and the remaining lines, including the `agent-browser --version`
|
||||
> assertion, were parsed by the **runner's** shell instead of executing inside
|
||||
> the image — and the runner has no agent-browser. The prose now lives above the
|
||||
> call, where an apostrophe is harmless.
|
||||
>
|
||||
> **The lint job had already caught this, and it went unread for 24 hours.**
|
||||
> `shellcheck` flagged it as `SC2289` at severity *error*, so the `actionlint`
|
||||
> job went red at run 186 on 2026-09-07 21:21 — the exact push that introduced
|
||||
> the guard — and stayed red for runs 187 and 188. `lint.yml` deliberately
|
||||
> excludes tag pushes (documented: the tagged tree was already linted on main,
|
||||
> and a tag-ref lint run would sort above the publish run), which is sound; the
|
||||
> broken assumption was different, namely that a tree whose lint FAILED would not
|
||||
> then be released. `docker-publish.yml` has no dependency on lint, so it built
|
||||
> for 50 minutes on a tree known to be defective.
|
||||
>
|
||||
> **Fixed, then gated.** The prose moved above the `exec_test` call so an
|
||||
> apostrophe cannot terminate anything, and the shell-lint logic moved out of
|
||||
> `lint.yml` into **`scripts/lint-shell.sh`** — now called by both `lint.yml` and
|
||||
> a new `lint-gate` job here that `resolve-versions` depends on. A release with a
|
||||
> lint error refuses in ~40 s instead of failing after fifty minutes. One copy,
|
||||
> not two: a duplicated check that drifts is the failure this repo keeps paying
|
||||
> for. The script also refuses to pass when `shellcheck` is absent, inheriting
|
||||
> the existing principle that a gate which cannot run must not pass.
|
||||
>
|
||||
> **`v1.8.14` was re-pointed** from `601fc98` to the fix commit. Nothing had
|
||||
> consumed the original tag — no `v1.8.14` image was ever published, only the
|
||||
> content-addressed `base-a365dd24de21`. `scripts/` does not feed the base hash,
|
||||
> so the re-run reuses that base and skips the 46-minute rebuild.
|
||||
|
||||
**A test that was quietly checking nothing, and a version number that was wrong.**
|
||||
Both found by delegating a read-only audit of this repo to a headless worker
|
||||
@@ -56,6 +93,83 @@ retraction of the earlier false "0.36.0 requires node >= 24" alert.
|
||||
No image content changes: `NODE_VERSION` still 22, no pins moved. This is a test
|
||||
and a docs correction only.
|
||||
|
||||
**The same bug class, twice in one file — and the second one was throwing away a
|
||||
proof the fleet cannot obtain any other way.** `scripts/smoke-test.sh`'s
|
||||
agent-browser guard captured the version *inside an `echo`, with `2>/dev/null`*:
|
||||
|
||||
```sh
|
||||
echo "resolved=[$r] version=[$(agent-browser --version 2>/dev/null | head -n1)]" >&2
|
||||
```
|
||||
|
||||
The exit code was discarded, so a binary that could not execute at all still
|
||||
**passed**, printing `version=[]`. Verified two-sided: a stub exiting 127 passes
|
||||
the old form and is caught by the new one.
|
||||
|
||||
Why that exit code matters more than most: smoke runs `platforms: linux/amd64` on
|
||||
an x86 runner, i.e. **native amd64**, making this line the fleet's only recurring
|
||||
amd64 runtime proof for agent-browser's `linux-x64` ELF. **No devbox can ever
|
||||
supply one** — every machine in the pi fleet is an Apple Silicon Mac
|
||||
(`mbp-m1-2020`; `tor-ms22` = Mac Studio `Mac13,1` M1 Max, verified 2026-08-17 by
|
||||
`system_profiler`; `emb-7kj4vr4g` = Apple Silicon, verified 4 ways 2026-09-07).
|
||||
The "amd64 runtime proof still needed" item that was sent to two devices was
|
||||
therefore asking for the impossible, while CI already had the answer and was
|
||||
discarding it. `Dockerfile.base:607` does assert it (`agent-browser --version &&`),
|
||||
but only when the base actually rebuilds — and v1.8.13's base was cached.
|
||||
|
||||
**The mailbox now announces replies that CLOSE your own asks.** `mempalace-toolkit`
|
||||
`21023e7` → `e45f6b4`, which adds `deriveClosed()` alongside `deriveOwed()`. The old
|
||||
path queried `status: open` and joined for a reply, which by construction can only
|
||||
surface asks *you owe someone else*; a terminal reply carries `status: applied`
|
||||
(or `blocked`/`failed`), so **the answer to your own question was structurally
|
||||
invisible** — the one notification a human actually wants. Measured: `emb-7kj4vr4g`
|
||||
closed the v1.8.13 rollout ask at 18:31Z with `status=applied`, the operator
|
||||
reasonably expected to hear about it, and the mailbox stayed silent while being
|
||||
correct by its own definition. Nine closed correlations were sitting unannounced.
|
||||
Shares the 1-hour resurface floor, so a close is announced once and is news rather
|
||||
than a nag.
|
||||
|
||||
This lands **because the base rebuilds**, which is worth stating explicitly: the
|
||||
CI-resolved `mempalace-toolkit` SHA is folded into the content-addressed base tag
|
||||
(`base-decide`), precisely so a toolkit-only fix cannot silently fail to land
|
||||
behind an unchanged `Dockerfile.base`. The floating `main` ref was left alone on
|
||||
purpose — the toolkit moving *forces* the rebuild rather than waiting for one.
|
||||
|
||||
**A consequence worth noting for the amd64 item above: this release actually
|
||||
collects that proof.** v1.8.13's base was cached, which is why
|
||||
`Dockerfile.base:607`'s `agent-browser --version &&` never ran. v1.8.14's base is
|
||||
not cached, so both that assertion and the new `EXPECTED_NODE_MAJOR` gate execute
|
||||
on a native `linux/amd64` runner. The fleet's first *kept* amd64 runtime proof for
|
||||
the `linux-x64` ELF should be an artefact of this build rather than something
|
||||
asked of a device that cannot supply it.
|
||||
|
||||
**Subtask delegation is documented — including the rung nobody built.** The image
|
||||
picks these up through their resolved refs (`pi-toolkit` `adfb553`,
|
||||
`pi-extensions` `c64c122`, the latter also refreshing the baked fallback skill):
|
||||
|
||||
- an operator-facing decision guide in pi-toolkit's `README.md`, built on the
|
||||
L0–L4 context ladder — how much of the parent session a child can see is the
|
||||
axis that explains nearly every observed good and bad behaviour;
|
||||
- the canonical `pi-extensions` skill gains the same ladder next to *Boundary
|
||||
discipline*, which until now diagnosed why an inherited transcript defeats a
|
||||
brief without offering any alternative to "don't fork that";
|
||||
- one bullet in the global `AGENTS.md`, so the choice is visible without loading
|
||||
a skill, and naming `pi-task` as a **CLI** — an agent hunting for a `pi_task`
|
||||
tool finds none and concludes it is unavailable.
|
||||
|
||||
What the ladder records: **L0/L1/L2 exist** in `pi-task` (`context.facts` /
|
||||
`.files` / `.commands`), **L4** is `fork`'s only behaviour (`getHeader()` +
|
||||
`getBranch()`, no offset or limit anywhere in the call chain), and **L3** — a
|
||||
truncated branch — **is not implemented by anything**, which is now written down
|
||||
instead of being a design idea somebody remembers.
|
||||
|
||||
Also recorded, found while writing the above: `pi-fork/src/runner.ts:188` reads
|
||||
`if (extensions !== null) args.push("--no-extensions")`. So `extensions: []` turns
|
||||
the capability floor **on** and `null` turns it **off** — and `null` is the
|
||||
documented way to "restore normal extension loading", so tidying `[]` to `null`
|
||||
as a no-op re-arms palace writes inside every fork child. `pi-task` hardcodes the
|
||||
flag and cannot drift this way. Documented in three places because the edit that
|
||||
triggers it looks harmless.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.13 — 2026-09-06
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shellcheck + syntax-check every shell script in this repo. Severity: error.
|
||||
#
|
||||
# SINGLE SOURCE OF TRUTH for two callers:
|
||||
# .gitea/workflows/lint.yml — advisory, every branch push and PR
|
||||
# .gitea/workflows/docker-publish.yml — the release GATE (lint-gate job)
|
||||
# Extracted from lint.yml on 2026-09-08 rather than copied, because a second
|
||||
# copy is exactly the drift this repo has been bitten by (see skillset's
|
||||
# pi-extensions mirror, refreshed the same evening after sitting 9579 B behind).
|
||||
#
|
||||
# WHY THIS CHECK EXISTS AT ALL
|
||||
# actionlint shellchecks workflow `run:` steps only. The repo's own scripts —
|
||||
# entrypoint.sh, scripts/*.sh, and the extensionless tools under
|
||||
# rootfs/usr/local/bin/ — were never shellchecked. A sibling repo with the same
|
||||
# gap shipped a broken `echo "$json" | python3 <<'EOF' ... json.load(sys.stdin)`
|
||||
# for two months: with no script argument python reads its SCRIPT from stdin,
|
||||
# so the heredoc IS stdin and json.load hits EOF. shellcheck flags that at
|
||||
# severity error (SC2259); nothing ever ran it.
|
||||
#
|
||||
# WHY THE RELEASE GATES ON IT (added 2026-09-08, the expensive way round)
|
||||
# v1.8.14's first attempt failed after build-base had already spent ~46 min:
|
||||
# scripts/smoke-test.sh had an apostrophe inside a single-quoted exec_test body
|
||||
# ("the fleet\'s"), which CLOSES the string, so the body truncated and its tail
|
||||
# ran on the CI runner instead of inside the image. shellcheck had already
|
||||
# caught it as SC2289 at severity error — the lint job went red on the very
|
||||
# push that introduced it and stayed red for 24 hours, unread. lint.yml
|
||||
# deliberately does not run on tag pushes (sound: the tagged tree was linted on
|
||||
# main, and a tag-ref lint run sorts above the publish run and makes a release
|
||||
# look finished early). The gap was never "lint the tag" — it was that a tree
|
||||
# whose lint FAILED could still be released. Hence a gate inside the publish
|
||||
# workflow, ~40 s, ahead of everything expensive.
|
||||
#
|
||||
# SEVERITY CHOICE
|
||||
# -S error is 0 findings across this repo when clean, so it is free to add.
|
||||
# -S warning is NOT free here (19x SC2088 tilde-in-quotes in
|
||||
# recreate-sanity-check.sh, plus assorted SC2016 — both intentional), and a
|
||||
# noisy gate trains people to ignore it. Error-only, matching the
|
||||
# SHELLCHECK_OPTS philosophy in lint.yml.
|
||||
#
|
||||
# Usage: bash scripts/lint-shell.sh [root] (default root: repo top level)
|
||||
set -uo pipefail
|
||||
|
||||
root="${1:-}"
|
||||
if [ -z "$root" ]; then
|
||||
root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
fi
|
||||
cd "$root" || { echo "::error::cannot cd to $root"; exit 2; }
|
||||
|
||||
# A gate that cannot run must not pass. Without this, a machine (or a CI job
|
||||
# whose install step was reordered away) without shellcheck would sail through
|
||||
# printing nothing, which is the failure mode this whole file exists to prevent.
|
||||
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||
echo "::error::shellcheck not found — the gate cannot run, so it must not pass" >&2
|
||||
echo " install it (apt-get install -y shellcheck) or run this in CI" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Union of two signals, because either alone misses a real case: a shebang scan
|
||||
# misses a sourced fragment with no shebang, and a *.sh glob misses the
|
||||
# extensionless tools in rootfs/usr/local/bin/. Silent skipping is precisely the
|
||||
# failure mode this gate exists to prevent, so err toward over-collecting.
|
||||
# -print0/mapfile -d '' so a path containing a space cannot silently split.
|
||||
mapfile -d '' -t all_files < <(find . -not -path './.git/*' -type f -print0)
|
||||
sh_files=()
|
||||
for f in "${all_files[@]}"; do
|
||||
case "$f" in *.sh) sh_files+=("$f"); continue;; esac
|
||||
if head -n1 "$f" 2>/dev/null | grep -qE '^#!.*\b(bash|sh)\b'; then
|
||||
sh_files+=("$f")
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Checking ${#sh_files[@]} shell file(s) with $(shellcheck --version | awk '/version:/{print $2}')"
|
||||
# A green tick over an empty file set is not a check.
|
||||
if [ "${#sh_files[@]}" -eq 0 ]; then
|
||||
echo "::error::no shell files found — the shebang scan or the checkout is wrong"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rc=0
|
||||
shellcheck -S error -f gcc "${sh_files[@]}" || rc=1
|
||||
|
||||
# bash -n catches a different class than shellcheck (unbalanced constructs it
|
||||
# declines to parse), so both run and both count.
|
||||
for f in "${sh_files[@]}"; do
|
||||
bash -n "$f" || { echo "::error file=$f::bash -n failed"; rc=1; }
|
||||
done
|
||||
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo "OK: ${#sh_files[@]} shell file(s) clean at severity error"
|
||||
fi
|
||||
exit "$rc"
|
||||
+24
-1
@@ -751,10 +751,33 @@ exec_test "pi-atelier registered from /opt, not npm: (volume-shadowing guard)" \
|
||||
# shadows it. The check that actually bites lives in
|
||||
# recreate-sanity-check.sh, which runs where the volume is real — that is
|
||||
# where a 7-week-old 0.27.0 was caught shadowing 0.35.2 on 2026-09-06.
|
||||
# EXECUTION is ASSERTED here, not printed. Until 2026-09-07 the version was
|
||||
# captured inside an echo with 2>/dev/null, so a binary that could not run at all
|
||||
# still PASSED and simply printed version=[] -- the same failure class as the bare
|
||||
# `node --version` two hundred lines up: a value displayed rather than compared.
|
||||
#
|
||||
# Why this exit code matters more than most: smoke runs `platforms: linux/amd64`
|
||||
# on an x86 runner, i.e. NATIVE amd64, so this is the fleet's only recurring
|
||||
# amd64 runtime proof for the linux-x64 ELF. No devbox can supply one -- every
|
||||
# machine in the pi fleet is an Apple Silicon Mac (mbp-m1-2020; tor-ms22 = Mac
|
||||
# Studio Mac13,1 M1 Max, verified 2026-08-17 by system_profiler; emb-7kj4vr4g =
|
||||
# Apple Silicon, 4 routes 2026-09-07). Asking a device for that proof is asking
|
||||
# for the impossible; CI already had it and was discarding it.
|
||||
#
|
||||
# KEEP PROSE OUT OF THE QUOTED BODY BELOW. On 2026-09-07 this explanation lived
|
||||
# INSIDE the single-quoted argument and contained an apostrophe ("the fleet's").
|
||||
# Inside '...' bash treats a backslash literally, so \' does not escape -- it
|
||||
# CLOSES the string. The body silently truncated, the remaining lines were parsed
|
||||
# by the RUNNER's shell instead of the container's, and `agent-browser --version`
|
||||
# ran on a host that has no agent-browser: "line 770: command not found", release
|
||||
# v1.8.14's smoke job failed after the base had already built. shellcheck caught
|
||||
# it as SC2289 the same day and the red lint job went unread for 24h.
|
||||
exec_test "agent-browser resolves under /usr (volume-shadowing guard, build-time half)" '
|
||||
p=$(command -v agent-browser) || { echo "agent-browser not on PATH" >&2; exit 1; }
|
||||
r=$(readlink -f "$p")
|
||||
echo "resolved=[$r] version=[$(agent-browser --version 2>/dev/null | head -n1)]" >&2
|
||||
v=$(agent-browser --version) || { echo "agent-browser did not EXECUTE" >&2; exit 1; }
|
||||
test -n "$v" || { echo "agent-browser --version produced no output" >&2; exit 1; }
|
||||
echo "resolved=[$r] version=[$(printf %s "$v" | head -n1)]" >&2
|
||||
case "$r" in /usr/*) ;; *) exit 1 ;; esac
|
||||
test ! -d "$HOME/.pi/npm-global/lib/node_modules/agent-browser" || exit 1
|
||||
echo ok
|
||||
|
||||
Reference in New Issue
Block a user