# OpenClaw on the Ahel Pi — Phase A (hardened gateway)

> ## ⛔ RETIRED FROM THE PRODUCT 2026-07-29
>
> OpenClaw was removed from Ahel on 2026-07-29, together with hermes, the old kanban page and the
> SSH dispatch lanes. **What replaced it:** the Arkenstone orchestrator — its agent registry
> (sectors → agents with a `SOUL.md` → typed actions), its durable task queue, and the `ahel-agent`
> runner that compute assets use to phone home and execute signed jobs. The chat `openclaw_task`
> tool, the coordinator task routes and the kanban dispatch that fed this Pi worker are all gone
> from the cockpit.
>
> **This directory is kept on purpose.** These service files are the rollback path if the worker is
> ever re-enabled. They are not deployed to by any current procedure and no live code reads them.
> The Pi keeps its monitoring role (Prometheus, Alertmanager, Loki, ntfy, all systemd); only the
> OpenClaw worker on it is being retired.
>
> Everything below this line describes the state as of 2026-07-26 and is a historical record.

Deployed: 2026-07-22. Plan: `docs/superpowers/specs/2026-07-22-openclaw-assessment.md` (Phase A only).

## What runs where

| Item | Value |
|---|---|
| Host | Raspberry Pi 4 4GB, `192.168.8.240` (`raspberry`, Debian 13, USB-SSD root) |
| Node.js | v24.18.0 (NodeSource apt repo `node_24.x`, keyring at `/usr/share/keyrings/nodesource.gpg` — no curl-pipe-bash) |
| OpenClaw | **2026.7.1-2** (`0790d9f`), global npm install (`sudo npm install -g openclaw`), binary `/usr/bin/openclaw`. Well past the CVE-2026-25253 fix (2026.1.29). |
| Config | `/home/admin/.openclaw/openclaw.json` — mode `600`, dir `700` |
| Service | `/etc/systemd/system/openclaw-gateway.service` (root-owned, runs as `admin`), enabled, verified to survive reboot (~90 s to listening after boot) |
| Listener | `127.0.0.1:18789` + `[::1]:18789` **only** (verified with `ss -tlnp`) |
| Idle RSS | ~360 MB |

## ⚠️ Hardening REMOVED 2026-07-24 (Karl: "don't set any blockers")

The Phase A tool restrictions below are HISTORICAL. Current live state: full
tool surface — `tools.exec.mode: full`, elevated enabled, browser enabled
(headless, system Chromium at /usr/bin/chromium), skills unrestricted,
`web_search` via the key-free DuckDuckGo provider (verified live same day).
Gateway auth/loopback binding and `plugins.deny: ["codex"]` are retained (the
latter because the codex app-server runtime hijacks openai agent runs — see
the Models section). Accepted risk, same posture as fleet-tools: a
prompt-injected agent can run arbitrary commands on the Pi as `admin`.
Config backups: `/home/admin/.openclaw/backups/unblock-*`.

## Hardening baseline (HISTORICAL — removed 2026-07-24, kept for re-hardening)

- `gateway.bind: loopback`, `gateway.auth.mode: token`. The token is a random
  32-byte hex generated on the Pi (`openssl rand -hex 32`) and lives **only** in
  the Pi's `openclaw.json`; the copy in this directory has the
  `__GATEWAY_TOKEN__` placeholder. Never commit the real token.
- `tools.profile: messaging` + deny `group:automation`, `group:runtime`,
  `group:fs`, `sessions_spawn`; `tools.exec.mode: deny`;
  `tools.elevated.enabled: false`.
- `skills.allowBundled: []`, no marketplace feeds, `allowUploadedArchives:
  false` — **zero skills, no ClawHub**, per the Feb 2026 supply-chain incident.
- `browser.enabled: false` — never enable browser automation on this Pi
  (Chromium needs 2–4 GB; the Pi has ~3.7 GB total).
- `session.dmScope: per-channel-peer`; DM policy is the default
  pairing/allowlist behavior (no channels are configured anyway).
- Sandbox is `off`: the Pi has no Docker/Podman backend, so OpenClaw's sandbox
  has nothing to run on. Enforcement is the deny-list + exec-deny above.
  Revisit if a container runtime ever lands on the Pi.
- WebChat (built into the gateway Control UI) is the **only** surface. No
  Telegram/WhatsApp/etc. until Phase C.

## Models

The checked-in OpenClaw source defines the bridge contract deployed on
2026-07-23:

