Integration Attribution Contract — AttributionContext
integration specs/integration/attribution-contract.kmd
Define o **contrato de atribuição e proveniência** que todo sistema externo (saude-publica e outros) carrega ao integrar com o Koder Iris via SDK. Separa dois problemas que costumam ser confundidos: (A) **certeza de identidade do paciente** — resolvida por chave qualificada `(issuer, patientId)` + vínculo por accession, um problema de *correção*; e (B) **proveniência/atribuição** — resolvida por um **envelope genérico** (`AttributionContext`: `source_system` + `org_path` de profundidade arbitrária + `operator` + ação/tempo), um problema de *auditoria*. O envelope é obrigatório e fail-closed no SDK, imutável e encadeado na trilha à prova de adulteração (RFC-036 C15), com PII fora da telemetria (observability-first) e um subconjunto espelhado em tags DICOM para portabilidade. É a materialização, no contrato de SDK, da pré-condição de **isolamento de tenant** que gateia a custódia central de PHI (stack#439).
Quando esta spec se aplica
Triggers primários
- Integrar um sistema externo ao Koder Iris via SDK
- Registrar proveniência/atribuição de um dado clínico no Iris
Todos os triggers
- Integrar um sistema externo ao Koder Iris (submeter ordem/worklist ou resultado de exame via SDK/API)
- Registrar quem/o-que originou um dado clínico no Iris (operador, sistema, versão, servidor, org)
- Modelar a chave de conta/principal de um sistema que integra com um serviço Koder que custodia PHI
- Vincular um resultado de exame (C-STORE) ao paciente/ordem que o solicitou
Corpo da especificação
Contrato de Atribuição de Integração — AttributionContext
1. Escopo e o corte em dois problemas
Todo sistema externo que escreve no Iris (submeter ordem/worklist ou
resultado de exame) via SDK DEVE carregar um AttributionContext. O
contrato resolve dois problemas distintos, com mecanismos distintos:
| Problema | Natureza | Mecanismo | |
|---|---|---|---|
| A | "A que paciente este dado pertence?" | Correção | Chave qualificada (issuer, patientId) + vínculo por accession (§3) |
| B | "Quem/o-que originou este dado?" | Auditoria | Envelope AttributionContext (§4), imutável e encadeado (§5) |
Confundir os dois leva a erro: proveniência não identifica paciente, e identidade de paciente não é metadado de auditoria mutável.
2. Formato e degrau
AttributionContext é um schema sobre JSON (degrau 2 da escada de
policies/language-and-format-creation.kmd) — não um formato novo. Apoia-se em
padrões externos consolidados (R6): DICOM Issuer of Patient ID, HL7
assigning authority, e o modelo W3C PROV (Agent/Activity/Entity) como
inspiração conceitual, sem carregar sua ontologia. Registrado em
registries/languages-and-formats.md no mesmo PR (R4).
3. Identidade de paciente (problema A) — normativo
R1 — Chave de paciente é sempre qualificada. O identificador de paciente
NUNCA é o patientId cru. A chave é o par (issuer, patientId), onde
issuer é a autoridade emissora do identificador (a assigning authority HL7 /
Issuer of Patient ID DICOM). O prontuário do saude-publica só é único dentro
de um município → o issuer é o município (ou a instância do saude-publica).
Sem issuer, patientId de tenants distintos colide num store central.
R2 — Vínculo por accession, não por adivinhação. A ligação
resultado→paciente é feita pelo accession number que a worklist cunhou, não
pelo PatientID que o equipamento devolve (que vem vazio/errado — o próprio
código admite). O resultado (C-STORE) carrega o accession; o Iris resolve
accession → ordem → (issuer, patientId). Um resultado sem accession resolvível
entra na fila de não-vinculados para reconciliação humana, nunca é
auto-atribuído por heurística de nome/ID.
R3 — issuer deriva do org_path, não é campo solto. O issuer de um
patientId é determinado pelo nível-tenant do AttributionContext (§4) que
criou a ordem — garantindo que identidade de paciente e tenant sejam a mesma
verdade, com um só caminho (R3 da escada de formatos).
4. Proveniência (problema B) — o envelope AttributionContext
O envelope é genérico: não modela o principal de nenhum sistema específico.
AttributionContext {
"schema_version": "1", // o envelope evolui sem quebrar integrações (R11)
"source_system": { // qual software originou, e onde rodava
"name": "saude-publica",
"version": "4.2.10+build.917", // OBRIGATÓRIO (R6)
"instance_id": "sp-botucatu-01", // instância lógica (LXC/container/tenant do integrador)
"host": "sp-lxc-3.botucatu", // hostname do servidor
"ip": "10.4.0.12", // IP de origem observado
"environment": "prd" // prd | stg | dev
},
"org_path": [ // hierarquia ORDENADA, profundidade arbitrária (R7)
{ "kind": "municipality", "id": "3506003", "label": "Botucatu" },
{ "kind": "facility", "id": "UBS-07", "label": "UBS Vila dos Lavradores" },
{ "kind": "sector", "id": "CARDIO", "label": "Cardiologia" }
],
"operator": { // quem operou (R8: opaco com mínimo)
"subject": "enio.silva", // OBRIGATÓRIO — o login no sistema-origem
"display_name": "Ênio Silva",
"claims": { "cns": "…", "role": "tecnico" } // bag opaca; o Iris guarda, não interpreta
},
"action": "submit-result", // submit-order | submit-result | amend | …
"occurred_at": "2026-07-20T15:01:00Z" // instante do ato no sistema-origem
}
R6 — source_system.version é obrigatório. Sem a versão do sistema-origem
no momento do envio/recebimento, o registro clínico não é audível ("qual versão
estava quando enviou os dados deste exame?"). O SDK recusa a chamada sem ela.
R7 — org_path é lista tipada, ordenada, de profundidade arbitrária. Cada
nível é { kind, id, label }. O SDK não interpreta kind — é rótulo do
integrador. saude-publica preenche [municipality, facility, sector]; um sistema
login/senha preenche [] ou [{tenant}]. O nível-topo do org_path é o
tenant que o Iris usa no isolamento (kdb RLS por kdb.tenant) e como issuer
(R3). org_path vazio ⇒ o source_system.instance_id é o tenant.
R8 — operator é opaco com mínimo obrigatório. Exigido: subject (o login).
display_name e claims são aceitos como bag e guardados sem interpretação —
assim login/senha e operador/município/unidade/setor cabem sem o Iris
privilegiar o modelo de nenhum integrador. O Iris nunca faz autorização com base
em claims; são dado de auditoria.
5. Garantias (o que faz disto contrato, não boa-vontade) — normativo
R9 — Obrigatório e fail-closed no SDK. Toda escrita exige o
AttributionContext. O SDK recusa (erro, não default silencioso) se faltar
o mínimo: source_system.name + source_system.version, operator.subject,
chave de paciente qualificada (§3) e — para submit-order — o nível-topo do
org_path. É assim que o Iris sempre conhece a proveniência.
R10 — Imutável e encadeado. A atribuição é gravada no ato do submit,
presa ao estudo/ordem, e dobrada na trilha à prova de adulteração do daemon
(cadeia seq/prev/hash já existente; RFC-036 C15). Correção posterior é um
evento amend novo na cadeia (com sua própria atribuição), nunca reescrita
do registro original.
R11 — Versionado. schema_version permite integradores em versões
diferentes. O Iris aceita versões conhecidas e rejeita desconhecidas com erro
explícito; campos novos são aditivos dentro de uma major.
R12 — PII fora da telemetria. operator.subject, operator.claims e a
chave de paciente são PII → vivem só no store de atribuição auditado, nunca
em métrica/label/trace/log estruturado de telemetria (cardinalidade + vazamento;
observability-first.kmd). O trace_id correlaciona atribuição e telemetria sem
copiar o PII para o lado observável. Retenção do dado de operador segue
identity-data-retention.kmd.
6. Subconjunto DICOM-nativo (portabilidade) — normativo
R13 — Espelhar o subconjunto portátil em tags DICOM. Para o exame ser
auto-descritivo fora do Iris, o SDK/daemon grava, no objeto DICOM, o subconjunto
abaixo. O envelope rico (IP, instance_id, subject, schema_version) excede
os slots limpos do DICOM e vive só no store de atribuição.
| Campo do envelope | Tag DICOM | VR |
|---|---|---|
issuer (R1) | Issuer of Patient ID (0010,0021) + Qualifiers Seq (0010,0024) | LO / SQ |
org_path[municipality/facility] | Institution Name (0008,0080) | LO |
org_path[sector] | Institutional Department Name (0008,1040) | LO |
operator.display_name | Operators' Name (0008,1070) | PN |
source_system.name+version | Contributing Equipment Sequence (0018,A001) (Purpose = SOURCE) | SQ |
| accession (R2) | Accession Number (0008,0050) | SH |
7. Exemplos trabalhados
7.1 saude-publica (hierárquico)
org_path = [municipality Botucatu, facility UBS-07, sector CARDIO],
operator.subject = enio.silva, source_system = saude-publica 4.2.10. Tenant =
municipality:3506003; issuer do prontuário = esse tenant; accession
BTC-FLUXO-001 liga o C-STORE à ordem.
7.2 Sistema login/senha (flat)
org_path = [] (ou [{ kind:"tenant", id:"clinica-x" }]), operator.subject = dr.house, source_system = clinicsoft 2.0. Tenant = source_system.instance_id
(ou o único nível). Mesmo envelope, zero campo específico do saude-publica — a
abstração fecha.
8. Testes
- T1 — Submeter resultado sem
AttributionContext→ SDK recusa (R9). (behavioral) - T2 — Dois
patientIdiguais sobissuerdistintos → dois pacientes distintos no store (R1). (behavioral) - T3 — C-STORE sem accession resolvível → fila de não-vinculados, nunca auto-atribuído (R2). (behavioral)
- T4 —
org_pathde profundidade 0, 1 e 3 → todos aceitos; nível-topo vira tenant (R7). (behavioral) - T5 — Métrica/trace emitidos por um submit → nenhum contém
operator.subject/patientId (R12). (estrutural) - T6 — Objeto DICOM gravado carrega Issuer of Patient ID + Contributing Equipment (R13). (golden)
- T7 —
amendde uma atribuição → novo elo na cadeia; registro original intacto e verificável (R10). (behavioral) - T8 —
schema_versiondesconhecida → erro explícito, não aceitação silenciosa (R11). (behavioral)
9. Relação com a decisão de custódia (stack#439)
Esta spec é independente da forma de consumo (F2/F3/F4): a atribuição é
necessária em qualquer topologia. Mas ela é a materialização, no contrato de
SDK, da pré-condição de isolamento de tenant que gateia a custódia central de
PHI (Forma 2 / F3 — stack#439). Implementá-la corretamente é pré-requisito, não
consequência, de qualquer decisão de centralizar. A decisão jurídica (Koder como
operadora/controladora) permanece do owner; esta spec não a antecipa.
10. Attestation — signed submissions (normative)
Sections 3–9 make attribution declared. This section makes it attested: the holder of the API key alone cannot forge a submission's authorship.
R14 — Every write is signed. The SDK produces a detached JWS (EdDSA/Ed25519)
over a canonical digest of sha256(DICOM object) ‖ canonical(AttributionContext) ‖ occurredAt ‖ nonce. The private key never leaves the integrator. Iris verifies
with the integrator's registered public key (via JWKS — reuse engines/sdk/go/auth).
Unverifiable signature → reject (fail-closed). The signature therefore binds the
exam and the attribution together: neither the patient nor the provenance can
be swapped without breaking it.
R15 — Replay protection. The signed payload carries a nonce and occurredAt;
Iris keeps a short-window seen-nonce cache and rejects duplicates and stale
timestamps.
Honest boundary (do not oversell). A signature proves key possession,
integrity, authorship, and non-repudiation, and binds attribution↔exam. It does
NOT prove network location. The source_system.ip is an observed claim: it
is enforced as a policy gate + anomaly alert (a valid signature from an
un-allowlisted IP is rejected/alerted, RFC-036 §7), never presented as
cryptographic proof. mTLS (client cert) is the closest channel anchor and MAY be
required per integrator.
R14a — The verification result is auditable. The kid, verified-at time, and
observed IP are folded into the immutable audit chain (R10), so any exam can later
be shown to have been signed by a specific key and verified at a specific time.
11. Trust anchor / enrollment (normative — guarantor choice is owner-decision)
Cryptography never establishes the key↔entity binding by itself: a keypair is anonymous. The binding "this key belongs to legal entity Y" is always created by an enrollment process run by an authority — that authority is the guarantor. Two tiers compose:
R16 — Baseline: Koder as Registration Authority. At onboarding, Koder performs
KYB (CNPJ + contract + responsible person) and registers the integrator's Ed25519
public key bound to {entity, CNPJ, system, allowed tenants}. Guarantor = Koder's
onboarding process. Pragmatic floor for Koder-hosted (Form 2).
R17 — Legal tier: ICP-Brasil e-CNPJ. Where juridical non-repudiation matters
(the laudo, or a client that demands it), the enrollment key is itself attested by
an ICP-Brasil e-CNPJ certificate (pessoa jurídica), validated against the
AC-Raiz chain — reuse services/crypto/signer. e-CPF (pessoa física) vs e-CNPJ
(pessoa jurídica) is the native dev-vs-company distinction: require e-CNPJ to put
the company on the hook.
R18 — Revocation is mandatory. Whatever the guarantor, the binding is only as good as key protection + revocation. CRL/OCSP for ICP-Brasil (signer already does it); a revocation list for Koder-RA. Without revocation the anchor is decorative.
The enrolled identity is recorded on source_system.enrolled_identity = { authority: "koder-ra" | "icp-brasil", entity_id: <CNPJ>, kid } and enters the
audit chain, so every exam carries who vouched for the key and which entity it maps
to.
Owner-decision. Which guarantor is required per consumption form (Koder-RA floor everywhere; ICP-Brasil e-CNPJ mandatory for the laudo, optional-per-client for integration attestation) has contractual/legal weight and is the owner's call. This spec defines the mechanism, not the mandate.
12. Third-party witnessing (normative — which witnesses is owner-decision)
A trusted third party (TTP) witnesses the communication. Its value comes from being controlled by neither party — so Koder, as a party to its own inbound, cannot witness it; a neutral external witness is required for that link. Witnesses split by what they attest, and for PHI every witness sees only the hash, never the content (LGPD):
R19 — Time witness (RFC 3161 TSA). Timestamp the hash of each exam/laudo at an
accredited TSA (ICP-Brasil Carimbo do Tempo) — reuse services/crypto/signer.
Proves existence-at-T independent of Koder's clock.
R20 — Immutability witness (public ledger anchor). Periodically anchor the hash of the audit-chain head to a public ledger (OpenTimestamps / Bitcoin). The internal chain proves order; the public anchor proves Koder did not rewrite history after the fact — verifiable by anyone, forever, at ~zero cost.
R21 — Sector hub (long-term). For Brazilian clinical interop, RNDS (DATASUS) is the real third-party health hub; if Iris feeds the national network, RNDS is the recognized witness of the clinical exchange (under its own legal framework — the one case a witness legitimately sees content).
Honest boundary. A witness attests existence/delivery, not truth-of-content: a TSA proves "this exact payload passed at T signed by key K", not that the exam is real or that operator X truly performed it. Truth-of-content rests on the signer.
Owner-decision. Which witnesses to require (TSA always? OpenTimestamps anchor? RNDS when?) is the owner's call. Committed architecture: this witnessing capability is a standalone Stack component (
services/crypto/veredix), NOT Iris-internal — Iris is its first consumer via the SDK. Both stages (internal infra
- external product) are committed in stack-RFC-042; the attestation slices in dicom#003 CALL that component, they do not contain its logic.
13. Attestation tests
- T9 — A submission with an invalid/absent signature is rejected (R14). (behavioral)
- T10 — Tampering with either the DICOM bytes or the AttributionContext after signing fails verification (R14 binding). (behavioral)
- T11 — A replayed (duplicate-nonce) submission is rejected (R15). (behavioral)
- T12 — A valid signature from an un-allowlisted IP is rejected/alerted, not accepted as proof (R14 boundary). (behavioral)
- T13 — A submission whose enrollment key is revoked is rejected (R18). (behavioral)
Referências
specs/consumption/taxonomy.kmdspecs/multi-tenancy/contract.kmdpolicies/multi-tenant-by-default.kmdpolicies/observability-first.kmdpolicies/identity-data-retention.kmdrfcs/stack-RFC-029-external-integration-surface.kmdrfcs/stack-RFC-032-integration-ports-and-localized-export-bindings.kmdrfcs/stack-RFC-036-secure-surface-and-tenant-security-alerting.kmdrfcs/stack-RFC-027-portable-self-hosted-export.kmdregistries/languages-and-formats.mdservices/media/dicom/backlog/pending/003-iris-integration-sdk-attribution-first.mdmeta/docs/stack/backlog/pending/439-rfc031-amendment-central-vs-client-hosted-phi-custody.kmd