BeyondTrust/POV

← Back to dashboard

Design: use cases per POV, and the accessor who ticks them off

Audience: contributor · Profile: pov · Read this when: you are changing the per-POV use-case checklist or who may tick it off.

Six slices, and one note for all of them, because the first had to carry the columns and the vocabulary the rest need — and deciding that twice is how they end up disagreeing. Slice 1 has no section of its own: it is the first half of this note, up to and including Progress: the checklist. The five after it get one each, and each opens by saying where it stands.

All of them are built. One question is still open — Entitle's Ephemeral-mode discriminator, in slice 2b — and nothing depends on the answer, which is why the SE-driven mint path exists.

The problem

services/personas.py ships eight roles and five cards each, and /use-cases renders them all. Every card resolves its readiness through feature_flags.enabled() — an instance-wide answer — and every card targets an estate page: /aws#instances, /vsphere, /k8s, /databases.

On a POV instance those pages are masked or 404. So every card resolves masked, and the catalog is complete, correct and unusable: a wall of grey saying "Not available on a POV instance". The page an SE would most want in front of a customer is the one that has nothing to say there.

The deeper reason is that a POV is not an instance. PovEnvironment carries pra_tenant_id, ps_tenant_id and entitle_tenant_id as three independent columns precisely because a Password-Safe-only evaluation, a PRA + Password Safe one and an all-three one are all normal shapes. Every one of them runs on an instance where pra_enabled is on. The flag cannot tell them apart, and it is the flag that every card was asking.

The third axis

Axis Module Power
Tenancy feature_flags.install_profile Gates. Subtracts, 404s routes
Role (demo profile only) services/personas.py Curates. Reorders, surfaces, never hides
This POV's products services/pov_cards.py + services/pov_use_cases.py States. Says what this environment can run

The third one is emphatically not a gate. The catalog is whole for all eight product mixes and the API serves every group and every card on every one of them (pinned); a mix decides each card's state, and nothing anywhere subtracts a card, 404s a route or disables a feature. Set the Entitle tenant on the POV row and its six cards are simply there, with no migration and nothing to re-enable. That is the property that keeps this from becoming a second install_profile.

What the pages then draw is a presentation decision on top of that, and all three draw only the in-scope cards — see One format, three surfaces for why, and for what the SE's page used to do instead.

The middle row is the one that moved. The 32 POV cards used to live in eight Persona.pov_use_cases tuples and render under role headings, which sorted a customer's remaining work by an axis belonging to the other install profile — and a POV instance has no role axis at all (personas.applies() is false there; see Personas). They now live in services/pov_cards, grouped by the product each one proves, because that is what a POV is bought and scoped by, with the OT cards pulled out on their own. Five groups, in this order:

Group Cards What it is
pra — Privileged Remote Access 10 Reaching a machine without a VPN, an inbound port or a password on screen
password_safe — Password Safe 8 + the runbook's 14 The lab's own accounts under management, and rotated
entitle — Entitle 6 Access that arrives when asked for and leaves on its own
ot — Operational Technology (OT) 7 The plant floor: process-network devices, the HMI, the PLC, the integrator
environment — Presenter to-do 5 Not a customer use case. The POV itself: who can reach it, what runs inside the customer's network, what teardown removes

A card's group is derived, never authored, with one exception: pov_cards.group_of returns the first product the card declares, or the environment group when it declares none. So requires_products is the single field deciding both a card's product group and its scope, and they cannot disagree. The exception is OT — "this is a plant-floor story" cannot be read off a card's products, so a card is filed there by being written in _OT_CARDS. The two-product cards outside OT both lead with pra; a test pins that the tuple order stays a decision.

The Skytap Password Safe Cloud POC runbook (services/pov_runbooks) is still its own registry, but its cards are listed under the Password Safe group (Runbook.group), after the product's own, rather than as a second Password Safe list. A tick on one is filed under password_safe.

The environment group is the presenter's to-do list, served with presenter: true. It is never hidden by the Setup tab's choice and never offered in its picker, it is counted apart (summary.todo_done / todo_total) rather than in the customer's numbers, and it is left off the customer's page, both Use cases pages and the after-POV summary. It is drawn in one place: its own section at the very end of the SE's Setup tab, where its items are ticked like any use case.

Three states, and three words that are not the estate page's

State Meaning Renders
ready This POV has the products, and the wire-up has run for them Links to the POV page tab
needs_wiring The tenant is set, the artifact is not No link; names what to run, points at the Wired tab
out_of_scope This POV has no tenant for that product at all Nothing — dropped before it reaches a row

out_of_scope is deliberately not masked. Masked means this instance's profile refuses the feature and no operator can change it. Out of scope means this customer's POV was not wired into that product — a scoping decision, not a fault. Borrowing the word would make a correctly scoped evaluation read as a misconfiguration, which is exactly the impression this whole change exists to remove.

Absence beats unwired when a card names two products. A card needing PRA and Password Safe on a POV with no PRA tenant cannot be run at all, so reporting "run the wire-up" would send an operator to a button that will skip the half they came for.

Where the line between the modules falls

services/pov_cards.py may not import database, and neither may services/personas.py — that one is load-bearing because api/docs_pages imports it deliberately, to survive without the api layer. So the POV resolvers take a dict of booleans, never a row:

{"pra": bool, "password_safe": bool, "entitle": bool,      # does this POV INCLUDE it?
 "wired": bool, "onboarded": bool, "entitle_wired": bool}  # has the wire-up RUN for it?

services/pov_use_cases.products_for is the one function that turns a PovEnvironment into that dict, and it reads the counts pov_wireup.describe already computes rather than re-deriving them. The import direction is one-way and asserted by test — and so is the other direction: nothing in the POV checklist stack may import services.personas except for the shared UseCase dataclass. A card is a card; a role is not.

The two halves of the dict are separate on purpose. "We did not include Entitle in this evaluation" and "we have not wired Entitle yet" are different sentences with different next actions, and only one of them is somebody's next click.

Progress: the checklist

PovUseCaseProgress — one row per card somebody touched, keyed (environment_id, card_id).

note and checked_by_kind ship in slice 1 though only slice 3 writes them, so that slice needs no migration — the discipline PovEnvironment already follows with its wire-up columns.

group_key records which group a card was ticked under, denormalised from the registry so a card that later moves keeps the answer it was ticked under. Nothing reads it — the summary groups off the live registry — and set_state rewrites it on every save, which is what healed the rows written while the checklist was grouped by role. It was called persona until then; database.py's migration list carries the rename.

This is the first thing in the card stack that writes, which is worth naming: a card navigates and never starts work. A tick is not that — it spends nothing, builds nothing, reaches no tenant — and it does not live in the registry at all. services/pov_cards stays a pure read; the write lands in api/pov.py, behind the auth every other POV action already carries.

Choosing the POV's use cases

A POV's tenants say which products it can reach. They do not say what the evaluation is for. So the first step of a POV's Setup tab is Use cases chosen, and the New POV form asks the same question as POV type:

POV type Use cases it tests
Password Safe Password Safe only
PRA PRA only
Total PASM (PRA + Password Safe) both, including the cards that need the two together, such as Password Safe credentials injected into a PRA session
Entitle Entitle only
Total PASM + Entitle all three
or tick the products any other mix

A card is in the POV when every product it names was chosen, so the combined PRA + Password Safe cards appear on a Total PASM POV and never on a Password-Safe-only one. The cards that name no product are the presenter's to-do list rather than use cases: they are not offered here and are on every POV. Inside the choice you can untick single use cases on the Setup tab, or a whole heading with the box beside it, which is where you leave out a demo — or all of OT — the customer does not care about.

This is the one place the checklist subtracts. A product with no tenant still renders its cards out_of_scope, because "no tenant picked yet" is somebody's next click. A card outside the chosen use cases is hidden instead. It is gone from the Use cases tab, from the customer's page and from the summary, and it is not counted in their totals: the SE has said this POV is not testing it. A write to a hidden card is refused, which only happens from a tab opened before the choice changed.

The rest of the Setup tab follows the choice. On a Password-Safe-only POV the PRA Gateway, the wire-up, the PRA/Password Safe link and the Entitle steps all show as skipped ("not in this POV's use cases"), even if those tenants are set.

PUT /api/pov/managed/{id}/use-case-scope with {products, excluded} sets it, behind the same owner gate as the Tenants edit. GET …/use-cases?all=1 is the picker's read: it keeps the hidden cards, flagged in_pov: false. The customer's page never asks for it.

Slice 2 — the ephemeral accessor

Built. The prospect gets a login of their own; slice 3 gives them something to do with it.

Identity

