lorebase-store 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_store/__init__.py +1 -0
- lorebase_store/_migrations/env.py +106 -0
- lorebase_store/_migrations/script.py.mako +27 -0
- lorebase_store/_migrations/versions/0001_initial_schema.py +1071 -0
- lorebase_store/_migrations/versions/0002_merge_attempt_details.py +74 -0
- lorebase_store/_migrations/versions/0003_webhooks.py +127 -0
- lorebase_store/_migrations/versions/0004_defer_revision_references.py +38 -0
- lorebase_store/_migrations/versions/0005_security_definer_reads.py +396 -0
- lorebase_store/_migrations/versions/0006_api_key_usage.py +48 -0
- lorebase_store/_migrations/versions/0007_document_extraction.py +154 -0
- lorebase_store/_migrations/versions/0008_definer_argument_checks.py +178 -0
- lorebase_store/_migrations/versions/0009_api_key_networks.py +33 -0
- lorebase_store/_migrations/versions/0010_overlay_branches.py +582 -0
- lorebase_store/_migrations/versions/0011_branch_scoped_search_index.py +229 -0
- lorebase_store/_migrations/versions/0012_overlay_neighborhood_lookups.py +185 -0
- lorebase_store/_migrations/versions/0013_web_sessions.py +72 -0
- lorebase_store/_migrations/versions/0014_embedding_jobs.py +70 -0
- lorebase_store/_migrations/versions/0015_append_only_audit_log.py +83 -0
- lorebase_store/_migrations/versions/0016_sealed_webhook_keys.py +55 -0
- lorebase_store/_migrations/versions/0017_background_branch_copies.py +77 -0
- lorebase_store/_migrations/versions/0018_search_lexicon.py +477 -0
- lorebase_store/_migrations/versions/0019_vault_bindings.py +56 -0
- lorebase_store/_migrations/versions/0020_outbox_consumers.py +150 -0
- lorebase_store/_migrations/versions/0021_schema_suggestions.py +114 -0
- lorebase_store/_migrations/versions/0022_read_audit_and_shared_rate_limits.py +133 -0
- lorebase_store/_migrations/versions/0023_agent_trace.py +96 -0
- lorebase_store/_migrations/versions/0024_embedding_template_keys.py +205 -0
- lorebase_store/_migrations/versions/0025_snapshot_refs.py +35 -0
- lorebase_store/_migrations/versions/0026_plain_reverse_link_index.py +112 -0
- lorebase_store/_migrations/versions/0027_vault_rotation_and_tombstones.py +145 -0
- lorebase_store/_migrations/versions/0028_operation_deferred_events.py +71 -0
- lorebase_store/_migrations/versions/0029_agent_trace_sessions.py +108 -0
- lorebase_store/_migrations/versions/0030_path_fanout.py +31 -0
- lorebase_store/_migrations/versions/0031_branch_builds.py +91 -0
- lorebase_store/_migrations/versions/0032_lineage_revisions.py +35 -0
- lorebase_store/_migrations/versions/0033_vault_moved_tokens.py +30 -0
- lorebase_store/_migrations/versions/0034_inference_endpoint_modes.py +73 -0
- lorebase_store/_migrations/versions/0035_search_truncation.py +104 -0
- lorebase_store/_migrations/versions/0036_lineage_maps.py +325 -0
- lorebase_store/_migrations/versions/0037_bm25_search.py +346 -0
- lorebase_store/auth/__init__.py +52 -0
- lorebase_store/auth/api_keys.py +161 -0
- lorebase_store/auth/audit.py +382 -0
- lorebase_store/auth/checkpoints.py +185 -0
- lorebase_store/auth/grants.py +161 -0
- lorebase_store/auth/principals.py +158 -0
- lorebase_store/auth/scope.py +70 -0
- lorebase_store/auth/sessions.py +153 -0
- lorebase_store/auth/unit.py +53 -0
- lorebase_store/auth/usage.py +70 -0
- lorebase_store/blobs.py +73 -0
- lorebase_store/bm25.py +346 -0
- lorebase_store/bulk.py +147 -0
- lorebase_store/bundle.py +1830 -0
- lorebase_store/changefeed.py +261 -0
- lorebase_store/commit/__init__.py +42 -0
- lorebase_store/commit/changes.py +67 -0
- lorebase_store/commit/errors.py +60 -0
- lorebase_store/commit/migrations.py +335 -0
- lorebase_store/commit/pipeline.py +754 -0
- lorebase_store/commit/schema.py +123 -0
- lorebase_store/db.py +58 -0
- lorebase_store/definer.py +50 -0
- lorebase_store/digests.py +99 -0
- lorebase_store/embeddings.py +444 -0
- lorebase_store/endpoint_modes.py +59 -0
- lorebase_store/extraction/__init__.py +61 -0
- lorebase_store/extraction/items.py +187 -0
- lorebase_store/extraction/materialize.py +364 -0
- lorebase_store/extraction/proposal.py +65 -0
- lorebase_store/extraction/review.py +168 -0
- lorebase_store/gc.py +970 -0
- lorebase_store/indexes.py +1033 -0
- lorebase_store/jobs.py +651 -0
- lorebase_store/keyring.py +72 -0
- lorebase_store/layout.py +335 -0
- lorebase_store/materialize/__init__.py +103 -0
- lorebase_store/materialize/apply.py +436 -0
- lorebase_store/materialize/background.py +1184 -0
- lorebase_store/materialize/branch.py +268 -0
- lorebase_store/materialize/build.py +536 -0
- lorebase_store/materialize/derived.py +540 -0
- lorebase_store/materialize/lineage_backfill.py +502 -0
- lorebase_store/materialize/links.py +266 -0
- lorebase_store/materialize/overlay.py +292 -0
- lorebase_store/materialize/paths.py +315 -0
- lorebase_store/materialize/rebuild.py +326 -0
- lorebase_store/materialize/rows.py +153 -0
- lorebase_store/materialize/sensitivity.py +66 -0
- lorebase_store/materialize/snapshot.py +205 -0
- lorebase_store/materialize/stale.py +444 -0
- lorebase_store/materialize/verify.py +321 -0
- lorebase_store/merging/__init__.py +39 -0
- lorebase_store/merging/attempts.py +334 -0
- lorebase_store/merging/lineage.py +123 -0
- lorebase_store/merging/service.py +652 -0
- lorebase_store/migrations.py +133 -0
- lorebase_store/models/__init__.py +136 -0
- lorebase_store/models/base.py +64 -0
- lorebase_store/models/enums.py +132 -0
- lorebase_store/models/governance.py +268 -0
- lorebase_store/models/ingestion.py +288 -0
- lorebase_store/models/materialized.py +277 -0
- lorebase_store/models/proposals.py +195 -0
- lorebase_store/models/queues.py +87 -0
- lorebase_store/models/tracing.py +116 -0
- lorebase_store/models/versioning.py +222 -0
- lorebase_store/models/webhooks.py +96 -0
- lorebase_store/nodestore.py +384 -0
- lorebase_store/objectstore.py +162 -0
- lorebase_store/outbox_events.py +166 -0
- lorebase_store/profiling/__init__.py +58 -0
- lorebase_store/profiling/delimited.py +109 -0
- lorebase_store/profiling/postgres.py +211 -0
- lorebase_store/profiling/record_files.py +127 -0
- lorebase_store/profiling/records.py +92 -0
- lorebase_store/profiling/sql_databases.py +298 -0
- lorebase_store/ratelimit.py +129 -0
- lorebase_store/reads.py +1491 -0
- lorebase_store/replicas.py +434 -0
- lorebase_store/repositories/__init__.py +152 -0
- lorebase_store/repositories/changesets.py +59 -0
- lorebase_store/repositories/deferred_events.py +142 -0
- lorebase_store/repositories/documents.py +305 -0
- lorebase_store/repositories/domains.py +97 -0
- lorebase_store/repositories/events.py +205 -0
- lorebase_store/repositories/index_view.py +489 -0
- lorebase_store/repositories/invocations.py +68 -0
- lorebase_store/repositories/materializations.py +276 -0
- lorebase_store/repositories/materialized.py +1129 -0
- lorebase_store/repositories/merges.py +417 -0
- lorebase_store/repositories/proposal_items.py +199 -0
- lorebase_store/repositories/proposals.py +184 -0
- lorebase_store/repositories/provenance.py +77 -0
- lorebase_store/repositories/refs.py +452 -0
- lorebase_store/repositories/revisions.py +273 -0
- lorebase_store/repositories/scope.py +33 -0
- lorebase_store/repositories/source_runs.py +123 -0
- lorebase_store/repositories/suggestions.py +638 -0
- lorebase_store/runtime.py +87 -0
- lorebase_store/schema_cache.py +50 -0
- lorebase_store/settings.py +879 -0
- lorebase_store/storage.py +1478 -0
- lorebase_store/telemetry.py +142 -0
- lorebase_store/testing.py +108 -0
- lorebase_store/tracing.py +505 -0
- lorebase_store/traverse.py +578 -0
- lorebase_store/vault/__init__.py +50 -0
- lorebase_store/vault/keys.py +216 -0
- lorebase_store/vault/quotes.py +133 -0
- lorebase_store/vault/rotation.py +209 -0
- lorebase_store/vault/settings.py +152 -0
- lorebase_store/vault/store.py +987 -0
- lorebase_store/vault/transfer.py +558 -0
- lorebase_store/webhooks/__init__.py +24 -0
- lorebase_store/webhooks/delivery.py +637 -0
- lorebase_store/webhooks/keys.py +126 -0
- lorebase_store/webhooks/repository.py +142 -0
- lorebase_store/webhooks/settings.py +138 -0
- lorebase_store/webhooks/signing.py +50 -0
- lorebase_store-0.2.3.dist-info/METADATA +13 -0
- lorebase_store-0.2.3.dist-info/RECORD +163 -0
- lorebase_store-0.2.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Vectors keyed by embedding template fingerprint (design 9.3).
|
|
2
|
+
|
|
3
|
+
`embeddings.fingerprint` holds the fingerprint of the embedding template the vector's text was
|
|
4
|
+
rendered with (`KindTemplate.fingerprint`) and joins the primary key, now `(domain_id,
|
|
5
|
+
object_hash, model, fingerprint)`: branches whose templates differ for the same content keep a
|
|
6
|
+
vector each, and readers look a vector up with the fingerprint of the template of the branch
|
|
7
|
+
they read.
|
|
8
|
+
|
|
9
|
+
Existing rows. The fingerprint of a vector is set when it can be told: the live Records
|
|
10
|
+
holding its object hash, in every materialized branch whose schema gives their Kind an
|
|
11
|
+
embedding template, all have a template of the same fingerprint. Other vectors keep the empty
|
|
12
|
+
fingerprint, which no reader looks up and garbage collection deletes. Every
|
|
13
|
+
`embedding_cursors` row loses its fingerprints, so the next pass of each branch renders each
|
|
14
|
+
Record's text once more: a Record whose text hash matches its vector costs nothing, one whose
|
|
15
|
+
text matches another stored text gets that vector copied without a model call (vectors left
|
|
16
|
+
without a fingerprint included, unless a collection deleted them first), and only texts
|
|
17
|
+
embedded nowhere go to the model. Until that pass, semantic search finds the vectors whose
|
|
18
|
+
fingerprint was set.
|
|
19
|
+
|
|
20
|
+
The downgrade keeps one vector per `(domain_id, object_hash, model)` and clears the cursors'
|
|
21
|
+
fingerprints the same way.
|
|
22
|
+
|
|
23
|
+
Revision ID: 0024
|
|
24
|
+
Revises: 0023
|
|
25
|
+
Create Date: 2026-10-27 09:00:00.000000
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import hashlib
|
|
31
|
+
import json
|
|
32
|
+
import re
|
|
33
|
+
from collections.abc import Mapping, Sequence
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import sqlalchemy as sa
|
|
37
|
+
from alembic import op
|
|
38
|
+
from sqlalchemy.dialects import postgresql
|
|
39
|
+
|
|
40
|
+
revision: str = "0024"
|
|
41
|
+
down_revision: str | Sequence[str] | None = "0023"
|
|
42
|
+
branch_labels: str | Sequence[str] | None = None
|
|
43
|
+
depends_on: str | Sequence[str] | None = None
|
|
44
|
+
|
|
45
|
+
TEMPLATES = "lb_migration_templates"
|
|
46
|
+
|
|
47
|
+
_FILL = sa.text(
|
|
48
|
+
f"INSERT INTO {TEMPLATES} (schema_hash, kind_id, fingerprint) "
|
|
49
|
+
"SELECT :h, k, f FROM unnest(CAST(:kinds AS text[]), CAST(:fps AS text[])) AS t(k, f)"
|
|
50
|
+
).bindparams(
|
|
51
|
+
sa.bindparam("kinds", type_=postgresql.ARRAY(sa.Text())),
|
|
52
|
+
sa.bindparam("fps", type_=postgresql.ARRAY(sa.Text())),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_BACKFILL = sa.text(
|
|
56
|
+
f"""
|
|
57
|
+
UPDATE embeddings e SET fingerprint = known.fingerprint
|
|
58
|
+
FROM (
|
|
59
|
+
SELECT m.domain_id, m.object_hash, min(t.fingerprint) AS fingerprint
|
|
60
|
+
FROM mat_records m
|
|
61
|
+
JOIN mat_schema s ON s.domain_id = m.domain_id AND s.branch = m.branch
|
|
62
|
+
JOIN {TEMPLATES} t ON t.schema_hash = s.schema_hash AND t.kind_id = m.kind_id
|
|
63
|
+
WHERE NOT m.deleted
|
|
64
|
+
GROUP BY m.domain_id, m.object_hash
|
|
65
|
+
HAVING count(DISTINCT t.fingerprint) = 1
|
|
66
|
+
) AS known
|
|
67
|
+
WHERE e.domain_id = known.domain_id AND e.object_hash = known.object_hash
|
|
68
|
+
"""
|
|
69
|
+
)
|
|
70
|
+
"""A vector gets the fingerprint every live Record holding its object hash agrees on."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Fingerprints as `lorebase_core.model.embedding` computed them at this revision (rendering
|
|
74
|
+
# version 1), on the stored compiled schema document. Frozen here so the backfill does not
|
|
75
|
+
# change with the application code.
|
|
76
|
+
_RENDERING_VERSION = 1
|
|
77
|
+
_KIND_FIELD = "_kind"
|
|
78
|
+
_SPECIAL_FIELDS = frozenset({_KIND_FIELD, "_key"})
|
|
79
|
+
_TOKEN = re.compile(r"\{\{|\}\}|\{([^{}]*)\}|[{}]")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _template_fields(source: str) -> tuple[str, ...] | None:
|
|
83
|
+
"""Placeholder names in order of first use; `None` when the template cannot be parsed."""
|
|
84
|
+
names: list[str] = []
|
|
85
|
+
for match in _TOKEN.finditer(source):
|
|
86
|
+
token = match.group(0)
|
|
87
|
+
if token in ("{{", "}}"):
|
|
88
|
+
continue
|
|
89
|
+
name = match.group(1)
|
|
90
|
+
if name is None or not name.strip():
|
|
91
|
+
return None
|
|
92
|
+
names.append(name.strip())
|
|
93
|
+
return tuple(dict.fromkeys(names)) or None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _kind_fingerprint(kind: Mapping[str, Any]) -> str | None:
|
|
97
|
+
"""Fingerprint of the Kind's embedding template; `None` without a usable one."""
|
|
98
|
+
search = kind.get("search") or {}
|
|
99
|
+
source = search.get("embedding_template")
|
|
100
|
+
if source is None or not source.strip():
|
|
101
|
+
return None
|
|
102
|
+
fields = _template_fields(source)
|
|
103
|
+
if fields is None:
|
|
104
|
+
return None
|
|
105
|
+
allow_sensitive = bool(search.get("embedding_sensitive"))
|
|
106
|
+
attributes: dict[str, str] = {}
|
|
107
|
+
for name in fields:
|
|
108
|
+
if name in _SPECIAL_FIELDS:
|
|
109
|
+
continue
|
|
110
|
+
attribute = next(
|
|
111
|
+
(a for a in (kind.get("attributes") or {}).values() if a["api_name"] == name), None
|
|
112
|
+
)
|
|
113
|
+
if (
|
|
114
|
+
attribute is None
|
|
115
|
+
or attribute.get("derivation_id") is not None
|
|
116
|
+
or attribute.get("vault") is True
|
|
117
|
+
or (attribute.get("sensitivity") and not allow_sensitive)
|
|
118
|
+
):
|
|
119
|
+
return None
|
|
120
|
+
attributes[name] = attribute["id"]
|
|
121
|
+
label = kind.get("display_name") or kind["api_name"]
|
|
122
|
+
return hashlib.sha256(
|
|
123
|
+
json.dumps(
|
|
124
|
+
{
|
|
125
|
+
"v": _RENDERING_VERSION,
|
|
126
|
+
"kind": kind["id"],
|
|
127
|
+
"template": source,
|
|
128
|
+
"attributes": attributes,
|
|
129
|
+
"label": label if _KIND_FIELD in fields else None,
|
|
130
|
+
},
|
|
131
|
+
sort_keys=True,
|
|
132
|
+
separators=(",", ":"),
|
|
133
|
+
).encode("utf-8")
|
|
134
|
+
).hexdigest()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _template_fingerprints(compiled: Mapping[str, Any]) -> dict[str, str]:
|
|
138
|
+
"""Fingerprint by Kind id of each Kind of a compiled schema with a usable template."""
|
|
139
|
+
found: dict[str, str] = {}
|
|
140
|
+
for kind in (compiled.get("kinds") or {}).values():
|
|
141
|
+
fingerprint = _kind_fingerprint(kind)
|
|
142
|
+
if fingerprint is not None:
|
|
143
|
+
found[kind["id"]] = fingerprint
|
|
144
|
+
return found
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _templates(bind: sa.Connection) -> None:
|
|
148
|
+
bind.execute(
|
|
149
|
+
sa.text(
|
|
150
|
+
f"CREATE TEMP TABLE {TEMPLATES} (schema_hash bytea NOT NULL, kind_id text NOT NULL, "
|
|
151
|
+
"fingerprint text NOT NULL, PRIMARY KEY (schema_hash, kind_id)) ON COMMIT DROP"
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
schemas = bind.execute(
|
|
155
|
+
sa.text("SELECT DISTINCT ON (schema_hash) schema_hash, compiled FROM mat_schema")
|
|
156
|
+
).all()
|
|
157
|
+
for schema_hash, compiled in schemas:
|
|
158
|
+
fingerprints = _template_fingerprints(compiled)
|
|
159
|
+
if fingerprints:
|
|
160
|
+
bind.execute(
|
|
161
|
+
_FILL,
|
|
162
|
+
{
|
|
163
|
+
"h": bytes(schema_hash),
|
|
164
|
+
"kinds": list(fingerprints),
|
|
165
|
+
"fps": list(fingerprints.values()),
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _clear_cursor_fingerprints() -> None:
|
|
171
|
+
op.execute("UPDATE embedding_cursors SET fingerprints = '{}'::jsonb")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def upgrade() -> None:
|
|
175
|
+
bind = op.get_bind()
|
|
176
|
+
# Row-level security is forced on the owner too: read and write the rows of every Domain.
|
|
177
|
+
bind.execute(sa.text("SELECT set_config('lorebase.bypass_rls', 'on', true)"))
|
|
178
|
+
op.add_column(
|
|
179
|
+
"embeddings",
|
|
180
|
+
sa.Column("fingerprint", sa.Text(), nullable=False, server_default=sa.text("''")),
|
|
181
|
+
)
|
|
182
|
+
op.alter_column("embeddings", "fingerprint", server_default=None)
|
|
183
|
+
op.drop_constraint(op.f("pk_embeddings"), "embeddings", type_="primary")
|
|
184
|
+
op.create_primary_key(
|
|
185
|
+
op.f("pk_embeddings"), "embeddings", ["domain_id", "object_hash", "model", "fingerprint"]
|
|
186
|
+
)
|
|
187
|
+
_templates(bind)
|
|
188
|
+
bind.execute(_BACKFILL)
|
|
189
|
+
_clear_cursor_fingerprints()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def downgrade() -> None:
|
|
193
|
+
bind = op.get_bind()
|
|
194
|
+
bind.execute(sa.text("SELECT set_config('lorebase.bypass_rls', 'on', true)"))
|
|
195
|
+
op.execute(
|
|
196
|
+
"DELETE FROM embeddings e USING embeddings other "
|
|
197
|
+
"WHERE other.domain_id = e.domain_id AND other.object_hash = e.object_hash "
|
|
198
|
+
"AND other.model = e.model AND other.fingerprint > e.fingerprint"
|
|
199
|
+
)
|
|
200
|
+
op.drop_constraint(op.f("pk_embeddings"), "embeddings", type_="primary")
|
|
201
|
+
op.drop_column("embeddings", "fingerprint")
|
|
202
|
+
op.create_primary_key(
|
|
203
|
+
op.f("pk_embeddings"), "embeddings", ["domain_id", "object_hash", "model"]
|
|
204
|
+
)
|
|
205
|
+
_clear_cursor_fingerprints()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Snapshot refs.
|
|
2
|
+
|
|
3
|
+
Adds `snapshot` to `ref_kind`: a snapshot ref holds the materialized rows of a past Revision
|
|
4
|
+
for reads at it (`lorebase_store.materialize.snapshot`). The value is added outside the
|
|
5
|
+
migration transaction, as Postgres requires before it can be used.
|
|
6
|
+
|
|
7
|
+
Downgrade deletes the snapshot refs (their rows go with them) and keeps the enum value:
|
|
8
|
+
Postgres cannot drop a value from an enum type, and no other ref uses it. The deletion is one
|
|
9
|
+
statement: snapshots are bounded (`read.time_travel.snapshot_max_per_domain` per Domain, each
|
|
10
|
+
at most `snapshot_max_entries` Records and Links plus its history), so it is not batched.
|
|
11
|
+
|
|
12
|
+
Revision ID: 0025
|
|
13
|
+
Revises: 0024
|
|
14
|
+
Create Date: 2026-10-27 09:00:00.000000
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
|
|
21
|
+
from alembic import op
|
|
22
|
+
|
|
23
|
+
revision: str = "0025"
|
|
24
|
+
down_revision: str | Sequence[str] | None = "0024"
|
|
25
|
+
branch_labels: str | Sequence[str] | None = None
|
|
26
|
+
depends_on: str | Sequence[str] | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def upgrade() -> None:
|
|
30
|
+
with op.get_context().autocommit_block():
|
|
31
|
+
op.execute("ALTER TYPE ref_kind ADD VALUE IF NOT EXISTS 'snapshot'")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def downgrade() -> None:
|
|
35
|
+
op.execute("DELETE FROM refs WHERE kind = 'snapshot'")
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""The plain reverse Link index again.
|
|
2
|
+
|
|
3
|
+
An earlier version of this revision replaced `ix_mat_links_reverse` with a covering index
|
|
4
|
+
(`INCLUDE (from_key, "values", ...)`) so that inbound Link hops of the neighborhood would be
|
|
5
|
+
index-only scans. It was withdrawn: a B-tree entry is limited to about 2.7 KB, so a Link with
|
|
6
|
+
large `values` could no longer be written ("index row size exceeds maximum"), and without
|
|
7
|
+
`values` the hop, which returns them, reads the heap anyway while the index doubles in size.
|
|
8
|
+
On a database that ran the withdrawn version this restores the plain index (built
|
|
9
|
+
concurrently per partition, revision 0011 style) and drops the covering one; elsewhere it
|
|
10
|
+
changes nothing.
|
|
11
|
+
|
|
12
|
+
Revision ID: 0026
|
|
13
|
+
Revises: 0025
|
|
14
|
+
Create Date: 2026-10-22 18:00:00.000000
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from typing import NamedTuple
|
|
21
|
+
|
|
22
|
+
import sqlalchemy as sa
|
|
23
|
+
from alembic import op
|
|
24
|
+
|
|
25
|
+
revision: str = "0026"
|
|
26
|
+
down_revision: str | Sequence[str] | None = "0025"
|
|
27
|
+
branch_labels: str | Sequence[str] | None = None
|
|
28
|
+
depends_on: str | Sequence[str] | None = None
|
|
29
|
+
|
|
30
|
+
PARENT = "mat_links"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BtreeIndex(NamedTuple):
|
|
34
|
+
name: str
|
|
35
|
+
definition: str
|
|
36
|
+
suffix: str
|
|
37
|
+
"""Partition indexes are named `<partition>_<suffix>`."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
OLD = BtreeIndex(
|
|
41
|
+
"ix_mat_links_reverse",
|
|
42
|
+
"(domain_id, branch, relation_id, to_key)",
|
|
43
|
+
"domain_id_branch_relation_id_to_key_idx",
|
|
44
|
+
)
|
|
45
|
+
NEW = BtreeIndex(
|
|
46
|
+
"ix_mat_links_reverse_covering",
|
|
47
|
+
'(domain_id, branch, relation_id, to_key) INCLUDE (from_key, "values", object_hash, '
|
|
48
|
+
"revision_generation, deleted)",
|
|
49
|
+
"reverse_covering_idx",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _partitions() -> list[str]:
|
|
54
|
+
found = op.get_bind().execute(
|
|
55
|
+
sa.text(
|
|
56
|
+
"SELECT c.relname FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid "
|
|
57
|
+
"WHERE i.inhparent = CAST(:parent AS regclass) ORDER BY c.relname"
|
|
58
|
+
),
|
|
59
|
+
{"parent": PARENT},
|
|
60
|
+
)
|
|
61
|
+
return list(found.scalars())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _state(name: str) -> tuple[bool, bool] | None:
|
|
65
|
+
"""`(valid, attached)` of an index, `None` when it does not exist."""
|
|
66
|
+
row = (
|
|
67
|
+
op.get_bind()
|
|
68
|
+
.execute(
|
|
69
|
+
sa.text(
|
|
70
|
+
"SELECT x.indisvalid, EXISTS (SELECT 1 FROM pg_inherits i "
|
|
71
|
+
"WHERE i.inhrelid = c.oid) FROM pg_class c JOIN pg_index x "
|
|
72
|
+
"ON x.indexrelid = c.oid WHERE c.relname = :name "
|
|
73
|
+
"AND c.relnamespace = (SELECT relnamespace FROM pg_class "
|
|
74
|
+
"WHERE oid = CAST(:parent AS regclass))"
|
|
75
|
+
),
|
|
76
|
+
{"name": name, "parent": PARENT},
|
|
77
|
+
)
|
|
78
|
+
.first()
|
|
79
|
+
)
|
|
80
|
+
return None if row is None else (bool(row[0]), bool(row[1]))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _build(index: BtreeIndex) -> None:
|
|
84
|
+
"""Build the index on every partition without blocking writes."""
|
|
85
|
+
op.execute(f"CREATE INDEX IF NOT EXISTS {index.name} ON ONLY {PARENT} {index.definition}")
|
|
86
|
+
with op.get_context().autocommit_block():
|
|
87
|
+
for partition in _partitions():
|
|
88
|
+
name = f"{partition}_{index.suffix}"
|
|
89
|
+
state = _state(name)
|
|
90
|
+
if state is not None and not state[0]:
|
|
91
|
+
op.execute(f"DROP INDEX CONCURRENTLY IF EXISTS {name}")
|
|
92
|
+
state = None
|
|
93
|
+
if state is None:
|
|
94
|
+
op.execute(f"CREATE INDEX CONCURRENTLY {name} ON {partition} {index.definition}")
|
|
95
|
+
if state is None or not state[1]:
|
|
96
|
+
op.execute(f"ALTER INDEX {index.name} ATTACH PARTITION {name}")
|
|
97
|
+
if _state(index.name) != (True, False):
|
|
98
|
+
raise RuntimeError(f"index {index.name} is not valid after attaching every partition")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _swap(old: BtreeIndex, new: BtreeIndex) -> None:
|
|
102
|
+
"""Build `new`, then drop `old` once `new` is valid."""
|
|
103
|
+
_build(new)
|
|
104
|
+
op.execute(f"DROP INDEX IF EXISTS {old.name}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def upgrade() -> None:
|
|
108
|
+
_swap(NEW, OLD)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def downgrade() -> None:
|
|
112
|
+
"""Nothing to undo: the plain index is the one revision 0025 had."""
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Vault key rotation, erasure tombstones and lineage, installation secrets (design 13.5).
|
|
2
|
+
|
|
3
|
+
- `vault.fingerprint_key` names the token key and scheme a row's fingerprint was made under
|
|
4
|
+
(`<token_key_id>/<scheme>`); rows stored before have none and are made again by the rotation
|
|
5
|
+
job (`lorebase_store.vault.rotation`).
|
|
6
|
+
- `vault_tombstones` keeps the fingerprints of erased values, so a later write of one is stored
|
|
7
|
+
erased or refused (`vault.erased_values`).
|
|
8
|
+
- `vault_lineage` records a Record that took over the vaulted values of another key of its Kind
|
|
9
|
+
(a key change), so erasing one erases both.
|
|
10
|
+
- `installation_secrets` holds a random secret made here: the key of the keyed digests
|
|
11
|
+
(Operation parameters, erased Record keys in the audit log) when no `vault.digest_key` and no
|
|
12
|
+
vault keys are configured.
|
|
13
|
+
|
|
14
|
+
Revision ID: 0027
|
|
15
|
+
Revises: 0026
|
|
16
|
+
Create Date: 2026-10-27 09:00:00.000000
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
import sqlalchemy as sa
|
|
24
|
+
from alembic import op
|
|
25
|
+
|
|
26
|
+
revision: str = "0027"
|
|
27
|
+
down_revision: str | Sequence[str] | None = "0026"
|
|
28
|
+
branch_labels: str | Sequence[str] | None = None
|
|
29
|
+
depends_on: str | Sequence[str] | None = None
|
|
30
|
+
|
|
31
|
+
TOMBSTONES = "vault_tombstones"
|
|
32
|
+
LINEAGE = "vault_lineage"
|
|
33
|
+
SECRETS = "installation_secrets"
|
|
34
|
+
|
|
35
|
+
DOMAIN_VISIBLE = (
|
|
36
|
+
"current_setting('lorebase.bypass_rls', true) = 'on' "
|
|
37
|
+
"OR domain_id = ANY (string_to_array(current_setting('lorebase.domain_ids', true), ','))"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _isolate(table: str) -> None:
|
|
42
|
+
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY")
|
|
43
|
+
op.execute(f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY")
|
|
44
|
+
op.execute(
|
|
45
|
+
f"CREATE POLICY domain_isolation ON {table} "
|
|
46
|
+
f"USING ({DOMAIN_VISIBLE}) WITH CHECK ({DOMAIN_VISIBLE})"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def upgrade() -> None:
|
|
51
|
+
op.add_column("vault", sa.Column("fingerprint_key", sa.Text(), nullable=True))
|
|
52
|
+
|
|
53
|
+
op.create_table(
|
|
54
|
+
TOMBSTONES,
|
|
55
|
+
sa.Column("domain_id", sa.Text(), nullable=False),
|
|
56
|
+
sa.Column("seq", sa.BigInteger(), sa.Identity(always=True), nullable=False),
|
|
57
|
+
sa.Column("kind_id", sa.Text(), nullable=False),
|
|
58
|
+
sa.Column("attribute_id", sa.Text(), nullable=False),
|
|
59
|
+
sa.Column("record_key", sa.Text(), nullable=True),
|
|
60
|
+
sa.Column("fingerprint", sa.LargeBinary(), nullable=False),
|
|
61
|
+
sa.Column("fingerprint_key", sa.Text(), nullable=True),
|
|
62
|
+
sa.Column(
|
|
63
|
+
"created_at",
|
|
64
|
+
sa.DateTime(timezone=True),
|
|
65
|
+
server_default=sa.text("now()"),
|
|
66
|
+
nullable=False,
|
|
67
|
+
),
|
|
68
|
+
sa.ForeignKeyConstraint(
|
|
69
|
+
["domain_id"],
|
|
70
|
+
["domains.id"],
|
|
71
|
+
name=op.f("fk_vault_tombstones_domain_id_domains"),
|
|
72
|
+
ondelete="CASCADE",
|
|
73
|
+
),
|
|
74
|
+
sa.PrimaryKeyConstraint("domain_id", "seq", name=op.f("pk_vault_tombstones")),
|
|
75
|
+
)
|
|
76
|
+
op.create_index(
|
|
77
|
+
op.f("ix_vault_tombstones_domain_id_fingerprint"), TOMBSTONES, ["domain_id", "fingerprint"]
|
|
78
|
+
)
|
|
79
|
+
_isolate(TOMBSTONES)
|
|
80
|
+
|
|
81
|
+
op.create_table(
|
|
82
|
+
LINEAGE,
|
|
83
|
+
sa.Column("domain_id", sa.Text(), nullable=False),
|
|
84
|
+
sa.Column("kind_id", sa.Text(), nullable=False),
|
|
85
|
+
sa.Column("record_key", sa.Text(), nullable=False),
|
|
86
|
+
sa.Column("previous_key", sa.Text(), nullable=False),
|
|
87
|
+
sa.Column(
|
|
88
|
+
"created_at",
|
|
89
|
+
sa.DateTime(timezone=True),
|
|
90
|
+
server_default=sa.text("now()"),
|
|
91
|
+
nullable=False,
|
|
92
|
+
),
|
|
93
|
+
sa.ForeignKeyConstraint(
|
|
94
|
+
["domain_id"],
|
|
95
|
+
["domains.id"],
|
|
96
|
+
name=op.f("fk_vault_lineage_domain_id_domains"),
|
|
97
|
+
ondelete="CASCADE",
|
|
98
|
+
),
|
|
99
|
+
sa.PrimaryKeyConstraint(
|
|
100
|
+
"domain_id", "kind_id", "record_key", "previous_key", name=op.f("pk_vault_lineage")
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
op.create_index(
|
|
104
|
+
op.f("ix_vault_lineage_domain_id_kind_id_previous_key"),
|
|
105
|
+
LINEAGE,
|
|
106
|
+
["domain_id", "kind_id", "previous_key"],
|
|
107
|
+
)
|
|
108
|
+
_isolate(LINEAGE)
|
|
109
|
+
|
|
110
|
+
op.create_table(
|
|
111
|
+
SECRETS,
|
|
112
|
+
sa.Column("name", sa.Text(), nullable=False),
|
|
113
|
+
sa.Column("secret", sa.Text(), nullable=False),
|
|
114
|
+
sa.Column(
|
|
115
|
+
"created_at",
|
|
116
|
+
sa.DateTime(timezone=True),
|
|
117
|
+
server_default=sa.text("now()"),
|
|
118
|
+
nullable=False,
|
|
119
|
+
),
|
|
120
|
+
sa.PrimaryKeyConstraint("name", name=op.f("pk_installation_secrets")),
|
|
121
|
+
)
|
|
122
|
+
# 244 random bits from the server's cryptographic generator (gen_random_uuid, Postgres 13+).
|
|
123
|
+
op.execute(
|
|
124
|
+
f"INSERT INTO {SECRETS} (name, secret) "
|
|
125
|
+
"VALUES ('digest', replace(gen_random_uuid()::text || gen_random_uuid()::text, '-', ''))"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def downgrade() -> None:
|
|
130
|
+
bind = op.get_bind()
|
|
131
|
+
# Row-level security is forced on the owner too: count the rows of every Domain.
|
|
132
|
+
bind.execute(sa.text("SELECT set_config('lorebase.bypass_rls', 'on', true)"))
|
|
133
|
+
for table in (TOMBSTONES, LINEAGE):
|
|
134
|
+
stored = bind.execute(sa.text(f"SELECT count(*) FROM {table}")).scalar_one()
|
|
135
|
+
if stored:
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
f"{stored} row(s) in {table}: without them erased values could be stored again "
|
|
138
|
+
"and erasure would miss earlier keys; keep this revision"
|
|
139
|
+
)
|
|
140
|
+
op.drop_table(SECRETS)
|
|
141
|
+
op.drop_index(op.f("ix_vault_lineage_domain_id_kind_id_previous_key"), table_name=LINEAGE)
|
|
142
|
+
op.drop_table(LINEAGE)
|
|
143
|
+
op.drop_index(op.f("ix_vault_tombstones_domain_id_fingerprint"), table_name=TOMBSTONES)
|
|
144
|
+
op.drop_table(TOMBSTONES)
|
|
145
|
+
op.drop_column("vault", "fingerprint_key")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Deferred Operation events.
|
|
2
|
+
|
|
3
|
+
`operation_deferred_events` holds the `operation.<side effect>` events of executions committed
|
|
4
|
+
on a branch whose Proposal is their review (agent writes). The merge that brings the execution
|
|
5
|
+
into its target writes them to the outbox and deletes them, in its transaction. Under the
|
|
6
|
+
`domain_isolation` policy like every table with a `domain_id`.
|
|
7
|
+
|
|
8
|
+
Revision ID: 0028
|
|
9
|
+
Revises: 0027
|
|
10
|
+
Create Date: 2026-10-27 10:00:00.000000
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
import sqlalchemy as sa
|
|
18
|
+
from alembic import op
|
|
19
|
+
from sqlalchemy.dialects import postgresql
|
|
20
|
+
|
|
21
|
+
revision: str = "0028"
|
|
22
|
+
down_revision: str | Sequence[str] | None = "0027"
|
|
23
|
+
branch_labels: str | Sequence[str] | None = None
|
|
24
|
+
depends_on: str | Sequence[str] | None = None
|
|
25
|
+
|
|
26
|
+
DEFERRED = "operation_deferred_events"
|
|
27
|
+
|
|
28
|
+
DOMAIN_VISIBLE = (
|
|
29
|
+
"current_setting('lorebase.bypass_rls', true) = 'on' "
|
|
30
|
+
"OR domain_id = ANY (string_to_array(current_setting('lorebase.domain_ids', true), ','))"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def upgrade() -> None:
|
|
35
|
+
op.create_table(
|
|
36
|
+
DEFERRED,
|
|
37
|
+
sa.Column("id", sa.BigInteger(), sa.Identity(always=True), nullable=False),
|
|
38
|
+
sa.Column("domain_id", sa.Text(), nullable=False),
|
|
39
|
+
sa.Column("branch", sa.Text(), nullable=False),
|
|
40
|
+
sa.Column("revision_id", sa.LargeBinary(), nullable=False),
|
|
41
|
+
sa.Column("invocation_id", sa.Text(), nullable=False),
|
|
42
|
+
sa.Column("event_type", sa.Text(), nullable=False),
|
|
43
|
+
sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
44
|
+
sa.Column(
|
|
45
|
+
"created_at",
|
|
46
|
+
sa.DateTime(timezone=True),
|
|
47
|
+
server_default=sa.text("now()"),
|
|
48
|
+
nullable=False,
|
|
49
|
+
),
|
|
50
|
+
sa.ForeignKeyConstraint(
|
|
51
|
+
["domain_id"],
|
|
52
|
+
["domains.id"],
|
|
53
|
+
name=op.f("fk_operation_deferred_events_domain_id_domains"),
|
|
54
|
+
ondelete="CASCADE",
|
|
55
|
+
),
|
|
56
|
+
sa.PrimaryKeyConstraint("id", name=op.f("pk_operation_deferred_events")),
|
|
57
|
+
)
|
|
58
|
+
op.create_index(
|
|
59
|
+
op.f("ix_operation_deferred_events_domain_id_branch"), DEFERRED, ["domain_id", "branch"]
|
|
60
|
+
)
|
|
61
|
+
op.execute(f"ALTER TABLE {DEFERRED} ENABLE ROW LEVEL SECURITY")
|
|
62
|
+
op.execute(f"ALTER TABLE {DEFERRED} FORCE ROW LEVEL SECURITY")
|
|
63
|
+
op.execute(
|
|
64
|
+
f"CREATE POLICY domain_isolation ON {DEFERRED} "
|
|
65
|
+
f"USING ({DOMAIN_VISIBLE}) WITH CHECK ({DOMAIN_VISIBLE})"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def downgrade() -> None:
|
|
70
|
+
op.drop_index(op.f("ix_operation_deferred_events_domain_id_branch"), table_name=DEFERRED)
|
|
71
|
+
op.drop_table(DEFERRED)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Agent trace session rows.
|
|
2
|
+
|
|
3
|
+
`agent_trace_sessions` holds the number of steps of each session and actor: the trace writer
|
|
4
|
+
raises it under the row lock in the transaction writing the steps, so the per-session cap holds
|
|
5
|
+
across Keepers. `agent_trace_session_domains` summarizes the steps of a session per Domain
|
|
6
|
+
(calls, notes, first and last step, tools, Proposals); sessions are listed from it instead of
|
|
7
|
+
grouping the steps of the retention period. Both are filled from the existing steps.
|
|
8
|
+
|
|
9
|
+
Revision ID: 0029
|
|
10
|
+
Revises: 0028
|
|
11
|
+
Create Date: 2026-10-27 11:00:00.000000
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
|
|
18
|
+
import sqlalchemy as sa
|
|
19
|
+
from alembic import op
|
|
20
|
+
from sqlalchemy.dialects import postgresql
|
|
21
|
+
|
|
22
|
+
revision: str = "0029"
|
|
23
|
+
down_revision: str | Sequence[str] | None = "0028"
|
|
24
|
+
branch_labels: str | Sequence[str] | None = None
|
|
25
|
+
depends_on: str | Sequence[str] | None = None
|
|
26
|
+
|
|
27
|
+
SESSIONS = "agent_trace_sessions"
|
|
28
|
+
DOMAINS = "agent_trace_session_domains"
|
|
29
|
+
|
|
30
|
+
BYPASS = "current_setting('lorebase.bypass_rls', true) = 'on'"
|
|
31
|
+
|
|
32
|
+
DOMAIN_VISIBLE = (
|
|
33
|
+
"current_setting('lorebase.bypass_rls', true) = 'on' "
|
|
34
|
+
"OR domain_id = ANY (string_to_array(current_setting('lorebase.domain_ids', true), ','))"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def upgrade() -> None:
|
|
39
|
+
op.create_table(
|
|
40
|
+
SESSIONS,
|
|
41
|
+
sa.Column("actor", sa.Text(), nullable=False),
|
|
42
|
+
sa.Column("session_id", sa.Text(), nullable=False),
|
|
43
|
+
sa.Column("steps", sa.Integer(), nullable=False),
|
|
44
|
+
sa.Column("last_at", sa.DateTime(timezone=True), nullable=False),
|
|
45
|
+
sa.PrimaryKeyConstraint("actor", "session_id", name=op.f("pk_agent_trace_sessions")),
|
|
46
|
+
)
|
|
47
|
+
op.create_table(
|
|
48
|
+
DOMAINS,
|
|
49
|
+
sa.Column("id", sa.BigInteger(), sa.Identity(always=True), nullable=False),
|
|
50
|
+
sa.Column("actor", sa.Text(), nullable=False),
|
|
51
|
+
sa.Column("session_id", sa.Text(), nullable=False),
|
|
52
|
+
sa.Column("domain_id", sa.Text(), nullable=True),
|
|
53
|
+
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
|
|
54
|
+
sa.Column("last_at", sa.DateTime(timezone=True), nullable=False),
|
|
55
|
+
sa.Column("calls", sa.Integer(), nullable=False),
|
|
56
|
+
sa.Column("notes", sa.Integer(), nullable=False),
|
|
57
|
+
sa.Column("inferred", sa.Boolean(), nullable=False),
|
|
58
|
+
sa.Column("tools", postgresql.ARRAY(sa.Text()), nullable=False),
|
|
59
|
+
sa.Column("proposals", postgresql.ARRAY(sa.Text()), nullable=False),
|
|
60
|
+
sa.PrimaryKeyConstraint("id", name=op.f("pk_agent_trace_session_domains")),
|
|
61
|
+
sa.UniqueConstraint(
|
|
62
|
+
"actor",
|
|
63
|
+
"session_id",
|
|
64
|
+
"domain_id",
|
|
65
|
+
name=op.f("uq_agent_trace_session_domains_actor_session_id_domain_id"),
|
|
66
|
+
postgresql_nulls_not_distinct=True,
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
op.create_index(
|
|
70
|
+
op.f("ix_agent_trace_session_domains_actor_last_at"), DOMAINS, ["actor", "last_at"]
|
|
71
|
+
)
|
|
72
|
+
op.create_index(
|
|
73
|
+
op.f("ix_agent_trace_session_domains_domain_id_last_at"), DOMAINS, ["domain_id", "last_at"]
|
|
74
|
+
)
|
|
75
|
+
# No Domain: readable and writable only with row-level security bypassed, as the trace
|
|
76
|
+
# repository does.
|
|
77
|
+
op.execute(f"ALTER TABLE {SESSIONS} ENABLE ROW LEVEL SECURITY")
|
|
78
|
+
op.execute(f"ALTER TABLE {SESSIONS} FORCE ROW LEVEL SECURITY")
|
|
79
|
+
op.execute(f"CREATE POLICY bypass_only ON {SESSIONS} USING ({BYPASS}) WITH CHECK ({BYPASS})")
|
|
80
|
+
op.execute(f"ALTER TABLE {DOMAINS} ENABLE ROW LEVEL SECURITY")
|
|
81
|
+
op.execute(f"ALTER TABLE {DOMAINS} FORCE ROW LEVEL SECURITY")
|
|
82
|
+
op.execute(
|
|
83
|
+
f"CREATE POLICY domain_isolation ON {DOMAINS} "
|
|
84
|
+
f"USING ({DOMAIN_VISIBLE}) WITH CHECK ({DOMAIN_VISIBLE})"
|
|
85
|
+
)
|
|
86
|
+
op.execute("SELECT set_config('lorebase.bypass_rls', 'on', true)")
|
|
87
|
+
op.execute(
|
|
88
|
+
f"INSERT INTO {SESSIONS} (actor, session_id, steps, last_at) "
|
|
89
|
+
"SELECT actor, session_id, count(*), max(at) FROM agent_trace_steps "
|
|
90
|
+
"GROUP BY actor, session_id"
|
|
91
|
+
)
|
|
92
|
+
op.execute(
|
|
93
|
+
f"INSERT INTO {DOMAINS} (actor, session_id, domain_id, started_at, last_at, calls, "
|
|
94
|
+
"notes, inferred, tools, proposals) "
|
|
95
|
+
"SELECT actor, session_id, domain_id, min(at), max(at), "
|
|
96
|
+
"count(*) FILTER (WHERE type = 'call'), count(*) FILTER (WHERE type = 'note'), "
|
|
97
|
+
"bool_or(inferred), "
|
|
98
|
+
"array_remove(array_agg(DISTINCT tool), NULL), "
|
|
99
|
+
"array_remove(array_agg(DISTINCT proposal_id), NULL) "
|
|
100
|
+
"FROM agent_trace_steps GROUP BY actor, session_id, domain_id"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def downgrade() -> None:
|
|
105
|
+
op.drop_index(op.f("ix_agent_trace_session_domains_domain_id_last_at"), table_name=DOMAINS)
|
|
106
|
+
op.drop_index(op.f("ix_agent_trace_session_domains_actor_last_at"), table_name=DOMAINS)
|
|
107
|
+
op.drop_table(DOMAINS)
|
|
108
|
+
op.drop_table(SESSIONS)
|