Twenty minutes, three files. A capture in the UK MOD's open sensor standard; the enriched record an analyst made from it; and the same governed corpus projected back into that standard, once per recipient, so a fusion node consumes what it is entitled to with no translation step in front of it. Every figure here was captured from the deployed container on 2026-08-20.
BSI Flex 335 — SAPIENT is Dstl's open standard for networks of autonomous
sensors: edge nodes register with a fusion node and then report what they detect. It is freely
published, its .proto files and reference middleware are Apache-2.0, and
NATO is adopting it as the counter-UAS interoperability standard. So the
interesting question is not whether this platform can read a drone detection — it is where a
governance platform sits in a sensor network. The answer this demo gives: between the
sensors and the fusion node, speaking the standard in both directions, deciding what each
recipient receives rather than handing over a file and trusting them to redact it.
http://127.0.0.1:8092 and log in. The two
counter-UAS profiles ship unapproved on purpose — approving one live is a good
thirty seconds, not a fault. Do not pick a local model from the analyst dropdown
while presenting over the public tunnel: it takes about 394 seconds and Cloudflare cuts the
response at 100.
The capture · counter_uas_sapient.bin
On the Run tab, under 1 · Dataset, choose
counter_uas_sapient.bin.
21 columns · 2,000 records · sapient_bsi_flex_335_v2_0 · 2,012 messages · 2,000 detection reports · 12 registration (not governed)
Three things in one line. The format was decided from the bytes —
rename the file notes.csv and it still reads as SAPIENT, because on the input
path of a governance platform a filename is a claim by whoever typed it. The capture is a
conversation, and 12 of its messages are nodes registering with the fusion node: those
describe the network rather than an observation of the world, so they are counted and
not turned into governed records. And 2,000 detection reports went in without
anybody converting anything to a spreadsheet first.
Say where the field names came from, because this is the part that is different from every other format the platform reads.
timestamp 2026-03-02T06:11:51Z node_id 018f3a2c-1f00-7a10-8000-000000000032 object_id CEJZ05XHT5GMEJ146S6JR410DD location_x -97.23268 ← longitude location_y 26.9091 ← latitude location_z 158.496 location_coordinate_system LOCATION_COORDINATE_SYSTEM_LAT_LNG_DEG_M location_datum LOCATION_DATUM_WGS84_E detection_confidence 0.96 object_info_sensor_modality RF object_info_cooperative true ← it identified itself object_info_id_type remote_id_serial classification Air:0.81 behaviour Approaching:0.83 signal_0_centre_frequency 2436999936 id 1596F6090228
Four of those columns are not fields in the standard at all.
object_info is a repeated {type, value} pair — SAPIENT's
declared extension point, whose vocabulary each node registers up front — and the
reader turns the type into the column name. So a sensor saying
type: "cooperative", value: "true" arrives as a governable column with
nothing written in Python. That is how a use case adds a field to this platform.
“The other two binary formats we read needed somebody to sit down with a
specification and type out a JSON table of what each tag means. That table is a place
errors live silently. SAPIENT ships its own schema — so we vendored the
.proto files verbatim, Crown copyright notice and all, and the decoder runs
off a descriptor compiled from them. There is no hand-written field table here to drift
out of date, and no generated code in the tree either. Upgrading to the next BSI Flex
version is dropping in new .proto files.”
“location_x is longitude and location_y is latitude
per the standard's own comment, and the enum underneath says which convention
is in force. We did not rename them to something friendlier on the way in. Renaming is
interpretation dressed as decoding — and interpretation is the profile author's job,
which is the next thing you will see.”
Under 2 · Profile, read the ranked picker before selecting.
✓ Counter-UAS SAPIENT capture — sensor network (unapproved) — describes 100.0% of columns ✓ DCSA applicant vetting — describes 0.0% of columns ✓ Counter-UAS Remote ID capture — describes 0.0% of columns ✓ Telecom CDR — describes 0.0% of columns ✗ 5 profile(s) cannot govern this file
A vetting profile can run on a drone capture — it would apply
lineage and a detection pass and describe none of it. Ranking on coverage is how “able”
is kept from reading as “appropriate”. Select the SAPIENT profile, which shows
unapproved, type your name, and press
Approve as named person. Its fingerprint is 3df3cd15a681cd36
before approval and identical after: approval records a decision, it does not modify the
thing decided on.
Press 4 · Run. Then stop on the second line for a moment.
PROMOTED · 2,000 records · 0 quarantined · composite 100.0 · 0.42 s lineage 2,000/2,000 stamped whether anyone asked or not pii_detect 0/2,000 ran on every record, tagged nothing
“Personal-data detection ran across two thousand detection reports and found nothing. Not because it was switched off — it ran on every record. There is nothing personal in this format to find. A SAPIENT DetectionReport carries position, confidence, classification, behaviour, RF signal. It carries no operator name, no home address, no classification marking, no U.S. Persons determination and no retention basis.”
“That is now three sensing standards in a row — MISB ST 0601 for full-motion video metadata, ASTM Remote ID off the air, and SAPIENT. Sensing standards carry observation, not entitlement. Which tells you where a governance platform's obligations actually begin, and it is not at the sensor.”
One thread does run back to a person: id — the field BSI Flex
335 documents as “ID of the object being reported (e.g. the tail number of
aircraft)” — carrying the Remote ID serial an RF node heard on 591 of the
2,000 reports. object_info_cooperative splits the corpus on exactly
that line: 591 aircraft identified themselves and 1,409 were painted by radar and said
nothing. What the sensor does not report is who the serial belongs to; that needs
a registry, and that join is enrichment. It is pseudonymous
— it becomes identifying only through a civil aviation registry this platform does not
hold — so the profile leaves it undeclared rather than asserting an answer that
is jurisdiction-dependent. Silence means “nobody has said”, so the detector keeps looking
at it. Remember this field; it carries the whole of Act Three.
Same file, one click
Open the line that reads 5 profile(s) cannot govern this file.
✗ 'Classification markings' cannot run on this dataset: missing element: security_classification ✗ 'U.S. Persons controls' cannot run on this dataset: missing element: us_person ✗ 'Declassification review' cannot run on this dataset: missing element: security_classification or creation_date remedy: remove it, or supply the missing element
The profile that governs the enriched record is refused on this one, by name, with the element each refusal turns on — and the answer arrives before a single record is touched, because whether a control can run is a property of the columns, not of any row. It does not quietly skip §2.3 and hand you a clean-looking output.
“A container that cannot carry the flag must not be allowed to quietly retire the control. If this platform ran the U.S. Persons control here it would report success on two thousand records and protect nobody, because there is no field in the standard saying who is a U.S. person. Governance obligations attach at enrichment, not at the sensor — and that generalises a long way past drones.”
counter_uas_detections.csv · and two captures you make on stage
Choose counter_uas_detections.csv, select
Counter-UAS Detections — ground sensor network, approve it.
27 columns · 2,000 records · CSV
Same drones, same afternoon. An analyst has since joined the registry, made a classification call, and recorded whether a lawful basis exists for holding the identity. Nine of the twenty-seven columns did not come off any antenna — they are judgements somebody made afterwards. That is the whole difference between Act One and Act Three.
Leave 3 · Recipient authority unticked and run. This is the airport security desk: cleared for the air picture, not for U.S. Persons information.
PROMOTED · 2,000 records · 0 withheld · composite 100.0 · 0.43 s lineage 2,000/2,000 classification 2,000/2,000 pii_detect 2,000/2,000 usper 2,000/2,000 changed 5 columns: remote_id_serial · operator_latitude · operator_longitude operator_name · operator_address declass_review 0/2,000
The drone's own position is untouched — a machine is not a protected
person, and minimising it would destroy the track the system exists to produce. So is
reporting_officer: §2.3 protects the subject of collection, not the
government employee who filed the report.
Click the Emission node and press SAPIENT DetectionReport (BSI Flex 335 v2.0). Save the file.
emission_sapient_detections_….bin 513,596 bytes Specification not held — the records are emitted and labelled non-conformant, not withheld.
Read that disclaimer out. The platform does not hold the BSI Flex 335 ICD,
so it does not claim conformance to it — but it does validate every record's
mandatory fields against the is_mandatory annotations carried in the
standard's own .proto files, rather than against a list maintained here that
could drift. That check earned itself the day it was written: it caught that
report_id was unset on every record.
Tick Recipient is USPER-authorised, run again, and emit again.
PROMOTED · 2,000 records · 89 withheld usper 2,000/2,000 changed 0 columns emission_sapient_detections_….bin 489,589 bytes
Nothing was masked for this recipient — and the file got smaller, because 89 detections left it entirely. Under E.O. 12333 §2.3 you need authority and a lawful basis for retaining the information. Those 89 involve U.S. persons and nobody recorded a reason for holding their identity, so the authorised recipient receives less than the unauthorised one did. Not a masked name — no record.
The round trip. Feed each downloaded .bin straight back into
1 · Dataset.
9 columns · 2,000 records · sapient_bsi_flex_335_v2_0
· 2,000 messages · 2,000 detection reports
9 columns · 1,911 records · sapient_bsi_flex_335_v2_0
· 1,911 messages · 1,911 detection reports
Then put the same detection side by side. It is the first record in both files.
report_id EPB3A4T7YKAYVW4KEDR3PRN2Z2 ← identical in both object_id DET-00000000 ← identical in both location_x -97.23268 ← identical in both location_y 26.9091 ← identical in both id [U.S. PERSON] airport security desk id 1596F6090228 federal partner, authorised
“That is our own reader, running the standard's own schema, accepting a file we produced. It is not a SAPIENT-flavoured export — a fusion node would take it.”
“The serial is the field carrying the entitlement. It is pseudonymous, but it resolves to a named person through the registry, and it is the field that reaches a SAPIENT emission — so the profile puts it in the minimisation set, and an unauthorised fusion node does not get a handle on the operator.”
“And the message id is the same in both files. Message ids derive from the profile fingerprint and the row's position, not from a clock, so re-emitting one governed corpus produces identical bytes. Two runs of a release set cannot differ for reasons that are not governed.”
Across the two captures: 456 serials minimised and 135 in the clear for the security desk; none minimised and 502 in the clear for the federal partner. 1,409 detections carry no serial at all in either — those are drones nobody heard a Remote ID broadcast from.
One corpus, one standard, two entitlements
| Recipient | Detections | Serial in the clear | What the fusion node receives |
|---|---|---|---|
| Airport security desk — not authorised | 2,000 | 135 | Mask 456 serials as [U.S. PERSON];
full air picture |
| Federal partner — authorised, basis stated | 1,911 | 502 | Release every serial, reason recorded |
| Federal partner — authorised, no basis | 89 | — | Withhold the detection does not leave |
Both files are valid BSI Flex 335 v2.0 captures, and the platform reads either of them straight back. The aircraft's position is never touched in either.
Optional · a terminal beat, because the button is not wired for this yet
Steps 07–09 produce each recipient's capture with two runs. The platform can also produce every release from one upload as a single archive with a manifest — but the UI's Produce the release set button is currently hardcoded to the governed CSV, so a SAPIENT release set is an API call today. Say so if you show it.
releases = [
{"name": "Airport security desk", "target": "sapient_detections"},
{"name": "Federal partner", "target": "sapient_detections",
"usper_authorised": true}
]
→ releases_0.1_….zip · 183,761 bytes
manifest.json
airport_security_desk_emission_sapient_detections_….bin 2,000 records
federal_partner_emission_sapient_detections_….bin 1,911 records
The manifest reports each release's authority, the controls applied, and that
mandatory-field validation passed — with conformant: false stated plainly. It
also says what it cannot tell you: the two releases hold different record counts, so
no per-record comparison is possible. An honest “no answer” rather than a misleading diff. The
pipeline runs once per distinct authority, not once per release, so two releases
cannot disagree about what was governed.
And short answers that are true
The standard's .proto files are in the repository verbatim
with their Crown copyright and Apache-2.0 notice, and decoding runs through a descriptor
compiled from them — no hand-typed field table, no generated code in the tree. A test
holds the descriptor to its sources, so a .proto change without a rebuild
fails the suite rather than the deployment.
Yes — and it is written down. Tuesday's counter-UAS CSV is a hand-rolled
version of a SAPIENT DetectionReport and nobody looked first. It is recorded in
docs/alternatives-considered.md beside the same mistake made with ODCS. The
rule that came out of it is: research the standard before designing a format. Conceding
those two is what makes the rest of that document worth reading.
Not today. The platform reads uploads, and a capture of length-delimited messages is the same shape as any other file. Live SAPIENT is TCP to the middleware and is separate work — it would not need Kafka, which is ruled out here by name for the laptop requirement, because SAPIENT already ships a broker.
Because we do not hold the ICD, and claiming conformance to a document you do not hold is how this goes wrong quietly. What is claimed is what was checked: structurally valid, every mandatory field verified against the standard's own annotations. A missing specification is not a reason to withhold governed data — it is a reason to label it accurately.
The profile governing the capture is a JSON file. The emission target — which column maps to which field of the standard, and the coordinate-system constants — is a JSON file. What stays code is the walker that turns bytes into values, and what a control does to a record. A target naming a format nothing implements is refused by name at load, with the refusal saying where the behaviour would go.
Nothing rests on it being right. A machine reading only ever proposes meanings — it cannot mark a field required, write a personal-data declaration, raise a profile's authority, approve anything, or choose which profile governs a run. Each is enforced in code with a test behind it.
The four things most likely to
| Symptom | Cause | Fix |
|---|---|---|
The .bin upload is refused, or the rail shows nothing |
The container was built without the sapient extra, so there is no
protobuf runtime to decode with |
docker compose up -d --build gdp — rebuild only the gdp
service. Rebuilding the tunnel changes the public hostname. |
| “no detection reports in this capture” | The file holds only the registration handshake, not the traffic | The refusal names what it did find. Use
counter_uas_sapient.bin. |
| Profile greyed out in the picker | Refused for this file, or not yet approved | Read the refusal panel — it names every missing element. If it says unapproved, select it and use the approve box. |
| Demo URL is dead | The tunnel container restarted and the hostname changed | docker compose logs tunnel | grep trycloudflare |
Every figure on this page was captured from the
deployed container on 2026-08-20, not estimated. A profile named telco sits in
the picker from an earlier upload and is unrelated to this demo. If a number on screen differs
from a number here, trust the screen and tell me.