Entitle's create_actor — or an SE pressing a button — mints a real users row, so login, the sign-in throttle, MFA and the audit trail are the ones already here rather than a second implementation of each. What makes it an accessor is one column:

accessor_env_id = Column(String(36), nullable=True, index=True)   # non-NULL ⇒ accessor

plus a PovAccessor table holding the binding and its provenance: which POV, who minted it, from where, and when it dies. The environment id is duplicated onto User deliberately — the guard below runs on every request and must not need a join.

The trap, and why the guard is where it is

User.effective_permissions_dict returns {} for a user none of the three permission columns says anything about, and require_permission read {} as unrestricted until empty maps stopped granting anything. When this was written, an accessor created "with no permissions" held every permission in the dashboard.

So the confinement is a path allowlist in api/auth.get_current_user, the one dependency every authenticated route resolves through — the same one-reader shape as feature_flags.enabled. An allowlist and not a denylist: a route added tomorrow is refused by default, which is the only direction this can fail safely in. Two entries today, /api/pov/accessor/self and /api/auth/me, and a test pins that it has not grown.

Three surfaces resolve users without going through that dependency, so each is closed where it lives:

Surface Why it matters What it does now
api/entitle_rest Grants dashboard permissions up to administrator, and resolves an actor by scanning users and matching a name. An accessor reaching that lookup is a direct escalation. Every route filters accessor_env_id IS NULL, on the query rather than the response
api/websocket._authenticate Its own resolver, so the path allowlist does not reach it. Job Live Output is not for a prospect. Refuses an accessor on both the JWT and the PAT branch
/api/users An admin PATCH setting is_admin, or clearing the binding, is two keystrokes to an operator account Accessors are hidden from the list, and every mutation route — patch, deactivate, permanent delete, PAT mint — refuses one with a 409 naming the Access tab

require_admin also refuses an accessor outright. No admin route is in the allowlist, so that is unreachable today — which is the point: the day one is added by mistake, an accessor still does not become an administrator.

The client-side half is a convenience, not a control. /api/auth/me reports accessor_env_id, the Alpine store keeps it, and requireAuth() sends an accessor to /pov/access rather than leaving them on a dashboard whose every call 403s. It reads localStorage, which the holder can edit, and it runs in their browser. The gate consults nothing the client sends.

The Entitle side

api/pov_accessor_rest.py serves Entitle's Ephemeral Accounts route set — the half api/entitle_rest.py deliberately omits, because dashboard users are permanent accounts and these are the opposite case:

GET  /api/pov/accessor/rest/get_assets            live POVs, "pov:<env_id>"
GET  /api/pov/accessor/rest/get_all_permissions
POST /api/pov/accessor/rest/create_actor          mint; returns the credentials
POST /api/pov/accessor/rest/delete_actor          delete
POST /api/pov/accessor/rest/check_config

There is deliberately no give_access: in Ephemeral mode create_actor is the grant.

Four things are copied from functions/fnworkloads/db_grant.py, each learned expensively there. The identity arrives in provisioning_data, not actor — reading only actor is what made every real ephemeral grant come back 400. delete_actor refuses any username without the povguest_ prefix, checked on the name in the request before any lookup, so a row that disagreed could not talk it into deleting an operator's account. create_actor returns the credentials, because handing them to the requester is the whole point of the call — and it returns them nested, under data.login_info, beside a data.actor block of exactly identifier, name, type and email. That shape is the whole of Provisioned Actor Data and it allows nothing else: the flat version was rejected in full, after the accessor had already been minted.

Its secret is its own (pov_accessor_rest_secret), fail-closed in the same shape as the standing adapter's: 503 when unset, one response for missing and wrong. An endpoint whose job is minting logins must not authenticate with a credential that already grants permissions.

The manual path, and why it exists

POST /api/pov/managed/{id}/accessors mints one directly from the POV page's Access tab. That is not a convenience feature — it is what makes this slice shippable and testable with no Entitle tenant at all, and it is how the open question below gets answered.

The password is in that response and nowhere else. There is no reveal endpoint, unlike the share link's: that password has to be re-readable because an SE reads it to a customer days later, while an accessor that lost its password is replaced, which is one click and leaves an audit line.

Lifetime

An accessor keeps its own clock, on pov_share's three-step rule — an explicit request, else the POV's own expires_at, else 14 days — and it is clamped so an accessor can never outlive the POV it can reach. Entitle owning expiry on its side is not the same as this instance knowing when to stop trusting a login: an integration that is removed, misconfigured, or simply never calls back would otherwise leave a working credential behind forever.

