From 888293dd1ec993db4acb0e409d446e33d071d31d Mon Sep 17 00:00:00 2001 From: abrichr Date: Mon, 27 Jul 2026 22:39:34 -0400 Subject: [PATCH] docs: give the phone path an answer that does not start with "configure TLS" The docs said, correctly, that reaching the decision surface from a phone requires the customer to terminate trusted TLS in front of the runner. That was the only phone path, and it disqualified the target buyer: a dental practice will not stand up an enterprise reverse proxy, so the phone path was documented as real and was unavailable to the people it was for. There are two paths now, and this says which is which. The hosted lane needs nothing on the customer's network. The runner dials OUT, so there is no inbound port, no port forward, no certificate, no reverse proxy and no static address, and it works behind NAT on ordinary broadband. Connect the computer once, enable remote decisions bound to the exact tenant and runner, and staff answer from a web page on a phone. What it carries is stated exactly, because "we send less" is only trustworthy if the reader can check it. The closed halt context is closed enums, bounded integers and booleans with no string field and no image, so the hosted service is structurally unable to hold a name, an MRN, an observed value or a workflow label. It is not scrubbed evidence. The one thing it gives up -- the target control's accessible name -- is named rather than glossed. The runner-local portal keeps its warning verbatim, because it is still true of that surface and it is still the right surface for an organisation that operates an ingress. It is now framed as the full-fidelity option rather than the only option, and the warning points at the alternative. The configuration reference gains the settings, the credential, the profile ceiling rule, and the note that every misconfiguration stops the console: a lane that looks on and is not is worse than one that is plainly off. --- docs/concepts/halt-learn-loop.md | 56 +++++++++++++++++++---- docs/guides/security-and-data-handling.md | 17 +++++-- docs/reference/configuration.md | 44 +++++++++++++++++- 3 files changed, 102 insertions(+), 15 deletions(-) diff --git a/docs/concepts/halt-learn-loop.md b/docs/concepts/halt-learn-loop.md index 8416ade..5303500 100644 --- a/docs/concepts/halt-learn-loop.md +++ b/docs/concepts/halt-learn-loop.md @@ -72,12 +72,14 @@ counts, and expiry. There is no free-text field anywhere in that envelope, so raw values and prose are **structurally unable** to travel rather than being stripped in transit. -The direct consequence is that a hosted dashboard shows **less** than the phone -on the runner — it can say the *shape* of a failure but not its content. That is -the design working, not a gap: +The direct consequence is that a hosted surface shows **less** than the runner — +it can say the *shape* of a failure but not its content. That is the design +working, not a gap: - "0 of 2 required identity signals confirmed the record on screen" is a bounded integer and crosses the boundary safely. +- "the OCR rung was tried and did not resolve a unique target" is a pair of + closed enums and crosses safely too. - "could not find the button labelled *Open*" names on-screen content, so it stays local. @@ -92,7 +94,45 @@ configuration and bound to an exact tenant and runner, and it carries a stronger authentication requirement. Unconfigured, tasks are issued for the local decision surface and the hosted view is read-only triage. -### Reaching it from a phone is your ingress, not ours +### Reaching it from a phone + +There are two ways, and they trade fidelity against what your network has to do. +Neither of them asks OpenAdapt to open a hole in it. + +#### The hosted lane — nothing to configure + +**This is the default answer for a practice without an IT department.** The +runner makes **outbound HTTPS requests only** to the control plane: no inbound +port, no port forward, no certificate, no reverse proxy, no static address. It +works behind NAT on an ordinary broadband line. + +1. Connect the desktop app to OpenAdapt Cloud once. It registers this computer + and stores its credential in the operating-system keychain. +2. Turn on remote decisions in your deployment configuration, bound to the exact + tenant and runner the control plane issued. +3. Staff open the hosted queue on a phone and sign in. It is a web page; there + is nothing to install. + +That is the whole list. **You do not terminate TLS.** The only TLS involved is +the runner's outbound connection to a public host with an ordinary public +certificate. + +What the phone shows on this lane is the *closed halt context*: which category +of check failed, which resolution rungs were tried and what each one returned, +which contracts a "Continue" will re-prove, and bounded counts. Every value is a +closed enum, a bounded integer, or a boolean — **there is no string field and no +image**, so the hosted service is structurally unable to hold a name, an MRN, an +observed value, or a workflow label. It is not scrubbed; it has nowhere to put +them. + +The one thing it gives up is the target control's own accessible name. The phone +says *"OpenAdapt could not find the button"* rather than *"the button labelled +`Open`"*, and it tells you a name exists that it is not showing you. + +#### The runner-local portal — full fidelity, on your own terms + +The portal on the runner serves everything, including the protected screenshot +crops. That is why it is the path with a network requirement. !!! warning "The portal is loopback-only until you publish it" Out of the box the decision portal binds `127.0.0.1` and advertises a @@ -101,7 +141,7 @@ decision surface and the hosted view is read-only triage. front of the runner — an enterprise reverse proxy, a VPN, or a ZTNA hostname — and to record that decision in configuration. - Plan for this before you pilot the phone path. See + Use the hosted lane above if you do not operate one. See [the portal settings](../reference/configuration.md#the-mobile-decision-portal) for the exact variables and [Deploy on-prem](../guides/deploy-on-prem.md#reaching-the-decision-portal-from-a-phone) @@ -110,9 +150,9 @@ decision surface and the hosted view is read-only triage. We did not punch a hole in your network for our convenience, and there is no "bind everything" switch to make a demo easier. The boundary in front of a runner that can see protected records is yours to open, deliberately, under your -own certificate and access policy — so the phone path inherits the -authentication, device posture, and logging you already run, instead of asking -you to trust a second one. +own certificate and access policy — so this path inherits the authentication, +device posture, and logging you already run, instead of asking you to trust a +second one. Every widening step is explicit and **fails closed**. A wildcard bind address is refused in every mode. So is a plaintext origin, an origin carrying a path or diff --git a/docs/guides/security-and-data-handling.md b/docs/guides/security-and-data-handling.md index 622435f..402582a 100644 --- a/docs/guides/security-and-data-handling.md +++ b/docs/guides/security-and-data-handling.md @@ -86,12 +86,19 @@ The same boundary governs the [attended decision path](../concepts/halt-learn-loop.md#where-a-halt-goes-the-attended-decision), where a halted run is answered by staff — including from a phone. -- **The decision surface is served by the runner, inside your boundary.** It is - a responsive web app on the runner itself, not a native mobile app and not a - hosted page. It is **loopback-only until you publish it** through your own - trusted TLS ingress; it fails closed on any partial configuration and offers - no self-signed bypass or wildcard bind. See +- **The full-evidence decision surface is served by the runner, inside your + boundary.** It is a responsive web app on the runner itself, not a native + mobile app and not a hosted page. It is **loopback-only until you publish it** + through your own trusted TLS ingress; it fails closed on any partial + configuration and offers no self-signed bypass or wildcard bind. See [the portal settings](../reference/configuration.md#the-mobile-decision-portal). +- **If you do not operate an ingress, use the hosted lane instead.** The runner + dials **out** to the control plane, so there is no inbound port, no + certificate, and nothing to configure on your network. That lane carries the + signed PHI-free task and the closed halt context only: closed enums, bounded + integers, and booleans, with **no string field and no image**. It is not + scrubbed evidence — it is an envelope that cannot represent a record. See + [Reaching it from a phone](../concepts/halt-learn-loop.md#reaching-it-from-a-phone). - **Protected evidence does not leave the runner.** The retained screen, the observed values, the OCR, and the failing target stay local. Projections and evidence crops are served `no-store` and are never written to a diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 2ef14a4..4e0bfa2 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -128,10 +128,50 @@ listens and what URL it advertises to a paired phone. Unconfigured, the portal binds `127.0.0.1` and advertises a loopback URL. That is a complete, working configuration *on that computer* — the pairing screen says a phone cannot reach it rather than minting a link that fails on - your network. Serving a phone is an explicit decision you make by standing up - trusted TLS in front of the runner. See + your network. Publishing **this** surface to a phone is an explicit decision + you make by standing up trusted TLS in front of the runner. See [Deploy on-prem](../guides/deploy-on-prem.md#reaching-the-decision-portal-from-a-phone). + **You do not have to.** The hosted lane below reaches a phone from anywhere + with nothing configured on your network; it carries less, and it says so. + +## Answering a halt on a phone with no ingress + +The runner dials **out** to the control plane — no inbound port, no port +forward, no certificate, no reverse proxy, no static address — so a phone +reaches the queue from anywhere. Turn it on in the deployment configuration, +bound to the exact tenant and runner the control plane issued for this machine: + +```yaml +human_decisions: + remote: + enabled: true + tenant_id: + runner_id: + # context_tier: remote_closed_context # the default +``` + +| Variable | Purpose | +|---|---| +| `OPENADAPT_RUNNER_TOKEN` | The per-runner credential the control plane issued. The desktop app stores it in the operating-system keychain when you connect this computer, and passes it to the engine; set it by hand only for a headless runner. | + +| Setting | Default | Purpose | +|---|---|---| +| `human_decisions.remote.enabled` | `false` | Must be literally `true`. A truthy string does not enable it. | +| `human_decisions.remote.tenant_id` | *(unset)* | Required when enabled. | +| `human_decisions.remote.runner_id` | *(unset)* | Required when enabled. | +| `human_decisions.remote.context_tier` | `remote_closed_context` | `remote_closed_context` (what broke, as closed enums and bounded integers) or `remote_identifiers` (identifiers and counts only). `local_full` is refused by name — protected evidence never leaves the runner. | + +The execution profile applies its own ceiling, and the **weaker** of the two +wins, so configuration cannot widen what a profile permits. + +!!! note "Every misconfiguration stops the console" + A missing runner credential, a deployment that did not enable remote + issuance, a read-only console, or a plaintext control-plane origin each + refuse to start rather than run a console whose phone lane is silently + absent. A lane that looks on and is not is worse than one that is plainly + off. + Every widening step fails closed, and the portal **does not start** on an invalid combination rather than falling back to something more exposed: