Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ improvising or retrying when something is off.
| Each package installs on its own within a size budget, checked in CI (privacy without desktop, capture without plotting) | On main ([#1270](https://github.com/OpenAdaptAI/OpenAdapt/pull/1270), [#1271](https://github.com/OpenAdaptAI/OpenAdapt/pull/1271), [#1279](https://github.com/OpenAdaptAI/OpenAdapt/pull/1279)) | CI | `.github/workflows/packages.yml` |
| See a compiled program as a map (every step, exact edge, loop and stop rule, with each step's checks) and play a recorded run over it; one React package that Desktop, app.openadapt.ai and openadapt.ai share | On main ([packages/program-graph](packages/program-graph/README.md)) | Real compiled OpenEMR referral program and a recorded scoreboard run, both on synthetic data | `@openadapt/program-graph`; `openadapt-flow visualize --format json` |
| Named sections for every compiled program ("Who: title, first name, last name", "Contact", "Create New Patient"), found without a model from the save, the recorded form structure, the fields and any narration; a person can rename, merge or split them on the review screen | On main | Unit tests; the certified OpenEMR referral bundle and a real recording of it on a local test copy with made-up patients | `openadapt-flow sections`; [SECTIONS.md](packages/flow/docs/SECTIONS.md) |
| Encrypt PHI at rest: every bundle file that can hold patient data under one sealed index (unknown files refused), web recordings from the first frame, and the run directories of sealed bundles; compile, induce, scaffold-verifier, qualify, admit, run, resume, undo, visualize, explain, emit-skill, for-each, sections and check mark read them in memory through one artifact reader and refuse without the key | On main | Synthetic (no-plaintext audit-hook test over record, compile and a VERIFIED run; per-frame cost measured) | `openadapt-flow seal`, `record`; [phi_at_rest.md](packages/flow/docs/phi_at_rest.md) |
| Encrypt PHI at rest: every bundle file that can hold patient data under one sealed index (unknown files refused), web recordings from the first frame, and the run directories of sealed bundles; compile, induce, scaffold-verifier, qualify, admit, run, resume, undo, visualize, explain, emit-skill, for-each, sections, check mark, compose, process, variants and route read them in memory through one artifact reader and refuse without the key | On main | Synthetic (no-plaintext audit-hook test over record, compile and a VERIFIED run; per-frame cost measured) | `openadapt-flow seal`, `record`; [phi_at_rest.md](packages/flow/docs/phi_at_rest.md) |
| Encryption at rest under a configured or locally generated key (0600): durable checkpoints by default; with `OPENADAPT_FLOW_AT_REST=strict`, every recording, bundle and run directory too. Each plaintext write is logged on stderr and in a ledger that Desktop shows as a notice | On main | Synthetic (key, opt-out, strict-mode and run-directory tests) | `openadapt_flow.at_rest`; [phi_at_rest.md](packages/flow/docs/phi_at_rest.md#default-opt-out-and-strict-mode) |
| Signed receipts (Seals), countersigned and timestamped, offline by default | Released | Unit tests, real TSA tokens | `openadapt-flow seal`, `countersign`, `local-seal` |
| Offline Seals signed with a key the customer holds: a key file, the OS keychain, or a PKCS#11 token or HSM; pinned keys with rotation and revocation; no fallback to a generated key | On main ([#1392](https://github.com/OpenAdaptAI/OpenAdapt/pull/1392)) | Unit tests with synthetic data (keychain and PKCS#11 through stand-ins) | `openadapt-flow local-seal --key`, `local-seal key create`, `pin`, `revoke`; `openadapt seal verify FOLDER` checks every sealed run in a folder; [OFFLINE_SEALS.md](packages/flow/docs/OFFLINE_SEALS.md#customer-held-keys) |
Expand Down
2 changes: 1 addition & 1 deletion llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ improvising or retrying when something is off.
| Each package installs on its own within a size budget, checked in CI (privacy without desktop, capture without plotting) | On main ([#1270](https://github.com/OpenAdaptAI/OpenAdapt/pull/1270), [#1271](https://github.com/OpenAdaptAI/OpenAdapt/pull/1271), [#1279](https://github.com/OpenAdaptAI/OpenAdapt/pull/1279)) | CI | `.github/workflows/packages.yml` |
| See a compiled program as a map (every step, exact edge, loop and stop rule, with each step's checks) and play a recorded run over it; one React package that Desktop, app.openadapt.ai and openadapt.ai share | On main ([packages/program-graph](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/program-graph/README.md)) | Real compiled OpenEMR referral program and a recorded scoreboard run, both on synthetic data | `@openadapt/program-graph`; `openadapt-flow visualize --format json` |
| Named sections for every compiled program ("Who: title, first name, last name", "Contact", "Create New Patient"), found without a model from the save, the recorded form structure, the fields and any narration; a person can rename, merge or split them on the review screen | On main | Unit tests; the certified OpenEMR referral bundle and a real recording of it on a local test copy with made-up patients | `openadapt-flow sections`; [SECTIONS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/SECTIONS.md) |
| Encrypt PHI at rest: every bundle file that can hold patient data under one sealed index (unknown files refused), web recordings from the first frame, and the run directories of sealed bundles; compile, induce, scaffold-verifier, qualify, admit, run, resume, undo, visualize, explain, emit-skill, for-each, sections and check mark read them in memory through one artifact reader and refuse without the key | On main | Synthetic (no-plaintext audit-hook test over record, compile and a VERIFIED run; per-frame cost measured) | `openadapt-flow seal`, `record`; [phi_at_rest.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/phi_at_rest.md) |
| Encrypt PHI at rest: every bundle file that can hold patient data under one sealed index (unknown files refused), web recordings from the first frame, and the run directories of sealed bundles; compile, induce, scaffold-verifier, qualify, admit, run, resume, undo, visualize, explain, emit-skill, for-each, sections, check mark, compose, process, variants and route read them in memory through one artifact reader and refuse without the key | On main | Synthetic (no-plaintext audit-hook test over record, compile and a VERIFIED run; per-frame cost measured) | `openadapt-flow seal`, `record`; [phi_at_rest.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/phi_at_rest.md) |
| Encryption at rest under a configured or locally generated key (0600): durable checkpoints by default; with `OPENADAPT_FLOW_AT_REST=strict`, every recording, bundle and run directory too. Each plaintext write is logged on stderr and in a ledger that Desktop shows as a notice | On main | Synthetic (key, opt-out, strict-mode and run-directory tests) | `openadapt_flow.at_rest`; [phi_at_rest.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/phi_at_rest.md#default-opt-out-and-strict-mode) |
| Signed receipts (Seals), countersigned and timestamped, offline by default | Released | Unit tests, real TSA tokens | `openadapt-flow seal`, `countersign`, `local-seal` |
| Offline Seals signed with a key the customer holds: a key file, the OS keychain, or a PKCS#11 token or HSM; pinned keys with rotation and revocation; no fallback to a generated key | On main ([#1392](https://github.com/OpenAdaptAI/OpenAdapt/pull/1392)) | Unit tests with synthetic data (keychain and PKCS#11 through stand-ins) | `openadapt-flow local-seal --key`, `local-seal key create`, `pin`, `revoke`; `openadapt seal verify FOLDER` checks every sealed run in a folder; [OFFLINE_SEALS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/OFFLINE_SEALS.md#customer-held-keys) |
Expand Down
15 changes: 11 additions & 4 deletions packages/flow/docs/phi_at_rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,11 @@ takes tens to hundreds of milliseconds, so encryption adds about 1% or less.

`record` (including `--with-check`), `compile`, `induce`,
`scaffold-verifier`, `seal`, `certify`, `qualify`, `admit`, `run`, `replay`,
`approve`, `resume`, `undo`, `visualize`, `explain`, `emit-skill`,
`for-each`, `sections` and `check mark` read sealed bundles, run directories
and encrypted recordings in memory, given the key, and keep what they write
inside a sealed set sealed. Each reads artifact files through one reader,
`approve`, `resume`, `undo`, `visualize` (including a process contract),
`explain`, `emit-skill`, `for-each`, `sections`, `check mark`, `compose`,
`process`, `variants` and `route` read sealed bundles, run directories and
encrypted recordings in memory, given the key, and keep what they write inside
a sealed set sealed. Each reads artifact files through one reader,
`openadapt_flow.artifacts`:

- `compile` of an encrypted recording writes a sealed bundle, under
Expand Down Expand Up @@ -254,6 +255,12 @@ inside a sealed set sealed. Each reads artifact files through one reader,
mode refuses them before they run.
- `sections` and `check mark` write `sections.json` and `check.json` back
into the sealed bundle or recording, encrypted.
- `compose` copies sealed child bundles into the composition as ciphertext.
`process`, `variants` and `route` read sealed child bundles and their
admission envelopes in place. The parent file each writes
(`composition.json`, `process-contract.json`, `variant-set.json`) holds
names, digests and pointers; by default it is named on stderr and in the
ledger, and strict mode refuses the command before it runs.

Without the key, each of these refuses before it reads anything and names the
variable to set. Any other command given a sealed path refuses with a message
Expand Down
8 changes: 7 additions & 1 deletion packages/flow/openadapt_flow/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13124,6 +13124,11 @@ def _qualify_default_form(argv: list[str]) -> list[str]:
"openadapt_flow.__main__:_cmd_explain",
"openadapt_flow.__main__:_cmd_emit_skill",
"openadapt_flow.__main__:_cmd_for_each",
"openadapt_flow.__main__:_cmd_visualize_process",
"openadapt_flow.__main__:_cmd_compose",
"openadapt_flow.__main__:_cmd_process",
"openadapt_flow.__main__:_cmd_variants",
"openadapt_flow.__main__:_cmd_route",
"openadapt_flow.cli_sections:cmd_sections",
"openadapt_flow.cli_check:cmd_check_mark",
"openadapt_flow.cli_admit:cmd_admit_run",
Expand Down Expand Up @@ -13352,7 +13357,8 @@ def _sealed_input_refusal(args: argparse.Namespace) -> Optional[str]:
"command. Commands that read sealed input: record, compile, "
"induce, scaffold-verifier, seal, certify, qualify, admit, run, "
"replay, approve, resume, undo, visualize, explain, emit-skill, "
"for-each, sections and check mark."
"for-each, sections, check mark, compose, process, variants and "
"route."
)
for root in sealed:
kind = sealed_set.header_kind(root)
Expand Down
25 changes: 16 additions & 9 deletions packages/flow/openadapt_flow/admitted_composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,14 +359,16 @@ def save(self, parent_dir: Path | str) -> Path:

@classmethod
def load(cls, parent_dir: Path | str) -> "ProcessContract":
from openadapt_flow import artifacts

parent = Path(parent_dir)
path = parent / PROCESS_CONTRACT_FILENAME
if not path.is_file():
if not artifacts.is_file(path):
raise ProcessContractError(
f"{parent} is not a process-contract artifact "
f"(no {PROCESS_CONTRACT_FILENAME})"
)
return cls.model_validate_json(path.read_text(encoding="utf-8"))
return cls.model_validate_json(artifacts.read_text(path))


class ProcessContractV1(ProcessContract):
Expand Down Expand Up @@ -416,16 +418,18 @@ def load_child_envelope(
) -> QualificationAdmissionEnvelope:
"""Load the child's v1 admission envelope from its pointer."""

from openadapt_flow import artifacts

path = resolve_pointer(parent_dir, child.envelope)
if not path.is_file():
if not artifacts.is_file(path):
raise ProcessContractError(
f"child {child.name!r} has no QualificationAdmissionEnvelope at "
f"{path}; a compiled recording (including a compose child under "
"composition.json) is not an independently admitted capability"
)
try:
return QualificationAdmissionEnvelope.model_validate_json(
path.read_text(encoding="utf-8")
artifacts.read_text(path)
)
except Exception as exc:
raise ProcessContractError(
Expand Down Expand Up @@ -500,9 +504,10 @@ def _pointer_for(src: Path, parent: Path) -> str:


def _load_admitted_bundle(name: str, bundle: Path) -> Workflow:
if (
not (bundle / "workflow.json").is_file()
and not (bundle / "workflow.json.enc").is_file()
from openadapt_flow import artifacts

if not artifacts.is_file(bundle / "workflow.json") and not artifacts.is_file(
bundle / "workflow.json.enc"
):
raise ProcessContractError(
f"child {name!r} at {bundle} is not an admitted workflow bundle"
Expand Down Expand Up @@ -531,6 +536,8 @@ def author_process_contract(
The parent does not copy recordings. Default sequencing is that order.
"""

from openadapt_flow import artifacts

if len(children) < 2:
raise ProcessContractError(
"process needs at least two independently admitted children"
Expand Down Expand Up @@ -560,7 +567,7 @@ def author_process_contract(
for child_name, envelope_src, bundle_src in children:
envelope_path = Path(envelope_src)
bundle_path = Path(bundle_src)
if not envelope_path.is_file():
if not artifacts.is_file(envelope_path):
errors.append(
f"child {child_name!r} has no QualificationAdmissionEnvelope at "
f"{envelope_path}; a compiled recording (including a compose "
Expand All @@ -570,7 +577,7 @@ def author_process_contract(
continue
try:
envelope = QualificationAdmissionEnvelope.model_validate_json(
envelope_path.read_text(encoding="utf-8")
artifacts.read_text(envelope_path)
)
except Exception as exc:
errors.append(
Expand Down
6 changes: 3 additions & 3 deletions packages/flow/openadapt_flow/compiler/compose_authoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pathlib import Path
from typing import Mapping, Optional, Sequence

from openadapt_flow import artifacts
from openadapt_flow.composition import (
CHILDREN_DIR,
TERMINAL_OUTCOMES,
Expand Down Expand Up @@ -123,9 +124,8 @@ def author_composition(

for child_name, src in children:
src_path = Path(src)
if (
not (src_path / "workflow.json").is_file()
and not (src_path / "workflow.json.enc").is_file()
if not artifacts.is_file(src_path / "workflow.json") and not artifacts.is_file(
src_path / "workflow.json.enc"
):
errors.append(
f"child {child_name!r} at {src_path} is not a workflow bundle"
Expand Down
6 changes: 4 additions & 2 deletions packages/flow/openadapt_flow/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,15 @@ def save(self, parent_dir: Path | str) -> Path:

@classmethod
def load(cls, parent_dir: Path | str) -> "Composition":
from openadapt_flow import artifacts

parent = Path(parent_dir)
path = parent / COMPOSITION_FILENAME
if not path.is_file():
if not artifacts.is_file(path):
raise CompositionError(
f"{parent} is not a composition artifact (no {COMPOSITION_FILENAME})"
)
return cls.model_validate_json(path.read_text(encoding="utf-8"))
return cls.model_validate_json(artifacts.read_text(path))


def is_composition_artifact(path: Path | str) -> bool:
Expand Down
26 changes: 17 additions & 9 deletions packages/flow/openadapt_flow/variants.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,22 +257,26 @@ def save(self, parent_dir: Path | str) -> Path:

@classmethod
def load(cls, parent_dir: Path | str) -> "VariantSet":
from openadapt_flow import artifacts

parent = Path(parent_dir)
path = parent / VARIANT_SET_FILENAME
if not path.is_file():
if not artifacts.is_file(path):
raise VariantSetError(
f"{parent} is not a variant-set artifact (no {VARIANT_SET_FILENAME})"
)
try:
return cls.model_validate_json(path.read_text(encoding="utf-8"))
return cls.model_validate_json(artifacts.read_text(path))
except ValueError as exc:
raise VariantSetError(f"variant set at {path} is invalid: {exc}") from exc


def is_variant_set_artifact(path: Path | str) -> bool:
"""True when ``path`` is a variant-set directory."""

return (Path(path) / VARIANT_SET_FILENAME).is_file()
from openadapt_flow import artifacts

return artifacts.is_file(Path(path) / VARIANT_SET_FILENAME)


class VariantInput(BaseModel):
Expand All @@ -290,14 +294,16 @@ class VariantInput(BaseModel):
def _admitted_spec(item: VariantInput, out_dir: Path) -> VariantSpec:
"""Build a variant spec, refusing anything that is not admitted as is."""

if not item.admission.is_file():
from openadapt_flow import artifacts

if not artifacts.is_file(item.admission):
raise VariantSetError(
f"variant {item.name!r} has no QualificationAdmissionEnvelope at "
f"{item.admission}; each variant keeps its own admission"
)
try:
envelope = QualificationAdmissionEnvelope.model_validate_json(
item.admission.read_text(encoding="utf-8")
artifacts.read_text(item.admission)
)
except Exception as exc:
raise VariantSetError(
Expand Down Expand Up @@ -340,13 +346,14 @@ def _admitted_spec(item: VariantInput, out_dir: Path) -> VariantSpec:
def _repair_candidate(bundle: Path) -> tuple[Optional[str], Optional[str]]:
"""Read the detached repair-candidate record a healed bundle carries."""

from openadapt_flow import artifacts
from openadapt_flow.repair.registration import detached_candidate_path

path = detached_candidate_path(bundle)
if not path.is_file():
if not artifacts.is_file(path):
return None, None
try:
record = json.loads(path.read_text(encoding="utf-8"))
record = json.loads(artifacts.read_text(path))
except (OSError, ValueError) as exc:
raise VariantSetError(
f"repair candidate record at {path} is unreadable"
Expand Down Expand Up @@ -415,16 +422,17 @@ def add_variant(parent_dir: Path | str, item: VariantInput) -> VariantSet:
def _check_lineage(bundle: Path, base: VariantSpec, spec: VariantSpec) -> None:
"""A promoted variant must carry the repair candidate that produced it."""

from openadapt_flow import artifacts
from openadapt_flow.repair.registration import detached_candidate_path

path = detached_candidate_path(bundle)
if not path.is_file():
if not artifacts.is_file(path):
raise VariantSetError(
f"--derived-from needs the repair candidate record the heal or teach "
f"wrote ({path}); without it the lineage is unproven"
)
try:
record = json.loads(path.read_text(encoding="utf-8"))
record = json.loads(artifacts.read_text(path))
except (OSError, ValueError) as exc:
raise VariantSetError(
f"repair candidate record at {path} is unreadable"
Expand Down
Loading
Loading