| Ref | Backend | Role |
|---|---|---|
| `openai/gpt-5.6-sol` | **native ChatGPT/Codex OAuth on the Pi** (provider id `openai`, PKCE login, token auto-refresh in the agent auth store) | **default** since 2026-07-24; research; fallback = claude sonnet |
| `ahel-claude/claude-code-sonnet` | M1 claude-shim `http://192.168.8.168:8091/v1` | cloud-allowed standard; fallback for the default |
| `ahel-claude/claude-code-opus` | same | cloud-allowed deep; no fallback |
| `ahel-pool/qwen3.6-35b-hauhau-aggressive-mtp-q4` | TB4 pool `http://192.168.8.168:8080/v1` | private standard/deep; no cloud fallback |

The short-lived `ahel-codex` M1-subshim provider (2026-07-24, same day) was
replaced by the native OAuth above; the M1 subshim (`infra/m1/com.ahel.subshim.plist`,
:8087) stays running as a general always-on subscription endpoint but OpenClaw
no longer uses it. Native login: `openclaw models auth login --provider openai`
over `ssh -L 1455:localhost:1455` (use the same sign-in method as your ChatGPT
account — SSO accounts fail the password form with `invalid_username_or_password`).

⚠️ The OAuth flow also installs the `@openclaw/codex` plugin, which routes
openai-model **agent runs** through a local Codex app-server needing exec —
incompatible with this Pi's `tools.exec.mode: deny` (symptom: agent turns fail
with "Codex app-server local execution is not available", then fall back).
`plugins.deny: ["codex"]` keeps agent runs on the embedded backend over plain
HTTPS OAuth; direct `infer model run` was never affected. Verified 2026-07-24:
`openclaw agent --agent main -m "Which model are you?"` → `openai/gpt-5.6-sol`.
Known wart: the `ahel-claude/claude-code-sonnet` fallback currently 400s on
agent-shaped requests against the M1 claude-shim (primary works, so low
priority; revisit if the fallback is ever needed in anger).

## Dedicated-worker strip-down + memory guard (2026-07-24)

The Pi is now a dedicated OpenClaw worker ("all primary things on the M1"):
netdata, lightdm/desktop (default target → `multi-user.target`), bluetooth,
and all 13 stale `avahi-alias@*.ahel.local` units are disabled. Kept:
`openclaw-gateway`, `ahel-agent`, ssh, cron, NetworkManager, avahi-daemon.
The GUI Pi card grays out by design without netdata (`ahel-gui/lib/prom.js`);
asset state flows via the `ahel-agent` binary (a deployed name) → coordinator. Gateway gets a systemd
memory guard (`openclaw-gateway-limits.conf` here → drop-in
`/etc/systemd/system/openclaw-gateway.service.d/limits.conf`): MemoryHigh
1.5 GiB / MemoryMax 2 GiB. Revert desktop: `systemctl set-default
graphical.target && systemctl enable --now lightdm`.

Both providers are OpenAI-compatible (`api: openai-completions`). The pool uses
the non-secret `sk-ahel-local-noauth` schema placeholder. The Claude provider
uses the sanitized `__CLAUDE_SHIM_TOKEN__` placeholder for the dedicated token;
the real `0600` values remain only on their owning hosts.

The Hauhau pool metadata is deployed at 32,768 context tokens with a measured
4,096-token output cap. The corrected Slice 0 raw-pool benchmark passed short,
23.5K, and 29.5K probes; the largest accepted request reported 29,512 input
tokens and produced the required 8-token marker. A separate, single synthetic
direct-pool probe set `max_tokens=4096` and suppressed EOS stopping: it returned
HTTP 200, 4,096 output tokens, finish category `length`, 17,029 response bytes,
and 171.538722 seconds. The bounded probe retained no assistant content.

Full local-agent promotion remains blocked. In the original five-case run, the
isolated-agent case reached a successful CLI envelope but returned
`confirmation_mismatch`. A metadata-only diagnostic found that the successful
OpenClaw 2026.7.1-2 CLI JSON root contained neither `payloads` nor `meta`, proving
an envelope mismatch with the harness assumption; no assistant content was
viewed or retained. After the reviewed parser fix, one isolated-only recheck
passed, making the effective evidence 4/5 while preserving the original 3/5
record. The forced-tool case remains `tool_not_observed`, the planned fail-closed
state until Slice 3 exposes paired tool-call/result evidence, and prevents
promotion of the local OpenClaw task/tool lane.

## How Karl reaches WebChat

```bash
ssh -L 18789:127.0.0.1:18789 admin@192.168.8.240
# then open:  http://127.0.0.1:18789/#token=<gateway token>
```

