Skip to content

Releases: FerroHEALTH/FerroFED

0.0.11

Choose a tag to compare

@github-actions github-actions released this 16 Oct 14:31
Immutable release. Only release title and notes can be modified.
v0.0.11
8cefc8f

Supported until 2031-10-16, 5 years from this release (Regulation (EU) 2024/2847 Art 13(8), (19)). A security fix ships as its own patch release on each supported line, as SECURITY.md says.

Upgrade notes

  • The operator console refuses offline_access in [oidc] scopes. Remove
    it from the list and set [oidc] offline_access = true instead (#643).
  • A receiving member's template that admits several COMPOSITION.category
    codes and assumes none now refuses every received document of its
    category with a 422 (MissingRequiredSuffix on <root>/category|code).
    Give the template's category an assumed value (AOM 1.4 assumed_value)
    (#714).
  • ferrofed_node_requests_total carries a new label, class: member
    for a member's own answer or failure, client-error for a 4xx it
    answered a routed request with. Such a 4xx now counts as
    outcome="node-error", where it was active. The shipped
    FerroFEDMemberFailing rule leaves class="client-error" out, so it
    pages on the same member failures as before; a rule or a panel of your
    own that reads outcome="node-error" should leave it out too (#982).

Added

  • Information a representative inserts, marked as theirs (#1003,
    Regulation (EU) 2025/327 Art 4(2) and Art 5): an issuer that sets
    act_is_representative = true on [auth.issuer.patient] declares that
    the RFC 8693 act claim of its patient tokens names a proxy or a legal
    representative. A document the FHIR face receives under such a token is
    written with every entry naming the representative as its provider, a
    PARTY_IDENTIFIED whose one identifier is the act sub, never the
    person's PARTY_SELF, and its access record carries the
    ehds-representative-inserted entity. A token whose act names the
    representative by nothing, by its own sub or by an identifier of the
    document's patient is 403 with nothing sent. Without the declaration
    the gateway reads no representative, and the person's own insertion
    stays PARTY_SELF. eehrxf 0.0.19 adds Representative and
    ReceivedComposition::inserted_by_representative; ehds-logging 0.0.24
    replaces AccessRecord::subject_inserted with inserted, an
    Insertion of the subject or a representative. RFC 8693 is vendored.
  • A configuration-invalid refusal on the operator console's
    configuration page shows the gateway's message, which names the key it
    refused and never a value, beside the status and the code.
  • The operator console is styled on the "Azure & Iron" brand tokens, in a
    light and a dark theme: tables with a header row and row separators,
    labelled form fields, refusals and incomplete answers in boxes that say
    what they are, and health states as words in a tinted cell. Every page
    fits a 320 px screen: a wide table scrolls sideways in its own box, never
    the page. The stylesheet is Tailwind CSS 4.3.3, built by cargo-leptos with
    no Node and no new JavaScript (#1037). The shell these pages sit in, its
    top bar, navigation and theme switch, is the one under Changed (#1075).
  • The electronic prescription and dispensation datasets of the
    interoperability component (#1038, Regulation (EU) 2025/327
    Art 14(1)(b) and (c)): eehrxf 0.0.21 carries, under the features
    prescription and dispensation, the crosswalks from the Xt-EHR
    EHDSMedicationPrescription and EHDSMedicationDispense 1.0.0 models to
    the HL7 Europe MPD 1.0.0 MedicationRequest-eu-mpd and
    MedicationDispense-eu-mpd profiles, held to both vendored packages. With
    fhir-r4, the new medication module reads a member's
    MedicationRequest and MedicationDispense into the datasets with a typed
    refusal for every rule a resource breaks, writes them back in the MPD
    form, and checks a resource and each Medication it contains against the
    vendored MPD profiles, naming what the check cannot evaluate. The number
    of dispensations a prescription item has left travels in the extension
    https://ferrofed.eu/fhir/StructureDefinition/remaining-dispensations on
    dispenseRequest, written only when the member states a value.
  • The members that serve electronic prescriptions and dispensations on the
    FHIR face (#1039, Regulation (EU) 2025/327 Art 14(1)(b) and (c)):
    [[fhir.serve]] declares, per category, the members that serve it on
    their own hl7-fhir-rest endpoint in the HL7 Europe MPD 1.0.0 profiles,
    and config check names them. A category no member is declared for is
    answered as not served on GET {fhir-base}/MedicationRequest,
    GET {fhir-base}/MedicationRequest/{id} and
    POST {fhir-base}/MedicationDispense: 404 with an OperationOutcome
    of the issue type not-supported, never an empty list, and no member is
    asked. GET {fhir-base}/metadata lists MedicationRequest and
    MedicationDispense only for a declared category. The start is refused
    for a declared member the registry does not hold, one with no endpoint of
    its own serving the category, a native endpoint of either category that
    leaves out its MPD profile, and a face with no resolver.
  • The search of a patient's prescriptions on the FHIR face (#1040,
    Regulation (EU) 2025/327 Art 14(1)(b)):
    GET {fhir-base}/MedicationRequest?patient.identifier=system|value asks
    every member declared for Electronic-Prescriptions that holds the
    patient at its own endpoint, by its own ehr_id alone, following the
    member's next links under its own base. The answer is one searchset
    with each member's MedicationRequest as the member sent it except its
    id and its entry's fullUrl, which carry the face's locator, the id
    the read takes (#1041); one Provenance naming the member, and a
    warning when two members hold a prescription under the same
    identifier. _count pages the answer through the gateway's own -offset
    parameter. A silent member, and a member the cross-reference could not
    answer for (not-resolved), fails the search under all-or-nothing and is
    named under partial; an unknown patient and a patient a consent
    decision hides get the same empty searchset. The search takes the
    composition- search scope over every template and is recorded under
    Electronic-Prescriptions.
  • The read of one prescription on the FHIR face (#1041, Regulation (EU)
    2025/327 Art 14(1)(b)): GET {fhir-base}/MedicationRequest/{id} reads
    a prescription by the id the search now gives it, which names the
    member's own endpoint, its ehr_id and its own id of the prescription,
    so the gateway holds no state between the two. The member the id names
    is asked alone, by its ehr_id and its own id (_id), and its
    MedicationRequest is answered as it sent it, with the id read and the
    member named in openEHR-federation-endpoint. An id with no owner
    answers 404 with one text whatever the reason; an ehr_id two members
    claim answers 409 and raises the integrity incident; a member answering
    two prescriptions for one id answers 409; a caller confined to one
    patient reads that patient's prescriptions alone. The read takes the
    composition- read scope over every template and is recorded under
    Electronic-Prescriptions. The search names each prescription by the
    same id, and its fullUrl by the read; a prescription whose parts do
    not fit an id is listed as its member sent it, with a warning naming
    the member and quoting no identifier.
  • The receipt of a dispensation on the FHIR face (#1042, Regulation (EU)
    2025/327 Art 14(1)(c)): POST {fhir-base}/MedicationDispense takes one
    MedicationDispense, held to the HL7 Europe MPD 1.0.0 profile before any
    member is asked, and writes it once to the one member that owns the
    prescription it names, its body as it was sent. The gateway resolves the
    patient from subject, asks every member holding the patient for its
    prescriptions by that member's own ehr_id, and matches the
    prescription's business identifier itself, so the identifier never
    reaches a member. A prescription named by the face's
    MedicationRequest/{id} alone is refused 422, since the member
    receives the body unchanged and can link it only by the identifier. A
    written dispensation answers 201 with a Location naming it by the
    face's id; no owner answers 404 as an unknown patient does; two members
    holding the prescription answer 409 and raise the new
    PrescriptionIdentifierCollision integrity incident; the owner's refusal
    answers 409 and its silence 504, the outcome unknown and the write
    never sent again. The receipt takes the composition- create scope over
    every template, is recorded under Electronic-Dispensations and
    Electronic-Prescriptions, and is counted by outcome in
    ferrofed_fhir_dispensations_total. Serving Electronic-Dispensations
    now needs fhir.receive_namespaces, the incident report keeps the last
    20 incidents of each kind, and the capability statement declares exactly
    the interactions the face serves (#1110).
  • The documentation of electronic prescriptions and dispensations on the
    FHIR face (#1045, Regulation (EU) 2025/327 Art 14(1)(b) and (c)): the
    book's prescriptions page now carries the member contract a serving
    member must meet, the duplicate-dispensation rule among it, every route's
    answers in one table, the locator ids, completeness under partial, the
    remaining-dispensations extension and its published definition, the
    access records and the dispensation counter, and the capability
    statement. The configuration reference, the instructions for use, the
    information sheet, the Annex II checklist and the rights of natural
    persons now name what the face serves and receives, and the information
    sheet lists the HL7 Europe MPD 1.0.0 package among its standards.
  • The FHIR R4 StructureDefinition of the remaining-dispensations extension
    (#1053): `eehr...
Read more

0.0.10

Choose a tag to compare

@github-actions github-actions released this 15 Oct 07:00
Immutable release. Only release title and notes can be modified.
v0.0.10
ef7e308

Supported until 2031-10-15, 5 years from this release (Regulation (EU) 2024/2847 Art 13(8), (19)). A security fix ships in the latest release, as SECURITY.md says.

Upgrade notes

  • A gateway with a registry outside the development profile must set
    [audit] destination (#623, #693), and it must be "repository" with an
    [audit.repository] table: "log" is refused there, because it records no
    caller and no patient and so cannot hold the access log Regulation (EU)
    2025/327 Annex II 3.2 asks for. Point the gateway at your Audit Record
    Repository, with spool_dir on durable storage, before you upgrade.
  • Set server.shutdown_timeout_ms to at least server.request_timeout_ms,
    or remove it so it takes the request timeout (#625). config check and
    serve now refuse a shorter drain, and the v0.0.9 example configuration
    set 10000 beside a request timeout of 30000. Then give the container
    longer to stop than server.drain_delay_ms plus the drain: the examples
    use stop_grace_period: 40s in Compose and
    terminationGracePeriodSeconds: 45 in Kubernetes.
  • If you run the Kubernetes example, move from deployment.yaml to
    statefulset.yaml (#628). Replace the encrypted storage class with
    your own. Wait until the dependency report, GET {base}/operator/dependencies with an operator token (#751), reads up for
    the audit repository, so the spool on the old emptyDir is drained, then
    delete the Deployment and apply the StatefulSet.
  • The listeners need no change: unset, [server.tls] and [metrics.tls]
    leave them on plain HTTP as before (#632). Once you set either, remove it
    before rolling back to an earlier release, which refuses it as an unknown
    key.
  • If [metrics] listen names an address other than a loopback one outside
    profile = "development", set [metrics] scrape_token_file to a file
    holding a scrape token and give your Prometheus the same token with
    authorization.credentials_file, or set [metrics.tls] client_ca_file
    (#635). Without either, serve and config check refuse the
    configuration. The Kubernetes example reads the token from the
    metrics-scrape-token key of the ferrofed-secrets Secret.
  • To run the admin listener's stored-query distribution outside the
    development profile, from the gateway's host or another, set
    operator_scope on the [[auth.issuer]] your operators use and send a
    token carrying it (#629, #635). Without one it answers
    401 unauthenticated, from a loopback peer as from any other, where a
    loopback peer was served before.
  • If a backend client reads patient data through the gateway with a
    system/ scope or a token whose sub is its client_id, set
    client_tokens_act_for_professional = true on its [[auth.issuer]] and
    have its tokens name the professional it acts for in
    extensions.ihe_iua.national_provider_identifier (#661). Until both
    hold, such a request is answered 401 natural-person-required. Then set
    [auth.issuer.assurance] on each issuer whose tokens reach patient data;
    without it no assurance level is required, and config check notes the
    issuer.
  • A document received on the FHIR face whose codings carry no display is
    now refused with a 422 unless [eehrxf.terminology] names a terminology
    server to look the displays up (#687). Senders that omit displays need the
    server configured.
  • Read the dependency report at GET {base}/operator/dependencies with a
    token that carries your issuer's operator_scope (#751). GET {base}/health/dependencies is no longer served and answers 404, so move
    every monitor that read it, and give it an operator token. ferrofed report
    reads the report with --operator-token-file. A proxy rule that kept
    /health/dependencies inside can go; the health family left open,
    {base}/health and {base}/health/readiness, names no member.
  • Move any stored-query definition you hold in the namespace
    eu.ferrofed.eehrxf, or a namespace under it, to a namespace of your own
    before you upgrade (#776). The gateway now reserves that namespace for its
    own read-only queries: a redb or PostgreSQL store, or a definition file,
    holding one there refuses the start, and config check refuses such a
    file.
  • If you wrote an [access_log] map on a development build, rewrite its
    categories with the codes of HL7 Europe's
    EEHRxFDocumentPriorityCategoryCS (#824), which are case-sensitive:
    patient-summary becomes Patient-Summaries, electronic-prescription
    Electronic-Prescriptions, electronic-dispensation
    Electronic-Dispensations, medical-imaging Medical-Imaging,
    medical-test-result Laboratory-Reports and discharge-report
    Discharge-Reports, in [access_log.templates],
    [access_log.archetypes] and the keys of
    [access_log.retention.categories]. Declare each national category in
    national_categories as <system>|<code>, the absolute URI of your
    Member State's code system (or one you control) and its code, and name it
    the same way in the map and the retention table. The old spellings and a
    national code without a system are refused when the configuration
    loads. A search of your Audit Record Repository for a category matches
    the ehds-category detail <system>|<code>, never the bare code.
  • The registry document refuses an [[organisation]] that manages no
    [[endpoint]] (§8.1: "an Organization manages 1..* Endpoints"), so
    config check and the gateway's start fail on it, naming the
    organisation (#874). Give the organisation an endpoint it manages, or
    remove it. A registry read from an mCSD directory or in the FHIR form
    keeps such an organisation, and selecting it still answers 404 with
    no-destination.

Added

  • The identity page of the book says how each member's ehr_ids reach the
    PIX Manager: the PIXm ITI-104 feed or the PMIR ITI-93 feed from each
    member, the bulk load of the EHRs that already exist, why the gateway
    feeds no Manager itself, and what to do when the hospital MPI does not
    answer PIXm (#622). It names SanteMPI 2.5.12 as a PIX Manager verified
    against two CDRs, with its setup and its limits, and the products checked
    that do not answer as the gateway needs.
  • The end-to-end lane resolves a patient through SanteMPI, a deployable PIX
    Manager each member feeds over ITI-93, and gets a federated query answered
    by both members (#622).
  • The access log: every federated query, stored-query execution, routed
    read and routed write that reaches patient data at a node is recorded
    with the verified caller, as Regulation (EU) 2025/327 Annex II 3.2 asks
    of an EHR system's logging component and the GDPR asks of an access
    trail (#623). Each record is a BALP AuditEvent sent through the
    [audit] spool to the Audit Record Repository. It names the provider,
    the person and their purposes of use, the patient and each ehr_id, the
    time, each endpoint asked with its node and outcome, and the categories
    of the data. The categories come from a map you declare in the new
    [access_log] table, from template and archetype ids to the Art 14(1)
    categories, national categories or none. An access the map cannot
    classify exactly is recorded unclassified, with its ids as evidence,
    and is never refused. The gateway stores the record before the answer
    leaves. An access whose record cannot be stored answers
    503 access-unrecorded with none of the data. A query run from the
    operator console is recorded with the operator as the caller. The book's
    audit page describes the record and gives an Annex II 3.2 checklist.
  • crates/ehds-logging 0.0.1, the European logging component as a library
    (#623): the Annex II 3.2 access record, the Art 14(1) categories, the
    category map and its classification, the sink trait, and, behind
    balp, the record written as a BALP AuditEvent through ihe-iti. It
    depends on nothing in FerroFED and on no interoperability component.
  • The book has a production guide that walks a deployment from nothing to a
    first federated query over two CDRs (#624): what to have in place, a
    checklist each CDR meets before admission, verifying and installing the
    release, the registry, an issuer recipe for Keycloak 26.2 or later that
    issues the RFC 9068 tokens the gateway accepts with the SMART on openEHR
    scopes and the purpose of use, the onward credentials, the PIX Manager,
    the access log, TLS, the checks before the start, admission, the first
    query and the operations that follow. The Keycloak recipe was verified
    against Keycloak 26.8.0.
  • deploy/nginx/ferrofed.conf is a reverse proxy for the gateway: TLS on the
    public address, the base path, the JWK Set and PMIR feed routes passed, the
    health family kept inside, and an access log that names the gateway's
    request id and never a query string (#624).
    scripts/checks/production-guide.sh runs config check over every TOML
    block of the guide, then serves that configuration behind the shipped
    file in nginx and checks each route; CI runs it in the release compose job.
  • [signing] next_key_file, a signing key the JWK Set at
    {base}/.well-known/jwks.json publishes ahead of a rotation and that never
    signs, so every replica publishes a new key before any replica signs with
    it (#627). next_key_algorithm names the algorithm the next key is meant
    for, and config check refuses a next key on another curve, naming
    signing.next_key_file, or one that is already the current or previous
    key. The book gives the three-step rotation for any number of replicas
    and the node cache time it assumes.
  • The metrics now show what the gateway's own clients see (#629).
    ferrofed_http_requests_total counts every answered request by method,
    route template and status class, and the OpenTelemetry HTTP server
    metrics `http_server_request_durat...
Read more

0.0.9

Choose a tag to compare

@github-actions github-actions released this 05 Oct 18:14
Immutable release. Only release title and notes can be modified.
v0.0.9
95659ac

Added

  • The operator views of the operator console (#276). After sign-in, which
    now exchanges the code at the provider's token endpoint with the PKCE
    verifier and checks the ID Token's signature, issuer, single audience,
    expiry and nonce before a session begins (the new [oidc] keys token_endpoint and
    jwks_uri), the console shows the member endpoints with their membership
    standing and last observed health (/members), the integrity incidents
    and the creating_system_id routing table (/integrity), the stored
    queries with their AQL (/stored-queries), and the gateway's
    self-description (/federation), each rendered on the server from the
    gateway's surface with the operator's own token. A view without a
    signed-in session sends the browser to sign-in, and a refusal shows the
    gateway's status and stable error code, a 401 with a link back to
    sign-in. The routing table and the stored queries are paged, 100 rows a
    page. With secure_cookie on, the console's cookies carry the __Host-
    prefix, and a new sign-in ends the session the browser held.

  • A read-only operator surface on the gateway: GET {base}/operator/incidents
    (each incident kind's count and the last 25 incidents of each kind),
    /operator/creating-systems (the routing table) and
    /operator/stored-queries (every held version, as the ITS-REST
    StoredQuery), admitted only for a token carrying the operator_scope its
    [[auth.issuer]] entry names as one whole scope token. The last two answer
    one page, {"items", "offset", "total"}, under the optional offset and
    limit parameters, with limit at most 100. It answers
    routing ids, counts and stored definitions only, and an incident names an
    ehr_id only when it is a bare UUID (§5.4.1, N33).

  • ferrofed conformance run --node-profile reports CP-18, CP-19 and CP-27
    against every active member (#573; §16.2; N26, N27, N34). The
    Federation-Node profile checks are the gateway's own: each member is read
    through its node client, with its onward credentials and the outbound
    gate, for the patient's EHR by its ehr_id alone, an EHR created with no
    subject, and its error statuses for an unknown EHR and an unparsable
    query, and the admission check runs as before. The access and consent
    checks need a refusal arranged with the node's own policy, so a run
    records them not observable with that reason. The harness runs the same
    checks against FerroEHR and EHRbase. The run also scores a new track 3
    scenario: the ENDPOINT attributes a client selects are each row's
    endpoint id and system_id (§9.4, N12, CP-37).

  • Every release attaches the conformance seed data,
    ferrofed-conformance-seed-data.json, with its SHA-256 in
    ferrofed-conformance-seed-data.json.sha256sum (#573). The file holds the
    vendored template and compositions conformance run writes, with their
    Apache-2.0 licence and notice, and --seed-data takes it in place of the
    vendored directory, checking each file it holds by SHA-256.

  • A mutual-TLS token endpoint alias on a host you name (#560; RFC 8705 §5).
    A node's [credentials."<id>"] section takes mtls_alias_hosts, each a
    host name with an optional port, and a fapi2 grant that uses mutual TLS
    then takes a token_endpoint of mtls_endpoint_aliases on one of those
    hosts, over https alone, as well as on the issuer's origin. An alias on
    any other host is still refused before a token request is sent. An entry
    that is no host, or the key in a section with no fapi2 grant over mutual
    TLS, refuses the configuration, naming the key. oauth-server-metadata
    0.0.2 adds AliasHost and Issuer::mtls_alias.

  • Mutual TLS toward a node and its authorization server, with RFC 8705
    client authentication and certificate-bound tokens (#492; §13.1, §13.4,
    N25, CP-17; FAPI 2.0 Security Profile §5.3.2.1). A node's
    [credentials."<id>"] section takes client_identity_file and
    trust_roots_file, the keys and the TLS type every identity service
    takes, and the node and its token endpoint are reached over one transport
    that presents that certificate, https under every profile; a section
    may name that material alone. An oauth2 grant, and a fapi2 grant, may
    authenticate with client_auth = "tls_client_auth" or
    "self_signed_tls_client_auth", sending client_id and no assertion
    (RFC 8705 §2), and with tls_client_certificate_bound_access_tokens = true take only tokens bound to the certificate (§3): a token whose cnf
    names another certificate, in the token response or as a JWT claim, is
    refused before the node is sent anything (§3.1, §3.2). It composes with
    token exchange per caller, whose actor token the gateway still signs. A
    fapi2 grant over mutual TLS uses the mtls_endpoint_aliases token
    endpoint (§5), needs the metadata to list tls_client_auth and state
    tls_client_certificate_bound_access_tokens as the grant uses them, and
    needs a client key only for a token exchange. A grant that sets both dpop_key_file and certificate binding,
    either without a client identity, an identity with no certificate, an
    http token endpoint or node, and TLS material in a service's own
    credentials section refuse the configuration. RFC 8705 is vendored
    under docs/specs/ietf-oauth/.

  • The operator console's skeleton, app/ferrofed-viewer (#275): a Leptos app
    rendered on the server and hydrated in the browser, with a
    backend-for-frontend, as its own binary, ferrofed-viewer, and its own
    image, ghcr.io/ferrohealth/ferrofed-viewer. It reaches the gateway over
    HTTP alone, as any client does, and holds no clinical data. Built so far: a
    landing page, GET /health and the healthcheck command, operator sign-in
    at an OpenID Provider with the authorization code grant, a nonce and
    PKCE, its state, nonce and verifier held on the server behind an
    opaque HttpOnly cookie in a bounded, short-lived pool of pending
    sign-ins that a flood of login starts cannot grow and that never touches
    the separate, bounded pool of signed-in sessions with their idle and
    absolute timeouts, configuration refusals that name a key and a line and
    never a value, and a typed client of the gateway's OPTIONS {base}/
    and ITS-REST surface called with the operator's own token. Every answer
    carries a per-response script nonce in its Content-Security-Policy. The
    console reads a TOML file with FERROFED_VIEWER__ overrides and a
    client_secret_file. The code exchange, the operator views (#276) and the
    query console (#277) are planned. The release lane builds, attests and
    pushes the image with the same SLSA provenance and SBOM attestations as
    the gateway image, CI lints the browser half for wasm32 and builds the
    site bundle, and the book's "The operator console (planned screens)" page
    says what is built and what is planned.

  • The NVI localizer authenticates with the Nuts grant (#539; Annex B §B.1,
    §B.4; the IG's Localization page, GF-Authentication, GFI-004 and
    GFI-005). [nl_gf.nvi.credentials.nuts] takes the table a node's onward
    credentials take and runs the same grant: a Verifiable Presentation of the
    gateway's credentials for a token bound to its DPoP key, cached until 30
    seconds before it expires and dropped on the NVI's 401. Every search
    carries the token with a proof of the key over the search URL without its
    query, and a nonce the NVI demands is answered once (RFC 9449 §7.1, §9).
    The token goes to the NVI alone. A refused grant leaves the localization
    unavailable, so the query fails closed (§14.1). An oauth2 or fapi2
    table under [nl_gf.nvi.credentials] is refused, naming its key, since the
    IG defines no such grant for the Localization Service. The
    nl-generic-functions NVI client takes an authorizer that makes each
    request's headers and is handed the request URL without the query that
    holds the pseudonym (0.0.12).

  • ferrofed conformance run --config <file> scores a configured deployment
    against the Connectathon tracks 1 to 7, 9 and 11 over ITS-REST (#546;
    §16.3, §16.4). It starts the gateway in-process from the configuration, or
    drives one already serving at --gateway <url>, as a client holding a
    bearer token from --token-file. It seeds a synthetic patient in the
    urn:oid:2.999 arc at every member the deployment's own cross-reference
    names, through each node's ITS-REST writes: the patient's EHR, the vendored
    template and one vendored composition, three EHRs with no subject, and the
    writes the scenarios make. It reads no EHR it did not create and removes
    nothing. It refuses to start without --allow-writes, against a profile
    other than development without
    --i-understand-this-writes-synthetic-data-to-the-nodes, for a patient
    outside the example arc, and for seed files other than the vendored ones,
    which it checks by SHA-256. The --gateway URL and every member endpoint
    are held to https, or http to a loopback host under the development
    profile alone, which the report names; the gateway client follows no
    redirect, and the caller's token is held as a secret that no report, log
    line or error carries. It writes the per-track and per-point report the
    harness run writes, with the same columns and results, and reports every
    scenario that needs fault injection, node-side capture or a gateway
    configured for it not-run with the reason, never pass; --node-profile
    adds the admission check of every member as the node profile findings. The
    end-to-end tracks call the same scenario checks, and a run against the
    harness reads what the harness scores and nothing more.

  • An end-to-end check that the AVG the gateway declares decomposable in
    OPTIONS {base}/ is the mean weighted by each node's count (§11.6.3, N39,
    CP-10, CP-32), behind FERROFED_E2E. With two values at one FerroEHR node
    and one at the other, each node is asked its SUM...

Read more

0.0.8

Choose a tag to compare

@github-actions github-actions released this 04 Oct 21:18
Immutable release. Only release title and notes can be modified.
v0.0.8
1ee0b2f

Security

  • The trace export records nothing of a client's traceparent or
    tracestate. The request span recorded the client's trace id and span id
    as a span link, and a client chooses its trace id freely, so a patient
    identifier hex-encoded in it reached the operator's trace collector. The
    gateway now reads neither header: it starts its own trace for every
    request, as before, and every trace id and span id an exported span
    carries is one it generated (#446).

  • A node is never sent a request without its configured onward credential,
    and never the caller's own Authorization header.

  • The error a client sees for a node no token could be obtained for is a
    fixed sentence and, when the token endpoint refused with one, its
    registered RFC 6749 §5.2 code. The token endpoint's description, its
    address and the network error go to the log alone, and the assertion and
    the token go nowhere.

  • A token endpoint URL with a user name, a password, a query or a fragment is
    refused at load, so no secret can ride in it into a log.

  • No request reaches a node, a cross-reference service or a store before its
    caller is verified. A token signed with none or an HMAC, of another
    typ than at+jwt, from an untrusted issuer, for another audience, or
    outside its validity window is refused, and an issuer that cannot be asked
    fails closed with 503. A patient/ scope admits nothing, because the
    gateway cannot bind it to the token's patient context, and a
    system/aql-* scope counts only for a listed backend client. The
    DEMOGRAPHIC API admits only the clients an issuer lists in
    demographic_clients. The client's token is never forwarded to a node.

  • Outside profile = "development", serve, config check,
    admission check and every reload refuse a credential sent to a URL that
    is not https, naming the key of the URL and of the credential: a registry
    endpoint with a [credentials] section, an oauth2 token endpoint, a PIX
    Manager with credentials, and a metrics.otlp_endpoint carrying a user
    name or a password. A reload keeps the profile the process started with.
    Under the development profile the same configuration starts, and the
    startup banner, a WARN log line and config check name each credential
    that travels unencrypted, by key, an XCPD responding gateway's XUA
    assertion included (#402).

  • A PIX Manager and an XCPD responding gateway are sent patient identifiers,
    so outside the development profile each must be https, with or without
    a credential configured. Under the development profile they are named in
    the banner, the log and config check like a cleartext credential (#402).

  • A reload whose file changes profile is refused with class profile, and
    the running configuration stays, so nothing the development profile admits
    can enter a process that started under another profile. A reload under
    another profile could admit an http XCPD gateway before (#402).

  • One module holds every transport rule: a URL a credential or a patient
    identifier is sent to must be https outside the development profile,
    and a key set or introspection endpoint the gateway verifies callers
    against must be https, or http to loopback, under every profile (#402).

  • Outside the development profile, the stored-query store's PostgreSQL
    connection string must set sslmode=require when it carries a password
    to a networked host: disable and prefer, the driver's default, which
    falls back to no TLS, are refused naming stored_queries.url by
    config check, the start and a reload, even though the store itself
    changes only on a restart. Under the development profile it starts and is
    named in the banner, the log and config check (#416).

  • No request reaches a node without the caller's identity: one that reaches
    the dispatcher with no verified caller is answered 500 and nothing is
    sent. The conveyed token never carries a patient identifier (§5.4.1, N33):
    it holds no IUA person_id, and the outbound gate reads every caller
    claim, refusing with nothing sent a request whose claims would carry the
    identifier its query was resolved on. A client's own
    openEHR-federation-client header never reaches a node.

Added

  • Mutual TLS to the PIX Manager, the PDQm Supplier, the PMIR Patient
    Identity Registry and the mCSD directory: [[pixm.manager]], [pdqm],
    [pmir] and [registry.mcsd] take client_identity (or
    client_identity_file) and trust_roots_file, as [xcpd] does, the
    client identity held as a secret. A file that cannot be read is refused
    naming its key, and material that is no PEM refuses the federation
    without quoting it. The testkit gains a mutual-TLS front that puts any
    harness server behind https with client authentication (#507).
  • A FAPI 2.0 grant as an onward credential, for a node whose authorization
    server follows the FAPI 2.0 Security Profile, as the BgZ/eOverdracht
    track of the Dutch binding does (Annex B §B.4a): a
    [credentials."<id>".fapi2] section names the server's issuer, and the
    gateway reads its RFC 8414 metadata once, holds it to the issuer and to
    everything the grant sends, authenticates with an ES256
    private_key_jwt assertion whose aud is the issuer, binds every token
    with DPoP, and sends the configured RFC 9396 authorization_details,
    taking only a token response that states the details granted. Token
    exchange per verified caller works as for oauth2. The client key's
    public half is published in the gateway's JWK Set. The authorization
    code grant, which needs a user agent, refuses the configuration. The
    testkit has a harness FAPI 2.0 authorization server, and the book has an
    Onward credentials page (#497, #88).
  • assertion_audience = "issuer" in an oauth2 section makes every client
    assertion name the authorization server's issuer, set in issuer, as its
    aud in place of the token endpoint (RFC 7523 §3; FAPI 2.0 §5.3.2.1).
    The token endpoint stays the default (#497).
  • nl-generic-functions 0.0.10 adds the oauth-metadata feature: the RFC
    8414 issuer identifier and metadata checks the Nuts grant built, now
    shared by both Annex B authentication tracks (#497).
  • The PDQm demographics step of Annex A §A.2, ahead of localization and
    resolution. With [pdqm], a patient named in a namespace the table lists
    is taken to a PDQm Patient Demographics Supplier first, by ITI-78 (an
    identifier search that asks for the master domain only) or, where the
    deployment declares transaction = "iti-119", by the ITI-119 $match
    with onlyCertainMatches. The identifier the one matched Patient carries
    in the master domain is localized and resolved in place of the client's,
    and the outbound gate withholds both from every node request. No match
    leaves every member not-resolved and fails nothing (N6); several
    matches, two master identifiers or an uncertain match refuse the
    resolution and fail the query 424, and the gateway never picks one. A
    Supplier outage follows the localization failure policy (§14.1). The
    Supplier is reached over https outside development, with no redirect,
    a bounded answer and its own timeout, which OPTIONS {base}/ declares as
    timeout.demographics_ms; each exchange is audited through [audit] as
    the PDQm Query or Match Consumer record, required outside development;
    the step shows as demographics on GET /health/dependencies and is
    counted in ferrofed_demographics_requests_total. The read of an EHR by
    subject takes the same step, and a reload applies a change to [pdqm]
    as it does to [pixm] (#487).
  • The ITI-119 Patient Demographics Match in ihe-iti 0.0.19:
    PdqmClient::match_patient posts a MatchInput to [base]/Patient/$match
    and reads the Match Output Bundle, every matched Patient with its score
    and match-grade, and its PDQm Match Consumer audit record (#487). The
    testkit gains a harness PDQm Supplier that answers ITI-78 and ITI-119 from
    synthetic Patients in the urn:oid:2.999 example arc.
  • The Nuts grant of the Dutch Generic Functions (Annex B §B.4) as an onward
    credential: a [credentials."<id>".nuts] section makes the gateway obtain
    that node's token from its authorization server with a Verifiable
    Presentation of the gateway's own credentials, signed with its did:web
    key, by the VP Token Grant Type of Nuts RFC021. The token is always bound
    to the section's dpop_key_file with DPoP, and every request to the node
    carries a proof of that key. The gateway checks the authorization server's
    metadata and Presentation Definition before any credential leaves, sends
    no expired credential, answers a demanded DPoP nonce once with a new
    presentation, caches the token until 30 seconds before it expires, and
    never logs or reports a credential, the presentation or the token. The
    client is nl-generic-functions feature nuts-auth, and the testkit has a
    harness Nuts node (#88).
  • The specifications the Annex B authentication tracks cite are vendored:
    the GF-Authentication pages of the NL-GF IG, Nuts RFC003, RFC021 and
    RFC022, nine OAuth RFCs, VC Data Model 1.1, DID 1.0, DID Resolution,
    Bitstring Status List and the did:web method, and DIF Presentation
    Exchange 2.0.0. The OpenID FAPI 2.0, OpenID4VCI and OpenID4VP documents
    are pinned and fetched, never committed (#88).
  • Mitz as the consent pre-filter of the Dutch binding (Annex B §B.6,
    N27a), configured under [nl_gf.mitz]. Before resolution the gateway asks
    Mitz's closed authorization question once for each candidate's care
    provider: may this data holder make the patient's data of the configured
    categories available to the verified caller, for TREAT or COC? The
    caller's professional (UZI number and role) and organisation (URA and
    type) come from the token claims each trusted issuer maps under
    [auth.issuer.requester]; a caller whose token lacks them is n...
Read more

0.0.7

Choose a tag to compare

@github-actions github-actions released this 03 Oct 20:36
Immutable release. Only release title and notes can be modified.
v0.0.7
d7d0b05

The v0.0.7 milestone, definitions and membership (§12.6, §12.7, §12b). A
versioned write goes only to the CDR that created the version. A template goes
to the node you name, or to several on request, with each member's outcome
reported. Stored queries live at the gateway as immutable versions, on an
embedded store, PostgreSQL or a read-only directory, distributed to the
members with drift reported and repaired from the admin listener. A check you
run before admitting a node tests the admission conditions. The gateway
exports metrics through OpenTelemetry, as Prometheus and over OTLP, records
each member's state from the calls it makes, holds every credential in a type
that never renders it, and prints a startup banner. Every release now carries
a compose file that starts the gateway alone in front of the CDRs you already
run. The gateway still authenticates no client; that is v0.0.8 (#80).

Added

  • Every release carries compose.yaml, ferrofed.toml and registry.toml,
    which start the gateway alone in front of the CDRs you already run, with no
    checkout of the repository (#385; no specification governs packaging: our
    own design). You download the three, edit the two TOML files (the gateway
    configuration and the registry document, in their documented formats, every
    value to change marked EDIT), put each credential in a file under
    secrets/, and run docker compose up --wait. The compose file runs the
    published ghcr.io/ferrohealth/ferrofed image at the release's version and
    mounts the two files and secrets/ read-only; its only variables are about
    the container (image tag, host address and port, CPU and memory). The
    gateway runs read-only, as uid 65532, with every capability dropped,
    no-new-privileges, CPU and memory limits, a 20-second stop grace period,
    the ferrofed healthcheck probe and the unless-stopped restart policy, so
    a configuration it refuses (exit 78, naming the key) is retried at Docker's
    doubling delay. The release lane attaches the three files to the draft and
    refuses to publish a draft missing any of them, scripts/checks/versions.sh
    and the release plan hold the image tag to the product version, and the CI
    job release compose renders the compose file and runs ferrofed config check over the two examples as attached. The repository's compose.yaml
    stays the four-node quickstart. The container page of the book has a
    section "The gateway from a release".

  • The operator can send a stored-query version the registry holds to the
    members that missed it (#342; §12.7 stored-query-drift, N44, CP-40; no
    specification gives drift repair a request, so this is our own design).
    POST /admin/stored-queries/{name}/{version}/distribute on the admin
    listener ([metrics] listen), with no body and the members in the
    targeting headers, sends the registry's held copy to them and changes
    nothing at the registry. The answer has the shape and statuses of a first
    distribution, and its new meta.registry member says held, where a
    first distribution's says stored. It is 404 for a version the
    registry does not hold, 400 for a request naming no member, a body, a
    deployment without distribution or an endpoint-targeted definition, and
    405 with an empty Allow at a read-only registry. Without the admin
    listener the action does not exist. Every second PUT of a held version
    stays 409 (stored-query-held), the same query naming members
    included. The book's stored-query page has a "Repairing drift" section.

  • A metrics surface (#281; no specification governs metrics). One
    OpenTelemetry meter provider counts the integrity incidents by kind
    (ferrofed_integrity_incidents_total), the requests sent to each member
    endpoint by the §11.1 outcome the per-endpoint report gives them
    (ferrofed_node_requests_total{endpoint, outcome}), how long each member
    took to answer (ferrofed_node_request_duration_seconds{endpoint}), and
    the registry reloads by result, applied or refused
    (ferrofed_registry_reloads_total). [metrics] listen serves the
    Prometheus text exposition at GET /metrics on an admin listener of its
    own, never the gateway's listener, and [metrics] otlp_endpoint pushes the
    same metrics to an OTLP gRPC collector; both are off by default. serve
    and config check refuse a listener on a non-loopback address unless
    [metrics] allow_remote = true is set, a listener on server.listen, and
    a collector that is no http:// URL. Every label value is drawn from a
    closed set or the registry document, never from a request (§5.4.1, N33).
    The book's operate section has a Metrics page listing every metric and its
    labels. The integrity incident webhook the architecture named is removed
    from the design: alert on the counter.

  • The stored-query registry runs on one of three backends, chosen with
    [stored_queries] backend (#268; §12.7, N44, CP-40). redb, the default,
    is the embedded file for one gateway, as before. postgres, behind the
    ferrofed-server cargo feature of the same name and off by default, keeps
    the definitions in one PostgreSQL database every replica shares: the
    primary key on the name and version with INSERT … ON CONFLICT DO NOTHING
    stores exactly one of two racing PUTs of a new version, the other is a
    409 (stored-query-held), and a read or an invocation reads the store
    again first, so every replica serves what any of them stored. Its
    connection string is a secret, url or url_file, TLS is rustls with the
    platform's roots, and the schema ferrofed and its table are created when
    absent. files is read-only: one file per
    {qualified_query_name}/{version}.aql, loaded and admitted at start; a
    PUT is a 405 with the new code stored-query-read-only and Allow: GET, OPTIONS, OPTIONS lists no PUT, and a malformed file refuses the
    start and config check, naming the file and never its content. The
    startup banner and log line name the backend kind and never its path or
    connection string.

  • PUT {base}/v1/ehr/{ehr_id} is checked against what the gateway already
    knows of the ehr_id before anything is sent (#289; §12.4, §12.5.2, N23,
    N42, CP-15; ITS-REST 1.1.0 ehr_create_with_id). When a resolution
    binding of the session or the ehr_id index places the ehr_id at a
    member other than the one the targeting headers name, the create is a
    409 with the new code ehr-id-held, no node is sent it, and the
    gateway logs a warning naming endpoint ids only; the message names the
    holding endpoints and the targeted one, never the ehr_id. A create at
    the member that holds the ehr_id is forwarded, so that node answers its
    own 409, and an ehr_id nothing places is forwarded as before and,
    once created, indexed at the targeted member, where a racing create at
    another member raises the index-insert alarm of §12b.2.

  • Stored-query definition fan-out, opt-in with [federation] fan_out_stored_queries beside the registry (#78; §12.7, N44, N43, CP-40).
    Off by default, refused by config check and at start without
    [stored_queries], and declared in OPTIONS {base}/ as
    definition.stored_query_fan_out, true only beside the registry. Where
    it is on, a stored-query PUT whose openEHR-federation-endpoint is *
    or whose targeting headers name members is stored at the registry first,
    then sent to each member as the registry's canonical AQL through
    openehr-its's generated client, and answered with the registry's
    StoredQuery and meta.federation on the template fan-out's statuses:
    200, 207 with complete false, or 424 and 504, the registry
    holding the definition whatever the members answered and nothing rolled
    back. A definition carrying a FROM ENDPOINT or ORGANISATION directive
    is refused for distribution with 400 definition-endpoint-targeted
    before anything is stored, and stays storable and executable federated.
    A GET of a version naming members reports drift per member: active
    where its copy is the same query, and node-error with
    error.code definition-differs or definition-missing otherwise, 207
    unless every named member matches. An invocation always runs the
    registry's AQL, never a member's copy. With the setting off, a registry
    PUT or version GET carrying openEHR-federation-endpoint or
    openEHR-federation-organisation is refused with
    400 stored-query-fan-out-unsupported, storing and reading nothing,
    where the header was ignored before.

  • The federated AQL answer carries the provenance headers (#288; §7a.3,
    N31, CP-24). openEHR-federation-endpoint lists the endpoints that
    contributed rows and openEHR-federation-system-id their nodes'
    system_ids, position for position, in registry order and in the
    comma-separated form of the §8.4 request header. An endpoint contributed
    when it answered active with a row_count above 0; one that answered no
    rows, failed or was never asked is not listed, an answer that fails lists
    none, and meta.federation.endpoints[] still names every endpoint (§11.1).
    The headers carry registry ids and system_ids, never a request value
    (§5.4.1).

  • Fan-out template upload, opt-in with [federation] fan_out_template_upload
    (#76; §12.6, N43, CP-34). Off by default, and OPTIONS {base}/ declares
    the configured value as definition.fan_out_template_upload. Where it is
    on, an ADL 1.4 or ADL 2 template upload whose openEHR-federation-endpoint
    is * (every active member) or whose targeting headers select several
    endpoints is sent to each member independently, byte-identical through the
    routed forwarding path, and nothing is rolled back. The answer is
    meta.federation with one endpoints[] entry per registry member and no
    node's body: 200 when every named member accepted, 207 with
    complete: false when some failed, and 424 or 504 when none...

Read more

0.0.6

Choose a tag to compare

@github-actions github-actions released this 03 Oct 06:33
Immutable release. Only release title and notes can be modified.
v0.0.6
52cb38b

The v0.0.4, v0.0.5 and v0.0.6 milestones in one release (no v0.0.4 or v0.0.5
tag was cut). The federated query now shapes its answer across nodes the way
a single CDR would: ORDER BY with LIMIT re-applied at the Tier, bounded
OFFSET pages, DISTINCT, decomposable aggregates and opt-in
version-identity dedup, each refused with a reason where it cannot be exact.
Every failure carries its §11.2 status and a stable error code. A request
under {base}/v1/ehr/{ehr_id} is routed to the one node that holds that EHR,
byte-identical, and every answer teaches the gateway which system created
each version. A client names nodes through the FROM ENDPOINT /
ORGANISATION directive or the targeting headers and selects their ENDPOINT
attributes, the registry also loads as FHIR Endpoint and Organization
resources, stored queries are held at the gateway as immutable versions, and
OPTIONS {base}/ declares all of it. The identifier-hygiene property is now
held by the track 10 leakage suite against both nodes: no client-chosen
request id, targeting header or path segment carries a patient identifier to
a node or into the log.

Added

  • The follow-up routing table learns from every answer (#64; §12.2, N21,
    CP-13). Each version uid in a federated query's rows, and each one a
    routed read names or answers with in its ETag, teaches a route for a
    creating_system_id the registry document does not map; one seen at two
    nodes raises the LearnedCreatingSystemConflict integrity incident and is
    routed on by neither. CP-13 is now scored. A follow-up read of a version
    under {base}/v1/ehr/{ehr_id}/… routes by the path ehr_id in the order
    of N41, never by the version's creating_system_id (§12a.1, §12.5.1, N22,
    N42a), and reaches the node byte-identical.

  • The federated stored-query registry (#77; §12.7, N44, N33, CP-40,
    CP-28). With [stored_queries] path set, the gateway holds stored queries
    itself in an embedded redb file that survives a restart, and
    OPTIONS {base}/ declares definition.stored_query_registry: true.
    PUT {base}/v1/definition/query/{name}/{version} stores the AQL on
    ITS-REST's semver segment: the name is [{namespace}::]{query-name}, the
    version major.minor.patch, and the text is analysed as an inline query
    would be, with each $parameter standing in for a bound value, and held
    as its canonical print. A definition that names the patient by a literal
    is refused 400 subject-literal, so no patient identifier is held at
    rest; a second PUT of a held name and version is refused
    409 stored-query-held and the held text stands, across a restart too.
    GET on the same path reads the ITS-REST StoredQuery back, and
    GET {base}/v1/definition/query/{pattern} lists the versions of every
    name the pattern starts. POST {base}/v1/query/{name}[/{version}] runs
    the stored query over every member exactly as if its text were sent
    inline, with the client's offset, fetch and query_parameters, the
    targeting, completeness and dedup headers and the budget; without a
    version the highest runs, and a {major} or {major}.{minor} prefix runs
    the highest it matches. The answer carries ITS-REST's name naming the
    gateway's definition. New codes: query-name-invalid,
    query-version-invalid, query-version-required, query-type-unsupported,
    subject-literal (each 400), stored-query-held (409) and
    stored-query-unknown (404). Without [stored_queries] the definition
    routes still answer 501. openehr-federation is 0.0.30 and adds
    aql::definition::Definition, the admission analysis of a stored
    definition. Definition fan-out to the nodes is #78.

  • Routing a path ehr_id in the order of §12.5.1 (#62; §12.5, N41,
    CP-33). A request to {base}/v1/ehr/{ehr_id} or below it goes to the node
    the targeting headers name, then the node a resolution binding of the
    client session names, then the node the new ehr_id index names, and for
    a read only, the one member an ask-all probe finds. The probe sends
    GET {base}/v1/ehr/{ehr_id} to every member at once within the per-node
    timeout and the overall budget (§11.5); a read of the EHR itself is
    answered from the owner's probe answer. A read no member holds is
    404 no-destination, one two members hold is 409 ehr-id-collision
    listing the claimants, and one a member did not answer for is 504
    (node-timeout, node-unreachable) or 424 (node-refused, and the new
    code node-error), naming the member: a member that gave no answer may
    hold the ehr_id, so the owner is unknown. A read that names no node no
    longer answers 501. A write none of the first three steps routes stays
    400 target-required, and nothing is probed. A step that names two
    members names none, and no later step picks one of them. The path
    ehr_id must be an openEHR HIER_OBJECT_ID, and any other value is 400
    with the new code
    ehr-id-invalid before any routing. The index learns from resolutions and
    from members' successful answers, holds ehr_ids and member ids only, in
    memory, and forgets the least recently used ehr_id past
    federation.ehr_index_capacity (default 100000; 0 is refused). Session
    bindings answer once client authentication lands (#80); the integrity
    incident of a collision is #63.

  • OPTIONS {base}/, the gateway's self-description (#73; §7a.2, N30,
    CP-23). The body validates against the vendored options-root.schema.json
    and is built from the running configuration: the federation id, the
    major.minor of the pinned specification, aql.fan_out from the node
    selection, the dedup default, modes and request header, the per-node and
    overall budgets, all-or-nothing with the best-effort opt-in when it is
    offered, the OFFSET strategy (bounded with its max_window, or
    reject, never a cursor), the decomposable aggregates (an empty list when
    none), nothing offered in the definition area, localization.on_failure: "closed", the ITS-REST areas, and every registry endpoint with its
    organisation, membership status, node, system_id, and product and
    version where the registry holds them. It declares no targeting mechanism,
    no resolution carrier, no asynchronous queries and no JWKS location, and it
    needs no patient identifier. OPTIONS on a path under {base}/v1/
    answers 204 with the methods served there in Allow, without asking a
    node, and 501 where the gateway serves nothing.

  • ENDPOINT attributes in rows (#72; §9.2, §9.3, §9.4, N12, N17, N18,
    CP-35, CP-37). A directed query that selects p/id or p/endpoint_id,
    p/organisation or p/organization_id, p/system_id or p/url through
    the FROM ENDPOINT variable gets each value in every row, as a string, from
    the registry entry of the endpoint the row came from: the values
    meta.federation.endpoints[] reports for it. No node is asked for an
    attribute, and a query that selects none keeps the row shape of a single
    CDR. The §9.4 example answers the §9.4 columns and rows, with columns[]
    paths in the ITS-REST form (/id, /system_id, /uid/value). An alias on
    an attribute keeps it apart from an EHR-derived column of the same name;
    giving both the same name is refused 400 with the new refusal
    endpoint-name-collision, and a path through the variable that is no §9.3
    attribute with endpoint-attribute-unknown. Under DISTINCT the
    attributes take part in which rows are equal. An attribute beside an
    aggregate recombined across endpoints is refused 400
    indecomposable-aggregate, and ordering on an attribute stays refused 400
    endpoint-variable. The 501 not-implemented answer for these queries is
    gone. openehr-federation 0.0.27 adds the attribute module with
    EndpointAttribute, ColumnSource::Endpoint(EndpointAttribute),
    Analysis::attributes, NodeAnswer::with_attributes and
    Merged::attributes; the engine adds Plan::annotating and
    FederatedAnswer::attributes. CP-37 is covered.

  • Track 10, the adversarial identifier-leakage suite, judged on node-side
    wire capture (#90; §16.3 track 10, §5.4, N33, N34, N5, CP-26). One
    synthetic patient identifier is supplied in each of the four positions the
    track names: an EHR_STATUS.subject.external_ref predicate, a
    PARTY_IDENTIFIED/DV_IDENTIFIER predicate, a SELECT projection, and
    the client's query string or headers (X-Request-Id, Authorization,
    Prefer, the targeting and completeness headers, and a free-form header).
    Each runs on the fan-out, on a query directed at one node, and on the
    single-node route, and the journal of the capturing proxy in front of each
    node must hold no occurrence of the identifier, its namespace or any of its
    fragments in the path, the query string, a header or the dispatched AQL,
    raw or percent-decoded; a refusal must be the 400 the specification
    names, with no node asked. Every dispatched request must locate its node by
    the node's own ehr_id alone, and a projected subject column must be the
    re-injected identifier. The converse check commits a COMPOSITION with
    the identifier in a DV_IDENTIFIER and finds it byte-identical at the
    node, compared by bytes and by digest. The gateway's own log (the request
    lines, the security events and the panic line) is searched too and must
    hold none of it. The suite runs against two mock nodes in the normal suite
    and against the two FerroEHR nodes of the harness behind FERROFED_E2E,
    and the conformance matrix records track 10 as covered for the gateway's
    half; CP-27, the node's own obligation, stays with the node profile (#93).
    ferrofed-testkit adds the leak module, the search over the proxy
    journal.

  • The openEHR-federation-endpoint and openEHR-federation-organisation
    request headers, the targeting mechanism beside the AQL (#71; §8.4,
    §8.4.1, N35, CP-28). Each carries a comma-separated list of registry ids,
    ove...

Read more

0.0.3

Choose a tag to compare

@github-actions github-actions released this 02 Oct 05:20
Immutable release. Only release title and notes can be modified.
v0.0.3
e9c7e5b

The first two federated milestones in one release (v0.0.2 and v0.0.3; no
v0.0.2 tag was cut). POST {base}/v1/query/aql answers one ITS-REST
RESULT_SET over two openEHR CDRs, with the patient resolved outside AQL
through a PIXm PIX Manager (or the development cross-reference) on either
patient carrier, and no directly identifying identifier sent to a node: the
rewrite refuses it, and an outbound gate re-checks every request before it
leaves. Also the Cargo workspace and its spec-named crates, the PIXm and PDQm
clients, the container and compose quickstart, the end-to-end harness with a
PIX Manager, the fuzz lane, and the release lane at SLSA Build Level 3,
rehearsed as v0.0.2-rc.1.

Added

  • The PDQm ITI-78 patient demographics query client in ihe-iti, feature
    pdqm (#119; PDQm 3.2.0, ITI TF-2 §2:3.78). pdqm::PdqmClient posts a
    PatientQuery to [base]/Patient/_search as a form body, so no URL carries
    a demographic value, and reads the searchset into the matching Patients
    (FHIR R4 Patient from fhir-types resources), the total, each match's
    score and match-grade, the OperationOutcome warnings and the next page
    link, which it follows only on the Supplier's origin. A 404 with a
    not-found issue for a query that names an identifier domain is the
    profile's unrecognised-domain answer; every other failure is a typed error
    that carries no value, URL or Supplier text. The ITI-78 artefacts of the PDQm
    3.2.0 package are vendored under docs/specs/ihe-pdqm/ by
    scripts/vendor/ihe-pdqm.sh. The OperationOutcome issue type moves to
    ihe_iti::outcome::IssueType, shared by PIXm and PDQm; ihe-iti is 0.0.5.
  • The identity-lifecycle hook for track 8 (#48): ResolutionBindings::identity_changed
    drops every resolution binding a merge or split at the identity source could
    have made stale, by ehr_id in every session or all of them for an
    unscoped change, and Federation::identity_changed is the entry point a PMIR
    subscription calls. The binding lifetime, federation.binding_ttl_ms, is the
    bound until one exists. Track 8 stays provisional and is not claimed.
  • The ask-all node selection of a deployment with no localizer (#46; §4.3
    Variant B, N4, N10). federation.node_selection = "ask-all" declares it,
    and a gateway that federates refuses to boot without the declaration
    (NodeSelectionUndeclared), so the choice is never a silent default. Every
    active member's cross-reference is asked, the query reaches only the members
    that return an ehr_id, and the others are not-resolved without failing
    the query (N6, N8). The selection is named in the startup log; the
    OPTIONS self-description follows with #73. The quickstart and the
    configuration page declare it.
  • A PIX Manager in the test harness, seeded by ITI-104 (#47). The testkit's
    pix::PixManager is a test device, not a PIXm implementation: an in-process
    loopback server that takes the PIXm 3.1.0 Patient Identity Feed FHIR
    (ITI-104, a conditional PUT Patient?identifier= held to the
    IHE.PIXm.Patient minimums, plus the Remove Patient Option's conditional
    DELETE) and answers ITI-83 $ihe-pix from what was fed, with each case of
    ITI TF-2 §3.83.4.2.2. The seed builder feeds it a synthetic patient in the
    urn:oid:2.999 arc with one ehr_id per node domain (seed::feed), and a
    capturing proxy in front of it journals and faults its traffic. The server's
    e2e suite now resolves through it, at one member and at both, while the
    failure classes stay on stubbed Managers. The IG's ITI-104 artefacts (the
    Source CapabilityStatement, the Patient profiles and the example
    Patients) are added to the vendored PIXm corpus.
  • The outbound identifier-hygiene gate and the security events of the
    analysis guard (#45, §5.4, N33, CP-26). Right before a request leaves for a
    node, the engine re-reads its AQL text (raw and as the printer escapes a
    literal), its paging members, its URL (raw and percent-decoded) and the
    headers the gateway adds, against the identifiers resolution consumed, and
    refuses to send a request that still carries one: nothing reaches the node,
    the query fails closed, and the refusal names the part of the request and
    never the value. The gate reads past the ehr_id literal the rewrite
    scoped the query to, so a short identifier that occurs inside the node's
    own ehr_id is answered. Every patient predicate the rewrite strips, every refused
    query and every gate stop is a security event under ferrofed::security,
    located by byte range and carrying no identifier. A clinician or facility
    predicate (composer, care facility, performer, committer) is dispatched
    unchanged; the same path compared with the patient identifier is refused.
    openehr-federation 0.0.5 adds Refusal::kind, Refusal::at and
    PatientQuery::stripped for those events.
  • The resolution step through a PIX Manager (#43). A [pixm] table selects
    the PIXm resolver: each [[pixm.manager]] names a PIX Manager's FHIR base,
    its credentials, and the ehr_id domain of every member it resolves, and
    [pixm.namespaces] maps a client's issuing namespace to a PIX assigning
    authority. One ITI-83 call per Manager resolves the patient at its members;
    only a member that knows the patient is asked its node query, a member that
    does not is not-resolved in meta.federation, a patient known nowhere is
    a 200 with no rows and complete: false, and a Manager that cannot answer
    fails the query 424 with no node asked. The patient identifier and its
    namespace reach the PIX Manager only. Boot is refused when a member has no
    Manager, when a member has two, or when [dev] and [pixm] are both set.
    The {node, ehr_id} set of each resolution is held in memory as the client
    session's resolution bindings, bounded by federation.binding_ttl_ms
    (default 15 minutes), and nothing derived from a patient identifier is
    stored or logged.
  • The PIXm ITI-83 client (#42): ihe-iti 0.0.3, feature pixm, asks a PIX
    Manager's Patient/$ihe-pix for the identifiers other domains hold for a
    patient, held to the vendored PIXm 3.1.0 OperationDefinition and read into
    a cross-reference, the profile's not-found answer, or a typed error (an
    unknown source or target domain, a rejection with its issue types, a
    timeout, a transport failure, a malformed answer). A 404 without a
    not-found issue never reads as "patient unknown". Identifier values are
    redacted in Debug and carried by no error. The IHE PIXm 3.1.0 package
    artefacts of ITI-83 are vendored under docs/specs/ihe-pixm/ by
    scripts/vendor/ihe-pixm.sh, pinned by version and tarball sha256, and the
    FHIR R4 model comes from fhir-types 0.1.107.
  • The fuzz lane (#134): four cargo fuzz targets over the untrusted inputs
    (the AQL rewrite over arbitrary text and parameters, the ITS-REST
    AdhocQueryExecute body through the façade's intake, a federated
    RESULT_SET with its meta.federation, and the OPTIONS {base}/ body),
    with seeds generated from the vendored golden cases and the specification's
    JSON examples by scripts/fuzz/seeds.sh. The rewrite target asserts the
    identifier-hygiene property of §5.4.1 (N33) on every query it accepts,
    string-function reconstructions included. fuzz.yml runs the targets
    weekly, on dispatch and on pull requests touching the code they read.
  • Both patient-identifier carriers resolve (#44; §5.4.3, N33, CP-38): an
    ENTRY-level subject predicate, …/subject/identifiers/id, is resolution
    input on equal terms with EHR_STATUS.subject.external_ref, with the
    DV_IDENTIFIER issuer or type as its issuing namespace. It is consumed
    and stripped exactly as external_ref is, so the same patient query through
    either carrier sends the same node query and returns the same rows. The same
    value in both carriers is consumed once; a second, different value, and
    qualifiers that name two namespaces, are refused with a 400, as are the
    assigner, a predicate on the identifier list, an ENTRY-level
    external_ref, and the carrier selected, ordered on or inside a function.
    openehr-federation is 0.0.4, and the interim Refusal::EntrySubject is
    gone.
  • The release lane at SLSA Build Level 3 (#31): the reusable
    release-build.yml builds ferrofed per target with cargo auditable,
    writes a CycloneDX and a syft SBOM, and attests the tarball's provenance and
    both SBOMs; the reusable release-image.yml builds the linux/amd64 and
    linux/arm64 image from the attested musl binaries, pushes it to
    ghcr.io/ferrohealth/ferrofed by digest, and attests the index and each
    platform manifest. A release is published only when the draft carries all
    eight assets of every target, and a pre-release is never marked latest.
  • The first federated query (#38): POST {base}/v1/query/aql answers one
    ITS-REST RESULT_SET over every member of the registry, with no federation
    syntax needed (N1, CP-1).
    • The gateway types the query_parameters, analyses the query with
      openehr-federation's aql rewrite, resolves the patient at every member
      through the configured cross-reference, and sends each member that knows
      the patient standard AQL keyed on its own ehr_id (N2, N7, CP-2, CP-4).
    • It fans out under the budget and re-injects the selected subject column
      as the resolution input (N5, CP-7). meta.federation names every endpoint
      (N16): a member that does not know the patient is not-resolved, a
      suspended endpoint and a second endpoint of one member are excluded.
    • A refused query is a 400 ITS-REST error that locates the fault and
      quotes nothing (§5.4.3). Without a cross-reference a patient query fails
      closed with 424 (decision A17), and without a registry the route stays
      501.
    • The configuration gains profile, [registry] document, [federation]
      (per_node_timeout_ms, `...
Read more

0.0.2-rc.1

0.0.2-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Oct 18:56
Immutable release. Only release title and notes can be modified.
v0.0.2-rc.1
0cca236

A pre-release that rehearses the release lane at SLSA Build Level 3 (#31):
attested binaries, the image ghcr.io/ferrohealth/ferrofed by digest, and
their SBOMs. It carries the first federated query (#38) and everything merged
since v0.0.1.

Added

  • The release lane at SLSA Build Level 3 (#31): the reusable
    release-build.yml builds ferrofed per target with cargo auditable,
    writes a CycloneDX and a syft SBOM, and attests the tarball's provenance and
    both SBOMs; the reusable release-image.yml builds the linux/amd64 and
    linux/arm64 image from the attested musl binaries, pushes it to
    ghcr.io/ferrohealth/ferrofed by digest, and attests the index and each
    platform manifest. A release is published only when the draft carries all
    eight assets of every target, and a pre-release is never marked latest.
  • The first federated query (#38): POST {base}/v1/query/aql answers one
    ITS-REST RESULT_SET over every member of the registry, with no federation
    syntax needed (N1, CP-1).
    • The gateway types the query_parameters, analyses the query with
      openehr-federation's aql rewrite, resolves the patient at every member
      through the configured cross-reference, and sends each member that knows
      the patient standard AQL keyed on its own ehr_id (N2, N7, CP-2, CP-4).
    • It fans out under the budget and re-injects the selected subject column
      as the resolution input (N5, CP-7). meta.federation names every endpoint
      (N16): a member that does not know the patient is not-resolved, a
      suspended endpoint and a second endpoint of one member are excluded.
    • A refused query is a 400 ITS-REST error that locates the fault and
      quotes nothing (§5.4.3). Without a cross-reference a patient query fails
      closed with 424 (decision A17), and without a registry the route stays
      501.
    • The configuration gains profile, [registry] document, [federation]
      (per_node_timeout_ms, overall_timeout_ms below the request timeout,
      default_namespace) and the [[dev.crossref]] rows of the development
      profile. config check loads the registry too.
    • The compose quickstart mounts docker/quickstart/registry.toml and
      ferrofed.toml, and the README shows one federated query returning the
      EHR of both nodes. The end-to-end test runs it against FerroEHR and
      EHRbase behind their capturing proxies, and no node request carries the
      patient identifier in any carrier (N33).
  • The fan-out engine, first increment (#37): ferrofed-engine's fanout
    module sends one request per in-scope node at once, each under a per-node
    deadline cut to the overall budget, with no retry and no hedging (§11.5,
    N38). A node still outstanding when the overall budget runs out is abandoned
    and reported time-out, without touching any other request, and its late
    answer contributes nothing. meta.federation is built from every outcome
    before the decision, so a failing answer carries it, with the effective
    timeout budget and complete: false. Under the all-or-nothing default an
    offline or time-out node fails the query 504, a node-error fails it
    424, 504 taking precedence; not-resolved and consent-denied fail
    nothing, so a patient found nowhere is a 200 with empty rows (§11.3, §11.4,
    N6, N37). Every endpoint record carries its node, system_id, managing
    organisation, base URL and latency_ms (§9.5, N40), and the rows of the
    active nodes are concatenated in endpoint id order until the merge (#52).
  • The crates.io lane behind the workspace publish switch (#32):
    publish-crates.yml runs on every v* tag, reads the publishable set from
    cargo metadata, packages it and publishes it in dependency order through
    crates.io Trusted Publishing, and is a successful no-op while the root
    [workspace.package] publish is false. The publish-dry-run job packages
    every library crate on every pull request, and
    scripts/release/publish-crates.sh is the shared implementation.
  • Node dispatch (#34): ferrofed-engine's dispatch module builds one
    openehr-its client per registry endpoint, rooted at the endpoint's base URL
    as the registry holds it plus the ITS-REST v1 segment (N28), and sends each
    node query as the generated POST {base}/v1/query/aql, once, with the
    per-node deadline and the gateway's request id. The answer maps to exactly
    one §11.1 status: a result set to active, a refused connection or broken
    stream to offline with its reason, a passed deadline to time-out, and a
    documented error, an undocumented status or a body that is not a result set
    to node-error carrying the node's own status and message (§11.2, N16,
    N40). A credential the provider cannot produce, or a request the client
    runtime refuses to compose, is a typed dispatch error, never an endpoint
    status. The engine names no HTTP engine directly, and a test holds it.
  • The AQL rewrite, first increment (#35): the aql feature of
    openehr-federation 0.0.3 parses a façade query with openehr-query, binds
    its query_parameters before analysis, and consumes the
    EHR_STATUS.subject.external_ref patient predicate with its namespace into a
    redacted Subject (§5.2). Each node receives the query scoped to its own
    ehr_id (§7.1, N7, N29); a selected subject or namespace column is
    re-injected after the merge (N5); and columns[] is rendered from the façade
    query alone (N17). The rewrite refuses with a typed 400, never quoting the
    identifier: a predicate outside the top-level AND chain or under another
    operator (§7.1), an integer identifier (decision A6), two different patients
    (A7), an unqualified identifier with no default namespace (A5), a query with
    no patient where a localizer decides the node set (A8), paging members that
    disagree with the query (A10), cross-node OFFSET (§11.6.2), an undirected
    aggregate (N14), and the identifier anywhere else in the query (§5.4.1),
    including rebuilt by CONCAT, CONCAT_WS or SUBSTRING over split
    literals, which the rewrite folds before the value test; a string function
    over a literal that cannot be folded is refused on an identifier-bearing path
    (decision A4). The reference implementation's 17 golden cases run as a
    corpus, each adjudicated. CI's test lane runs every feature.
  • The static registry (#36): ferrofed-registry 0.0.1 loads the federation's
    membership from a reviewed TOML bootstrap document ([[organisation]],
    [[node]] with its system_id and [[node.identifier]], [[endpoint]])
    with deny_unknown_fields throughout into an immutable RegistrySnapshot.
    A document with a dangling reference, a duplicate id, a system_id shared by
    two nodes (compared without ASCII case), an endpoint without exactly one
    managing organisation, a connection type other than openehr-rest-query, an
    unusable base URL or a node without an endpoint refuses to load (N19, N20,
    N21, §12b.2). NodeId, EndpointId and SystemId are distinct types with
    no conversion between them (N32, §12a.1).
  • The resolver seam and the development cross-reference (#36):
    ferrofed-identity 0.0.1 carries PatientRef, the patient identifier
    redacted in every rendering and never serialized (§5.4, N33), the Resolver
    trait (N3, §5.2), and StaticResolver, a fixed table from a synthetic
    identifier to each member's ehr_id that a configuration can enable only
    under profile = "development". It is FerroFED's own testing device, not an
    identity binding.
  • The container image: docker/Dockerfile puts the ferrofed musl binary on
    distroless static, digest-pinned, as the numeric non-root user 65532, with
    no shell and a read-only root; scripts/release/stage-dist.sh stages the
    binaries of a published release after checking their checksums. compose.yaml
    starts the gateway beside two member CDRs, FerroEHR 4.3.1 and EHRbase 2.36.0,
    every image pinned by digest in docs/VERSIONS.md and held there by the
    versions guard (#30).
  • The test harness in tools/ferrofed-testkit (#39): FerroEHR 4.3.1 and
    EHRbase 2.36.0 as two nodes on their own documented databases, pinned by
    digest and started only behind the FERROFED_E2E gate; a capturing and
    fault proxy in front of each node, whose journal records every request
    (method, path, query, headers, body) and which refuses, delays or answers
    with a chosen status per node; and a synthetic seed builder that writes
    EHRs, the template and compositions over ITS-REST alone, with patient
    identifiers only inside the urn:oid:2.999 example arc. CI runs the
    container suite in its own e2e (containers) job, and the versions guard
    holds the image pins to docs/VERSIONS.md.

Changed

  • The repository moved to the FerroHEALTH organization,
    https://github.com/FerroHEALTH/FerroFED, with the roadmap board as
    https://github.com/orgs/FerroHEALTH/projects/1; every link, the image name
    ghcr.io/ferrohealth/ferrofed and the crates' repository follow it (#123).
  • The crate layout names every crate that may be published after the
    specification it implements, one crate per specification with a feature per
    layer or profile (#106): openehr-federation 0.0.1 (the Federation Tier
    wire types, formerly ferrofed-wire, with the aql and merge features),
    ihe-iti 0.0.1 (features pixm, pdqm, mcsd, pmir, xcpd) and
    nl-generic-functions 0.0.1 (features nvi, mitz, lrza, nuts-auth).
    The names are held on crates.io by 0.0.0 placeholders. ferrofed-registry,
    ferrofed-identity and ferrofed-engine move under app/ and are never
    published. A new CI job lints every feature of the published crates on its
    own, and the architecture test also fails when a binding crate depends on
    FerroFED.
  • The openehr-* family moves to 0.0.74, the lockstep release with the AST
    visitor, spans, parameter binding and the federation directive in
    `openeh...
Read more

0.0.1

Choose a tag to compare

@github-actions github-actions released this 01 Oct 13:13
Immutable release. Only release title and notes can be modified.
v0.0.1
acbd59a

The first release: the repository setup, the documentation site on
https://ferrofed.eu/, the architecture of record, the Cargo workspace and the
server binary's shape (configuration, telemetry, health, readiness and
shutdown). The ferrofed binary serves health and readiness only; the
federated query follows from v0.0.2.

Added

  • The Cargo workspace (#28): the root manifest with the family lint set, the
    release profile and the publish = false switch every library crate
    inherits; the openehr-* family declared as one lockstep pin group at 0.0.72,
    with the versions guard failing when one member moves alone; deny.toml; the
    serde_json::Value ban in clippy.toml; and every crate of the architecture's
    crate map as a documented placeholder, with the ferrofed binary over a thin
    library run path and the testkit's pin-matrix reader asserting each crate's
    specification constant against docs/VERSIONS.md.

  • The server binary shape (#29): ferrofed serve and ferrofed config check;
    configuration from a TOML file and FERROFED__ environment overrides with
    unknown keys refused, _file siblings for every secret, outbound
    credentials per endpoint id, and a typed refusal (exit 78) on any bad value,
    a broken log filter included; auto, json and pretty console formats;
    GET /, GET /health and GET /health/readiness over an indicator
    registry; the request id, the panic catch (a 500 that carries neither the
    panic message nor anything the client sent), the request timeout and the
    body ceiling; a bounded drain on SIGTERM; and 501 for every path of the
    ITS-REST surface until the façade lands. The request log records the
    method, the matched route, the status, the latency and the request id, and
    never a body, the AQL text, a header value, an unmatched path, or a query
    value other than the digit-only offset and fetch, so a façade query's
    patient identifier reaches no log line at any level (§5.4.3).

  • The conformance matrix (#41): conformance/matrix.tsv with every
    conformance point of section 17, conformance/tracks.tsv with the section
    16.3 tracks (track 8 deferred as provisional) and
    conformance/requirements.tsv with the reachability of all 46 requirements,
    derived from the vendored specification by scripts/conformance/matrix.sh;
    the // conformance: test marker; the tier-1 conformance-matrix guard; and
    the matrix rendered into the book's Evaluate part.

  • The clinical-path storage rule (#40): an engine test reads the crate graph
    and fails when a library crate reaches a storage implementation or the
    application crate.

Fixed

  • scripts/checks/crate-version-guard.sh no longer exits early on the change
    that adds the root Cargo.toml, where the base has none.

0.0.1-rc.1

0.0.1-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Oct 12:30
Immutable release. Only release title and notes can be modified.
v0.0.1-rc.1
4e5b467

A pre-release that rehearses the release lane (#14): the repository setup,
the documentation site and the architecture of record, with no binaries.

Added

  • The architecture of record, docs/architecture.md, from the first research
    pass (#16, evidence on #18 to #27): the published openehr-* crates as the
    openEHR surface at the planned 0.0.74 pin, the request pipeline, the AQL
    rewrite and identifier hygiene on openehr-query's AST, the façade split
    over the ITS-REST route tables, the identity seams with each binding (PIXm,
    mCSD, PMIR, XCPD, the Dutch Generic Functions) in its own crate, the
    security handoff, the registry and its storage, the fan-out, completeness
    and cross-node merge with the LIMIT agreement check, the hand-written wire
    types, the crate map with the publish switch, the conformance instrument,
    the test topology, the milestone map, and the decision register, every
    entry decided by the owner on 2026-10-01.
  • The documentation site on ferrofed.eu: a landing page at / and an mdBook
    under /docs/ organised by reader intent (Evaluate, Operate, Integrate,
    Contribute), built on every pull request and deployed from main by
    docs.yml, with the pinned docs toolchain, the vendored mermaid assets, the
    favicon set and its favicon-sync guard, and the README badge block (#12).
  • The release lane, .github/workflows/release.yml: a signed v* tag is
    checked against CITATION.cff and the product row of docs/VERSIONS.md,
    the version's CHANGELOG.md section becomes the release notes, and the
    release is created as a draft and published once the asset set it promises
    (none before there is code) is verified. docs/release.md is the cut (#14).
  • The Business Source License 1.1 with Vernum Projecten B.V. as the Licensor
    and copyright holder (#1), and the contribution-licence terms, the
    pull-request checkbox and the contribution-licence-guard check (#3).
  • The working discipline under .claude/: the rules, the hooks (no AI
    attribution, dangerous-command guard, format and comment-style on edit,
    the SessionStart tracker summary), the issue-loop skills, the
    spec-researcher and implementer agents, and the tracked project memory.
    CLAUDE.md states the product, the design-phase status and the hard rules
    (#6), with the identifier-hygiene rule for everything the gateway composes
    for a node.
  • The vendored specification corpora under docs/specs/, each with a
    PROVENANCE.md and a fetch script under scripts/vendor/: the Federation
    Tier with AQL specification at v0.9.0 (release candidate, CC0 1.0), its
    reference implementation (Apache-2.0) as evidence, the openEHR ITS-REST
    1.1.0 OpenAPI documents and the openEHR AQL 1.1.0 source with its grammar
    (#7).
  • One pin matrix, docs/VERSIONS.md, and scripts/checks/versions.sh, the
    guard that fails on drift between the matrix and every file repeating a pin
    (#8).
  • The GitHub setup: issue and pull-request templates, CODEOWNERS, Dependabot,
    the label set and tracker helpers under scripts/gh/, and the CI workflows
    (the tier-1 guards with the Rust tier gated until a workspace exists, one
    conclusion check, CodeQL, Scorecard and SonarQube Cloud), and
    docs/ci-cd.md, the design of the workflows and the repository settings
    (#9, #10, #11).
  • The community and governance set: CODE_OF_CONDUCT.md, GOVERNANCE.md,
    SUPPORT.md, AI_STATEMENT.md, CITATION.cff, llms.txt, and the root
    toolchain, format and lint configuration (#15).