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.
Files changed (126) hide show
  1. lorebase_keeper/__init__.py +1 -0
  2. lorebase_keeper/__main__.py +456 -0
  3. lorebase_keeper/agents/__init__.py +1 -0
  4. lorebase_keeper/agents/budget.py +247 -0
  5. lorebase_keeper/agents/context.py +529 -0
  6. lorebase_keeper/agents/explain.py +582 -0
  7. lorebase_keeper/agents/feed.py +259 -0
  8. lorebase_keeper/agents/markdown.py +219 -0
  9. lorebase_keeper/agents/pack.py +403 -0
  10. lorebase_keeper/agents/settings.py +111 -0
  11. lorebase_keeper/app.py +373 -0
  12. lorebase_keeper/auth/__init__.py +4 -0
  13. lorebase_keeper/auth/api_keys.py +127 -0
  14. lorebase_keeper/auth/audit.py +356 -0
  15. lorebase_keeper/auth/cache.py +53 -0
  16. lorebase_keeper/auth/config.py +618 -0
  17. lorebase_keeper/auth/deps.py +183 -0
  18. lorebase_keeper/auth/keyring.py +14 -0
  19. lorebase_keeper/auth/oidc.py +242 -0
  20. lorebase_keeper/auth/permissions.py +142 -0
  21. lorebase_keeper/auth/ports.py +212 -0
  22. lorebase_keeper/auth/principals.py +377 -0
  23. lorebase_keeper/auth/provider.py +250 -0
  24. lorebase_keeper/auth/rbac.py +247 -0
  25. lorebase_keeper/auth/read_audit.py +357 -0
  26. lorebase_keeper/auth/runtime.py +113 -0
  27. lorebase_keeper/auth/sessions.py +452 -0
  28. lorebase_keeper/bootstrap.py +151 -0
  29. lorebase_keeper/client_address.py +129 -0
  30. lorebase_keeper/context.py +226 -0
  31. lorebase_keeper/db.py +93 -0
  32. lorebase_keeper/deps.py +47 -0
  33. lorebase_keeper/errors.py +257 -0
  34. lorebase_keeper/health.py +61 -0
  35. lorebase_keeper/kindhealth/__init__.py +6 -0
  36. lorebase_keeper/kindhealth/config.py +51 -0
  37. lorebase_keeper/kindhealth/markdown.py +163 -0
  38. lorebase_keeper/kindhealth/models.py +179 -0
  39. lorebase_keeper/kindhealth/report.py +154 -0
  40. lorebase_keeper/kindhealth/scan.py +512 -0
  41. lorebase_keeper/middleware.py +285 -0
  42. lorebase_keeper/pagination.py +87 -0
  43. lorebase_keeper/ratelimit/__init__.py +10 -0
  44. lorebase_keeper/ratelimit/bucket.py +118 -0
  45. lorebase_keeper/ratelimit/config.py +283 -0
  46. lorebase_keeper/ratelimit/enforce.py +393 -0
  47. lorebase_keeper/ratelimit/limiter.py +149 -0
  48. lorebase_keeper/ratelimit/quotas.py +204 -0
  49. lorebase_keeper/ratelimit/shared.py +242 -0
  50. lorebase_keeper/readiness.py +257 -0
  51. lorebase_keeper/reading/__init__.py +1 -0
  52. lorebase_keeper/reading/caching.py +251 -0
  53. lorebase_keeper/reading/compiler.py +678 -0
  54. lorebase_keeper/reading/cursors.py +56 -0
  55. lorebase_keeper/reading/diff.py +408 -0
  56. lorebase_keeper/reading/graph.py +268 -0
  57. lorebase_keeper/reading/history.py +152 -0
  58. lorebase_keeper/reading/points.py +410 -0
  59. lorebase_keeper/reading/problems.py +74 -0
  60. lorebase_keeper/reading/record_history.py +722 -0
  61. lorebase_keeper/reading/records.py +780 -0
  62. lorebase_keeper/reading/requests.py +107 -0
  63. lorebase_keeper/reading/runtime.py +145 -0
  64. lorebase_keeper/reading/search.py +708 -0
  65. lorebase_keeper/reading/sensitivity.py +882 -0
  66. lorebase_keeper/reading/settings.py +339 -0
  67. lorebase_keeper/reading/snapshots.py +148 -0
  68. lorebase_keeper/reading/tree.py +98 -0
  69. lorebase_keeper/reading/views.py +264 -0
  70. lorebase_keeper/reading/visibility.py +93 -0
  71. lorebase_keeper/routes/__init__.py +1 -0
  72. lorebase_keeper/routes/admin.py +597 -0
  73. lorebase_keeper/routes/agents.py +160 -0
  74. lorebase_keeper/routes/branches.py +438 -0
  75. lorebase_keeper/routes/derivations.py +215 -0
  76. lorebase_keeper/routes/documents.py +497 -0
  77. lorebase_keeper/routes/domain_api.py +59 -0
  78. lorebase_keeper/routes/domains.py +220 -0
  79. lorebase_keeper/routes/events.py +187 -0
  80. lorebase_keeper/routes/kind_health.py +166 -0
  81. lorebase_keeper/routes/operations.py +392 -0
  82. lorebase_keeper/routes/proposal_items.py +563 -0
  83. lorebase_keeper/routes/proposals.py +1107 -0
  84. lorebase_keeper/routes/read.py +493 -0
  85. lorebase_keeper/routes/revisions.py +523 -0
  86. lorebase_keeper/routes/schema.py +682 -0
  87. lorebase_keeper/routes/schema_inference.py +437 -0
  88. lorebase_keeper/routes/schema_suggestions.py +328 -0
  89. lorebase_keeper/routes/sessions.py +286 -0
  90. lorebase_keeper/routes/source_preview.py +232 -0
  91. lorebase_keeper/routes/sources.py +200 -0
  92. lorebase_keeper/routes/trace.py +506 -0
  93. lorebase_keeper/routes/vault.py +332 -0
  94. lorebase_keeper/routes/webhooks.py +298 -0
  95. lorebase_keeper/routes/whoami.py +88 -0
  96. lorebase_keeper/services/__init__.py +1 -0
  97. lorebase_keeper/services/agent_writes.py +1035 -0
  98. lorebase_keeper/services/branches.py +337 -0
  99. lorebase_keeper/services/config.py +431 -0
  100. lorebase_keeper/services/derivation_preview.py +279 -0
  101. lorebase_keeper/services/document_urls.py +114 -0
  102. lorebase_keeper/services/domains.py +118 -0
  103. lorebase_keeper/services/errors.py +196 -0
  104. lorebase_keeper/services/operation_catalog.py +806 -0
  105. lorebase_keeper/services/operation_events.py +199 -0
  106. lorebase_keeper/services/operations.py +1229 -0
  107. lorebase_keeper/services/pii_import.py +222 -0
  108. lorebase_keeper/services/proposal_reads.py +158 -0
  109. lorebase_keeper/services/proposals.py +850 -0
  110. lorebase_keeper/services/reads.py +286 -0
  111. lorebase_keeper/services/schema.py +601 -0
  112. lorebase_keeper/services/schema_inference.py +390 -0
  113. lorebase_keeper/services/schema_suggestions.py +418 -0
  114. lorebase_keeper/services/source_preview.py +127 -0
  115. lorebase_keeper/settings.py +313 -0
  116. lorebase_keeper/tracing/__init__.py +1 -0
  117. lorebase_keeper/tracing/capture.py +350 -0
  118. lorebase_keeper/tracing/config.py +157 -0
  119. lorebase_keeper/tracing/explain.py +97 -0
  120. lorebase_keeper/tracing/middleware.py +359 -0
  121. lorebase_keeper/tracing/notes.py +80 -0
  122. lorebase_keeper/tracing/recorder.py +180 -0
  123. lorebase_keeper-0.2.3.dist-info/METADATA +21 -0
  124. lorebase_keeper-0.2.3.dist-info/RECORD +126 -0
  125. lorebase_keeper-0.2.3.dist-info/WHEEL +4 -0
  126. 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
+ }