Commit graph

8 commits

Author SHA1 Message Date
Claude
8aba1f7f5c Rebuild the stand-in historian on CI Server item names
The three open Phase 5 findings were one defect: the stand-in was keyed on
CI Server POINT names (PS_STN_WET_WELL_LEVEL) when the historian is keyed on
CI Server ITEM names (AID.WRPS.STN.LEVEL). Modbus carries register numbers,
not names, so those two layers are free to differ - and do. Reconciling
against the register map, as planned, would only have proved the first three
namespaces agreed with each other.

Rebuilt from WRPS/05-scada/modbus, so item names, sample rates, retention and
timestamp semantics come from the machine rather than from a guess.

(a) Level tag does not join. PS_STN_WET_WELL_LEVEL becomes a tag row in its
    own right; LIT-101 is marked NOT HISTORISED - a field input on %IW0 that
    never reaches SCADA. It was the only seed row carrying two addresses.
    public.historian_items holds the item-to-tag mapping, generated by
    scripts/gen_historian_items.py and enforced non-empty at generate, at
    deploy and at verify.

(b) first_alarm/last_alarm returned UTC. Converted inside the measure, so it
    stays in Cube and happens once. Aggregate first, convert after - the other
    order picks the wrong row across a DST fall-back. Returned as a formatted
    string with a companion site_timezone measure. Storage being UTC is now
    confirmed, not assumed: all 49 points carry TIME_ZONE "Date+time GMT" and
    every history group CORRECT_DAYLIGHT=0. This answers Phase 4 task 4.

(c) High level alarm filed against the wrong equipment. Both sides were right
    about different things; the defect was asserting equipment twice. The
    history now carries no equipment column at all - faithful, since CI
    Server's section tree stops at the station and three pumps. Equipment is
    reached bit -> tag -> equipment via public.alarm_bits.

Alarms are derived, not stored: CI Server's ALARM_HISTORY group is empty
because every item imports with alarming off. Decomposing the alarm word needs
no configuration that does not exist.

Three things the SCADA config changed that were never filed as faults:
  - retention is 7 days, not 30. The advisory path was reporting a month of
    evidence drawn from a week of data
  - the analogue rate is 5 s, not 60. Two measures multiplied sample counts by
    a hardcoded 60 - a twelvefold overstatement that read as plausible
  - the deadband warning in process_values.yml was wrong and was steering
    people away from the correct measure

db/002_fixtures.sql now asserts its own counts at load and cross-checks the
alarm derivation against two independent signals. Those prove the pipeline,
not the plant.

db/README-standin-historian.md documents removal: the seam between generation
and contract, and twelve assumptions about imh that are NOT confirmed. Two of
them fail silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 11:42:02 +10:00
Claude
5e0984b2cf Open the operator console to cicore1 only, without Authelia
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>
2026-08-28 11:44:04 +10:00
Claude
f4746cb39b Drop the auth check, and say what each hostname is for
Leaving the auth.yokogawa.tech note in verify.sh was a standing nudge towards a
fourth name, which is the opposite of what this host needs. Removed.

In its place, the thing that was actually missing: a list, where the Caddy
blocks live, of which hostname serves what and which one a control-room PC
needs. lin001 is shared and already carries a dozen *.yokogawa.tech services -
the cost of a name is not the DNS change, it is that nobody else can tell what
it is for. Notes that cube.yokogawa.tech is the one nothing depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:40:35 +10:00
Claude
b4c20324f5 Withdraw the auth.yokogawa.tech request - it blocks nothing yet
Raised as an item for Dan, which was premature. It blocks exactly one thing: a
demo from a control-room PC. Development and the Phase 8 eval both run from
outside the VNet, where auth.yokogawa.tech resolves normally, and the demo it
would block cannot happen until the Azure OpenAI account lands anyway.

The finding stays in verify.sh as information rather than a failure, so it is
in front of whoever prepares that demo instead of failing every run until then.

Also worth recording: this was inferred from lin001 resolving against 10.0.0.5,
not tested on cicore1. Same DC and same zone, so very likely identical, but it
has not been confirmed on the machine it concerns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:16:48 +10:00
Claude
8d4d731832 Raise the auth.yokogawa.tech pinpoint record, and check for it
The DC is authoritative for a yokogawa.tech zone holding pinpoint records only,
so from inside the VNet every name not in that zone is NXDOMAIN rather than
forwarded. ai and influx are in it. auth is not.

So an operator on cicore1 resolves ai.yokogawa.tech, reaches Caddy, is
redirected to https://auth.yokogawa.tech/?rd=... and cannot resolve it. The
assistant is reachable and cannot be signed into. Nothing caught this because
influx is the only other pinpointed hostname and LAN traffic to it uses the
bypassed API paths, which never touch the portal - ai.yokogawa.tech is the
first thing here a LAN client must interactively sign in to.

verify.sh now resolves auth.yokogawa.tech alongside the hostnames that redirect
to it, and fails rather than shrugging when it is missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:13:49 +10:00
Claude
eb8cfaa787 Stop verify.sh reporting two things it cannot see
Both found running it on lin001 straight after the Caddy blocks went in.

The public-endpoint check called lf, cube and api unreachable. They are not:
the host resolves yokogawa.tech through the DC, which holds pinpoint records
only, and those three have none. Nothing is wrong with them and the check
cannot tell from here - so it now says so, and says to check from outside the
VNet, instead of printing a failure that means nothing.

403 now has its own message. It is what Caddy plus Authelia return for a
hostname with a site block and no access_control rule, which is the state
between applying the Caddy blocks and applying the Authelia rule - the one
case an operator is most likely to hit and least likely to recognise.

And the log check ran after the agent_ro test, which deliberately attempts a
forbidden INSERT that pg-ai logs. verify.sh was flagging an error line it had
just written itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:03:21 +10:00
Claude
76af3156fa Serve the operator page and /ask from one origin
api.yokogawa.tech has a public A record but no pinpoint record on the DC, so
it does not resolve from inside the VNet at all. The browser called it by
hostname, which means an operator on cicore1 would have loaded the page and
had every question fail on DNS - the exact gap Phase 7's gate exists to catch,
and one an engineer's laptop cannot see.

Caddy now routes /ask under ai.yokogawa.tech to ai-api, inside a route block
so import authelia still runs first: forward_auth sorts after handle in the
default directive order, and outside a route the handles would be terminal and
the gate would never run. Only /ask is routed - the Phase 9 publisher rule is
scoped to api.yokogawa.tech and a wider route here would leave it inert.

Also fixes the fallback it replaces. The build arg defaults to "", and
`?? "https://api.yokogawa.tech"` does not catch an empty string, so the
documented real-deployment build resolved the API base to "" and posted /ask
at ai-web, which 404s it. verify.sh and deploy.sh now check both the route and
whether the built bundle carries the hostname.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:48:22 +10:00
Claude
34d2ccc576 Scaffold the WRPS plant operations assistant repository
Build spec and host brief carried in from C:\Claude and WRPS/02-env; the
plant model (equipment, tags, alarm bitmask, enums, unit conversions) is
derived from WRPS/04-plc/register-map.csv, WRPS/05-scada/modbus/scada-points.csv
and WRPS-CTL-003.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 13:56:32 +10:00