Token: `sudo -u admin grep -A2 '"auth"' /home/admin/.openclaw/openclaw.json` on
the Pi (key `gateway.auth.token`), or run `openclaw dashboard --no-open` on the
Pi. The token goes in the URL fragment (`#token=…`), so it is not sent to any
server or logged in access logs.

## Ahel task wrapper

`ahel_openclaw_task.py` is the fixed M1-to-Pi task boundary. Deployment
installs it root-owned and executable at
`/usr/local/libexec/ahel-openclaw-task`; it accepts one versioned JSON object
on stdin and never accepts shell, command, script, cwd, environment, or
caller-supplied flag fields.

The one-shot create contract is deliberately fixed to an OpenClaw agent
message scheduled at `+1s`, an isolated session, the server-selected agent and
model, no fallback, the dedicated HTTPS coordinator callback, and a 1,800
second timeout. Supported inspection and cleanup operations are `status`,
`list`, `get`, `cancel`, and `cancelDeclaration`. Cancellation removes the cron
declaration and then cancels only running `cron` tasks whose exact `sourceId`
matches the validated OpenClaw job ID; a second bounded enumeration closes the
normal remove/run race. Declaration cleanup matches only the exact trusted
`declarationKey` and treats absence as success. Cleanup discovers and
deduplicates every matching job before mutation, attempts every removal, and
then reconciles active runs for all discovered IDs even if a removal or an
earlier active cancellation failed. The two running-task enumerations are
independent: failure of either one is recorded while the other still runs and
may cancel matches against the retained discovered ID set. Any partial failure
is returned only after the complete bounded best-effort cleanup pass.

Every OpenClaw call uses argv execution with `shell=False`, a fixed
`HOME=/home/admin` and `PATH=/usr/bin:/bin`, a 35-second timeout, and concurrent
bounded stdout/stderr draining. The wrapper retains at most 64 KiB, discards
task-cancel text, returns only normalized IDs/booleans, and never forwards job
prompts, task content, or raw stderr. The byte cap is shared across both pipes;
crossing it signals the supervising thread to terminate, kill if necessary,
and boundedly reap the child without waiting for the normal CLI timeout. IDs
must begin with an ASCII alphanumeric character, preventing any identifier
from becoming an OpenClaw option token. The installed OpenClaw 2026.7.1-2
surface is pinned as follows:

```text
openclaw cron add ... --json
openclaw cron status --json
openclaw cron list --all --json
openclaw cron get <job-id>
openclaw cron rm <job-id> --json
openclaw tasks list --json --runtime cron --status running
openclaw tasks cancel <run-id-or-task-id>
```

The wrapper is not a general OpenClaw CLI proxy. Changes to the installed CLI
envelopes or flags must first be represented by failing fixture tests and then
reviewed before deployment.

## Security audit (openclaw security audit --deep, 2026-07-23)

`0 critical · 3 warn · 1 info`. Remaining warns, all reviewed:

1. `gateway.trusted_proxies_missing` — N/A: no reverse proxy; the audit's own
   suggested fix is "keep the Control UI local-only", which is the deployment.
2. `models.weak_tier` — false positive: the audit cannot map the custom shim
   ids (`claude-code-sonnet`) to a Claude tier. The shim drives current
   Sonnet/Opus via Karl's Claude CLI on the M1.
3. `gateway.probe_failed (missing scope: operator.read)` — the deep probe
   authenticates as a paired *device* and none exist yet (CLI uses the shared
   gateway token, `openclaw devices list` is empty). Gateway is fail-closed,
   token-authed, loopback-only; the first WebChat connection will run the
   pairing flow.

Info summary at deploy time: groups open=0/allowlist=0, elevated disabled,
webhooks disabled, browser control disabled.

## Slice 0 superseded deployment gate (2026-07-23)

**Failed closed at the now-superseded candidate-tier live gate; deployment
rolled back.** The operator subsequently removed that candidate tier and its
fallback policy from the current bridge architecture. It is no longer a model,
fallback, gate, or blocker. Fresh lab refreshes were healthy with zero warnings,
and M1, the pool, the existing shim, and the Pi Gateway were reachable.

The first credential attempt exposed a real two-hop SSH quoting defect in the
provisioner and stopped before Pi token creation. Cleanup removed the new M1
token and left the live services unchanged. After commits `9b67b75` and
`19cb92d` fixed and reviewed that defect, a fresh preflight proved both token
targets absent and both live configs unchanged. The corrected provisioner then
installed the dedicated `0600` token files without printing their value.

The reviewed M1 shim advertised three IDs at the time:

- the now-removed candidate tier
- `claude-code-sonnet`
- `claude-code-opus`

