messagefoundry 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- messagefoundry/__init__.py +108 -0
- messagefoundry/__main__.py +1155 -0
- messagefoundry/api/__init__.py +27 -0
- messagefoundry/api/app.py +1581 -0
- messagefoundry/api/approvals.py +184 -0
- messagefoundry/api/auth_models.py +211 -0
- messagefoundry/api/auth_routes.py +655 -0
- messagefoundry/api/field_authz.py +96 -0
- messagefoundry/api/models.py +374 -0
- messagefoundry/api/security.py +247 -0
- messagefoundry/api/tls.py +47 -0
- messagefoundry/auth/__init__.py +39 -0
- messagefoundry/auth/data/common_passwords.NOTICE +13 -0
- messagefoundry/auth/data/common_passwords.txt +10000 -0
- messagefoundry/auth/identity.py +71 -0
- messagefoundry/auth/ldap.py +264 -0
- messagefoundry/auth/notifications.py +68 -0
- messagefoundry/auth/passwords.py +53 -0
- messagefoundry/auth/permissions.py +120 -0
- messagefoundry/auth/policy.py +153 -0
- messagefoundry/auth/ratelimit.py +55 -0
- messagefoundry/auth/service.py +1323 -0
- messagefoundry/auth/tokens.py +26 -0
- messagefoundry/auth/totp.py +174 -0
- messagefoundry/checks.py +174 -0
- messagefoundry/config/__init__.py +30 -0
- messagefoundry/config/active_environment.py +80 -0
- messagefoundry/config/ai_policy.py +140 -0
- messagefoundry/config/code_sets.py +260 -0
- messagefoundry/config/connections_edit.py +200 -0
- messagefoundry/config/connections_file.py +287 -0
- messagefoundry/config/db_lookup.py +117 -0
- messagefoundry/config/environments.py +116 -0
- messagefoundry/config/ingest_time.py +83 -0
- messagefoundry/config/models.py +240 -0
- messagefoundry/config/reference.py +158 -0
- messagefoundry/config/response.py +83 -0
- messagefoundry/config/run_context.py +153 -0
- messagefoundry/config/settings.py +1311 -0
- messagefoundry/config/state.py +99 -0
- messagefoundry/config/tls_policy.py +110 -0
- messagefoundry/config/wiring.py +1918 -0
- messagefoundry/console/__init__.py +20 -0
- messagefoundry/console/__main__.py +274 -0
- messagefoundry/console/_async.py +107 -0
- messagefoundry/console/change_password.py +111 -0
- messagefoundry/console/client.py +552 -0
- messagefoundry/console/connections.py +324 -0
- messagefoundry/console/login.py +107 -0
- messagefoundry/console/mfa.py +205 -0
- messagefoundry/console/reauth.py +94 -0
- messagefoundry/console/search.py +57 -0
- messagefoundry/console/service_control.py +137 -0
- messagefoundry/console/sessions.py +122 -0
- messagefoundry/console/shell.py +410 -0
- messagefoundry/console/status.py +377 -0
- messagefoundry/console/users_page.py +282 -0
- messagefoundry/console/widgets.py +553 -0
- messagefoundry/generators/README.md +27 -0
- messagefoundry/generators/__init__.py +15 -0
- messagefoundry/generators/_core.py +589 -0
- messagefoundry/generators/_hl7data.py +428 -0
- messagefoundry/generators/adt.py +286 -0
- messagefoundry/generators/all_types.py +24 -0
- messagefoundry/generators/bar.py +28 -0
- messagefoundry/generators/dft.py +20 -0
- messagefoundry/generators/mdm.py +39 -0
- messagefoundry/generators/mfn.py +46 -0
- messagefoundry/generators/oml.py +32 -0
- messagefoundry/generators/orl.py +30 -0
- messagefoundry/generators/orm.py +23 -0
- messagefoundry/generators/oru.py +21 -0
- messagefoundry/generators/ras.py +20 -0
- messagefoundry/generators/rde.py +54 -0
- messagefoundry/generators/siu.py +64 -0
- messagefoundry/generators/vxu.py +20 -0
- messagefoundry/hl7schema.py +75 -0
- messagefoundry/last_resort.py +55 -0
- messagefoundry/logging_setup.py +332 -0
- messagefoundry/parsing/__init__.py +64 -0
- messagefoundry/parsing/consistency.py +166 -0
- messagefoundry/parsing/groups.py +228 -0
- messagefoundry/parsing/message.py +453 -0
- messagefoundry/parsing/peek.py +237 -0
- messagefoundry/parsing/split.py +120 -0
- messagefoundry/parsing/summary.py +46 -0
- messagefoundry/parsing/tree.py +128 -0
- messagefoundry/parsing/validate.py +95 -0
- messagefoundry/parsing/x12/__init__.py +46 -0
- messagefoundry/parsing/x12/delimiters.py +140 -0
- messagefoundry/parsing/x12/errors.py +30 -0
- messagefoundry/parsing/x12/interchange.py +232 -0
- messagefoundry/parsing/x12/message.py +200 -0
- messagefoundry/parsing/x12/peek.py +207 -0
- messagefoundry/pipeline/__init__.py +21 -0
- messagefoundry/pipeline/alert_sinks.py +486 -0
- messagefoundry/pipeline/alerts.py +100 -0
- messagefoundry/pipeline/cert_expiry.py +219 -0
- messagefoundry/pipeline/cluster.py +955 -0
- messagefoundry/pipeline/cluster_sqlserver.py +444 -0
- messagefoundry/pipeline/config_convergence.py +137 -0
- messagefoundry/pipeline/dryrun.py +450 -0
- messagefoundry/pipeline/engine.py +756 -0
- messagefoundry/pipeline/leader_tasks.py +158 -0
- messagefoundry/pipeline/reference_sync.py +369 -0
- messagefoundry/pipeline/retention.py +289 -0
- messagefoundry/pipeline/security_notify.py +168 -0
- messagefoundry/pipeline/state_convergence.py +143 -0
- messagefoundry/pipeline/wiring_runner.py +1722 -0
- messagefoundry/py.typed +0 -0
- messagefoundry/redaction.py +71 -0
- messagefoundry/scaffold.py +321 -0
- messagefoundry/secrets_dpapi.py +129 -0
- messagefoundry/store/__init__.py +46 -0
- messagefoundry/store/audit_tee.py +67 -0
- messagefoundry/store/base.py +758 -0
- messagefoundry/store/crypto.py +166 -0
- messagefoundry/store/keyprovider.py +192 -0
- messagefoundry/store/postgres.py +3447 -0
- messagefoundry/store/sqlserver.py +3014 -0
- messagefoundry/store/store.py +3790 -0
- messagefoundry/timezone.py +207 -0
- messagefoundry/transports/__init__.py +50 -0
- messagefoundry/transports/base.py +269 -0
- messagefoundry/transports/database.py +693 -0
- messagefoundry/transports/file.py +551 -0
- messagefoundry/transports/framing.py +164 -0
- messagefoundry/transports/loopback.py +53 -0
- messagefoundry/transports/mllp.py +644 -0
- messagefoundry/transports/remotefile.py +664 -0
- messagefoundry/transports/rest.py +281 -0
- messagefoundry/transports/signing.py +321 -0
- messagefoundry/transports/soap.py +507 -0
- messagefoundry/transports/tcp.py +307 -0
- messagefoundry/transports/timer.py +146 -0
- messagefoundry/transports/x12.py +323 -0
- messagefoundry-0.1.0.dist-info/METADATA +212 -0
- messagefoundry-0.1.0.dist-info/RECORD +142 -0
- messagefoundry-0.1.0.dist-info/WHEEL +4 -0
- messagefoundry-0.1.0.dist-info/entry_points.txt +2 -0
- messagefoundry-0.1.0.dist-info/licenses/LICENSE +662 -0
- messagefoundry-0.1.0.dist-info/licenses/NOTICE +27 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Centralized field-level (property) authorization for API responses (WP-9; ASVS 8.1.2 / 8.2.3).
|
|
4
|
+
|
|
5
|
+
Some response properties carry PHI — the patient-identifying ``summary``, and exception text
|
|
6
|
+
(``error`` / ``last_error``) that can quote field values — and must be withheld from a caller who may
|
|
7
|
+
see the rest of the object but lacks the unlocking permission. This module is the **single declarative
|
|
8
|
+
place** that maps each PHI-bearing property to the :class:`~messagefoundry.auth.Permission` that
|
|
9
|
+
unlocks it, plus the one helper that enforces it. Centralizing it means the policy lives in one
|
|
10
|
+
auditable spot instead of being re-implemented inline per endpoint, where a new endpoint or field could
|
|
11
|
+
silently leak PHI (the Broken Object Property Level Authorization risk, ASVS 8.2.3).
|
|
12
|
+
|
|
13
|
+
**Read-side only.** The API exposes no client-writable PHI properties — mutations are coarse, separately
|
|
14
|
+
permission-gated actions (replay / purge / reload / connection-control) — so there is no per-field
|
|
15
|
+
*write* authorization surface today. See docs/SECURITY.md "Field-level authorization" for the model and
|
|
16
|
+
the trigger that would add one.
|
|
17
|
+
|
|
18
|
+
The full message **body** (``MessageDetail.raw``) is governed separately, at the endpoint, by the
|
|
19
|
+
coarser whole-body ``messages:view_raw`` gate — not by this per-property map.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections.abc import Sequence
|
|
25
|
+
from typing import TypeVar
|
|
26
|
+
|
|
27
|
+
from pydantic import BaseModel
|
|
28
|
+
|
|
29
|
+
from messagefoundry.api.models import (
|
|
30
|
+
CapturedResponseInfo,
|
|
31
|
+
DeadLetterRow,
|
|
32
|
+
EventInfo,
|
|
33
|
+
MessageDetail,
|
|
34
|
+
MessageSummary,
|
|
35
|
+
OutboxInfo,
|
|
36
|
+
)
|
|
37
|
+
from messagefoundry.auth import Identity, Permission
|
|
38
|
+
|
|
39
|
+
#: Response model → {property → Permission that unlocks it}. The single source of truth for which
|
|
40
|
+
#: response properties are PHI-gated and by which permission; :func:`redact_unauthorized` nulls a
|
|
41
|
+
#: property when the caller lacks its permission. Both summary-tier PHI fields gate on
|
|
42
|
+
#: ``messages:view_summary`` today (the body, gated by ``messages:view_raw``, is handled at the
|
|
43
|
+
#: endpoint). Add a row here when a new PHI-bearing response property is introduced.
|
|
44
|
+
PHI_FIELDS: dict[type[BaseModel], dict[str, Permission]] = {
|
|
45
|
+
MessageSummary: {
|
|
46
|
+
"summary": Permission.MESSAGES_VIEW_SUMMARY,
|
|
47
|
+
"error": Permission.MESSAGES_VIEW_SUMMARY,
|
|
48
|
+
},
|
|
49
|
+
DeadLetterRow: {
|
|
50
|
+
"summary": Permission.MESSAGES_VIEW_SUMMARY,
|
|
51
|
+
"last_error": Permission.MESSAGES_VIEW_SUMMARY,
|
|
52
|
+
},
|
|
53
|
+
# The single-message detail view and its nested rows (#120). Redaction keys on the EXACT type
|
|
54
|
+
# (no MRO walk), so MessageDetail must be declared explicitly even though it subclasses
|
|
55
|
+
# MessageSummary — otherwise its inherited PHI ``summary``/``error`` would be returned un-gated.
|
|
56
|
+
# Gated on view_summary (NOT view_raw) so the same logical fields (error / last_error / detail)
|
|
57
|
+
# sit on one tier across the list and detail surfaces; the detail route already requires view_raw,
|
|
58
|
+
# so a view_raw gate here would be dead code. The raw body stays on the route's view_raw gate.
|
|
59
|
+
MessageDetail: {
|
|
60
|
+
"summary": Permission.MESSAGES_VIEW_SUMMARY,
|
|
61
|
+
"error": Permission.MESSAGES_VIEW_SUMMARY,
|
|
62
|
+
},
|
|
63
|
+
OutboxInfo: {
|
|
64
|
+
"last_error": Permission.MESSAGES_VIEW_SUMMARY,
|
|
65
|
+
},
|
|
66
|
+
EventInfo: {
|
|
67
|
+
"detail": Permission.MESSAGES_VIEW_SUMMARY,
|
|
68
|
+
},
|
|
69
|
+
CapturedResponseInfo: {
|
|
70
|
+
"detail": Permission.MESSAGES_VIEW_SUMMARY,
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
M = TypeVar("M", bound=BaseModel)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def gated_properties(model_cls: type[BaseModel]) -> dict[str, Permission]:
|
|
78
|
+
"""The PHI property→permission map declared for ``model_cls`` (empty if it has none)."""
|
|
79
|
+
return PHI_FIELDS.get(model_cls, {})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def redact_unauthorized(model: M, identity: Identity) -> M:
|
|
83
|
+
"""Return ``model`` with each PHI property the caller may **not** see set to ``None`` — a no-op
|
|
84
|
+
when the caller holds every relevant permission. The single per-property read gate (ASVS 8.2.3)."""
|
|
85
|
+
withheld = {
|
|
86
|
+
prop: None for prop, perm in gated_properties(type(model)).items() if not identity.has(perm)
|
|
87
|
+
}
|
|
88
|
+
return model.model_copy(update=withheld) if withheld else model
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def count_exposed(models: Sequence[BaseModel]) -> int:
|
|
92
|
+
"""How many ``models`` still carry a non-empty PHI property — call **after** redaction, so the
|
|
93
|
+
count reflects what is actually returned. Fed to the server-side PHI-exposure audit."""
|
|
94
|
+
return sum(
|
|
95
|
+
1 for m in models if any(getattr(m, prop, None) for prop in gated_properties(type(m)))
|
|
96
|
+
)
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Response schemas for the localhost API.
|
|
4
|
+
|
|
5
|
+
These are the wire contract the console (and any other client) sees — deliberately
|
|
6
|
+
separate from the internal SQLite rows and channel-config models so storage/runtime
|
|
7
|
+
changes don't leak into the API. Message *list* responses carry metadata only; the raw
|
|
8
|
+
body (PHI) appears only in the single-message detail view, which is audited.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from messagefoundry.config.ai_policy import AiDataScope, AiMode, DataClass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChannelInfo(BaseModel):
|
|
21
|
+
id: str
|
|
22
|
+
name: str
|
|
23
|
+
enabled: bool
|
|
24
|
+
running: bool
|
|
25
|
+
source_type: str
|
|
26
|
+
destinations: list[str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MessageSummary(BaseModel):
|
|
30
|
+
id: str
|
|
31
|
+
channel_id: str
|
|
32
|
+
received_at: float
|
|
33
|
+
source_type: str | None
|
|
34
|
+
control_id: str | None
|
|
35
|
+
message_type: str | None
|
|
36
|
+
status: str
|
|
37
|
+
error: str | None
|
|
38
|
+
event: str | None = None # latest processing event (received/delivered/failed/dead/replayed)
|
|
39
|
+
summary: str | None = None # ingest-derived: MRN/name (+ order/accession for ORM/ORU)
|
|
40
|
+
metadata: str | None = None # code/operator-attached values (mechanism TBD)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MessageList(BaseModel):
|
|
44
|
+
total: int
|
|
45
|
+
limit: int
|
|
46
|
+
offset: int
|
|
47
|
+
messages: list[MessageSummary]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class OutboxInfo(BaseModel):
|
|
51
|
+
id: str
|
|
52
|
+
destination_name: str
|
|
53
|
+
status: str
|
|
54
|
+
attempts: int
|
|
55
|
+
next_attempt_at: float
|
|
56
|
+
last_error: str | None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EventInfo(BaseModel):
|
|
60
|
+
ts: float
|
|
61
|
+
event: str
|
|
62
|
+
destination: str | None
|
|
63
|
+
detail: str | None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MessageDetail(MessageSummary):
|
|
67
|
+
"""Full single-message view, including the raw body and delivery/audit trail."""
|
|
68
|
+
|
|
69
|
+
raw: str
|
|
70
|
+
outbox: list[OutboxInfo]
|
|
71
|
+
events: list[EventInfo]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CapturedResponseInfo(BaseModel):
|
|
75
|
+
"""One captured request/response reply (ADR 0013). ``outcome``/``detail`` are visible with the
|
|
76
|
+
message-read permission; ``body`` is PHI and populated only when the caller also holds the raw-body
|
|
77
|
+
permission (``None`` otherwise, and ``None`` once retention has purged it)."""
|
|
78
|
+
|
|
79
|
+
destination_name: str
|
|
80
|
+
response_seq: int
|
|
81
|
+
outcome: str
|
|
82
|
+
detail: str | None
|
|
83
|
+
captured_at: float
|
|
84
|
+
body: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MessageResponses(BaseModel):
|
|
88
|
+
"""The captured-reply history for one message (ADR 0013), ordered by destination then seq."""
|
|
89
|
+
|
|
90
|
+
message_id: str
|
|
91
|
+
responses: list[CapturedResponseInfo]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OutboundPayloadInfo(BaseModel):
|
|
95
|
+
"""One outbound delivery's **transformed payload** (#14 parity tool). ``payload`` is the PHI body
|
|
96
|
+
MEFOR routed/transformed for ``destination_name``; it is returned in full only to a caller holding
|
|
97
|
+
``MESSAGES_VIEW_RAW``, and every access is audited. (Distinct from :class:`OutboxInfo`, which is
|
|
98
|
+
the body-free delivery *metadata* shown in the message-detail view.)"""
|
|
99
|
+
|
|
100
|
+
destination_name: str
|
|
101
|
+
status: str
|
|
102
|
+
payload: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class OutboundPayloads(BaseModel):
|
|
106
|
+
"""The transformed outbound payloads for one message — one entry per destination (#14). Populated
|
|
107
|
+
on both simulate/shadow and live runs (the transformed payload is retained on the done outbound
|
|
108
|
+
row in either mode), enabling the ``tee compare`` parity check against Corepoint's output."""
|
|
109
|
+
|
|
110
|
+
message_id: str
|
|
111
|
+
payloads: list[OutboundPayloadInfo]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ReplayResult(BaseModel):
|
|
115
|
+
message_id: str
|
|
116
|
+
requeued: int
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PurgeResult(BaseModel):
|
|
120
|
+
cancelled: int
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class DeadLetterRow(BaseModel):
|
|
124
|
+
"""One dead-lettered delivery (a message→destination that exhausted its retries)."""
|
|
125
|
+
|
|
126
|
+
outbox_id: str
|
|
127
|
+
message_id: str
|
|
128
|
+
channel_id: str
|
|
129
|
+
destination_name: str
|
|
130
|
+
attempts: int
|
|
131
|
+
last_error: str | None
|
|
132
|
+
failed_at: float # when the delivery was dead-lettered (outbox.updated_at)
|
|
133
|
+
control_id: str | None
|
|
134
|
+
message_type: str | None
|
|
135
|
+
received_at: float
|
|
136
|
+
summary: str | None = None # PHI-bearing (MRN/name); display is audited
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DeadLetterList(BaseModel):
|
|
140
|
+
total: int
|
|
141
|
+
limit: int
|
|
142
|
+
offset: int
|
|
143
|
+
dead_letters: list[DeadLetterRow]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class DeadLetterReplayRequest(BaseModel):
|
|
147
|
+
# Connection names; bounded so an over-long value can't reach the store query (ASVS 1.3.3).
|
|
148
|
+
channel_id: str | None = Field(None, max_length=256) # scope replay to one inbound (None = all)
|
|
149
|
+
destination_name: str | None = Field(None, max_length=256) # scope to one outbound (None = all)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class DeadLetterReplayResult(BaseModel):
|
|
153
|
+
requeued: int
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class PendingApprovalResponse(BaseModel):
|
|
157
|
+
"""Returned (HTTP 202) when a high-value action is held for dual-control approval (ASVS 2.3.5)
|
|
158
|
+
instead of executing inline. A distinct second approver must release it via ``/approvals``."""
|
|
159
|
+
|
|
160
|
+
approval_id: str
|
|
161
|
+
operation: str
|
|
162
|
+
status: str = "pending_approval"
|
|
163
|
+
detail: str
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class PendingApprovalInfo(BaseModel):
|
|
167
|
+
"""One open (still-pending, unexpired) approval request in the approver's queue."""
|
|
168
|
+
|
|
169
|
+
id: str
|
|
170
|
+
operation: str
|
|
171
|
+
label: str
|
|
172
|
+
requester: str
|
|
173
|
+
requested_at: float
|
|
174
|
+
expires_at: float | None = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ApprovalList(BaseModel):
|
|
178
|
+
approvals: list[PendingApprovalInfo]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ApprovalDecisionResult(BaseModel):
|
|
182
|
+
"""The outcome of approving or rejecting a pending request. On approval, ``result`` carries the
|
|
183
|
+
executed operation's summary (e.g. ``{"requeued": 3}``)."""
|
|
184
|
+
|
|
185
|
+
operation: str
|
|
186
|
+
requested_by: str
|
|
187
|
+
approved_by: str | None = None
|
|
188
|
+
rejected_by: str | None = None
|
|
189
|
+
result: dict[str, Any] | None = None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ReloadRequest(BaseModel):
|
|
193
|
+
# Directory of code-first config modules to load + apply. Optional: omitted/None reloads the
|
|
194
|
+
# server's startup --config dir. Any value must resolve within an allowed reload root (the
|
|
195
|
+
# startup dir or [api].config_reload_roots) — the loader executes Python from it. Length-bounded
|
|
196
|
+
# (ASVS 1.3.3); the allow-list confinement remains the real control.
|
|
197
|
+
config_dir: str | None = Field(None, max_length=4096)
|
|
198
|
+
# dry_run: validate the graph against THIS environment (loads + build-checks connectors, which
|
|
199
|
+
# resolves env() values for the target) and report the result WITHOUT swapping the live graph.
|
|
200
|
+
# The promote pre-flight: catch a missing env value / bad spec before it goes live.
|
|
201
|
+
dry_run: bool = False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ReloadResult(BaseModel):
|
|
205
|
+
"""Summary of the graph that is now live after a reload — or, for a dry run, the graph that
|
|
206
|
+
*would* go live (``dry_run=True``; ``running`` then reflects the still-current graph)."""
|
|
207
|
+
|
|
208
|
+
inbound: int
|
|
209
|
+
outbound: int
|
|
210
|
+
routers: int
|
|
211
|
+
handlers: int
|
|
212
|
+
running: bool
|
|
213
|
+
dry_run: bool = False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ConnectionRow(BaseModel):
|
|
217
|
+
"""One endpoint (a channel's source, or one of its destinations) for the connections
|
|
218
|
+
dashboard. Fields are role-dependent: source rows carry read/inbound-errored/idle and the
|
|
219
|
+
listen peer/port; destination rows carry queue/written/dead/backlog/delivered-age and the
|
|
220
|
+
remote peer/port. Unused fields are None so the UI can render blanks."""
|
|
221
|
+
|
|
222
|
+
role: str # "source" | "destination"
|
|
223
|
+
channel_id: str
|
|
224
|
+
channel_name: str
|
|
225
|
+
destination: str | None # destination name; None for the source row
|
|
226
|
+
name: str # display name
|
|
227
|
+
status: str # "running" | "stopped"
|
|
228
|
+
direction: str # "in" (source) | "out" (destination)
|
|
229
|
+
method: str # connection method/protocol, e.g. MLLP / File / TCP / REST
|
|
230
|
+
peer: str | None # MLLP host or file directory
|
|
231
|
+
port: int | None
|
|
232
|
+
queue_depth: int | None
|
|
233
|
+
idle_seconds: float | None
|
|
234
|
+
alerts_active: int # stubbed 0 until the alerts feature exists
|
|
235
|
+
errored: int | None # source: inbound errors; destination: dead-lettered
|
|
236
|
+
read: int | None # source only: inbound received
|
|
237
|
+
written: int | None # destination only: delivered
|
|
238
|
+
backlog_seconds: float | None # destination only; None = unknown/stalled
|
|
239
|
+
delivered_age_seconds: float | None # destination only; age of oldest queued item
|
|
240
|
+
simulated: bool | None = None # destination only; True = egress-suppressed shadow lane (#15)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class StatsResponse(BaseModel):
|
|
244
|
+
outbox_by_status: dict[str, int]
|
|
245
|
+
# NOT-DONE rows (pending|inflight) across every stage (ingress + routed + outbound) — a
|
|
246
|
+
# whole-pipeline drain gauge, vs outbox_by_status which sees only the outbound stage. Defaults to 0
|
|
247
|
+
# so a client reading an older engine (no field) degrades gracefully.
|
|
248
|
+
in_pipeline: int = 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class Health(BaseModel):
|
|
252
|
+
status: str = "ok"
|
|
253
|
+
# WP-L3-07 (ASVS 13.4.6): the build version is a fingerprinting detail, disclosed only to an
|
|
254
|
+
# authenticated caller. A tokenless liveness probe gets ``status`` with ``version`` omitted/None.
|
|
255
|
+
version: str | None = None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class EngineInfo(BaseModel):
|
|
259
|
+
version: str
|
|
260
|
+
uptime_seconds: float
|
|
261
|
+
pid: int
|
|
262
|
+
channels_total: int
|
|
263
|
+
channels_running: int
|
|
264
|
+
channels_stopped: int
|
|
265
|
+
outbox_by_status: dict[str, int]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class DbInfo(BaseModel):
|
|
269
|
+
path: str
|
|
270
|
+
size_bytes: int # db file + -wal + -shm
|
|
271
|
+
disk_free_bytes: int
|
|
272
|
+
journal_mode: str
|
|
273
|
+
messages: int
|
|
274
|
+
events: int
|
|
275
|
+
audit: int
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class SystemStatus(BaseModel):
|
|
279
|
+
engine: EngineInfo
|
|
280
|
+
db: DbInfo
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class IntegrityResult(BaseModel):
|
|
284
|
+
ok: bool
|
|
285
|
+
detail: str
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ClusterStatus(BaseModel):
|
|
289
|
+
"""This node's cluster posture (Track B Step 7), from the cheap in-memory coordinator gates — no DB
|
|
290
|
+
round-trip. ``clustered`` is False on a single node (NullCoordinator), where ``is_leader`` is always
|
|
291
|
+
True and ``config_version`` is 0. ``role`` (Workstream A5) is the operator-facing active-passive
|
|
292
|
+
role: ``"single-node"`` when not clustered, else ``"primary"`` when this node is the leader (it runs
|
|
293
|
+
the graph) or ``"standby"`` when it is a warm follower (no listeners bound, no workers running)."""
|
|
294
|
+
|
|
295
|
+
node_id: str
|
|
296
|
+
clustered: bool
|
|
297
|
+
is_leader: bool
|
|
298
|
+
role: str
|
|
299
|
+
config_version: int
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ClusterNode(BaseModel):
|
|
303
|
+
"""One node in the cluster (Track B Step 7). ``is_leader`` is the DERIVED live leader (the durable
|
|
304
|
+
``nodes.is_leader`` heartbeat flag filtered for freshness, so a crashed ex-leader's stale flag is not
|
|
305
|
+
reported). ``started_at``/``last_seen`` are epoch seconds, ``None`` only for the single-node
|
|
306
|
+
synthetic self-entry."""
|
|
307
|
+
|
|
308
|
+
node_id: str
|
|
309
|
+
host: str | None
|
|
310
|
+
pid: int | None
|
|
311
|
+
status: str
|
|
312
|
+
started_at: float | None
|
|
313
|
+
last_seen: float | None
|
|
314
|
+
is_leader: bool
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class ClusterNodeList(BaseModel):
|
|
318
|
+
"""Cluster membership (Track B Step 7). ``leader_node_id`` is the node_id of the single derived
|
|
319
|
+
leader (from the ``nodes.is_leader`` heartbeat flag), or ``None`` if no fresh node currently holds
|
|
320
|
+
it. ``lease_owner`` / ``lease_expires_at`` (Workstream A5) are the **authoritative** leadership-lease
|
|
321
|
+
state — who holds the self-fencing lease and the DB-clock epoch at which it expires (when a standby
|
|
322
|
+
could acquire if the leader stops renewing). ``lease_owner`` normally equals ``leader_node_id``; a
|
|
323
|
+
brief divergence during failover is expected (the lease is the source of truth). ``lease_expires_at``
|
|
324
|
+
is ``None`` single-node (no lease)."""
|
|
325
|
+
|
|
326
|
+
nodes: list[ClusterNode]
|
|
327
|
+
leader_node_id: str | None
|
|
328
|
+
lease_owner: str | None
|
|
329
|
+
lease_expires_at: float | None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class AiPolicy(BaseModel):
|
|
333
|
+
"""The effective AI-assistance policy for the IDE gate. ``assist_permitted`` is the
|
|
334
|
+
identity-dependent bit: ``True``/``False`` when the caller's RBAC can be evaluated, ``None`` when
|
|
335
|
+
no/invalid token under enabled auth made it unknown (a tokenless read still gets mode/scope, so a
|
|
336
|
+
central ``off`` is honored)."""
|
|
337
|
+
|
|
338
|
+
mode: AiMode
|
|
339
|
+
data_scope: AiDataScope
|
|
340
|
+
environment: str | None # the free-form active-environment NAME (ADR 0017)
|
|
341
|
+
data_class: DataClass | None = None # PHI posture (synthetic|phi), if resolvable
|
|
342
|
+
production: bool | None = None # production-tier posture, if resolvable
|
|
343
|
+
assist_permitted: bool | None
|
|
344
|
+
reason: str | None = None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class ConnectionMetadata(BaseModel):
|
|
348
|
+
"""Static metadata for one connection (operability Tier 4). ``metadata`` is the operator's
|
|
349
|
+
free-form label table (owner / runbook / environment); ``settings`` is **secret-scrubbed**
|
|
350
|
+
(``env()`` refs shown as ``{"env": key}``, inline credentials redacted). No live probe — use
|
|
351
|
+
``POST /connections/{name}/test`` for reachability."""
|
|
352
|
+
|
|
353
|
+
name: str
|
|
354
|
+
direction: str # "in" (inbound) | "out" (outbound)
|
|
355
|
+
method: str # connector type, e.g. "mllp" / "file" / "rest"
|
|
356
|
+
running: bool
|
|
357
|
+
router: str | None = None # inbound only
|
|
358
|
+
metadata: dict[str, Any] | None = None # operator labels
|
|
359
|
+
settings: dict[str, Any] # secret-scrubbed view
|
|
360
|
+
simulated: bool | None = None # outbound only; True = egress-suppressed shadow lane (#15)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class ConnectionTestResult(BaseModel):
|
|
364
|
+
"""Result of ``POST /connections/{name}/test`` — a reachability probe that sends no real payload.
|
|
365
|
+
``supported`` is False when the connector has nothing external to probe (a bound listen source, a
|
|
366
|
+
timer); ``success`` is the reachability outcome; ``detail`` carries the failure / not-supported
|
|
367
|
+
reason."""
|
|
368
|
+
|
|
369
|
+
name: str
|
|
370
|
+
direction: str # "in" | "out"
|
|
371
|
+
supported: bool
|
|
372
|
+
success: bool
|
|
373
|
+
duration_ms: float
|
|
374
|
+
detail: str | None = None
|