Radiology Transcript Interpreter · research proof of concept
Documentation
These sections are rendered directly from the checked-in specifications, plans, and operator documents. The Markdown files remain authoritative.
Public demo deployment plan
The complete artifact-demo packaging, VPS provisioning, SSH deployment, hardening, verification, rollback, monitoring, and teardown plan.
# Public Artifact Demo Deployment Plan
**Status:** Proposed implementation and execution plan
**Target:** `168.235.65.28`, reached first as `$DEMO_BOOTSTRAP_HOST` and then as `$DEMO_HOST`
**Application:** Radiology Transcript Interpreter research proof of concept
**Deployment form:** Artifact-backed public demo, not the live NLP research stack
**Confirmed host:** Debian GNU/Linux 13 (trixie), x86-64, approximately 8 GB RAM and 197 GB disk
## 1. Outcome
This plan creates a public demonstration of the existing interpreter UI using
only the reviewed generated transcription and its immutable checked-in R0–R5
artifacts.
The deployed system permits a visitor to:
1. open the interpreter;
2. load the reviewed generated transcription;
3. run the allowlisted proof-of-concept operation;
4. inspect R0 through R5;
5. inspect approved project documentation; and
6. inspect a truthful, read-only demo health view.
It does not deploy the live Stanza, Grew, UCxn, MoCCA, FrameNet, SWI-Prolog, or
RadLex service processes. It does not accept arbitrary transcription for
interpretation. It contains no historical source reports, production data,
credentials, private corpora, evaluation state, or user-supplied clinical
text.
The governing deployment invariant is:
~~~text
publicly reachable behavior
⊆ reviewed synthetic demo
+ immutable artifact inspection
+ approved documentation
+ read-only demo health
~~~
The deployment must never imply:
~~~text
artifact-backed demonstration = live NLP execution
artifact validity = clinical correctness
public availability = production readiness
~~~
## 2. Why This Is the Fastest Path
The repository already provides:
- the Go hub and UI;
- checked-in POC linguistic and semantic artifacts;
- artifact digest verification;
- the JSON Run operation;
- the Vanilla-JavaScript SPA;
- R0–R5 acceptance tests;
- semantic HTML and security-header tests;
- systemd experience and operational scripts; and
- Make-based build and verification entry points.
The local infrastructure Makefile currently builds and supervises the complete
research stack. Its start target intentionally starts the Python linguistics
and Prolog services before the hub. Its service units also contain
research-machine paths under `/home/raddev`.
Using that full path on a VPS would:
- require at least 4 GB RAM rather than 2 GB;
- install several gigabytes of Python, model, opam, ontology, and research
resources;
- require private or locally pinned dependencies;
- expose unused capability and evaluation operations unless further protected;
and
- take longer while producing no additional behavior in the current UI, whose
Run operation resolves only the reviewed immutable POC.
The shortest sound path is therefore a small public-demo specialization of the
existing Go infrastructure.
## 3. Assumptions and Required Inputs
### 3.1 SSH
The confirmed local private-key path is used by reference and is never copied
into the repository, release bundle, logs, or documentation output. Bootstrap
access and routine administrative access are distinct:
~~~sh
export DEMO_SSH_KEY=/home/raddev/.ssh/id_ed25519
export DEMO_BOOTSTRAP_HOST=root@168.235.65.28
export DEMO_HOST=radnlp-admin@168.235.65.28
export DEMO_ADMIN_SOURCE_IPV4=107.161.20.175
ssh -4 -i "$DEMO_SSH_KEY" -o IdentitiesOnly=yes "$DEMO_BOOTSTRAP_HOST" true
~~~
The root target exists only to establish the named administrator safely. The
bootstrap procedure creates `radnlp-admin` with a locked password, copies the
already-confirmed public key, grants explicit administrative sudo access, and
validates `/etc/sudoers.d/radnlp-admin` with `visudo -c`. It does not expose or
print private-key material.
Before leaving the bootstrap phase, two independent key-authenticated
`radnlp-admin` sessions must succeed and this command must pass without a
prompt:
~~~sh
ssh -4 -i "$DEMO_SSH_KEY" -o IdentitiesOnly=yes "$DEMO_HOST" 'sudo -n true'
~~~
The VPS-observed source address for RADPAIR was independently confirmed on
2026-09-07 as `107.161.20.175`. Host and provider firewalls permit SSH only
from `107.161.20.175/32`. Administrative commands force IPv4 so the selected
path matches that rule. Reconfirm the source through `$SSH_CONNECTION` before
each firewall change. If it differs, stop and review the new address rather
than widening or silently rewriting the allowlist.
The administrator is key-only and has passwordless sudo because the automated
provision, deployment, rollback, and recovery operations require noninteractive
root actions. Compromise of this key therefore means administrative compromise;
the key must remain local and its permissions must remain restricted.
The application runs as the separate unprivileged `radnlp-demo` service
identity and never uses the administrative account.
### 3.2 Domain
Public HTTPS requires a DNS name supplied separately:
~~~sh
export DEMO_DOMAIN=nlp-and-expert.elementsketchpad.com
~~~
DNS inspection on 2026-09-07 found Cloudflare proxy A and AAAA responses for
this name. The adopted minimal deployment does not place Cloudflare in the
request path. Before Caddy starts, change this one record to DNS-only with an A
record for `168.235.65.28`; remove its AAAA record unless the VPS is separately
confirmed and configured for the published IPv6 address. Do not change other
`elementsketchpad.com` records.
Once direct DNS and ports 80/443 reach this VPS, Caddy can obtain and renew a
public certificate and redirect HTTP to HTTPS automatically. Retaining the
Cloudflare proxy would add a CDN/TLS/operator boundary and requires a separate
recorded architecture decision rather than an incidental DNS setting.
### 3.3 Release identity
The deployment uses an immutable committed revision:
~~~sh
export DEMO_RELEASE=v0.1.1
export DEMO_ARCH=amd64
~~~
The existing `v0.1.0` tag is not moved. Public-demo implementation changes
must be committed as `v0.1.1` before promotion. A different later identity
requires an explicit orchestration decision; it is never inferred from a dirty
worktree.
### 3.4 Host baseline
Read-only preflight on 2026-09-07 confirmed:
- Debian GNU/Linux 13 (trixie);
- x86-64 architecture;
- approximately 8 GB RAM with no swap;
- approximately 197 GB disk with more than 180 GB free;
- SSH listening publicly on TCP 22;
- LLMNR listeners on TCP/UDP 5355, which the host firewall will not expose;
- outbound HTTPS for operating-system and Caddy installation;
- inbound SSH during provisioning.
These observations are not permanent assumptions. Preflight rechecks them
before any mutation. The chosen recovery model does not depend on a provider
console: it retains the current root session, retains key-only root login from
RADPAIR, tests fresh administrator sessions, and automatically rolls back each
firewall change unless the new path is explicitly confirmed.
The plan does not assume Docker, Kubernetes, a database, object storage, a
managed load balancer, or a secrets manager.
## 4. Target Architecture
~~~text
Internet
│
│ TCP 80/443
▼
┌───────────────────────────────────────┐
│ Caddy │
│ public TLS · HTTP→HTTPS · proxy │
└───────────────────┬───────────────────┘
│ loopback HTTP
▼
┌───────────────────────────────────────┐
│ rad-nlp Go hub │
│ public-demo route profile │
│ 127.0.0.1:8170 │
└───────────────────┬───────────────────┘
│ read-only files
▼
┌───────────────────────────────────────┐
│ reviewed POC artifacts │
│ approved Markdown documentation │
│ release manifest and identities │
└───────────────────────────────────────┘
~~~
Only Caddy binds public interfaces. The Go process binds
`127.0.0.1:8170`. No Python or Prolog service is installed or started.
## 5. Public-Demo Application Profile
### 5.1 Profile selection
Add an explicit deployment profile to the hub:
~~~text
research
public-demo
~~~
`research` remains the default for local compatibility. The public service
must start with an explicit argument such as:
~~~sh
--deployment-profile public-demo
~~~
Route registration is selected by profile. Public safety must not depend on
registering every research route and rejecting it later in middleware.
### 5.2 Public route allowlist
The public-demo profile registers only these operations:
| Method | Route | Purpose |
|---|---|---|
| GET | `/` | Redirect to the interpreter |
| GET | `/livez` | Process liveness |
| GET | `/readyz` | Demo artifact/document readiness |
| GET | `/assets/basic-web-theme.css` | Local UI stylesheet |
| GET | `/assets/interpreter-app.js` | Local SPA application |
| GET | `/interpretations/new` | Draft/test-data view |
| POST | `/api/v1/interpretations` | Exact allowlisted POC Run |
| GET | `/interpretations/{execution}` | Stable R0–R5 execution view |
| GET | `/interpretations/{execution}/artifacts/{artifact}` | Reviewed synthetic artifacts |
| GET | `/documentation` | Approved documentation index |
| GET | `/documentation/{document}` | Stable documentation section |
| GET | `/toolchains/rad-nlp-research` | Read-only public-demo health |
Every other route returns 404 or 405. In particular, the public profile does
not register:
- capability proxies;
- Stanza, Grew, UCxn, MoCCA, FrameNet, Prolog, or RadLex API routes;
- evaluation creation;
- evaluation records or artifacts;
- generic filesystem or document paths;
- generic Prolog, graph-rule, or model operations; or
- operational mutation endpoints.
### 5.3 Run boundary
The current Run endpoint already:
- requires JSON;
- requires the same-origin SPA header;
- uses a bounded request body;
- accepts only the two allowlisted profiles;
- resolves only the exact reviewed transcription; and
- rejects other transcription with a structured 422 response.
Public-demo acceptance tests preserve all of those obligations. No future
arbitrary-input interpreter is enabled automatically in this profile.
### 5.4 Public readiness
Public `/readyz` must mean:
~~~text
hub responds
∧ POC manifest loads
∧ artifact digests match
∧ approved documentation loads
∧ UI assets are embedded
~~~
It must not contact absent research services.
The Toolchain health page must state:
- deployment profile: artifact-backed public demo;
- application version and build revision;
- POC artifact identity and digest status;
- documentation status;
- live NLP toolchain: not deployed in this profile; and
- research/clinical scope boundary.
It must not display a false full-toolchain READY state.
### 5.5 State and cookies
The public-demo profile has no evaluation mutation and does not require a CSRF
secret or CSRF cookie. Avoid creating either in this profile.
The application may keep a private state directory for runtime compatibility,
but successful operation should require no mutable application data. A restart
must reconstruct all public truth from the release bundle.
## 6. Public Documentation Boundary
The public documentation bundle is an explicit allowlist. It initially
contains:
1. the original RadLex-to-Prolog compiler specification;
2. the radiology transcript conceptual specification;
3. the linguistic-semantic refinement specification;
4. the interpreter implementation plan;
5. the Interpreter UI specification;
6. a concise public project and scope overview.
The detailed deployment and orchestration plans remain available in the local
research documentation UI but are not placed in the public release bundle.
They contain host identifiers, administrative paths, recovery procedures, and
other operational detail that a visitor does not need.
The public profile does not automatically publish every Markdown file in the
repository. In particular, exclude:
- machine-specific operator notes;
- absolute development-machine paths;
- internal evaluation artifacts;
- orchestration scratch state;
- credentials or environment files;
- private-source acquisition instructions; and
- documents not reviewed for public release.
Documentation remains rendered as escaped text. It does not execute embedded
HTML, JavaScript, remote images, or third-party assets.
## 7. Release Bundle
### 7.1 Required Make targets
Extend `infrastructure/Makefile` with:
~~~text
demo-check
demo-build
demo-bundle
demo-smoke
demo-bootstrap-admin
demo-harden-host
demo-provision
demo-deploy
demo-activate-public
demo-status
demo-rollback
~~~
Their intended contracts are:
| Target | Contract |
|---|---|
| `demo-check` | Validate release identity, public route registration, document allowlist, and source/artifact digests |
| `demo-build` | Build a static Linux hub binary for `DEMO_ARCH` |
| `demo-bundle` | Create the explicit deployment tree, manifest, archive, and checksum |
| `demo-smoke` | Exercise a deployed base URL without SSH |
| `demo-bootstrap-admin` | Idempotently establish and verify `radnlp-admin` through the temporary root bootstrap target |
| `demo-harden-host` | Audit, apply the SSH-only nftables policy, patch Debian, configure security updates, and verify any reboot |
| `demo-provision` | Install host prerequisites and validated inactive service configuration without opening web ports |
| `demo-deploy` | Upload, verify, unpack, promote, restart, and smoke one release over loopback |
| `demo-activate-public` | Validate DNS/Caddy, open 80/443, and run public HTTPS acceptance with fail-closed rollback |
| `demo-status` | Read remote unit status and public readiness |
| `demo-rollback` | Promote an explicitly named prior installed release |
The local interpreter and infrastructure final gates remain prerequisites.
### 7.2 Static build
The Go hub should be built with an explicit target:
~~~sh
CGO_ENABLED=0 GOOS=linux GOARCH="$DEMO_ARCH" go build -trimpath -o dist/demo-root/bin/radnlp-toolchain-hub ./cmd/hub
~~~
The build embeds:
- UI CSS;
- UI JavaScript;
- application version; and
- a trustworthy commit/build identity supplied from the committed release.
The build must not fabricate a clean release identity from a dirty worktree.
### 7.3 Bundle layout
The archive expands to:
~~~text
rad-nlp-demo-$DEMO_RELEASE/
├── bin/
│ └── radnlp-toolchain-hub
├── config/
│ └── public-demo.json
├── interpreter/
│ └── artifacts/
│ ├── poc-linguistic.json
│ ├── poc-manifest.json
│ └── poc-result.json
├── documentation/
│ └── approved source documents
├── share/
│ ├── Caddyfile.example
│ └── radnlp-public-demo.service
├── RELEASE
└── MANIFEST.sha256
~~~
The implementation may preserve selected repository-relative paths internally
if that materially simplifies the first bundle, but the archive remains an
explicit allowlist.
The bundle must not contain:
- `.git`;
- `setup/.venv`;
- `setup/cache`;
- research corpora;
- historical report data;
- the UCxn source worktree;
- FrameNet or MoCCA databases;
- the RadLex OWL source or full generated runtime;
- infrastructure evaluation state;
- CSRF secrets;
- SSH material;
- environment files; or
- logs.
### 7.4 Local release gate
Before packaging:
~~~sh
make -C interpreter verify-poc
make -C infrastructure test-interpreter-ui
make -C infrastructure test-ui
make -C infrastructure test-ui-operations
make -C infrastructure demo-check
~~~
After packaging:
1. enumerate every archive member and compare it to the allowlist;
2. verify `MANIFEST.sha256`;
3. scan the expanded bundle for known secret formats and forbidden paths;
4. assert no `/home/raddev` path occurs;
5. run the binary from the expanded bundle in a temporary directory;
6. run `demo-smoke` against that temporary instance; and
7. record the archive SHA-256.
## 8. VPS Provisioning and Initial Hardening
### 8.1 Preflight
From the development machine:
~~~sh
ssh -4 -i "$DEMO_SSH_KEY" -o IdentitiesOnly=yes "$DEMO_BOOTSTRAP_HOST" 'set -eu; uname -m; . /etc/os-release; printf "%s %s\n" "$ID" "$VERSION_ID"; printf "SSH_CONNECTION=%s\n" "$SSH_CONNECTION"; free -h; df -h /'
~~~
Confirm:
- expected architecture;
- Debian 13;
- at least 2 GB RAM;
- at least 10 GB free disk;
- correct system clock;
- working DNS;
- the effective SSH configuration;
- installed firewall tooling and current nftables rules;
- all users with UID 0 or sudo-equivalent access;
- enabled services and public listeners;
- pending package upgrades and whether a reboot is already required;
- the VPS-observed SSH source is exactly `107.161.20.175`; and
- no existing listeners that conflict with ports 80, 443, or loopback 8170.
Record this baseline before changing the host. Unexpected administrators,
firewall rules, repositories, or services are a stop condition for diagnosis;
they are not silently overwritten.
### 8.2 Establish the named administrator
Use the confirmed root session only to create `radnlp-admin`, install the
confirmed public key with mode `0600`, grant noninteractive sudo through a
root-owned mode-`0440` sudoers fragment, and validate the result. Install the
Debian `sudo` package first if this minimal image does not already contain it.
Keep the original root session open while two new administrator sessions are
tested. Verify login, `sudo -n true`, distribution identity, and a harmless
privileged read. Do not change the SSH daemon policy yet.
Root SSH remains a key-only recovery path restricted by firewall to RADPAIR.
The administrator must also survive the patched-host reboot.
### 8.3 SSH-only firewall
Debian's native nftables service is the host-firewall authority. Do not layer
UFW, firewalld, and hand-written nftables rules on the same host.
Install the Debian `nftables` package if absent, without enabling an unreviewed
default ruleset. Save the effective pre-change rules before installing the
reviewed configuration.
The initial ruleset must:
- accept loopback traffic;
- accept established and related traffic;
- accept the confirmed SSH port only when the IPv4 source is
`107.161.20.175/32`;
- accept the ICMP and ICMPv6 traffic required for correct networking;
- default-drop unsolicited inbound traffic for both IPv4 and IPv6;
- leave outbound traffic allowed;
- omit TCP 80 and 443; and
- omit TCP/UDP 5355 and port 8170.
Validate the ruleset syntax before loading it. Before each live firewall
change, save the current rules and schedule a two-minute automatic restoration
of that exact saved ruleset. Load the candidate while the original root session
remains open, confirm fresh `radnlp-admin` and root connections from RADPAIR,
inspect the effective rules, and cancel the rollback only after both pass. If a
new session fails, allow the timed restoration to run or restore immediately
through the still-open session.
Apply the equivalent SSH-only policy at the VPS provider firewall when that
facility is available: TCP 22 from `107.161.20.175/32`, with no broader SSH
source. Provider-firewall changes remain an external operator step unless its
API is explicitly placed in scope.
### 8.4 Operating-system patching
After the administrator and SSH-only firewall pass:
1. refresh package metadata from the already-configured Debian repositories;
2. apply the current Debian 13 security and stable updates;
3. install and enable the Debian `unattended-upgrades` package for security
updates;
4. inspect conffile decisions rather than overwriting local configuration
blindly;
5. reboot if required; and
6. re-verify the named administrator, sudo, nftables, clock, disk, listeners,
and package state after reboot.
The upgrade/reboot gate must pass before application packages are installed.
### 8.5 Application-host packages
Install only:
- CA certificates;
- curl;
- GnuPG/keyring support required by the Caddy repository;
- Caddy;
- standard systemd tooling.
Use the official Caddy Debian/Ubuntu package instructions rather than a
third-party script. The official package supplies and runs Caddy as a systemd
service. Because the official package may start Caddy immediately, install it
only after the SSH-only firewall is active. Replace its default configuration
before any web port is opened.
Do not install Python, PyTorch, Stanza, opam, OCaml, Grew, or SWI-Prolog for the
artifact demo.
### 8.6 Service account and directories
Create a non-login system identity and explicit filesystem ownership:
~~~text
user/group: radnlp-demo
release root: /opt/radnlp-demo/releases
current symlink: /opt/radnlp-demo/current
runtime state: /var/lib/radnlp-demo
unit: /etc/systemd/system/radnlp-public-demo.service
Caddy config: /etc/caddy/Caddyfile
~~~
Release directories are owned by root and read-only to the application.
Runtime state is owned by `radnlp-demo`. The application user has no shell,
sudo rights, or write access to its binary, configuration, artifacts, or
documentation.
### 8.7 Public web-port activation
TCP 80 and 443 are deliberately absent throughout host hardening, package
installation, application deployment, and loopback verification. Add them to
the nftables and provider-firewall allowlists only after all of the following
are true:
1. the exact release is committed, tagged, bundled, and locally accepted;
2. the application runs as `radnlp-demo` on `127.0.0.1:8170`;
3. loopback liveness, readiness, Run, documentation, and route-denial tests
pass on the VPS;
4. the Caddyfile passes `caddy validate`;
5. `$DEMO_DOMAIN` resolves to `168.235.65.28` and any AAAA record resolves to
the same host's configured IPv6 address;
6. Caddy is ready to proxy only to loopback; and
7. fresh administrator and key-only root sessions from RADPAIR, sudo, and the
nftables rollback path remain verified.
Open 80 and 443, start or reload Caddy, and immediately run public TLS and
application acceptance. If activation fails, close 80/443 again while leaving
SSH available for recovery.
Port 8170 is never opened in either firewall. The Python and Prolog ports are
absent.
### 8.8 Final SSH policy
Retain key-based SSH authentication. Do not automate disabling alternative
authentication until:
- a second key-authenticated session is open; and
- the exact Debian 13 SSH defaults have been inspected;
- the named administrator has survived a reboot; and
- deployment and rollback administration work through `$DEMO_HOST`.
At that final gate, install a small validated SSH configuration fragment that
disables password authentication while retaining key-only login for
`radnlp-admin` and root. Validate with `sshd -t`, reload rather than restart
SSH, and prove fresh connections to both accounts before closing existing
sessions. The firewall remains the source boundary: TCP 22 accepts only
`107.161.20.175/32`. Root recovery is an intentional part of this no-console
model, not a temporary exception.
## 9. Service Configuration
### 9.1 Application unit
The installed systemd service should have the following effective shape:
~~~ini
[Unit]
Description=rad-nlp public artifact demo
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=radnlp-demo
Group=radnlp-demo
WorkingDirectory=/opt/radnlp-demo/current
ExecStart=/opt/radnlp-demo/current/bin/radnlp-toolchain-hub --host 127.0.0.1 --port 8170 --deployment-profile public-demo --config /opt/radnlp-demo/current/config/public-demo.json --state-dir /var/lib/radnlp-demo
Restart=on-failure
RestartSec=2
TimeoutStartSec=30
TimeoutStopSec=15
KillMode=control-group
UMask=0027
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadWritePaths=/var/lib/radnlp-demo
MemoryMax=384M
TasksMax=64
[Install]
WantedBy=multi-user.target
~~~
The actual unit must be tested with the target Debian 13 systemd release.
Hardening options are retained only when compatible with the static Go binary
and required DNS/network behavior.
### 9.2 Caddy
The production Caddyfile is deliberately small:
~~~caddyfile
$DEMO_DOMAIN {
encode zstd gzip
reverse_proxy 127.0.0.1:8170
}
~~~
Before a domain is available, provisioning validates this template with the
reserved placeholder `demo.invalid` and leaves Caddy stopped. Public activation
substitutes the validated domain, validates the exact production file again,
and only then starts or reloads Caddy.
Caddy owns public TLS. The application continues to emit its CSP,
X-Content-Type-Options, X-Frame-Options, and Referrer-Policy headers.
Enable HSTS only after:
- HTTPS succeeds publicly;
- certificate renewal is healthy;
- the domain is intended to remain HTTPS-only; and
- rollback does not require serving the domain over HTTP.
### 9.3 Logs
The Go process writes operational errors to the system journal. It must not log
request bodies or transcription text.
Caddy access logging is initially disabled unless there is a concrete
operational need. If enabled later:
- do not log request or response bodies;
- use short retention;
- restrict journal/file access;
- account for visitor IP addresses as data; and
- document the retention decision.
No third-party browser analytics, fonts, scripts, error collectors, or content
services are introduced.
## 10. Deployment Procedure
### 10.1 Build
From the repository root:
~~~sh
make -C infrastructure demo-bundle DEMO_RELEASE="$DEMO_RELEASE" DEMO_ARCH="$DEMO_ARCH"
~~~
Expected outputs:
~~~text
dist/rad-nlp-demo-$DEMO_RELEASE-$DEMO_ARCH.tar.gz
dist/rad-nlp-demo-$DEMO_RELEASE-$DEMO_ARCH.tar.gz.sha256
~~~
### 10.2 Harden and provision once
~~~sh
make -C infrastructure demo-bootstrap-admin DEMO_BOOTSTRAP_HOST="$DEMO_BOOTSTRAP_HOST" DEMO_HOST="$DEMO_HOST" DEMO_SSH_KEY="$DEMO_SSH_KEY"
make -C infrastructure demo-harden-host DEMO_HOST="$DEMO_HOST" DEMO_SSH_KEY="$DEMO_SSH_KEY" DEMO_ADMIN_SOURCE_IPV4="$DEMO_ADMIN_SOURCE_IPV4"
make -C infrastructure demo-provision DEMO_HOST="$DEMO_HOST" DEMO_SSH_KEY="$DEMO_SSH_KEY"
~~~
These targets are idempotent and have distinct safety boundaries. Bootstrap
creates and verifies the named administrator. Host hardening installs the
SSH-only nftables policy, patches Debian, enables security updates, and verifies
the rebooted host. Provision installs application prerequisites, creates
identities and directories, installs the service unit and inactive Caddy
configuration, validates both configurations, and leaves the prior application
release untouched.
They do not modify DNS or open TCP 80/443.
### 10.3 Deploy a release
~~~sh
make -C infrastructure demo-deploy DEMO_HOST="$DEMO_HOST" DEMO_SSH_KEY="$DEMO_SSH_KEY" DEMO_RELEASE="$DEMO_RELEASE" DEMO_ARCH="$DEMO_ARCH"
~~~
The target performs these operations:
1. upload archive and checksum to an explicit temporary filename;
2. verify the archive checksum remotely;
3. create a new release directory;
4. extract without overwriting another release;
5. verify the internal manifest;
6. run the new binary's local readiness smoke on an unused loopback port;
7. atomically update `/opt/radnlp-demo/current`;
8. restart `radnlp-public-demo.service`;
9. wait for loopback readiness;
10. keep Caddy and public web ports inactive;
11. run the complete loopback acceptance suite; and
12. remove only the explicitly uploaded temporary files.
If loopback smoke fails, the target restores the prior current symlink and
restarts the prior release. A separate `demo-activate-public` target validates
DNS and Caddy, opens 80/443, reloads Caddy, and runs public HTTPS smoke. If
public activation fails, it closes 80/443 again and reports the exact failure.
### 10.4 Activate public HTTPS
~~~sh
make -C infrastructure demo-activate-public DEMO_HOST="$DEMO_HOST" DEMO_SSH_KEY="$DEMO_SSH_KEY" DEMO_DOMAIN="$DEMO_DOMAIN"
~~~
This is the first operation permitted to open TCP 80/443 or start Caddy with
the production site. It verifies DNS first and closes the web ports again if
public acceptance fails.
### 10.5 Verify
~~~sh
make -C infrastructure demo-smoke DEMO_BASE_URL="https://$DEMO_DOMAIN"
~~~
Then inspect:
~~~sh
make -C infrastructure demo-status DEMO_HOST="$DEMO_HOST" DEMO_SSH_KEY="$DEMO_SSH_KEY"
~~~
## 11. Public Acceptance Matrix
### 11.1 Network and TLS
- DNS resolves to the intended VPS.
- TCP 80 redirects to HTTPS.
- TCP 443 serves a trusted certificate for `$DEMO_DOMAIN`.
- TCP 22 is reachable only from `107.161.20.175/32`; TCP 80 and 443 are
publicly reachable.
- Port 8170 accepts loopback connections only.
- No Python, Prolog, database, or model port is listening.
### 11.2 Application truth
- `/livez` returns live.
- `/readyz` returns ready only after artifact and documentation checks pass.
- The visible UI version matches its DOM version and asset keys.
- The health page identifies the deployment as an artifact-backed public demo.
- The health page explicitly states that the live NLP toolchain is not
deployed.
- Refresh and deep links restore R0–R5 truthfully.
### 11.3 Run behavior
- The reviewed generated transcription produces the expected stable execution.
- The Run response is JSON.
- Edited or arbitrary transcription is rejected with 422.
- Missing SPA proof is rejected.
- Non-JSON input is rejected.
- Cross-origin Run attempts are not allowed.
- Request size and timeout bounds remain active.
### 11.4 Route denial
The following are absent in public-demo mode:
~~~text
/api/v1/stanza/*
/api/v1/grew/*
/api/v1/ucxn/*
/api/v1/mocca/*
/api/v1/framenet/*
/api/v1/conllu/*
/api/v1/radlex/*
/api/v1/evaluations/*
/toolchains/*/evaluations/*
~~~
Evaluation POST, raw research evaluation artifacts, and arbitrary capability
operations must not become reachable through method changes or alternate path
forms.
### 11.5 Data boundary
- The archive inventory exactly matches the release allowlist.
- No historical report archive or CSV is present.
- No full generated corpus is present.
- No credential, key, token, cookie secret, or environment file is present.
- No development-machine absolute path is rendered publicly.
- The only transcription and IR artifacts are the reviewed no-PII POC.
- Documentation is the reviewed public allowlist.
- Application and proxy logs contain no transcription bodies.
### 11.6 Reliability
- The application recovers after a process kill.
- The service starts after reboot.
- A corrupt artifact prevents readiness.
- A missing document prevents readiness or removes only the explicitly
optional document according to the documented policy.
- Twenty concurrent UI reads do not breach the 384 MB service limit.
- Repeated exact Run requests remain stable and bounded.
### 11.7 Accessibility and UI
- Draft, Run, R0–R5, documentation, and health acceptance suites pass through
the public HTTPS origin.
- Keyboard interaction remains complete.
- No third-party frontend request occurs.
- The CSP allows only the intended local application behavior.
## 12. Monitoring and Routine Operation
### 12.1 External monitoring
An optional uptime monitor may call only:
~~~text
GET https://$DEMO_DOMAIN/livez
GET https://$DEMO_DOMAIN/readyz
~~~
It does not submit transcription or fetch raw artifacts.
### 12.2 Operator checks
~~~sh
ssh -4 -i "$DEMO_SSH_KEY" -o IdentitiesOnly=yes "$DEMO_HOST" 'systemctl is-active radnlp-public-demo.service caddy'
ssh -4 -i "$DEMO_SSH_KEY" -o IdentitiesOnly=yes "$DEMO_HOST" 'curl --fail --silent http://127.0.0.1:8170/readyz'
ssh -4 -i "$DEMO_SSH_KEY" -o IdentitiesOnly=yes "$DEMO_HOST" 'journalctl -u radnlp-public-demo.service -u caddy --since today --no-pager'
~~~
### 12.3 Updates
Operating-system security updates are applied through Debian's configured
security repositories and `unattended-upgrades`. Caddy is updated from its
official stable repository.
Application updates always use a new immutable release directory. No deployed
release is edited in place.
## 13. Rollback
List installed releases and identify the exact target:
~~~sh
ssh -4 -i "$DEMO_SSH_KEY" -o IdentitiesOnly=yes "$DEMO_HOST" 'find /opt/radnlp-demo/releases -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | sort'
~~~
Rollback is explicit:
~~~sh
make -C infrastructure demo-rollback DEMO_HOST="$DEMO_HOST" DEMO_SSH_KEY="$DEMO_SSH_KEY" DEMO_RELEASE="<installed-prior-release>"
~~~
The rollback target:
1. verifies the named release and manifest;
2. records the current target;
3. atomically changes the current symlink;
4. restarts the application;
5. waits for loopback readiness;
6. runs public smoke tests; and
7. restores the original target if rollback verification fails.
Rollback never deletes a release.
## 14. Teardown
Teardown is a separate, explicitly invoked operation. It:
1. disables and stops `radnlp-public-demo.service`;
2. removes the Caddy site configuration and reloads Caddy;
3. leaves release directories intact by default;
4. leaves DNS unchanged;
5. leaves the service account intact unless explicitly requested; and
6. reports the remaining paths.
Removal of `/opt/radnlp-demo` or `/var/lib/radnlp-demo` is never implicit in
a stop or uninstall command.
## 15. Later Full-Stack Demonstration
A live NLP demonstration is a separate deployment:
~~~text
public artifact demo ≠ live research toolchain
~~~
It would require:
- at least 4 GB RAM, with 8 GB preferred;
- a CPU-only Python/PyTorch installation;
- Stanza models;
- Grew and UCxn resources;
- SWI-Prolog and the generated RadLex runtime bundle;
- authentication or a private operator boundary;
- request queuing and concurrency limits;
- stronger abuse controls;
- durable execution storage;
- an explicit sensitive-input policy; and
- a new security review.
The current public-demo plan does not silently evolve into that topology.
## 16. Implementation Sequence
| Order | Work item | Completion evidence |
|---|---|---|
| 1 | Freeze the deployment contract and establish local/remote read-only baselines | Orchestration, regression, host inventory, and recovery prerequisites are recorded |
| 2 | Add Debian 13 administrator, nftables, patching, and SSH-hardening automation | Scripts are idempotent, syntax-checked, and fail closed |
| 3 | Establish `radnlp-admin` and harden the host with SSH only exposed | Fresh login, firewall, update, and post-reboot gates pass |
| 4 | Introduce deployment-profile configuration | Unit tests prove research and public route sets differ |
| 5 | Implement public readiness, health truth, and documentation allowlist | Corrupt/missing artifact tests, public health assertions, and forbidden-document tests pass |
| 6 | Add relocatable bundle layout and `demo-check`/`demo-build`/`demo-bundle` | Bundle runs outside the checkout; archive and manifest gates pass |
| 7 | Add hardened systemd/Caddy configuration and idempotent provision/deploy/rollback scripts | Local tests and Debian 13 configuration validation pass |
| 8 | Add loopback and public-origin smoke/denial tests | Full acceptance is executable in both phases |
| 9 | Commit and tag the public-demo release | Clean committed identity matches bundle and visible UI version |
| 10 | Provision and deploy through `$DEMO_HOST` behind closed web ports | VPS loopback acceptance passes with only SSH public |
| 11 | Activate DNS, TLS, and public web ports | Public HTTPS acceptance passes; only SSH/80/443 are exposed |
| 12 | Reboot, rollback, restore, and finalize SSH policy | Recovery evidence and final operational handoff are recorded |
## 17. Definition of Done
The public demo is complete only when:
1. every public route is explicitly allowlisted;
2. the bundle contains only approved files;
3. the application runs without the full research toolchain;
4. no arbitrary transcription can be interpreted;
5. no sensitive or machine-private material is deployed;
6. TLS, firewall, systemd hardening, readiness, and rollback are verified;
7. local interpreter/infrastructure regressions pass;
8. public HTTPS acceptance and denial tests pass;
9. the release is committed and tagged without moving `v0.1.0`; and
10. the health view describes exactly what is and is not running.
## 18. Authoritative External References
- [Caddy installation](https://caddyserver.com/docs/install)
- [Caddy automatic HTTPS](https://caddyserver.com/docs/automatic-https)
- [Caddy reverse proxy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)
- [Running Caddy with systemd](https://caddyserver.com/docs/running)
- [Debian security information](https://www.debian.org/security/)
- [Debian firewall and nftables guidance](https://www.debian.org/doc/manuals/debian-handbook/sect.firewall-packet-filtering.en.html)
- [Debian package-management and unattended-upgrades guidance](https://www.debian.org/doc/manuals/debian-reference/ch02.en.html)
- [systemd execution sandboxing](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html)
- [systemd resource controls](https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html)