Authenticated non-streaming probes recorded only status, non-empty state, and
timing. The removed candidate returned HTTP 502 with empty content in 3.833
seconds; a direct sanitized CLI classification returned exit 1, valid JSON,
and `is_error=true`. Sonnet returned HTTP 200 with non-empty content in 5.098
seconds, and Opus returned HTTP 200 with non-empty content in 3.687 seconds. No
response text was retained.

A reviewed retry with workdir-hardening commits `47873d6`, `bcae7ad`, and
`94fe162` then deployed only the server, runner, and plist. Launchd ran the
reviewed runner, the authenticated LAN catalog again advertised the three IDs
above, and only the required candidate completion was repeated. The candidate
again failed closed: HTTP 502, empty content, 2.923 seconds. Read-only inspection
retained only structured flags and alias-presence booleans: the CLI returned
exit 1 with `is_error=true`, zero API duration, no API status or cost, and an
`auth_or_session` category. Both installed CLI help and the installed target
recognized the removed alias; there were no invalid-model,
unavailable-subscription, API-key, or billing signals. No error text was
recorded.

A final read-only `claude auth status --json` classification returned exit 1
with structured status, `logged_in=false`, and the first-party provider. No
identity, organization, credential, or raw status output was retained. This
narrows the actionable failure category to `gui_session_not_logged_in`.

Per the promotion contract then in force, the failed candidate gate stopped
the run before Pi config replacement. The accepted Hauhau 32K baseline was not rerun through
OpenClaw, the output cap was not measured, the five-case promotion benchmark
and deep security audit were not run, and no results JSON was created. Hauhau
was **not** promoted to the local OpenClaw task lane.

The M1 service was rolled back to its prior plist and runner. The newly copied
server and runner were removed because they had not existed at preflight. The
Pi config byte-matches its backup, remains `0600`, and the Gateway remains
active on `127.0.0.1:18789` and `[::1]:18789`. The dedicated token files remain
in place for a recoverable retry; they were not copied to the repository.

Rollback inputs from this attempt remain on their owning hosts:

- M1 plist:
  `/Users/admin/.config/ahel/claude-shim/backups/20260723T090949Z/com.ahel.claude-shim.plist`
- Pi config:
  `/home/admin/.openclaw/backups/slice0-20260723T090949Z/openclaw.json`

This section records the superseded candidate-tier attempts only. The current
Sonnet/Opus deployment and promotion evidence follow.

## Slice 0 Sonnet/Opus deployment (2026-07-23)

Fresh inventory was healthy with zero warnings. Existing dedicated shim tokens
were retained; credential provisioning was not rerun. The final reviewed M1
server, runner, and plist hashes match their checked-in sources, and launchd is
running `/Users/admin/dev/ahel/infra/m1/claude-shim-run.sh` in `gui/501`.
Authenticated `/v1/models` advertises exactly:

- `claude-code-sonnet`
- `claude-code-opus`

Minimal non-streaming live gates retained no response text:

| Model | HTTP | Non-empty | Seconds | Category |
|---|---:|---:|---:|---|
| `claude-code-sonnet` | 200 | true | 4.736 | `none` |
| `claude-code-opus` | 200 | true | 4.582 | `none` |

The Pi config was transformed on the Pi from the sanitized checked-in source.
The existing `gateway.auth.token` value was preserved, the dedicated Pi shim
token was substituted, validation passed before atomic replacement, and the
live file remains `0600`. Its secrets-redacted SHA-256 is
`99caf0ccc25860e7af690cec4a970c92753e268125122fb7c5cfba19f41de2f8`, matching
the checked-in source. The deployed model policy is Sonnet primary, Opus
available for explicit deep work, and zero fallbacks.

The first direct restart attempt was rejected by Pi sudo/polkit and did not
restart the service. The config was restored immediately. After revalidation,
the same atomic transform was reapplied and the approved audited fleet-tools
relay issued exactly `systemctl restart openclaw-gateway.service` on node `pi`
with `sudo=true`: HTTP 200, exit code 0. Gateway PID changed from 1844 to
210050. The unit is active, `openclaw health` succeeds, and listeners remain
limited to `127.0.0.1:18789` and `[::1]:18789`.

Corrected promotion benchmark (`docs/superpowers/plans/openclaw-slice0-results.json`):

| Case | Result | Status/category | Input/output tokens | Seconds |
|---|---|---|---|---:|
| `short` | pass | `200` / `none` | 22 / 8 | 4.875 |
| `ctx24k` | pass | `200` / `none` | 23,506 / 8 | 54.500 |
| `ctx30k` | pass | `200` / `none` | 29,512 / 8 | 27.519 |
| `forced_tool` | fail closed | `0` / `tool_not_observed` | unavailable | 66.858 |
| `isolated_agent` | fail | `0` / `confirmation_mismatch` | unavailable | 45.239 |