Reaping

pov_accessor_service.teardown runs first in run_env_destroy, taking the position the share link used to hold. That position belongs to whatever a person outside the account can be holding, and an accessor is that — but it is a credential into this dashboard rather than a door into a lab, so its window is the one worth making shortest. Revoking deletes the user row rather than deactivating it: an inactive row is still a username every actor lookup in the codebase has to remember to skip, and the one that forgets is an escalation.

expiry_reaper enqueues the identical pov_env_destroy job, so the auto-delete timer is covered by the same hook with no second code path.

The backstop is a sweep on the POV reconcile pass, outside its platform loop: a fresh POV instance starts with the auto-delete timer off, and whether a prospect's login should still work has nothing to do with whether Skytap is reachable. It catches accessors past their expiry and accessors whose POV already reached destroyed.

The use-case record is not removed with them. That distinction is the point: an accessor is a credential, and the checklist is the account of what the evaluation covered.

Slice 2b — registering the adapter with Entitle

Built. POV page → Access → Register with Entitle creates the REST integration in this POV's own Entitle tenant, so a prospect requests access there and the login is minted, and removed, by the grant itself.

The gap it had to close first, which was two gaps

register_rest took no tenant context. Without one it authenticates with the global Entitle key and writes the global owner and workflow ids into the HCL — two separate readers of the singleton, and threading the context through only one of them is worse than neither: you authenticate as one tenant and name another's ids. So ctx now reaches both _hcl_fields(ctx) (whose ids the HCL names) and _apply_hcl_sync (whose key applies it), and a test pins both halves.

This was never a live bug. The one caller was cloud_function_service, behind cloud_functions_enabled, which is _DEMO_ONLY — on an estate instance the singleton is the tenant, and that path is unchanged: with no ctx the fallback is exactly what it was.

A lighter tenant context

pov_wireup.entitle_context refused without an agent token and an SSH key. Both are real prerequisites for the SSH ephemeral-accounts connector, which reaches a private target from inside the network — and neither applies to a REST adapter, which Entitle calls over public HTTPS. Reusing it would have blocked accessor registration on prerequisites that do not exist for it.

So entitle_tenant_ctx is the part every integration in a tenant shares — whose tenant, and the owner and workflow ids _common_attrs_hcl refuses without — and entitle_context layers the SSH-specific three (agent token, sudo user, private key) on top rather than duplicating the resolution.

Everything is checked before anything is created

A registration that half-succeeds leaves an integration in a customer's Entitle tenant that this dashboard has no state for and therefore cannot remove. So each prerequisite is a refusal with the remedy in it, reported on the row as accessor_blocker and shown on the page instead of the button rather than beside a live one:

Missing Why it is fatal rather than a warning
An Entitle tenant on this POV There is nowhere to register it
pov_accessor_rest_secret The adapter answers 503 to everything, so the integration would be created and then reject every call Entitle made — which looks like an Entitle fault and is not
This instance's public URL Entitle calls the adapter from its own cloud
HTTPS on that URL Entitle will not call a plaintext endpoint — the same refusal, for the same reason, the broker enrolment makes about the agent endpoint

Teardown: the tap before the drain

pov_accessor_entitle.teardown runs before pov_accessor_service.teardown, and that order is the point rather than housekeeping. While the integration is live Entitle can mint a new accessor, so removing the logins first races a destroy against a grant and can leave one created after the step that removed them. The destroy order is now: the integration, the logins it mints, the share link, then the customer's appliance objects.

A failed deregistration keeps the terraform state so a re-run can finish it — the rule the per-VM wire-up teardown already follows. An integration left in a customer's tenant is untidy; one this dashboard can no longer find is worse, and it is standing access.

The open question, still open

Entitle's Ephemeral-mode discriminator is unconfirmed against a live tenant: register_rest assumes the API infers the mode from allow_creating_accounts and the route key set. The person who can settle it is the operator who just pressed Register, so the page asks them — open the integration in Entitle and check its Connection setting says Ephemeral Accounts. If it says Standing, the discriminator is real and belongs in register_rest. Until somebody has looked, the SE-driven mint is the path that does not depend on the answer, and it is why that path exists.

Slice 3 — the accessor writes

