Repository navigation
0.1.0 is the first version of diagnos ever published — see
MIGRATING.md if you are coming from an internal or from-source checkout of the former imgexam
monorepo.
Added
-
One package,
diagnos, with extras.pip install diagnosis the base: no dependencies at all, and four
commands —diagnos docs(Help Center, Developer Central, Trust Center),diagnos version(version, interpreter,
platform and installed extras,--jsontoo),diagnos setupanddiagnos help.diagnos[sdk]adds the SDK
(from diagnos import Diagnos),diagnos[cli]the full command line,diagnos[api]the REST API (diagnos-api);
openbaoanddev-certssit on top.import diagnosworks on the base install, and a missing extra raises
MissingExtraErrornaming the command that adds it.diagnos-clianddiagnos-apiare no longer separate
packages;MIGRATING.mdmaps the old names. -
diagnos setup sdk | cli | api: installs one layer step by step — the SDK into your project withuv add, the CLI
into the environment that runsdiagnos(or as auv tool), the API as its signed image with Docker. Every
command is shown first and confirmed once (default no);--dry-runand--yesfor scripts. The API image is
verified by the pulled digest with cosign (3.1.3+) against the release workflow's identity, and thedocker runis
printed by that digest. A missinguvor Docker exits with69and its install page — nothing is ever installed
behind your back. -
wrappers/contract.json: the language-neutral contract of the four base commands, for wrappers in other
languages; the Python base is tested against it. -
Signed releases: every wheel, the sdist and
SHA256SUMScarry a cosign keyless signature and a SLSA provenance
attestation; PyPI gets PEP 740 attestations through Trusted Publishing; the GitHub release attaches every file,
bundle and the commands that verify them (SECURITY.md). -
Images on GHCR, for
linux/amd64andlinux/arm64:ghcr.io/diagnos-tech/diagnos-apiand
ghcr.io/diagnos-tech/diagnos(the CLI), built from the released wheels, with SBOM and provenance, signed by
digest. They run as uid10001with root-owned, read-only code and no pip or uv. -
Renamed the project from
imgexamtodiagnos, end to end: packages (imgexam→diagnos,imgexam-cli→
diagnos-cli,imgexam-api→diagnos-api), modules (imgexam→diagnos,imgexam_cli→diagnos_cli,
imgexam_api→diagnos_api), the public class (Imgexam→Diagnos) and exceptions (ImgexamError→
DiagnosError,ImgexamPermissionError→DiagnosPermissionError), everyIMGEXAM_*environment variable to
DIAGNOS_*, the CLI command (imgexam→diagnos), the Docker image (imgexam-api→diagnos-api), and the
default vault URL tohttps://vault.diagnos.health. SeeMIGRATING.mdfor the full list,
including what was deliberately left unrenamed (theimgexam-*-v1cryptographic labels and the OpenBao static
seal key id). -
Consumer-driven Pact contract test suite (
contracts/) between the SDK and the vault:make contract/
make contract-check, a deterministic, normalizedcontracts/diagnos-sdk-diagnos-vault.json, and provider states
covering the clock, enrollment (pending, approved, denied, forgotten), lock, patients, exams and files. See
contracts/README.md. -
Makefileas the single entry point for every check a contributor or CI runs (sync,fmt,lint,types,
test,cov,contract,contract-check,check). -
GitHub Actions:
CIon every pull request (lint,mypy --strict, the docs, the Pact contract, the Rust enclave,
every Python suite on CPython 3.11 and 3.13 with a coverage floor, and zizmor and actionlint over the workflows),
behind one required check;Packageon pull requests intomain;Releaseonv*tags onmain; monthly
grouped Dependabot updates intodevelop, whose minor and patch updates merge on their own once CI passes; and a
weekly OpenSSF Scorecard. -
A self-hosted coverage badge, published to the orphan
badgesbranch byCIon every release. -
Bilingual (🇺🇸/🇧🇷) documentation across the repository, enforced by
scripts/check_docs.py(every document ships
asNAME.md+NAME.pt-BR.md) andscripts/check_bilingual.py(every module, class and public function's
docstring, Python and Rust). -
Repository layout: the three packages live under
apps/(apps/sdk,apps/cli,apps/api);contracts/,
docs/andscripts/stay at the root. -
update(..., expected_latest_version_id=...)(CLI--expect-version): the vault refuses the write if another
version was saved since. Reservations that meet another writer's pending version are retried briefly (1.5 s, 3 s),
and commits lost to the network or a5xxare replayed with backoff. -
Patient
tags(sealed list labels):create(..., tags=[...]),update(..., tags=...)(Nonekeeps them), CLI
--tag. -
DIAGNOS_TIME_PRECISION/Settings.time_precision: truncate dates to the workspace's anonymization precision
before sealing, as the web app does;diagnos.truncate_timestampanddiagnos.to_iso_instantfor your own dates. -
ProtocolErrorfor a vault answer the protocol does not allow (e.g. a signed size that is not the body's); the API
answers it with502andcode: protocol_error. -
Folders:
Drive.create_folder(), andparent_id=onupload,upload_many,listanditer_all(CLI
files mkdirand--folder; APIPOST /v1/drives/{sg}/foldersandparent_id). -
Workspace-wide files:
vault.drives.list()/iter_all()across every group the session may list, filtered by
security_group,exam_id,parent_idandinclude_pending;vault.drives.get(),name_of(),download()and
iter_download()read any file by node id alone (CLIfiles listwithout--group,files get/downloadby id). -
DIAGNOS_STORAGE_HOSTS/Settings.storage_hosts: presigned storage URLs are followed only over HTTPS to an
allowed host or its subdomains — by default the user-content domaindiagnosusercontent.comand R2's
r2.cloudflarestorage.com— so ciphertext and SSE-C keys never go to a host nobody chose. -
CLI:
--groupis optional onpatients create,exams create,files uploadandfiles mkdir— it falls back to
DIAGNOS_GROUP, then (for an exam) the patient's group, then the session's only group, and says which it picked;
with several groups it stops and lists them. -
Patients.index(id)/Exams.index(id): a document's metadata (group, versions, flags) without opening its record. -
CLI session agent:
diagnos loginkeeps the session for the commands that follow, untildiagnos logout, the idle
timeout (DIAGNOS_AGENT_IDLE_MINUTES, 8 hours by default) orSIGTERM. The session stays in one background
process, in the SDK's locked memory; later commands send it their arguments over a private Unix socket and it runs
them, streaming output, prompts and exit codes back — the keys never leave that process, nothing is written to disk
and no OS keychain is used.DIAGNOS_AGENT=offkeeps every command in its own process.diagnos statusshows the
agent. -
CLI
diagnos logout: revokes the kept session, wipes its keys and stops the agent. -
CLI global options (
--json,--quiet,--no-color,--token,--vault-url) go anywhere on the line:
diagnos patients list --jsonworks. -
API
diagnos-api dev-certs [DIR]: a throwaway local CA, alocalhostserver pair and a client pair in one step
(keys0600, 30 days, never overwritten without--force; needs thediagnos[dev-certs]extra outside a
checkout).python -m diagnos.apiruns the API likediagnos-api. -
diagnos.UploadSource, exported at the top level. A.dcmfile is typedapplication/dicom, so the vault
classifies it.
Changed
- Patients and exams speak the vault's current protocol (
vault.patients,vault.exams, and the matching CLI
commands and API routes). A document belongs to exactly one security group (security_group=takes one id;
DocumentIndex.security_group_id); version state lives inDocumentIndex.streams(withlatest_version_id,
versionsandpending_version_idshorthands for thedatastream); patients' version routes carry
/streams/data; every version is sealed under a per-version key derived from the vault'ssecurity_context, as a
rawsalt ‖ nonce ‖ ciphertextbody; every write carries the sealed summary (encrypted_index). Pinned by vectors
sealed with the web app's own code and by 13 new contract interactions. PatientRecordandExamRecordnow mirror the web app's records:identifiers(vault-sealed),address,
race_identity;report_lexical/report_html,modality,exam_date.legal_id,ExamRecord.descriptionand
ExamRecord.reportare gone. Dates are stored the way the web app stores them (UTC ISO 8601 instants).- A misspelled record field (
birthdate) is refused with the field it resembles; any other unknown field is kept, so
a read-modify-write never drops a field the web app added. Validation errors on records never echo the value. list()returns rows with the decrypted summary (PatientListItem/ExamListItem); the CLI shows it with
--summaryand the API with?summary=true— rows stay anonymous by default.get()returns the newest content like the web app (a newer draft wins);include_draft=False/--committed
read committed versions only.- Archive and delete no longer re-upload the record: they are flag changes without a new version. New
restore()
(CLIrestore, APIPOST …/restore) takes a document out of the trash. Exams.createno longer takesmodality=: modality is a sealed record field; onlypatient_idis sent in clear.- Files speak the vault's current protocol (
vault.drives, CLIfiles, API/v1/drives): the/nodesroutes;
a DEK per file or folder, wrapped for its group (imgexam-node-dek-v1); the name sealed under it
(imgexam-node-name-v1); a content key derived from the vault'ssecurity_context; the web app's SSE-C key (the
content key's sister) on thePUT, on every multipart part and on theGET; up to 100 files per reservation, with
an idempotencyclient_refeach; multipart parts signed in waves of up to 200 and aborted on failure. Pinned by
vectors generated with the web app's upload code and by 5 new contract interactions. - The secretstream framing now always ends with its own
FINALframe (empty when the size is an exact multiple of
1 MiB), as the web app writes it;encrypted_size()follows. DriveNodemirrors the vault's node (kind,parent_id,encrypted_name,encrypted_keys,media_kind,
optimized_variants, …);StagedNodereplacesUploadedNode.upload()ofbytesor an anonymous stream requiresname=: every node carries a sealed name.- An upload confirmed as
missingraisesConflictErrorwith codeUploadIncomplete. - API: a node read under another group's
{sg}answers404;limitis bounded to 1–200. - A missing group key (
GroupKeyUnavailable) is a permission error: API403withcode: group_key_unavailable
(was a generic500), CLI exit code3. Listings degrade instead of failing: a file whose group key the session
lacks is listed withname: null(CLI: 🔒), like document summaries. - CLI
files … --jsonprints{node, name}per file, the same shape as the API. - CLI:
--filetogether with inline record flags (--legal-name,--title, …) is a usage error naming the flags;
the flags used to be dropped without a word.--file -reads the record from stdin. - CLI: a bare
diagnosprints the help with where to start and exits0(was2).
Removed
DIAGNOS_SSE_CandSettings.sse_c: files always use SSE-C exactly as the web app does, and documents never do,
so there is nothing left to choose.
Fixed
- CLI: decrypted fields, file names, vault-issued ids and the approval URL are printed literally instead of being
parsed asrichmarkup (a[html]label disappeared; a record could restyle the terminal). - API:
404on an unknown route and405on a wrong method now answer with the uniform
{"error": {code, message, trace_id}}envelope. - Docker image: the build context's ignore file is at the repository root (the one under
api/was never read, so
local.venv/target/could reach the build), and uv is pinned to a version that reads the currentuv.lock. GET /time: the SDK previously sent aPOSTwith an id body and read nothing back, so clock sync failed before
the first signed request; it now sends theGETthe vault actually serves and reads the raw{"result": <ms>}
response.- The per-response entropy seed: the SDK read it from an
X-Session-Seedheader that the vault no longer sends; it
is now read fromrandom_seedin the JSON envelope of every signed response, where the vault actually puts it. __version__is now read from the installed package's metadata instead of being hard-coded, so the SDK reports
its real version to the vault during enrollment.- CLI
files downloadwithout-owrites to the last segment of the decrypted name only: the web app stores a
relative path there, and a name such as../../.bashrccould otherwise write outside the working directory. The
API'sContent-Dispositionoffers the same base name. - Contract harness: running with
-p no:cacheproviderno longer crashes the partial-run check. - CLI: a local file problem (an
-ointo a directory that does not exist, an unreadable upload) prints
I/O errorand exits1instead of a Python traceback. - CLI:
--no-colorandNO_COLORdrop every ANSI escape, bold and dim included, not only colors. - API:
/openapi.jsonand/docsrequire the client certificate like every other route (FastAPI mounts its own
schema routes outside the app's dependencies);/redocis gone.
Known issues
- Multipart uploads (files above 64 MiB) are covered by unit tests, not by the contract: the vault's provider
verification has no S3 endpoint for R2. Whether R2 accepts SSE-C on those parts is one of the questions still open
on the vault side — seedocs/COMPATIBILITY.md.
Verify
ID="https://github.com/diagnos-tech/integration/.github/workflows/release.yml@refs/tags/v0.1.0"
ISSUER="https://token.actions.githubusercontent.com"
cosign verify-blob --bundle SHA256SUMS.sigstore.json --certificate-identity "$ID" --certificate-oidc-issuer "$ISSUER" SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
cosign verify-blob --bundle images.txt.sigstore.json --certificate-identity "$ID" --certificate-oidc-issuer "$ISSUER" images.txt
cosign verify "$(grep diagnos-api@ images.txt)" --certificate-identity "$ID" --certificate-oidc-issuer "$ISSUER"
gh attestation verify <file> --repo diagnos-tech/integration --cert-identity "$ID"