An operator standing at the SCADA console should not complete a Duo push to ask a question, and nobody outside the plant should reach the assistant at all. The ai.yokogawa.tech Caddy block now admits remote_ip 10.0.0.21 (yau-poc-cicore1, static) and returns 403 to everything else. Applied on lin001 2026-08-28; snapshot at ~/Caddyfile.bak-ai-scadaonly-20260828. This also settles why the console could not reach the assistant at all: auth.yokogawa.tech has no pinpoint record on the DC, so a LAN browser got a correct 302 to the portal and then died on DNS. It went unnoticed because the device agents write to Influx over the /api/v2/write MFA bypass and never touch the portal - no browser had ever hit Authelia from inside the VNet. Verified before applying that an IP matcher can work here: Caddy sees real client addresses, and WireGuard peers arrive masqueraded as 172.19.0.6 so they do not match and are refused along with the internet. What this costs, recorded in section 14 as a shortcut and not as a security control: it is an IP allowlist on a flat network with no OT/IT boundary, so anything that can take 10.0.0.21 inherits unauthenticated access; Langfuse traces are now anonymous, so there is no record of who asked what; and the assistant is out of browser reach over the VPN. It is in scope for the section 2 OT/safety review, which is still outstanding. api.yokogawa.tech is unchanged and still fully gated - Phase 9 publishing depends on Remote-User/Remote-Groups and stays there. The now-inert ai.yokogawa.tech entry in the Authelia rule is deliberately left in place so restoring the gate is a Caddy reload rather than an Authelia restart that logs out every user on the host. verify.sh treats 403-from-lin001 as the pass for the deny arm and states plainly that the allow arm can only be proved from cicore1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69 lines
4.6 KiB
Markdown
69 lines
4.6 KiB
Markdown
# CLAUDE.md — rules to keep front of mind
|
||
|
||
Plant Operations Assistant for the **Waterloo Road Pump Station (WRPS)**, deployed onto an
|
||
existing, live, shared Docker host. Full detail: `BUILD-AI-CONTAINERS.md` (the build spec) and
|
||
`YAU_Linux_Host_Onboarding.md` (the host brief). **Where the two conflict, the host brief wins.**
|
||
|
||
## The three lines this system does not cross
|
||
|
||
1. **No instructions for safety-critical actions.** Procedural questions get document identity,
|
||
revision, effective date and verbatim prerequisites — never synthesised steps.
|
||
2. **No recommended setpoints or operating parameters.** Advisory questions get evidence, ranges,
|
||
outcomes, documented limits, then an explicit deferral. Never a number presented as the answer.
|
||
3. **No answers outside the evidence.** Zero rows means "no records found", never a plausible figure.
|
||
|
||
These are **code paths, not prompt instructions.** A safety rule living only in a prompt is not
|
||
implemented. The classifier runs first; each class has a Pydantic contract validated in Python
|
||
after generation and before returning. Contract failure -> regenerate once -> error. Never return.
|
||
|
||
When class is uncertain, choose the **more restrictive** class. Procedural beats Reference.
|
||
Advisory beats Historical. Partly-advisory is advisory.
|
||
|
||
## Host rules — inherited, non-negotiable
|
||
|
||
- Growing data goes on **`/datadisk`**, never `/`. Root is 62 GB and has hit 100% before.
|
||
- **No published host ports for anything we build.** Join the external `proxy` network, reach it
|
||
through Caddy. Some existing containers do publish (`caddy`, `wireguard`, `mosquitto`,
|
||
`chirpstack-gateway-bridge`, `openplc-runtime`) — they carry non-HTTP protocols. We do not.
|
||
- **Never omit `import authelia`** from a Caddyfile block — it silently makes a service public.
|
||
One deliberate exception, applied 2026-08-28 at the customer's direction: `ai.yokogawa.tech`
|
||
admits `remote_ip 10.0.0.21` (`cicore1`) unauthenticated and 403s everything else. It does
|
||
not generalise — do not copy it, and do not widen it. `api.yokogawa.tech` stays gated.
|
||
- `~/authelia/configuration.yml` is **root-owned**: `sudo`, back up as `.bak-<purpose>-<date>`.
|
||
Restarting Authelia **logs out every active user** — announce it first.
|
||
- AD group membership must be **DIRECT**. Nested membership silently fails.
|
||
- `pg-ai` and `cube` are pinned — **do not add them to Watchtower's update list**.
|
||
- **`openplc-runtime` is live control for this demo.** Never restart, update or reconfigure it as
|
||
a side effect of AI work. Do not copy its published-port pattern, and never change its
|
||
`10.0.0.17` binding to `0.0.0.0` — that binding is what keeps unauthenticated Modbus off the
|
||
internet.
|
||
- Never install on, write to or restart `cicore1`. Never exceed read-only on `imh`.
|
||
- **No secrets in Git, compose files, logs or error messages.** `0600` env files under `~/ai/`.
|
||
|
||
## Verification
|
||
|
||
`docker ps` showing "Up" is **not** proof. `curl -sI` the public URL, expect `302` to the auth
|
||
portal, and read the container logs. Prove each layer without the LLM first: prove Cube returns
|
||
the right number by hand, prove retrieval finds the right procedure by hand, *then* wire the agent.
|
||
|
||
## Working conventions
|
||
|
||
- Prefer additive changes. Snapshot config before editing.
|
||
- **Do not invent schema.** `imh` is pending — inspect it, or ask. Fixtures are marked as fixtures.
|
||
- Store UTC. Convert to `SITE_TIMEZONE` exactly once, in Cube. Never do timezone maths in a prompt.
|
||
- Fix failures in the classifier, Cube or ingestion — **not by adding instructions to the prompt.**
|
||
- **A document becomes citable only after a human confirms its number, revision and effective date** —
|
||
`confirm_header()` at a terminal, or the Phase 9 review screen. Never add a path that ingests an
|
||
unconfirmed header. The web-facing role may only make a document **less** citable (withdraw);
|
||
publishing and restoring go through `ai-docs-worker`, which has no HTTP surface.
|
||
- When something fails, add the failing case to `eval/testset.jsonl` *before* fixing it.
|
||
- Small commits, one concern each. If a change alters an accepted phase, re-run that phase's gate.
|
||
|
||
## The plant
|
||
|
||
Waterloo Road Pump Station: a three-pump wastewater station. Wet well `WW-101` (0–7000 mm,
|
||
120 m³/m), duty/assist/assist pumps `PU-301/302/303` on a common VSD speed reference, discharging
|
||
through manifold `MAN-301` against 22 m static lift. Spill weir at 6000 mm, `LSHH-102` at 5500 mm.
|
||
Control runs on `openplc-runtime`; Yokogawa CI Server on `cicore1` polls it over Modbus TCP and
|
||
historises the result. Source of truth for tags: `db/seed/tags.csv`, derived from
|
||
`WRPS/04-plc/register-map.csv` and `WRPS/05-scada/modbus/scada-points.csv`.
|