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.

Authoritative source: infrastructure/PUBLIC-DEMO-DEPLOYMENT-PLAN.md · Permanent section link

# 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)