lorebase-keeper 0.2.3__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.
- lorebase_keeper/__init__.py +1 -0
- lorebase_keeper/__main__.py +456 -0
- lorebase_keeper/agents/__init__.py +1 -0
- lorebase_keeper/agents/budget.py +247 -0
- lorebase_keeper/agents/context.py +529 -0
- lorebase_keeper/agents/explain.py +582 -0
- lorebase_keeper/agents/feed.py +259 -0
- lorebase_keeper/agents/markdown.py +219 -0
- lorebase_keeper/agents/pack.py +403 -0
- lorebase_keeper/agents/settings.py +111 -0
- lorebase_keeper/app.py +373 -0
- lorebase_keeper/auth/__init__.py +4 -0
- lorebase_keeper/auth/api_keys.py +127 -0
- lorebase_keeper/auth/audit.py +356 -0
- lorebase_keeper/auth/cache.py +53 -0
- lorebase_keeper/auth/config.py +618 -0
- lorebase_keeper/auth/deps.py +183 -0
- lorebase_keeper/auth/keyring.py +14 -0
- lorebase_keeper/auth/oidc.py +242 -0
- lorebase_keeper/auth/permissions.py +142 -0
- lorebase_keeper/auth/ports.py +212 -0
- lorebase_keeper/auth/principals.py +377 -0
- lorebase_keeper/auth/provider.py +250 -0
- lorebase_keeper/auth/rbac.py +247 -0
- lorebase_keeper/auth/read_audit.py +357 -0
- lorebase_keeper/auth/runtime.py +113 -0
- lorebase_keeper/auth/sessions.py +452 -0
- lorebase_keeper/bootstrap.py +151 -0
- lorebase_keeper/client_address.py +129 -0
- lorebase_keeper/context.py +226 -0
- lorebase_keeper/db.py +93 -0
- lorebase_keeper/deps.py +47 -0
- lorebase_keeper/errors.py +257 -0
- lorebase_keeper/health.py +61 -0
- lorebase_keeper/kindhealth/__init__.py +6 -0
- lorebase_keeper/kindhealth/config.py +51 -0
- lorebase_keeper/kindhealth/markdown.py +163 -0
- lorebase_keeper/kindhealth/models.py +179 -0
- lorebase_keeper/kindhealth/report.py +154 -0
- lorebase_keeper/kindhealth/scan.py +512 -0
- lorebase_keeper/middleware.py +285 -0
- lorebase_keeper/pagination.py +87 -0
- lorebase_keeper/ratelimit/__init__.py +10 -0
- lorebase_keeper/ratelimit/bucket.py +118 -0
- lorebase_keeper/ratelimit/config.py +283 -0
- lorebase_keeper/ratelimit/enforce.py +393 -0
- lorebase_keeper/ratelimit/limiter.py +149 -0
- lorebase_keeper/ratelimit/quotas.py +204 -0
- lorebase_keeper/ratelimit/shared.py +242 -0
- lorebase_keeper/readiness.py +257 -0
- lorebase_keeper/reading/__init__.py +1 -0
- lorebase_keeper/reading/caching.py +251 -0
- lorebase_keeper/reading/compiler.py +678 -0
- lorebase_keeper/reading/cursors.py +56 -0
- lorebase_keeper/reading/diff.py +408 -0
- lorebase_keeper/reading/graph.py +268 -0
- lorebase_keeper/reading/history.py +152 -0
- lorebase_keeper/reading/points.py +410 -0
- lorebase_keeper/reading/problems.py +74 -0
- lorebase_keeper/reading/record_history.py +722 -0
- lorebase_keeper/reading/records.py +780 -0
- lorebase_keeper/reading/requests.py +107 -0
- lorebase_keeper/reading/runtime.py +145 -0
- lorebase_keeper/reading/search.py +708 -0
- lorebase_keeper/reading/sensitivity.py +882 -0
- lorebase_keeper/reading/settings.py +339 -0
- lorebase_keeper/reading/snapshots.py +148 -0
- lorebase_keeper/reading/tree.py +98 -0
- lorebase_keeper/reading/views.py +264 -0
- lorebase_keeper/reading/visibility.py +93 -0
- lorebase_keeper/routes/__init__.py +1 -0
- lorebase_keeper/routes/admin.py +597 -0
- lorebase_keeper/routes/agents.py +160 -0
- lorebase_keeper/routes/branches.py +438 -0
- lorebase_keeper/routes/derivations.py +215 -0
- lorebase_keeper/routes/documents.py +497 -0
- lorebase_keeper/routes/domain_api.py +59 -0
- lorebase_keeper/routes/domains.py +220 -0
- lorebase_keeper/routes/events.py +187 -0
- lorebase_keeper/routes/kind_health.py +166 -0
- lorebase_keeper/routes/operations.py +392 -0
- lorebase_keeper/routes/proposal_items.py +563 -0
- lorebase_keeper/routes/proposals.py +1107 -0
- lorebase_keeper/routes/read.py +493 -0
- lorebase_keeper/routes/revisions.py +523 -0
- lorebase_keeper/routes/schema.py +682 -0
- lorebase_keeper/routes/schema_inference.py +437 -0
- lorebase_keeper/routes/schema_suggestions.py +328 -0
- lorebase_keeper/routes/sessions.py +286 -0
- lorebase_keeper/routes/source_preview.py +232 -0
- lorebase_keeper/routes/sources.py +200 -0
- lorebase_keeper/routes/trace.py +506 -0
- lorebase_keeper/routes/vault.py +332 -0
- lorebase_keeper/routes/webhooks.py +298 -0
- lorebase_keeper/routes/whoami.py +88 -0
- lorebase_keeper/services/__init__.py +1 -0
- lorebase_keeper/services/agent_writes.py +1035 -0
- lorebase_keeper/services/branches.py +337 -0
- lorebase_keeper/services/config.py +431 -0
- lorebase_keeper/services/derivation_preview.py +279 -0
- lorebase_keeper/services/document_urls.py +114 -0
- lorebase_keeper/services/domains.py +118 -0
- lorebase_keeper/services/errors.py +196 -0
- lorebase_keeper/services/operation_catalog.py +806 -0
- lorebase_keeper/services/operation_events.py +199 -0
- lorebase_keeper/services/operations.py +1229 -0
- lorebase_keeper/services/pii_import.py +222 -0
- lorebase_keeper/services/proposal_reads.py +158 -0
- lorebase_keeper/services/proposals.py +850 -0
- lorebase_keeper/services/reads.py +286 -0
- lorebase_keeper/services/schema.py +601 -0
- lorebase_keeper/services/schema_inference.py +390 -0
- lorebase_keeper/services/schema_suggestions.py +418 -0
- lorebase_keeper/services/source_preview.py +127 -0
- lorebase_keeper/settings.py +313 -0
- lorebase_keeper/tracing/__init__.py +1 -0
- lorebase_keeper/tracing/capture.py +350 -0
- lorebase_keeper/tracing/config.py +157 -0
- lorebase_keeper/tracing/explain.py +97 -0
- lorebase_keeper/tracing/middleware.py +359 -0
- lorebase_keeper/tracing/notes.py +80 -0
- lorebase_keeper/tracing/recorder.py +180 -0
- lorebase_keeper-0.2.3.dist-info/METADATA +21 -0
- lorebase_keeper-0.2.3.dist-info/RECORD +126 -0
- lorebase_keeper-0.2.3.dist-info/WHEEL +4 -0
- lorebase_keeper-0.2.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Side effects of stored Operations (design 5.7, 10.5): outbox events of a committed execution.
|
|
2
|
+
|
|
3
|
+
An Operation names its side effects (`side_effects: [risk_flag_raised]`). A committed execution
|
|
4
|
+
writes one `operation.<name>` event per name to the outbox, in the transaction of its last
|
|
5
|
+
Revision, its invocation record and its audit entry: the events exist if and only if the
|
|
6
|
+
execution committed. An execution on a task branch whose Proposal is its review (agent writes)
|
|
7
|
+
defers its events instead (`lorebase_store.repositories.deferred_events`): the merge of the
|
|
8
|
+
Proposal writes them, once, in its transaction, naming the target branch, the merged head, the
|
|
9
|
+
source branch and the Proposal. Dry runs, refused executions and replays of an idempotency key
|
|
10
|
+
write none. Consumers of the outbox (the webhook worker) deliver them later.
|
|
11
|
+
|
|
12
|
+
The payload names the execution (Operation, invocation, principal, branch, Revisions, head),
|
|
13
|
+
carries the parameters as recorded in the invocation (`recorded_parameters`: values that may be
|
|
14
|
+
sensitive are `***` for every subscriber) and up to `side_effect_max_edits` edits. Values of
|
|
15
|
+
sensitive Attributes in the edits are wrapped with their effective tags
|
|
16
|
+
(`lorebase_store.outbox_events.sensitive_value`), and values of vault Attributes are their
|
|
17
|
+
vault references as committed, never the personal data: the webhook worker shows them only to
|
|
18
|
+
webhooks cleared for those tags. A parameter or value whose JSON holds `$sensitive` or `$vault`
|
|
19
|
+
keys and is not a committed vault reference is wrapped with every tag, so caller-written JSON
|
|
20
|
+
cannot pose as a wrapper or open a vault value for a webhook not cleared for everything.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections import defaultdict
|
|
26
|
+
from collections.abc import Mapping, Sequence
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
31
|
+
|
|
32
|
+
from lorebase_core.canonical import from_hex, hex
|
|
33
|
+
from lorebase_core.model import Attribute, Operation, Schema, Sensitivity, looks_like_reference
|
|
34
|
+
from lorebase_core.validate import DataChange, RecordUpsert
|
|
35
|
+
from lorebase_keeper.reading.sensitivity import Clearance, Masking
|
|
36
|
+
from lorebase_keeper.services.operation_catalog import edit_document
|
|
37
|
+
from lorebase_store.outbox_events import carries_markers, operation_event_type, sensitive_value
|
|
38
|
+
from lorebase_store.repositories import MaterializedRepository
|
|
39
|
+
from lorebase_store.repositories.deferred_events import DeferredEventRepository
|
|
40
|
+
from lorebase_store.repositories.events import OutboxRepository
|
|
41
|
+
from lorebase_store.telemetry import OPERATION_EVENTS
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class _Wrapping(Masking):
|
|
46
|
+
"""Keeps every value, wrapping each one that needs a clearance with its tags; a value of an
|
|
47
|
+
Attribute the schema does not know needs every clearance."""
|
|
48
|
+
|
|
49
|
+
def values(
|
|
50
|
+
self,
|
|
51
|
+
attributes: Mapping[str, Attribute],
|
|
52
|
+
values: Mapping[str, Any],
|
|
53
|
+
kinds: Sequence[str],
|
|
54
|
+
derived: Mapping[str, Any] | None = None,
|
|
55
|
+
*,
|
|
56
|
+
omit: bool | None = None,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
out: dict[str, Any] = {}
|
|
59
|
+
for attribute_id, attribute in attributes.items():
|
|
60
|
+
source = derived if attribute.is_derived and derived is not None else values
|
|
61
|
+
if attribute_id in source:
|
|
62
|
+
out[attribute.api_name] = self.value(attribute, kinds, source[attribute_id])
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
def value(self, attribute: Attribute | None, kinds: Sequence[str], value: Any) -> Any:
|
|
66
|
+
if value is None:
|
|
67
|
+
return None
|
|
68
|
+
committed_reference = (
|
|
69
|
+
attribute is not None and attribute.in_vault and looks_like_reference(value)
|
|
70
|
+
)
|
|
71
|
+
if attribute is None or (carries_markers(value) and not committed_reference):
|
|
72
|
+
return sensitive_value(frozenset(Sensitivity), value)
|
|
73
|
+
tags = self.tags(attribute)
|
|
74
|
+
return sensitive_value(tags, value) if tags else value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def guarded(parameters: Mapping[str, Any]) -> dict[str, Any]:
|
|
78
|
+
"""Recorded parameters, each whose JSON holds `$sensitive` or `$vault` keys wrapped with
|
|
79
|
+
every tag (see module doc)."""
|
|
80
|
+
return {
|
|
81
|
+
name: sensitive_value(frozenset(Sensitivity), value) if carries_markers(value) else value
|
|
82
|
+
for name, value in parameters.items()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def wrapping(masking: Masking) -> Masking:
|
|
87
|
+
"""A masking that wraps sensitive values with the tags `masking` gives them."""
|
|
88
|
+
return _Wrapping(Clearance(), masking.settings, masking.reference, masking.effective)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def _committed(
|
|
92
|
+
schema: Schema, changes: Sequence[DataChange], rows: MaterializedRepository
|
|
93
|
+
) -> list[DataChange]:
|
|
94
|
+
"""`changes` with the values of vault Attributes as committed (vault references); a value
|
|
95
|
+
without a reference is left out, so no plain personal data reaches the outbox."""
|
|
96
|
+
keys: dict[str, list[str]] = defaultdict(list)
|
|
97
|
+
for change in changes:
|
|
98
|
+
if isinstance(change, RecordUpsert):
|
|
99
|
+
kind = schema.kinds.get(change.record.kind)
|
|
100
|
+
if kind is not None and any(a.in_vault for a in kind.attributes.values()):
|
|
101
|
+
keys[kind.id].append(change.record.key)
|
|
102
|
+
if not keys:
|
|
103
|
+
return list(changes)
|
|
104
|
+
stored = {kind_id: await rows.records(kind_id, found) for kind_id, found in keys.items()}
|
|
105
|
+
out: list[DataChange] = []
|
|
106
|
+
for change in changes:
|
|
107
|
+
if not isinstance(change, RecordUpsert) or change.record.kind not in stored:
|
|
108
|
+
out.append(change)
|
|
109
|
+
continue
|
|
110
|
+
record = change.record
|
|
111
|
+
kind = schema.kinds[record.kind]
|
|
112
|
+
row = stored[record.kind].get(record.key)
|
|
113
|
+
values = dict(record.values)
|
|
114
|
+
for attribute in kind.attributes.values():
|
|
115
|
+
if not attribute.in_vault or attribute.id not in values:
|
|
116
|
+
continue
|
|
117
|
+
held = None if row is None else row.values.get(attribute.id)
|
|
118
|
+
if looks_like_reference(held):
|
|
119
|
+
values[attribute.id] = held
|
|
120
|
+
else:
|
|
121
|
+
del values[attribute.id]
|
|
122
|
+
out.append(RecordUpsert(record=record.model_copy(update={"values": values})))
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def write_side_effects(
|
|
127
|
+
session: AsyncSession,
|
|
128
|
+
domain_id: str,
|
|
129
|
+
operation: Operation,
|
|
130
|
+
*,
|
|
131
|
+
schema: Schema,
|
|
132
|
+
masking: Masking,
|
|
133
|
+
rows: MaterializedRepository,
|
|
134
|
+
changes: Sequence[DataChange],
|
|
135
|
+
execution: Mapping[str, Any],
|
|
136
|
+
max_edits: int,
|
|
137
|
+
defer: bool = False,
|
|
138
|
+
) -> list[int]:
|
|
139
|
+
"""Write the outbox events of a committed execution of `operation` in the caller's
|
|
140
|
+
transaction; the event ids. `execution` holds the fields every event carries (branch,
|
|
141
|
+
invocation, principal, Revisions, head, recorded parameters). With `defer` (the branch's
|
|
142
|
+
Proposal is the review) the events wait for the merge of the Proposal: none is written
|
|
143
|
+
now and the result is empty."""
|
|
144
|
+
if not operation.side_effects:
|
|
145
|
+
return []
|
|
146
|
+
listed = await _committed(schema, changes[:max_edits], rows)
|
|
147
|
+
shown = wrapping(masking)
|
|
148
|
+
body: dict[str, Any] = {
|
|
149
|
+
"domain": domain_id,
|
|
150
|
+
**execution,
|
|
151
|
+
"operation": operation.api_name,
|
|
152
|
+
"operation_id": operation.id,
|
|
153
|
+
"edit_count": len(changes),
|
|
154
|
+
"edits": [edit_document(schema, change, shown) for change in listed],
|
|
155
|
+
"edits_truncated": len(changes) > max_edits,
|
|
156
|
+
}
|
|
157
|
+
if defer:
|
|
158
|
+
deferred = DeferredEventRepository(session, domain_id)
|
|
159
|
+
for name in operation.side_effects:
|
|
160
|
+
await deferred.defer(
|
|
161
|
+
branch=str(execution["branch"]),
|
|
162
|
+
revision=from_hex(str(execution["head"])),
|
|
163
|
+
invocation_id=str(execution["invocation_id"]),
|
|
164
|
+
event_type=operation_event_type(name),
|
|
165
|
+
payload={**body, "side_effect": name},
|
|
166
|
+
)
|
|
167
|
+
return []
|
|
168
|
+
outbox = OutboxRepository(session)
|
|
169
|
+
ids = [
|
|
170
|
+
await outbox.enqueue(domain_id, operation_event_type(name), {**body, "side_effect": name})
|
|
171
|
+
for name in operation.side_effects
|
|
172
|
+
]
|
|
173
|
+
return ids
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def counted(ids: Sequence[int]) -> None:
|
|
177
|
+
"""Count events whose transaction committed."""
|
|
178
|
+
if ids:
|
|
179
|
+
OPERATION_EVENTS.add(len(ids))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def execution_fields(
|
|
183
|
+
*,
|
|
184
|
+
branch: str,
|
|
185
|
+
invocation_id: str,
|
|
186
|
+
principal: str,
|
|
187
|
+
revisions: Sequence[bytes],
|
|
188
|
+
head: bytes,
|
|
189
|
+
parameters: Mapping[str, Any],
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
"""The execution fields of an event payload."""
|
|
192
|
+
return {
|
|
193
|
+
"branch": branch,
|
|
194
|
+
"invocation_id": invocation_id,
|
|
195
|
+
"principal": principal,
|
|
196
|
+
"revisions": [hex(r) for r in revisions],
|
|
197
|
+
"head": hex(head),
|
|
198
|
+
"parameters": guarded(parameters),
|
|
199
|
+
}
|