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,150 @@
|
|
|
1
|
+
"""Per-consumer delivery state of the outbox; webhooks may be cleared for sensitive values.
|
|
2
|
+
|
|
3
|
+
- `outbox_consumers`: the consumers of the outbox, `webhooks` (the webhook worker) registered
|
|
4
|
+
here. Global: a consumer reads the events of every Domain.
|
|
5
|
+
- `outbox_pending`: one row per event a consumer has not handed off yet. A trigger on `outbox`
|
|
6
|
+
adds a row for every registered consumer in the transaction that writes the event, whatever
|
|
7
|
+
writes it; the consumer deletes its row in the transaction that hands the event off. Rows go
|
|
8
|
+
with their event and their consumer. Domain-scoped with row-level security like `outbox`.
|
|
9
|
+
- `outbox.delivered_at` and `outbox.attempts`, the state of the single consumer they served,
|
|
10
|
+
are dropped: events not yet handed off to webhooks become pending rows of `webhooks`.
|
|
11
|
+
Pruning finds events by `created_at` (new index).
|
|
12
|
+
- `webhooks.clearances`: sensitivity tags whose values a webhook receives in clear (default:
|
|
13
|
+
none, every sensitive value masked).
|
|
14
|
+
|
|
15
|
+
Revision ID: 0020
|
|
16
|
+
Revises: 0019
|
|
17
|
+
Create Date: 2026-10-25 10:00:00.000000
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
|
|
24
|
+
import sqlalchemy as sa
|
|
25
|
+
from alembic import op
|
|
26
|
+
from sqlalchemy.dialects import postgresql
|
|
27
|
+
|
|
28
|
+
revision: str = "0020"
|
|
29
|
+
down_revision: str | Sequence[str] | None = "0019"
|
|
30
|
+
branch_labels: str | Sequence[str] | None = None
|
|
31
|
+
depends_on: str | Sequence[str] | None = None
|
|
32
|
+
|
|
33
|
+
DOMAIN_VISIBLE = (
|
|
34
|
+
"current_setting('lorebase.bypass_rls', true) = 'on' "
|
|
35
|
+
"OR domain_id = ANY (string_to_array(current_setting('lorebase.domain_ids', true), ','))"
|
|
36
|
+
)
|
|
37
|
+
WEBHOOKS_CONSUMER = "webhooks"
|
|
38
|
+
|
|
39
|
+
FAN_OUT = """
|
|
40
|
+
CREATE FUNCTION outbox_fan_out() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
41
|
+
BEGIN
|
|
42
|
+
INSERT INTO outbox_pending (consumer, event_id, domain_id)
|
|
43
|
+
SELECT c.name, NEW.id, NEW.domain_id FROM outbox_consumers c;
|
|
44
|
+
RETURN NULL;
|
|
45
|
+
END
|
|
46
|
+
$$
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def upgrade() -> None:
|
|
51
|
+
bind = op.get_bind()
|
|
52
|
+
# Row-level security is forced on the owner too: move the rows of every Domain.
|
|
53
|
+
bind.execute(sa.text("SELECT set_config('lorebase.bypass_rls', 'on', true)"))
|
|
54
|
+
op.create_table(
|
|
55
|
+
"outbox_consumers",
|
|
56
|
+
sa.Column("name", sa.Text(), nullable=False),
|
|
57
|
+
sa.Column(
|
|
58
|
+
"created_at",
|
|
59
|
+
sa.DateTime(timezone=True),
|
|
60
|
+
server_default=sa.text("now()"),
|
|
61
|
+
nullable=False,
|
|
62
|
+
),
|
|
63
|
+
sa.PrimaryKeyConstraint("name", name=op.f("pk_outbox_consumers")),
|
|
64
|
+
)
|
|
65
|
+
op.create_table(
|
|
66
|
+
"outbox_pending",
|
|
67
|
+
sa.Column("consumer", sa.Text(), nullable=False),
|
|
68
|
+
sa.Column("event_id", sa.BigInteger(), nullable=False),
|
|
69
|
+
sa.Column("domain_id", sa.Text(), nullable=False),
|
|
70
|
+
sa.ForeignKeyConstraint(
|
|
71
|
+
["consumer"],
|
|
72
|
+
["outbox_consumers.name"],
|
|
73
|
+
name=op.f("fk_outbox_pending_consumer_outbox_consumers"),
|
|
74
|
+
ondelete="CASCADE",
|
|
75
|
+
),
|
|
76
|
+
sa.ForeignKeyConstraint(
|
|
77
|
+
["event_id"],
|
|
78
|
+
["outbox.id"],
|
|
79
|
+
name=op.f("fk_outbox_pending_event_id_outbox"),
|
|
80
|
+
ondelete="CASCADE",
|
|
81
|
+
),
|
|
82
|
+
sa.PrimaryKeyConstraint("consumer", "event_id", name=op.f("pk_outbox_pending")),
|
|
83
|
+
)
|
|
84
|
+
op.create_index(op.f("ix_outbox_pending_event_id"), "outbox_pending", ["event_id"])
|
|
85
|
+
op.execute("ALTER TABLE outbox_pending ENABLE ROW LEVEL SECURITY")
|
|
86
|
+
op.execute("ALTER TABLE outbox_pending FORCE ROW LEVEL SECURITY")
|
|
87
|
+
op.execute(
|
|
88
|
+
"CREATE POLICY domain_isolation ON outbox_pending "
|
|
89
|
+
f"USING ({DOMAIN_VISIBLE}) WITH CHECK ({DOMAIN_VISIBLE})"
|
|
90
|
+
)
|
|
91
|
+
bind.execute(
|
|
92
|
+
sa.text("INSERT INTO outbox_consumers (name) VALUES (:name)"),
|
|
93
|
+
{"name": WEBHOOKS_CONSUMER},
|
|
94
|
+
)
|
|
95
|
+
bind.execute(
|
|
96
|
+
sa.text(
|
|
97
|
+
"INSERT INTO outbox_pending (consumer, event_id, domain_id) "
|
|
98
|
+
"SELECT :name, id, domain_id FROM outbox WHERE delivered_at IS NULL"
|
|
99
|
+
),
|
|
100
|
+
{"name": WEBHOOKS_CONSUMER},
|
|
101
|
+
)
|
|
102
|
+
op.execute(FAN_OUT)
|
|
103
|
+
op.execute(
|
|
104
|
+
"CREATE TRIGGER outbox_fan_out AFTER INSERT ON outbox "
|
|
105
|
+
"FOR EACH ROW EXECUTE FUNCTION outbox_fan_out()"
|
|
106
|
+
)
|
|
107
|
+
op.drop_index("ix_outbox_undelivered", table_name="outbox")
|
|
108
|
+
op.drop_column("outbox", "delivered_at")
|
|
109
|
+
op.drop_column("outbox", "attempts")
|
|
110
|
+
op.create_index(op.f("ix_outbox_created_at"), "outbox", ["created_at"])
|
|
111
|
+
op.add_column(
|
|
112
|
+
"webhooks",
|
|
113
|
+
sa.Column(
|
|
114
|
+
"clearances",
|
|
115
|
+
postgresql.JSONB(astext_type=sa.Text()),
|
|
116
|
+
server_default=sa.text("'[]'::jsonb"),
|
|
117
|
+
nullable=False,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def downgrade() -> None:
|
|
123
|
+
bind = op.get_bind()
|
|
124
|
+
bind.execute(sa.text("SELECT set_config('lorebase.bypass_rls', 'on', true)"))
|
|
125
|
+
op.drop_column("webhooks", "clearances")
|
|
126
|
+
op.drop_index(op.f("ix_outbox_created_at"), table_name="outbox")
|
|
127
|
+
op.add_column(
|
|
128
|
+
"outbox",
|
|
129
|
+
sa.Column("attempts", sa.Integer(), server_default=sa.text("0"), nullable=False),
|
|
130
|
+
)
|
|
131
|
+
op.add_column("outbox", sa.Column("delivered_at", sa.DateTime(timezone=True), nullable=True))
|
|
132
|
+
# Events the webhook worker has handed off (or that it never had pending) are delivered.
|
|
133
|
+
bind.execute(
|
|
134
|
+
sa.text(
|
|
135
|
+
"UPDATE outbox o SET delivered_at = now() WHERE NOT EXISTS ("
|
|
136
|
+
"SELECT 1 FROM outbox_pending p WHERE p.consumer = :name AND p.event_id = o.id)"
|
|
137
|
+
),
|
|
138
|
+
{"name": WEBHOOKS_CONSUMER},
|
|
139
|
+
)
|
|
140
|
+
op.create_index(
|
|
141
|
+
"ix_outbox_undelivered",
|
|
142
|
+
"outbox",
|
|
143
|
+
["id"],
|
|
144
|
+
unique=False,
|
|
145
|
+
postgresql_where=sa.text("delivered_at IS NULL"),
|
|
146
|
+
)
|
|
147
|
+
op.execute("DROP TRIGGER outbox_fan_out ON outbox")
|
|
148
|
+
op.execute("DROP FUNCTION outbox_fan_out()")
|
|
149
|
+
op.drop_table("outbox_pending")
|
|
150
|
+
op.drop_table("outbox_consumers")
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Schema suggestions aggregated from document extraction (design 9.2 step 10).
|
|
2
|
+
|
|
3
|
+
`schema_suggestions` holds, per Domain, the new Kinds, Attributes and Relations extraction found
|
|
4
|
+
outside the schema, one row per `fingerprint`, with support, examples and sample values, and
|
|
5
|
+
the reviewer's decision. Domain-scoped with row-level security like every table of 0001.
|
|
6
|
+
|
|
7
|
+
Revision ID: 0021
|
|
8
|
+
Revises: 0020
|
|
9
|
+
Create Date: 2026-10-25 10:00:00.000000
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
import sqlalchemy as sa
|
|
17
|
+
from alembic import op
|
|
18
|
+
from sqlalchemy.dialects import postgresql
|
|
19
|
+
|
|
20
|
+
revision: str = "0021"
|
|
21
|
+
down_revision: str | Sequence[str] | None = "0020"
|
|
22
|
+
branch_labels: str | Sequence[str] | None = None
|
|
23
|
+
depends_on: str | Sequence[str] | None = None
|
|
24
|
+
|
|
25
|
+
DOMAIN_VISIBLE = (
|
|
26
|
+
"current_setting('lorebase.bypass_rls', true) = 'on' "
|
|
27
|
+
"OR domain_id = ANY (string_to_array(current_setting('lorebase.domain_ids', true), ','))"
|
|
28
|
+
)
|
|
29
|
+
_JSON = postgresql.JSONB(astext_type=sa.Text())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _json(name: str, default: str) -> sa.Column[object]:
|
|
33
|
+
return sa.Column(name, _JSON, server_default=sa.text(f"'{default}'::jsonb"), nullable=False)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def upgrade() -> None:
|
|
37
|
+
op.create_table(
|
|
38
|
+
"schema_suggestions",
|
|
39
|
+
sa.Column("domain_id", sa.Text(), nullable=False),
|
|
40
|
+
sa.Column("id", sa.Text(), nullable=False),
|
|
41
|
+
sa.Column("fingerprint", sa.Text(), nullable=False),
|
|
42
|
+
sa.Column("suggestion_type", sa.Text(), nullable=False),
|
|
43
|
+
sa.Column("name", sa.Text(), nullable=False),
|
|
44
|
+
sa.Column("kind_ref", sa.Text(), nullable=True),
|
|
45
|
+
sa.Column("target_ref", sa.Text(), nullable=True),
|
|
46
|
+
_json("labels", "[]"),
|
|
47
|
+
sa.Column("support", sa.Integer(), server_default=sa.text("0"), nullable=False),
|
|
48
|
+
sa.Column("confidence", sa.Float(), server_default=sa.text("0"), nullable=False),
|
|
49
|
+
_json("observations", "{}"),
|
|
50
|
+
_json("examples", "[]"),
|
|
51
|
+
_json("samples", "[]"),
|
|
52
|
+
_json("sources", "[]"),
|
|
53
|
+
sa.Column("status", sa.Text(), server_default=sa.text("'pending'"), nullable=False),
|
|
54
|
+
sa.Column("decided_by", sa.Text(), nullable=True),
|
|
55
|
+
sa.Column("decided_at", sa.DateTime(timezone=True), nullable=True),
|
|
56
|
+
sa.Column("proposal_id", sa.Text(), nullable=True),
|
|
57
|
+
sa.Column("branch", sa.Text(), nullable=True),
|
|
58
|
+
sa.Column("last_job_id", sa.Text(), nullable=True),
|
|
59
|
+
sa.Column(
|
|
60
|
+
"created_at",
|
|
61
|
+
sa.DateTime(timezone=True),
|
|
62
|
+
server_default=sa.text("now()"),
|
|
63
|
+
nullable=False,
|
|
64
|
+
),
|
|
65
|
+
sa.Column(
|
|
66
|
+
"updated_at",
|
|
67
|
+
sa.DateTime(timezone=True),
|
|
68
|
+
server_default=sa.text("now()"),
|
|
69
|
+
nullable=False,
|
|
70
|
+
),
|
|
71
|
+
sa.CheckConstraint(
|
|
72
|
+
"suggestion_type IN ('kind', 'attribute', 'relation')",
|
|
73
|
+
name=op.f("ck_schema_suggestions_suggestion_type"),
|
|
74
|
+
),
|
|
75
|
+
sa.CheckConstraint(
|
|
76
|
+
"status IN ('pending', 'accepted', 'rejected')",
|
|
77
|
+
name=op.f("ck_schema_suggestions_status"),
|
|
78
|
+
),
|
|
79
|
+
sa.ForeignKeyConstraint(
|
|
80
|
+
["domain_id"],
|
|
81
|
+
["domains.id"],
|
|
82
|
+
name=op.f("fk_schema_suggestions_domain_id_domains"),
|
|
83
|
+
ondelete="CASCADE",
|
|
84
|
+
),
|
|
85
|
+
sa.PrimaryKeyConstraint("domain_id", "id", name=op.f("pk_schema_suggestions")),
|
|
86
|
+
)
|
|
87
|
+
op.create_index(
|
|
88
|
+
op.f("ix_schema_suggestions_domain_id_fingerprint"),
|
|
89
|
+
"schema_suggestions",
|
|
90
|
+
["domain_id", "fingerprint"],
|
|
91
|
+
unique=True,
|
|
92
|
+
)
|
|
93
|
+
op.create_index(
|
|
94
|
+
op.f("ix_schema_suggestions_domain_id_status_support"),
|
|
95
|
+
"schema_suggestions",
|
|
96
|
+
["domain_id", "status", "support"],
|
|
97
|
+
unique=False,
|
|
98
|
+
)
|
|
99
|
+
op.execute("ALTER TABLE schema_suggestions ENABLE ROW LEVEL SECURITY")
|
|
100
|
+
op.execute("ALTER TABLE schema_suggestions FORCE ROW LEVEL SECURITY")
|
|
101
|
+
op.execute(
|
|
102
|
+
"CREATE POLICY domain_isolation ON schema_suggestions "
|
|
103
|
+
f"USING ({DOMAIN_VISIBLE}) WITH CHECK ({DOMAIN_VISIBLE})"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def downgrade() -> None:
|
|
108
|
+
op.drop_index(
|
|
109
|
+
op.f("ix_schema_suggestions_domain_id_status_support"), table_name="schema_suggestions"
|
|
110
|
+
)
|
|
111
|
+
op.drop_index(
|
|
112
|
+
op.f("ix_schema_suggestions_domain_id_fingerprint"), table_name="schema_suggestions"
|
|
113
|
+
)
|
|
114
|
+
op.drop_table("schema_suggestions")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Read audit log and shared rate limit buckets.
|
|
2
|
+
|
|
3
|
+
`audit_read_log` holds the records of reads (`keeper.auth.audit.reads`) in hash chains of its
|
|
4
|
+
own, one per Domain plus an installation-wide one, computed like those of `audit_log`. It is
|
|
5
|
+
append-only for every role but its owner, like `audit_log` (revision 0015), and under the
|
|
6
|
+
`domain_isolation` policy.
|
|
7
|
+
|
|
8
|
+
`rate_limit_buckets` holds the token buckets of `keeper.limits.rate.backend: postgres`, shared
|
|
9
|
+
by every Keeper. It is UNLOGGED: no WAL, nothing replicated, emptied after a crash of the
|
|
10
|
+
server (buckets start full again).
|
|
11
|
+
|
|
12
|
+
Revision ID: 0022
|
|
13
|
+
Revises: 0021
|
|
14
|
+
Create Date: 2026-10-25 09:00:00.000000
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
|
|
21
|
+
import sqlalchemy as sa
|
|
22
|
+
from alembic import op
|
|
23
|
+
from sqlalchemy.dialects import postgresql
|
|
24
|
+
|
|
25
|
+
revision: str = "0022"
|
|
26
|
+
down_revision: str | Sequence[str] | None = "0021"
|
|
27
|
+
branch_labels: str | Sequence[str] | None = None
|
|
28
|
+
depends_on: str | Sequence[str] | None = None
|
|
29
|
+
|
|
30
|
+
READS = "audit_read_log"
|
|
31
|
+
BUCKETS = "rate_limit_buckets"
|
|
32
|
+
REVOKED = "UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER"
|
|
33
|
+
|
|
34
|
+
DOMAIN_VISIBLE = (
|
|
35
|
+
"current_setting('lorebase.bypass_rls', true) = 'on' "
|
|
36
|
+
"OR domain_id = ANY (string_to_array(current_setting('lorebase.domain_ids', true), ','))"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _application_role() -> str | None:
|
|
41
|
+
config = op.get_context().config
|
|
42
|
+
if config is None or "database_settings" not in config.attributes:
|
|
43
|
+
raise RuntimeError("run this migration through the Lorebase Alembic environment")
|
|
44
|
+
role: str | None = config.attributes["database_settings"].application_role
|
|
45
|
+
return role
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _quoted(role: str) -> str:
|
|
49
|
+
quoted: str = op.get_bind().execute(sa.text("SELECT quote_ident(:r)"), {"r": role}).scalar_one()
|
|
50
|
+
return quoted
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _holders() -> list[str]:
|
|
54
|
+
"""Roles other than the owner holding a revoked privilege on the read log (default
|
|
55
|
+
privileges of the owner grant them at creation)."""
|
|
56
|
+
return list(
|
|
57
|
+
op.get_bind()
|
|
58
|
+
.execute(
|
|
59
|
+
sa.text(
|
|
60
|
+
"SELECT DISTINCT quote_ident(r.rolname) FROM pg_class c "
|
|
61
|
+
"CROSS JOIN LATERAL aclexplode(c.relacl) a "
|
|
62
|
+
"JOIN pg_roles r ON r.oid = a.grantee "
|
|
63
|
+
"WHERE c.oid = to_regclass(:t) AND a.grantee <> c.relowner "
|
|
64
|
+
"AND a.privilege_type IN "
|
|
65
|
+
"('UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER') ORDER BY 1"
|
|
66
|
+
),
|
|
67
|
+
{"t": READS},
|
|
68
|
+
)
|
|
69
|
+
.scalars()
|
|
70
|
+
.all()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def upgrade() -> None:
|
|
75
|
+
op.create_table(
|
|
76
|
+
READS,
|
|
77
|
+
sa.Column("seq", sa.BigInteger(), sa.Identity(always=True), nullable=False),
|
|
78
|
+
sa.Column("domain_id", sa.Text(), nullable=True),
|
|
79
|
+
sa.Column(
|
|
80
|
+
"at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False
|
|
81
|
+
),
|
|
82
|
+
sa.Column("actor", sa.Text(), nullable=False),
|
|
83
|
+
sa.Column("action", sa.Text(), nullable=False),
|
|
84
|
+
sa.Column("target", sa.Text(), nullable=True),
|
|
85
|
+
sa.Column(
|
|
86
|
+
"details",
|
|
87
|
+
postgresql.JSONB(astext_type=sa.Text()),
|
|
88
|
+
server_default=sa.text("'{}'::jsonb"),
|
|
89
|
+
nullable=False,
|
|
90
|
+
),
|
|
91
|
+
sa.Column("prev_hash", sa.LargeBinary(), nullable=True),
|
|
92
|
+
sa.Column("entry_hash", sa.LargeBinary(), nullable=False),
|
|
93
|
+
sa.CheckConstraint(
|
|
94
|
+
"octet_length(entry_hash) = 32", name=op.f("ck_audit_read_log_entry_hash_h32")
|
|
95
|
+
),
|
|
96
|
+
sa.PrimaryKeyConstraint("seq", name=op.f("pk_audit_read_log")),
|
|
97
|
+
)
|
|
98
|
+
op.create_index(op.f("ix_audit_read_log_domain_id_seq"), READS, ["domain_id", "seq"])
|
|
99
|
+
op.create_index(op.f("ix_audit_read_log_entry_hash"), READS, ["entry_hash"], unique=True)
|
|
100
|
+
op.execute(f"ALTER TABLE {READS} ENABLE ROW LEVEL SECURITY")
|
|
101
|
+
op.execute(f"ALTER TABLE {READS} FORCE ROW LEVEL SECURITY")
|
|
102
|
+
op.execute(
|
|
103
|
+
f"CREATE POLICY domain_isolation ON {READS} "
|
|
104
|
+
f"USING ({DOMAIN_VISIBLE}) WITH CHECK ({DOMAIN_VISIBLE})"
|
|
105
|
+
)
|
|
106
|
+
op.execute(f"REVOKE {REVOKED} ON TABLE {READS} FROM PUBLIC")
|
|
107
|
+
roles = set(_holders())
|
|
108
|
+
role = _application_role()
|
|
109
|
+
if role is not None:
|
|
110
|
+
roles.add(_quoted(role))
|
|
111
|
+
for quoted in sorted(roles):
|
|
112
|
+
op.execute(f"REVOKE {REVOKED} ON TABLE {READS} FROM {quoted}")
|
|
113
|
+
|
|
114
|
+
op.create_table(
|
|
115
|
+
BUCKETS,
|
|
116
|
+
sa.Column("key", sa.LargeBinary(), nullable=False),
|
|
117
|
+
sa.Column("tokens", sa.Double(), nullable=False),
|
|
118
|
+
sa.Column("updated", sa.Double(), nullable=False),
|
|
119
|
+
sa.Column("full_at", sa.Double(), nullable=False),
|
|
120
|
+
sa.Column("granted", sa.Double(), nullable=False),
|
|
121
|
+
sa.CheckConstraint("octet_length(key) = 32", name=op.f("ck_rate_limit_buckets_key_h32")),
|
|
122
|
+
sa.PrimaryKeyConstraint("key", name=op.f("pk_rate_limit_buckets")),
|
|
123
|
+
prefixes=["UNLOGGED"],
|
|
124
|
+
)
|
|
125
|
+
op.create_index(op.f("ix_rate_limit_buckets_full_at"), BUCKETS, ["full_at"])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def downgrade() -> None:
|
|
129
|
+
op.drop_index(op.f("ix_rate_limit_buckets_full_at"), table_name=BUCKETS)
|
|
130
|
+
op.drop_table(BUCKETS)
|
|
131
|
+
op.drop_index(op.f("ix_audit_read_log_entry_hash"), table_name=READS)
|
|
132
|
+
op.drop_index(op.f("ix_audit_read_log_domain_id_seq"), table_name=READS)
|
|
133
|
+
op.drop_table(READS)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Agent trace steps.
|
|
2
|
+
|
|
3
|
+
`agent_trace_steps` holds the steps of traced agent sessions (`keeper.trace`): one row per
|
|
4
|
+
traced request and per note an agent attached to its session. Debugging data, written best
|
|
5
|
+
effort and pruned after the retention period; under the `domain_isolation` policy like every
|
|
6
|
+
table with a `domain_id`.
|
|
7
|
+
|
|
8
|
+
Revision ID: 0023
|
|
9
|
+
Revises: 0022
|
|
10
|
+
Create Date: 2026-10-26 09: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 = "0023"
|
|
22
|
+
down_revision: str | Sequence[str] | None = "0022"
|
|
23
|
+
branch_labels: str | Sequence[str] | None = None
|
|
24
|
+
depends_on: str | Sequence[str] | None = None
|
|
25
|
+
|
|
26
|
+
STEPS = "agent_trace_steps"
|
|
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 _json(name: str) -> sa.Column[object]:
|
|
35
|
+
return sa.Column(name, postgresql.JSONB(astext_type=sa.Text()), nullable=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def upgrade() -> None:
|
|
39
|
+
op.create_table(
|
|
40
|
+
STEPS,
|
|
41
|
+
sa.Column("seq", sa.BigInteger(), sa.Identity(always=True), nullable=False),
|
|
42
|
+
sa.Column("session_id", sa.Text(), nullable=False),
|
|
43
|
+
sa.Column("actor", sa.Text(), nullable=False),
|
|
44
|
+
sa.Column("principal_type", sa.Text(), nullable=True),
|
|
45
|
+
sa.Column("domain_id", sa.Text(), nullable=True),
|
|
46
|
+
sa.Column(
|
|
47
|
+
"at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False
|
|
48
|
+
),
|
|
49
|
+
sa.Column("type", sa.Text(), nullable=False),
|
|
50
|
+
sa.Column("tool", sa.Text(), nullable=True),
|
|
51
|
+
sa.Column("method", sa.Text(), nullable=True),
|
|
52
|
+
sa.Column("route", sa.Text(), nullable=True),
|
|
53
|
+
sa.Column("status", sa.Integer(), nullable=True),
|
|
54
|
+
sa.Column("latency_ms", sa.Double(), nullable=True),
|
|
55
|
+
sa.Column("cache", sa.Text(), nullable=True),
|
|
56
|
+
sa.Column("branch", sa.Text(), nullable=True),
|
|
57
|
+
_json("query"),
|
|
58
|
+
_json("result"),
|
|
59
|
+
_json("refs"),
|
|
60
|
+
_json("explain"),
|
|
61
|
+
sa.Column("note", sa.Text(), nullable=True),
|
|
62
|
+
sa.Column("proposal_id", sa.Text(), nullable=True),
|
|
63
|
+
sa.Column("request_id", sa.Text(), nullable=True),
|
|
64
|
+
sa.Column("trace_id", sa.Text(), nullable=True),
|
|
65
|
+
sa.Column("inferred", sa.Boolean(), server_default=sa.text("false"), nullable=False),
|
|
66
|
+
sa.Column("fingerprint", sa.Text(), nullable=True),
|
|
67
|
+
sa.CheckConstraint("type IN ('call', 'note')", name=op.f("ck_agent_trace_steps_type")),
|
|
68
|
+
sa.PrimaryKeyConstraint("seq", name=op.f("pk_agent_trace_steps")),
|
|
69
|
+
)
|
|
70
|
+
op.create_index(
|
|
71
|
+
op.f("ix_agent_trace_steps_actor_session_id_seq"), STEPS, ["actor", "session_id", "seq"]
|
|
72
|
+
)
|
|
73
|
+
op.create_index(op.f("ix_agent_trace_steps_session_id_seq"), STEPS, ["session_id", "seq"])
|
|
74
|
+
op.create_index(op.f("ix_agent_trace_steps_domain_id_at"), STEPS, ["domain_id", "at"])
|
|
75
|
+
op.create_index(op.f("ix_agent_trace_steps_at"), STEPS, ["at"])
|
|
76
|
+
op.create_index(
|
|
77
|
+
op.f("ix_agent_trace_steps_actor_fingerprint_at"),
|
|
78
|
+
STEPS,
|
|
79
|
+
["actor", "fingerprint", "at"],
|
|
80
|
+
postgresql_where=sa.text("inferred"),
|
|
81
|
+
)
|
|
82
|
+
op.execute(f"ALTER TABLE {STEPS} ENABLE ROW LEVEL SECURITY")
|
|
83
|
+
op.execute(f"ALTER TABLE {STEPS} FORCE ROW LEVEL SECURITY")
|
|
84
|
+
op.execute(
|
|
85
|
+
f"CREATE POLICY domain_isolation ON {STEPS} "
|
|
86
|
+
f"USING ({DOMAIN_VISIBLE}) WITH CHECK ({DOMAIN_VISIBLE})"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def downgrade() -> None:
|
|
91
|
+
op.drop_index(op.f("ix_agent_trace_steps_actor_fingerprint_at"), table_name=STEPS)
|
|
92
|
+
op.drop_index(op.f("ix_agent_trace_steps_at"), table_name=STEPS)
|
|
93
|
+
op.drop_index(op.f("ix_agent_trace_steps_domain_id_at"), table_name=STEPS)
|
|
94
|
+
op.drop_index(op.f("ix_agent_trace_steps_session_id_seq"), table_name=STEPS)
|
|
95
|
+
op.drop_index(op.f("ix_agent_trace_steps_actor_session_id_seq"), table_name=STEPS)
|
|
96
|
+
op.drop_table(STEPS)
|