Built. GET /pov/access takes no env id in the URL, and neither does anything under /api/pov/accessor/self/*: every one of them resolves the POV from current_user.accessor_env_id. "Could an accessor write to somebody else's POV?" is not guarded here — there is no parameter to guard.

That is also why the write routes needed no change to the allowlist. The allowlisted prefix is /self, and everything under it is env-from-session by construction; a write placed anywhere else would have had to widen that list, which is exactly the moment somebody should stop and think.

A comment is not a verdict

PovUseCaseProgress.state gained a third value, the empty string. Somebody typing "couldn't get this working" on a card they have not marked is exactly the feedback the feature exists to capture, and making them claim they covered it first would be a lie the UI put in their mouth. _summarize counts done and skipped by name, so a note-only row is neither — which is the truth.

Two writers on one row

set_state's note became tri-state, and this is the sharpest bug the slice could have shipped:

None   leave whatever note is there alone   (the default)
""     clear it
text   replace it

The SE's tick button sends a state and no note. With note defaulting to "" and written unconditionally — which is what slice 1 did, correctly, when only one party wrote these rows — an operator ticking a card the customer had just commented on would silently erase the comment. That is the one piece of evidence in this feature that cannot be reconstructed, and it is a regression test rather than a note here.

Every tick records checked_by_kind, the column slice 1 shipped unused, so the operator's page can say whose it was. "We showed them" and "they did it themselves" are not the same claim to take into a renewal conversation.

What the accessor may see

Two projections, both written for this audience:

share_view The lab link and its expiry. Drops share_id — the publish set's own id, which exists so this dashboard can revoke exactly that one later and means nothing to a customer. No password: revealing it is its own POST, audited under the same action as the operator's own reveal, because it is a second door onto a live credential
wired_view Per VM: what it is, where it is on their own lab network, and which kinds of access exist — a brokered session, a vaulted credential, access on request. Never pra_jump_id, ps_managed_system_id, entitle_integration_id or wiring_error: those are ids inside a customer's own appliance, meaningful only to teardown, and an error message written for an operator

Naming what goes in, rather than subtracting from api/pov._serialize, is the point. Subtraction is how one of them comes back on the next edit to the serializer.

The customer's note is rendered back on the operator's page with x-text, never x-html: it is prose typed by somebody outside the account.

Slice 4 — the page an SE actually clicks

Built, and it is the one that closes the complaint this document opens with.

Slices 1 to 3 built the per-POV checklist and put it a click deeper, on the POV's own page. The nav's Use cases page was left exactly as it was — on purpose, with a test pinning that it must not change — so it went on opening to twenty-six greyed-out cards saying "Not available on a POV instance". The wall survived four merges because the fix had been built somewhere else.

So on a POV instance that page now leads with a POV: pick one from the selector, get its checklist. Measured on the same instance, that is 26 masked cards leading the page before and 25 runnable ones after.

Collapsed turned out not to be enough. The first build of this slice left the instance-wide catalog underneath the lead, collapsed behind one click. Every group and every card was still rendered — still in the DOM, still in find-in-page, and still one stray click from being the wall the lead exists to replace, on the page whose whole job is to stop being that wall. So on a POV instance the instance-wide catalog is not rendered at all: x-if, not x-show, because hidden is a claim about CSS and this is a claim about the DOM.

Which axis does that is the part worth reading twice, because it looks like the rule this page rests on. A persona may reorder and emphasise and never subtract — unchanged, and still pinned on the estate profile: the groups assignment may not .filter(. This is the profile choosing which of two catalogs the page is about. The instance-wide one answers "can this instance run it?", which a POV page has no use for; the per-POV one answers "can I run this on this POV?". An estate instance renders the first, complete and unfiltered, exactly as it did before; a POV instance renders the second, and nothing else.

One format, three surfaces

The checklist is rendered in one dense, BeyondTrust-branded format everywhere it appears: sections with an uppercase group header and a right-floated count, and one row per card with the title and its summary on a single line, separated by an em-dash. It replaced a three-column grid of tall cards, which was correct and which nobody scanned mid-call — a POV carries 32 product cards plus up to 14 runbook ones.

Surface Who writes The tick
/use-cases, the POV lead nobody a static ✓ / – mark and the word for it
/pov/<id>#use-cases the SE a checkbox, and the whole row is a click target
the accessor's page the customer a checkbox, plus an always-visible note box

The read-only surface gets a mark, not a checkbox. It performs no writes (pinned), and a checkbox there would be a control that looks live and records nothing — worse than no control, because a tick somebody believed is a card they will not demo again. For the same reason the mark is blank rather than an empty box outline: an outlined square that does nothing when clicked is a control.

out_of_scope cards are not drawn on any of the three, and a group left with nothing in it renders no heading either. All three go through an inScope() helper with the same name and the same one-line body, pinned by a test, because the failure mode is one surface being edited and the others not.

The SE's page used to be the exception: it rendered those cards in a dashed section at the foot of each group, headed not part of this POV, on the reasoning that a scoping decision is worth reading and a card an SE demoed anyway is still a card they demoed. In practice a POV scoped to one product put two whole products' worth of un-tickable rows — most of the page — between the SE and the handful of cards they were there for, under a heading that disagreed with the in-scope denominator printed directly above it. Which products an evaluation includes is now stated once, on the Overview tab, where the tenants that decide it already live.

The stylesheet is templates/_checklist_styles.html — one definition, reached through a new {% block head %} in base.html by the two surfaces that extend the shell, and by a direct {% include %} from the accessor page, which does not extend it and must not start. Three copies of a palette would be three chances for the customer-facing surface to drift, and two of the three files are never open at the same time. Every selector is prefixed chk-, and no element carrying one also carries a Tailwind utility.

No backend. The lead reads /api/pov/managed and the same /api/pov/managed/{id}/use-cases the POV's own page reads, so the two cannot disagree about what a POV can run, and every way that read can fail — an estate instance 404s the router, an expired session 401s — is a non-event rather than an error on a page whose main content loaded fine. It performs no writes: ticking a card off stays on the POV's own page, where every other action on that POV already lives.

Slice 5 — what a POV leaves behind

Built. The record was never the problem: run_env_destroy marks the row destroyed rather than deleting it, and pov_use_cases.destroy_note deliberately keeps the checklist as that record's contents. What was missing is that nothing could reach it. api/pov.list_managed filters destroyed rows out — right for a list of things you can act on — so a finished evaluation appeared nowhere and you needed its raw uuid to see it again. Evidence that survived teardown with no way back to it is the same as losing it at the moment somebody wants it, which is a renewal conversation weeks later.

Two questions, two shapes

services/pov_summary answers them separately because they are separately asked:

archive(db, limit) "Which evaluations have we run?" A light row per POV, and deliberately not _serialize: that builds five describes per row — gateway, resource broker, wire-up, share, accessors — each asking a question about a living environment, every one meaningless and a wasted query for a POV that is gone. Coverage comes from one aggregate over the progress rows rather than resolving the whole catalog per row. It says when it truncated, because a list silently cut is one an SE trusts and should not
build(db, env) "What happened in that one?" The whole account: coverage per group, every card somebody touched, and what they said. Groups with nothing in scope are dropped, so on a scoped POV that breakdown is the product summary — the rows left standing are what the customer bought

/api/pov/managed/archive is declared before /managed/{env_id} — the third time this repo has met that trap, and pinned for the third time. Below it, "archive" is captured as an environment id and the endpoint answers "No such POV environment".

/summary has no status filter, deliberately: it is the endpoint written for after a POV is over, and filtering the finished ones out would leave it describing only the evaluations whose story is not finished being told.

The claim the data supports

"They took part" means they ticked something. A login issued and a login used are two different facts, and only the second is evidence. took_part is computed from progress rows carrying checked_by_kind='accessor' — never from the existence of a PovAccessor — and the archive table reports the two separately: "2 ticked by them" against "1 login, unused". Conflating them would put a claim in a renewal conversation that the data does not support.

Only cards somebody touched are in the summary. A summary listing every untouched card would be the catalog again, and the catalog is not what happened. Roles with nothing in scope are dropped from the breakdown for the same reason — a Password-Safe-only evaluation has several, and "0 of 0" against each is noise in a document read once.

Nothing here writes or reaches the network

A summary that made platform calls would fail for exactly the POVs it exists to describe, whose environment is gone. Products are read off the row — the only thing that can work for a destroyed POV, and the honest question for a live one, since what an evaluation covered was decided when its tenants were chosen.

The take-away is copied, not downloaded: what an SE does with this is paste it into a renewal note or a CRM, and a file on disk is one more step to the same place. The markdown is built in the browser from what the page already loaded.