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.
Files changed (163) hide show
  1. lorebase_store/__init__.py +1 -0
  2. lorebase_store/_migrations/env.py +106 -0
  3. lorebase_store/_migrations/script.py.mako +27 -0
  4. lorebase_store/_migrations/versions/0001_initial_schema.py +1071 -0
  5. lorebase_store/_migrations/versions/0002_merge_attempt_details.py +74 -0
  6. lorebase_store/_migrations/versions/0003_webhooks.py +127 -0
  7. lorebase_store/_migrations/versions/0004_defer_revision_references.py +38 -0
  8. lorebase_store/_migrations/versions/0005_security_definer_reads.py +396 -0
  9. lorebase_store/_migrations/versions/0006_api_key_usage.py +48 -0
  10. lorebase_store/_migrations/versions/0007_document_extraction.py +154 -0
  11. lorebase_store/_migrations/versions/0008_definer_argument_checks.py +178 -0
  12. lorebase_store/_migrations/versions/0009_api_key_networks.py +33 -0
  13. lorebase_store/_migrations/versions/0010_overlay_branches.py +582 -0
  14. lorebase_store/_migrations/versions/0011_branch_scoped_search_index.py +229 -0
  15. lorebase_store/_migrations/versions/0012_overlay_neighborhood_lookups.py +185 -0
  16. lorebase_store/_migrations/versions/0013_web_sessions.py +72 -0
  17. lorebase_store/_migrations/versions/0014_embedding_jobs.py +70 -0
  18. lorebase_store/_migrations/versions/0015_append_only_audit_log.py +83 -0
  19. lorebase_store/_migrations/versions/0016_sealed_webhook_keys.py +55 -0
  20. lorebase_store/_migrations/versions/0017_background_branch_copies.py +77 -0
  21. lorebase_store/_migrations/versions/0018_search_lexicon.py +477 -0
  22. lorebase_store/_migrations/versions/0019_vault_bindings.py +56 -0
  23. lorebase_store/_migrations/versions/0020_outbox_consumers.py +150 -0
  24. lorebase_store/_migrations/versions/0021_schema_suggestions.py +114 -0
  25. lorebase_store/_migrations/versions/0022_read_audit_and_shared_rate_limits.py +133 -0
  26. lorebase_store/_migrations/versions/0023_agent_trace.py +96 -0
  27. lorebase_store/_migrations/versions/0024_embedding_template_keys.py +205 -0
  28. lorebase_store/_migrations/versions/0025_snapshot_refs.py +35 -0
  29. lorebase_store/_migrations/versions/0026_plain_reverse_link_index.py +112 -0
  30. lorebase_store/_migrations/versions/0027_vault_rotation_and_tombstones.py +145 -0
  31. lorebase_store/_migrations/versions/0028_operation_deferred_events.py +71 -0
  32. lorebase_store/_migrations/versions/0029_agent_trace_sessions.py +108 -0
  33. lorebase_store/_migrations/versions/0030_path_fanout.py +31 -0
  34. lorebase_store/_migrations/versions/0031_branch_builds.py +91 -0
  35. lorebase_store/_migrations/versions/0032_lineage_revisions.py +35 -0
  36. lorebase_store/_migrations/versions/0033_vault_moved_tokens.py +30 -0
  37. lorebase_store/_migrations/versions/0034_inference_endpoint_modes.py +73 -0
  38. lorebase_store/_migrations/versions/0035_search_truncation.py +104 -0
  39. lorebase_store/_migrations/versions/0036_lineage_maps.py +325 -0
  40. lorebase_store/_migrations/versions/0037_bm25_search.py +346 -0
  41. lorebase_store/auth/__init__.py +52 -0
  42. lorebase_store/auth/api_keys.py +161 -0
  43. lorebase_store/auth/audit.py +382 -0
  44. lorebase_store/auth/checkpoints.py +185 -0
  45. lorebase_store/auth/grants.py +161 -0
  46. lorebase_store/auth/principals.py +158 -0
  47. lorebase_store/auth/scope.py +70 -0
  48. lorebase_store/auth/sessions.py +153 -0
  49. lorebase_store/auth/unit.py +53 -0
  50. lorebase_store/auth/usage.py +70 -0
  51. lorebase_store/blobs.py +73 -0
  52. lorebase_store/bm25.py +346 -0
  53. lorebase_store/bulk.py +147 -0
  54. lorebase_store/bundle.py +1830 -0
  55. lorebase_store/changefeed.py +261 -0
  56. lorebase_store/commit/__init__.py +42 -0
  57. lorebase_store/commit/changes.py +67 -0
  58. lorebase_store/commit/errors.py +60 -0
  59. lorebase_store/commit/migrations.py +335 -0
  60. lorebase_store/commit/pipeline.py +754 -0
  61. lorebase_store/commit/schema.py +123 -0
  62. lorebase_store/db.py +58 -0
  63. lorebase_store/definer.py +50 -0
  64. lorebase_store/digests.py +99 -0
  65. lorebase_store/embeddings.py +444 -0
  66. lorebase_store/endpoint_modes.py +59 -0
  67. lorebase_store/extraction/__init__.py +61 -0
  68. lorebase_store/extraction/items.py +187 -0
  69. lorebase_store/extraction/materialize.py +364 -0
  70. lorebase_store/extraction/proposal.py +65 -0
  71. lorebase_store/extraction/review.py +168 -0
  72. lorebase_store/gc.py +970 -0
  73. lorebase_store/indexes.py +1033 -0
  74. lorebase_store/jobs.py +651 -0
  75. lorebase_store/keyring.py +72 -0
  76. lorebase_store/layout.py +335 -0
  77. lorebase_store/materialize/__init__.py +103 -0
  78. lorebase_store/materialize/apply.py +436 -0
  79. lorebase_store/materialize/background.py +1184 -0
  80. lorebase_store/materialize/branch.py +268 -0
  81. lorebase_store/materialize/build.py +536 -0
  82. lorebase_store/materialize/derived.py +540 -0
  83. lorebase_store/materialize/lineage_backfill.py +502 -0
  84. lorebase_store/materialize/links.py +266 -0
  85. lorebase_store/materialize/overlay.py +292 -0
  86. lorebase_store/materialize/paths.py +315 -0
  87. lorebase_store/materialize/rebuild.py +326 -0
  88. lorebase_store/materialize/rows.py +153 -0
  89. lorebase_store/materialize/sensitivity.py +66 -0
  90. lorebase_store/materialize/snapshot.py +205 -0
  91. lorebase_store/materialize/stale.py +444 -0
  92. lorebase_store/materialize/verify.py +321 -0
  93. lorebase_store/merging/__init__.py +39 -0
  94. lorebase_store/merging/attempts.py +334 -0
  95. lorebase_store/merging/lineage.py +123 -0
  96. lorebase_store/merging/service.py +652 -0
  97. lorebase_store/migrations.py +133 -0
  98. lorebase_store/models/__init__.py +136 -0
  99. lorebase_store/models/base.py +64 -0
  100. lorebase_store/models/enums.py +132 -0
  101. lorebase_store/models/governance.py +268 -0
  102. lorebase_store/models/ingestion.py +288 -0
  103. lorebase_store/models/materialized.py +277 -0
  104. lorebase_store/models/proposals.py +195 -0
  105. lorebase_store/models/queues.py +87 -0
  106. lorebase_store/models/tracing.py +116 -0
  107. lorebase_store/models/versioning.py +222 -0
  108. lorebase_store/models/webhooks.py +96 -0
  109. lorebase_store/nodestore.py +384 -0
  110. lorebase_store/objectstore.py +162 -0
  111. lorebase_store/outbox_events.py +166 -0
  112. lorebase_store/profiling/__init__.py +58 -0
  113. lorebase_store/profiling/delimited.py +109 -0
  114. lorebase_store/profiling/postgres.py +211 -0
  115. lorebase_store/profiling/record_files.py +127 -0
  116. lorebase_store/profiling/records.py +92 -0
  117. lorebase_store/profiling/sql_databases.py +298 -0
  118. lorebase_store/ratelimit.py +129 -0
  119. lorebase_store/reads.py +1491 -0
  120. lorebase_store/replicas.py +434 -0
  121. lorebase_store/repositories/__init__.py +152 -0
  122. lorebase_store/repositories/changesets.py +59 -0
  123. lorebase_store/repositories/deferred_events.py +142 -0
  124. lorebase_store/repositories/documents.py +305 -0
  125. lorebase_store/repositories/domains.py +97 -0
  126. lorebase_store/repositories/events.py +205 -0
  127. lorebase_store/repositories/index_view.py +489 -0
  128. lorebase_store/repositories/invocations.py +68 -0
  129. lorebase_store/repositories/materializations.py +276 -0
  130. lorebase_store/repositories/materialized.py +1129 -0
  131. lorebase_store/repositories/merges.py +417 -0
  132. lorebase_store/repositories/proposal_items.py +199 -0
  133. lorebase_store/repositories/proposals.py +184 -0
  134. lorebase_store/repositories/provenance.py +77 -0
  135. lorebase_store/repositories/refs.py +452 -0
  136. lorebase_store/repositories/revisions.py +273 -0
  137. lorebase_store/repositories/scope.py +33 -0
  138. lorebase_store/repositories/source_runs.py +123 -0
  139. lorebase_store/repositories/suggestions.py +638 -0
  140. lorebase_store/runtime.py +87 -0
  141. lorebase_store/schema_cache.py +50 -0
  142. lorebase_store/settings.py +879 -0
  143. lorebase_store/storage.py +1478 -0
  144. lorebase_store/telemetry.py +142 -0
  145. lorebase_store/testing.py +108 -0
  146. lorebase_store/tracing.py +505 -0
  147. lorebase_store/traverse.py +578 -0
  148. lorebase_store/vault/__init__.py +50 -0
  149. lorebase_store/vault/keys.py +216 -0
  150. lorebase_store/vault/quotes.py +133 -0
  151. lorebase_store/vault/rotation.py +209 -0
  152. lorebase_store/vault/settings.py +152 -0
  153. lorebase_store/vault/store.py +987 -0
  154. lorebase_store/vault/transfer.py +558 -0
  155. lorebase_store/webhooks/__init__.py +24 -0
  156. lorebase_store/webhooks/delivery.py +637 -0
  157. lorebase_store/webhooks/keys.py +126 -0
  158. lorebase_store/webhooks/repository.py +142 -0
  159. lorebase_store/webhooks/settings.py +138 -0
  160. lorebase_store/webhooks/signing.py +50 -0
  161. lorebase_store-0.2.3.dist-info/METADATA +13 -0
  162. lorebase_store-0.2.3.dist-info/RECORD +163 -0
  163. 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)