Site profile — configuring a deployment¶
A toolbox that names one lab's server, one lab's institution and one country's mains frequency in its source code is a toolbox only that lab can run. Those are facts about a deployment, not about the software, so they live in a git-ignored config/site.local.json describing the machine in front of you. This is the operator SOP for that file: what it holds, how to make one, and what the toolbox does when there isn't one. The code is ssvep.runtime.site; the contract is schemas/site_profile.schema.json; a fully commented template is config/site.example.json. Issue: #165.
1. The default is generic, and that is the point¶
A clone with no profile behaves generically: no institution stamped into BIDS, no push destination, no pre-filled operator, PowerLineFrequency recorded as n/a. A lab is the configured special case — set up once per acquisition PC — not the built-in assumption.
That inversion is most of the work of being shareable. Before it, the generic path existed only in the sense that someone could edit the constants: it was never the path anybody ran, so nothing kept it working. Now it is what an ordinary pytest -q exercises from end to end (tests/conftest.py pins the suite to the generic profile), and a student cloning the repository on a laptop gets a toolbox that records, converts and analyses without ever being told about a lab it has nothing to do with.
Nothing about safety or governance is relaxed by being generic. The photosensitivity warnings, the always-available abort, the consent gate and de-identification are code, not configuration — see §5.
2. Making one¶
Copy config/site.example.json to config/site.local.json and edit it. *.local.json is already git-ignored, so a profile never reaches GitHub — which matters, because it names a server and may list the people who operate the machine.
Three kinds, all one schema:
profile |
What it means | Typically |
|---|---|---|
generic |
Anything unconfigured — a student laptop, a fresh clone, a demo machine. | Everything blank. |
lab |
A research deployment collecting under an ethics approval. | Institution + governance + push destination + operator list. |
teaching |
A classroom machine. No approval, and normally no destination. | push.allowed: false, locked: true, a dataset name for the course. |
The kind is a label on one schema, not three schemas. It is what the GUI shows and what a person reading the file understands at a glance; every actual restriction is expressed by the fields.
3. What each block does¶
| Block | Effect |
|---|---|
institution |
InstitutionName / InstitutionAddress / InstitutionalDepartmentName in every recording's BIDS *_eeg.json. A field left empty is omitted from the sidecar, never written as "n/a": an absent key says the dataset does not record it, "n/a" asserts that somebody looked and there was none. |
governance |
The approval this deployment collects under. Stamped as HowToAcknowledge (and License) on the raw dataset and on derivatives, since a derivative is bound by whatever governs the data it came from. Optional — ethics approval is a property of some deployments, never a precondition for running the toolbox. approval is a list, because a lab can run under more than one approved protocol at a time; its entry shape belongs to #161, and one field is already settled: an entry names headsets, never electrode_types, because electrode type is derived from the headset. |
dataset |
Name / Authors / Acknowledgements in BIDS/dataset_description.json and the derivatives dataset. Research-team names only — never a participant. |
acquisition.line_frequency_hz |
Mains frequency here: 60 in North America, 50 across most of the rest of the world. null means unknown and BIDS then records "n/a", which is honest where a hardcoded 60 was one country's fact presented as a universal one. |
push |
The deployment's default destination, and whether pushing is permitted at all (§4). |
operators |
Who may be named as the operator of a recording (§6). |
locked |
The GUI will not offer to edit a locked profile. A guardrail so a student does not change the class configuration by accident — not security: anyone with the machine can edit the file. |
4. Push: a profile can forbid, never enable¶
push.host / user / remote_base are the deployment's default destination; config/push.local.json (the Setup tab writes it) overrides them per machine, and every file the GUI has ever written names all of those fields explicitly, so existing lab machines are unaffected by the change.
There is deliberately no enabled key in a site profile — writing one is refused with an error that says so. A profile that could switch transmission on would be a profile that widens what the code does, and a fresh machine must never transmit data unasked. Turning auto-push on stays an opt-in made on the machine itself.
push.allowed: false goes the other way and forbids pushing outright. The refusal is at the transport, not merely at the toggle: test_connection, remote_index and push_paths all stop before touching the network, so a caller that never consulted a setting still cannot transmit. allowed is read from the profile every time and is never written into push.local.json — a machine-local file that anyone can edit must not be able to lift a restriction the deployment declared.
That flag is a guardrail, not the protection. The actual protection is that a teaching machine has no key on the research server and the server does not list it. Do not relax the server side because the toolbox has a flag. See PUSH.md for the transport and the consent gate.
5. Standing constraint: a profile may only restrict¶
A site profile may only ever make the toolbox more restrictive, never less. The photosensitivity warnings and the always-available abort (COMPLIANCE.md R6) can never be weakened by configuration — in a room of undergraduates they matter more, not less — and neither can the consent gate (R5) or de-identification (R2).
This is not enforced by guarding those checks against a profile. It is enforced by there being no key that could reach them, and by tests/test_site_profile.py failing if one is ever added: one test walks every property name in the schema and rejects anything that looks like a switch turning a safeguard off, and another runs the safeguards under all three profile kinds plus a maximally-permissive one and asserts they still fire. If a deployment genuinely needs to be stricter than the default, express that as a field whose restrictive value is the non-default one, the way push.allowed does.
6. Operators: provenance, not access control¶
COMPLIANCE.md R8 wants every recording stamped with who collected it — that identifies the researcher, never the participant, and it is the only way to ask "who ran this?" two years later.
A non-empty operators.whitelist turns the operator field into a selection: a name must be chosen before recording, and ssvep.runtime.auth.resolve_operator refuses a name that is not on the list. No whitelist means free text. default is blank unless you set it, because a pre-filled name is a name that ends up on someone else's recording.
The whitelist is not an access control and must not be described as one. It is a file anyone with the machine can edit, so refusing an unlisted name protects nothing. What actually protects the data is elsewhere, and the toolbox does not simulate it:
- the server — an account exists on it or it does not, an SSH key is registered or it is not. The push transport authenticates with the OS SSH-key chain under
BatchMode=yes, and there is no secret anywhere in this repository, so a machine without a key simply cannot transmit. A class cannot push because of that, not because of a flag; - the consent gate, re-read off each session's sidecar at the moment of any transfer, never inherited from whoever asked for it;
- the lab's own storage controls (R4), which this layer sits on top of and never relaxes.
What a list does buy is that names stay spelled the same way across a study and nobody types a colleague's name from memory. That is the whole claim.
7. Missing is fine; broken is fatal¶
State of config/site.local.json |
What happens |
|---|---|
| Absent | Generic profile. Normal, supported, silent. |
| Present and valid | That deployment's profile. |
| Present and unparseable — bad JSON, an unknown kind, a misspelled key, a default operator nobody may select | SiteProfileError, loudly. Never a fall back to generic. |
The asymmetry is deliberate. "Absent" means nobody has configured this machine, which is a normal state. "Present and broken" means somebody did configure it and got it wrong — and silently demoting that machine to generic would take the institution and the ethics reference off real participant data while reporting success. Unknown keys are refused rather than ignored for the same reason: a profile is read once, by nobody, on a machine that then records participants, and a mistyped institution would otherwise load as a deployment that stamps nothing.
8. Still to come¶
The GUI half of #165 is not built yet: there is no first-run prompt offering to create a profile, no profile-creation form, no window-title display of the active profile, and the Run tab's operator field is not yet wired to operators.whitelist / locked. Until then a profile is made by copying the example file. The API those screens need is here already — site.load_profile, site.save_profile, site.SiteProfile.describe, auth.operators, auth.requires_selection, auth.default_operator_name, auth.resolve_operator.