Repository navigation
0.0.3
·
626 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
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::PdqmClientposts a
PatientQueryto[base]/Patient/_searchas a form body, so no URL carries
a demographic value, and reads thesearchsetinto the matching Patients
(FHIR R4Patientfromfhir-typesresources), thetotal, each match's
score andmatch-grade, theOperationOutcomewarnings and thenextpage
link, which it follows only on the Supplier's origin. A404with a
not-foundissue 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 underdocs/specs/ihe-pdqm/by
scripts/vendor/ihe-pdqm.sh. TheOperationOutcomeissue type moves to
ihe_iti::outcome::IssueType, shared by PIXm and PDQm;ihe-itiis 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, byehr_idin every session or all of them for an
unscoped change, andFederation::identity_changedis 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 anehr_id, and the others arenot-resolvedwithout failing
the query (N6, N8). The selection is named in the startup log; the
OPTIONSself-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::PixManageris 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 conditionalPUT Patient?identifier=held to the
IHE.PIXm.Patientminimums, plus the Remove Patient Option's conditional
DELETE) and answers ITI-83$ihe-pixfrom 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.999arc with oneehr_idper 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
SourceCapabilityStatement, thePatientprofiles 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 theehr_idliteral the rewrite
scoped the query to, so a short identifier that occurs inside the node's
ownehr_idis answered. Every patient predicate the rewrite strips, every refused
query and every gate stop is a security event underferrofed::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-federation0.0.5 addsRefusal::kind,Refusal::atand
PatientQuery::strippedfor 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 theehr_iddomain 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 isnot-resolvedinmeta.federation, a patient known nowhere is
a200with no rows andcomplete: false, and a Manager that cannot answer
fails the query424with 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 byfederation.binding_ttl_ms
(default 15 minutes), and nothing derived from a patient identifier is
stored or logged. - The PIXm ITI-83 client (#42):
ihe-iti0.0.3, featurepixm, asks a PIX
Manager'sPatient/$ihe-pixfor the identifiers other domains hold for a
patient, held to the vendored PIXm 3.1.0OperationDefinitionand 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). A404without a
not-foundissue never reads as "patient unknown". Identifier values are
redacted inDebugand carried by no error. The IHE PIXm 3.1.0 package
artefacts of ITI-83 are vendored underdocs/specs/ihe-pixm/by
scripts/vendor/ihe-pixm.sh, pinned by version and tarball sha256, and the
FHIR R4 model comes fromfhir-types0.1.107. - The fuzz lane (#134): four
cargo fuzztargets over the untrusted inputs
(the AQL rewrite over arbitrary text and parameters, the ITS-REST
AdhocQueryExecutebody through the façade's intake, a federated
RESULT_SETwith itsmeta.federation, and theOPTIONS {base}/body),
with seeds generated from the vendored golden cases and the specification's
JSON examples byscripts/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.ymlruns 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-levelsubjectpredicate,…/subject/identifiers/id, is resolution
input on equal terms withEHR_STATUS.subject.external_ref, with the
DV_IDENTIFIERissuerortypeas its issuing namespace. It is consumed
and stripped exactly asexternal_refis, 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 a400, as are the
assigner, a predicate on the identifier list, anENTRY-level
external_ref, and the carrier selected, ordered on or inside a function.
openehr-federationis 0.0.4, and the interimRefusal::EntrySubjectis
gone. - The release lane at SLSA Build Level 3 (#31): the reusable
release-build.ymlbuildsferrofedper target withcargo auditable,
writes a CycloneDX and a syft SBOM, and attests the tarball's provenance and
both SBOMs; the reusablerelease-image.ymlbuilds thelinux/amd64and
linux/arm64image from the attested musl binaries, pushes it to
ghcr.io/ferrohealth/ferrofedby 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/aqlanswers one
ITS-RESTRESULT_SETover 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'saqlrewrite, resolves the patient at every member
through the configured cross-reference, and sends each member that knows
the patient standard AQL keyed on its ownehr_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.federationnames every endpoint
(N16): a member that does not know the patient isnot-resolved, a
suspended endpoint and a second endpoint of one member areexcluded. - A refused query is a
400ITS-REST error that locates the fault and
quotes nothing (§5.4.3). Without a cross-reference a patient query fails
closed with424(decision A17), and without a registry the route stays
501. - The configuration gains
profile,[registry] document,[federation]
(per_node_timeout_ms,overall_timeout_msbelow the request timeout,
default_namespace) and the[[dev.crossref]]rows of the development
profile.config checkloads the registry too. - The compose quickstart mounts
docker/quickstart/registry.tomland
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 gateway types the
- The fan-out engine, first increment (#37):
ferrofed-engine'sfanout
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 reportedtime-out, without touching any other request, and its late
answer contributes nothing.meta.federationis built from every outcome
before the decision, so a failing answer carries it, with the effective
timeoutbudget andcomplete: false. Under the all-or-nothing default an
offlineortime-outnode fails the query504, anode-errorfails it
424,504taking precedence;not-resolvedandconsent-deniedfail
nothing, so a patient found nowhere is a200with empty rows (§11.3, §11.4,
N6, N37). Every endpoint record carries its node,system_id, managing
organisation, base URL andlatency_ms(§9.5, N40), and the rows of the
activenodes are concatenated in endpoint id order until the merge (#52). - The crates.io lane behind the workspace
publishswitch (#32):
publish-crates.ymlruns on everyv*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] publishisfalse. Thepublish-dry-runjob packages
every library crate on every pull request, and
scripts/release/publish-crates.shis the shared implementation. - Node dispatch (#34):
ferrofed-engine'sdispatchmodule builds one
openehr-itsclient per registry endpoint, rooted at the endpoint's base URL
as the registry holds it plus the ITS-RESTv1segment (N28), and sends each
node query as the generatedPOST {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 toactive, a refused connection or broken
stream toofflinewith its reason, a passed deadline totime-out, and a
documented error, an undocumented status or a body that is not a result set
tonode-errorcarrying 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
aqlfeature of
openehr-federation0.0.3 parses a façade query withopenehr-query, binds
itsquery_parametersbefore analysis, and consumes the
EHR_STATUS.subject.external_refpatient predicate with its namespace into a
redactedSubject(§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); andcolumns[]is rendered from the façade
query alone (N17). The rewrite refuses with a typed400, never quoting the
identifier: a predicate outside the top-levelANDchain 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-nodeOFFSET(§11.6.2), an undirected
aggregate (N14), and the identifier anywhere else in the query (§5.4.1),
including rebuilt byCONCAT,CONCAT_WSorSUBSTRINGover 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-registry0.0.1 loads the federation's
membership from a reviewed TOML bootstrap document ([[organisation]],
[[node]]with itssystem_idand[[node.identifier]],[[endpoint]])
withdeny_unknown_fieldsthroughout into an immutableRegistrySnapshot.
A document with a dangling reference, a duplicate id, asystem_idshared by
two nodes (compared without ASCII case), an endpoint without exactly one
managing organisation, a connection type other thanopenehr-rest-query, an
unusable base URL or a node without an endpoint refuses to load (N19, N20,
N21, §12b.2).NodeId,EndpointIdandSystemIdare distinct types with
no conversion between them (N32, §12a.1). - The resolver seam and the development cross-reference (#36):
ferrofed-identity0.0.1 carriesPatientRef, the patient identifier
redacted in every rendering and never serialized (§5.4, N33), theResolver
trait (N3, §5.2), andStaticResolver, a fixed table from a synthetic
identifier to each member'sehr_idthat a configuration can enable only
underprofile = "development". It is FerroFED's own testing device, not an
identity binding. - The container image:
docker/Dockerfileputs theferrofedmusl binary on
distroless static, digest-pinned, as the numeric non-root user65532, with
no shell and a read-only root;scripts/release/stage-dist.shstages 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 indocs/VERSIONS.mdand 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 theFERROFED_E2Egate; 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 theurn:oid:2.999example arc. CI runs the
container suite in its owne2e (containers)job, and the versions guard
holds the image pins todocs/VERSIONS.md.
Fixed
ihe-iti0.0.4 names PMIR's transactions as ITI-93 and ITI-94 in its
description, feature list and README. ITI-104, which it attributed to PMIR,
is the PIXm Patient Identity Feed FHIR (#47).
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/ferrofedand the crates'repositoryfollow 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-federation0.0.1 (the Federation Tier
wire types, formerlyferrofed-wire, with theaqlandmergefeatures),
ihe-iti0.0.1 (featurespixm,pdqm,mcsd,pmir,xcpd) and
nl-generic-functions0.0.1 (featuresnvi,mitz,lrza,nuts-auth).
The names are held on crates.io by 0.0.0 placeholders.ferrofed-registry,
ferrofed-identityandferrofed-enginemove underapp/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
openehr-query, and the router builder, operation matcher, credentials
provider and per-call options inopenehr-its(FerroEHR #3505 to #3514).
Security
- A configuration that does not parse no longer quotes its source line
(#133). The TOML reader's error printed the offending line, so a malformed
[dev]row could put its patient identifier, and a mistyped inline secret
its value, into theconfig checkoutput and the boot failure. The refusal
now names the file, the line and column, the key path (devalone for any
line of the[dev]table) and the kind of fault, and none of the text.