The historical benchmark is 3/5 overall. Cloud routes and private-model
metadata are live, but the local task/tool lane is not promoted. `forced_tool`
remains an explicit Slice 3 evidence dependency; at that point
`isolated_agent` was a separate live functional acceptance blocker. Its
metadata-only diagnosis found that the successful
OpenClaw 2026.7.1-2 CLI JSON root contained neither `payloads` nor `meta`, so the
harness's expected envelope did not match the installed CLI; no assistant
content was viewed or retained.

After reviewed parser fix `97c4265`, exactly one `isolated_agent`-only recheck
was run and stored separately in
`docs/superpowers/plans/openclaw-slice0-isolated-recheck.json`; the historical
five-case JSON above was not changed. The recheck passed with CLI status 0,
category `none`, 9,034 bounded output bytes, and 44.279534 seconds. A pass on
this code path requires the exact confirmation marker in the nested Gateway
result. The live envelope did not yield numeric usage, so both token metrics are
recorded as unavailable. Effective evidence is therefore 4/5, but the local
task/tool lane remains unpromoted because `forced_tool` is still outstanding.

The separate Hauhau output-cap gate used one synthetic, non-streaming direct
pool request with `max_tokens=4096`, EOS stopping suppressed, a 1 MiB bounded
read, and no retry. It returned HTTP 200, `usage.completion_tokens=4096`, finish
category `length`, 17,029 response bytes, and 171.538722 seconds. Only those
metrics were retained; assistant content was neither printed nor stored.

The post-deployment deep audit returned `0 critical · 3 warn · 1 info`. The
warnings remain the reviewed loopback trusted-proxy notice, custom Sonnet tier
mapping, and paired-device `operator.read` probe described above. Elevated
tools, webhooks, internal hooks, and browser control remain disabled.

Current rollback inputs:

- M1 prior plist:
  `/Users/admin/.config/ahel/claude-shim/backups/20260723T090949Z/com.ahel.claude-shim.plist`
- Pi pre-Slice-0 config:
  `/home/admin/.openclaw/backups/slice0-20260723T103800Z/openclaw.json`

For Pi rollback, atomically restore the backup as `0600`, validate it through
`OPENCLAW_CONFIG_PATH`, then restart only `openclaw-gateway.service` through the
audited fleet-tools relay. For M1 rollback, restore the prior plist and use
`bootout` → wait until absent → `bootstrap` → `kickstart`; remove the reviewed
server/runner only if restoring the pre-Slice-0 state.

## Verification log (2026-07-22)

- `ss -tlnp` → gateway only on `127.0.0.1:18789` / `[::1]:18789`.
- `openclaw health` → event loop ok.
- Smoke, both providers, from the Pi (post-reboot):
  - `openclaw agent -m "Reply with exactly the word OK and nothing else."`
    (default = ahel-claude/claude-code-sonnet) → `OK`
  - `openclaw infer model run --gateway --model ahel-pool/qwen3.6-35b-heretic --prompt …` → `OK`
- Full reboot test: unit auto-started, listener + Control UI back within ~90 s.

## Files here

- `openclaw.json` — sanitized copy of the deployed config (token placeholder).
- `openclaw-gateway.service` — the exact unit installed at
  `/etc/systemd/system/openclaw-gateway.service`.

## Re-deploy from scratch

```bash
scp openclaw.json openclaw-gateway.service admin@192.168.8.240:/tmp/
ssh admin@192.168.8.240
  mkdir -p ~/.openclaw/workspace && chmod 700 ~/.openclaw
  sed "s/__GATEWAY_TOKEN__/$(openssl rand -hex 32)/" /tmp/openclaw.json > ~/.openclaw/openclaw.json
  chmod 600 ~/.openclaw/openclaw.json && openclaw config validate
  sudo cp /tmp/openclaw-gateway.service /etc/systemd/system/ && sudo systemctl daemon-reload
  sudo systemctl enable --now openclaw-gateway
```

## Out of scope / next phases

- Phase B: viking-tool federation as a custom tool (cloud-realm audience).
- Phase C: one messaging surface after a quiet week.
- Claude Code CLI *on the Pi* (memo's CLI-reuse path) was **not** needed:
  Ahel already has the M1 claude-shim (`services/` subshim pattern, :8091),
  which OpenClaw consumes as a plain OpenAI provider. No Anthropic credentials
  exist on the Pi.
