lorebase-keeper 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lorebase_keeper/__init__.py +1 -0
- lorebase_keeper/__main__.py +456 -0
- lorebase_keeper/agents/__init__.py +1 -0
- lorebase_keeper/agents/budget.py +247 -0
- lorebase_keeper/agents/context.py +529 -0
- lorebase_keeper/agents/explain.py +582 -0
- lorebase_keeper/agents/feed.py +259 -0
- lorebase_keeper/agents/markdown.py +219 -0
- lorebase_keeper/agents/pack.py +403 -0
- lorebase_keeper/agents/settings.py +111 -0
- lorebase_keeper/app.py +373 -0
- lorebase_keeper/auth/__init__.py +4 -0
- lorebase_keeper/auth/api_keys.py +127 -0
- lorebase_keeper/auth/audit.py +356 -0
- lorebase_keeper/auth/cache.py +53 -0
- lorebase_keeper/auth/config.py +618 -0
- lorebase_keeper/auth/deps.py +183 -0
- lorebase_keeper/auth/keyring.py +14 -0
- lorebase_keeper/auth/oidc.py +242 -0
- lorebase_keeper/auth/permissions.py +142 -0
- lorebase_keeper/auth/ports.py +212 -0
- lorebase_keeper/auth/principals.py +377 -0
- lorebase_keeper/auth/provider.py +250 -0
- lorebase_keeper/auth/rbac.py +247 -0
- lorebase_keeper/auth/read_audit.py +357 -0
- lorebase_keeper/auth/runtime.py +113 -0
- lorebase_keeper/auth/sessions.py +452 -0
- lorebase_keeper/bootstrap.py +151 -0
- lorebase_keeper/client_address.py +129 -0
- lorebase_keeper/context.py +226 -0
- lorebase_keeper/db.py +93 -0
- lorebase_keeper/deps.py +47 -0
- lorebase_keeper/errors.py +257 -0
- lorebase_keeper/health.py +61 -0
- lorebase_keeper/kindhealth/__init__.py +6 -0
- lorebase_keeper/kindhealth/config.py +51 -0
- lorebase_keeper/kindhealth/markdown.py +163 -0
- lorebase_keeper/kindhealth/models.py +179 -0
- lorebase_keeper/kindhealth/report.py +154 -0
- lorebase_keeper/kindhealth/scan.py +512 -0
- lorebase_keeper/middleware.py +285 -0
- lorebase_keeper/pagination.py +87 -0
- lorebase_keeper/ratelimit/__init__.py +10 -0
- lorebase_keeper/ratelimit/bucket.py +118 -0
- lorebase_keeper/ratelimit/config.py +283 -0
- lorebase_keeper/ratelimit/enforce.py +393 -0
- lorebase_keeper/ratelimit/limiter.py +149 -0
- lorebase_keeper/ratelimit/quotas.py +204 -0
- lorebase_keeper/ratelimit/shared.py +242 -0
- lorebase_keeper/readiness.py +257 -0
- lorebase_keeper/reading/__init__.py +1 -0
- lorebase_keeper/reading/caching.py +251 -0
- lorebase_keeper/reading/compiler.py +678 -0
- lorebase_keeper/reading/cursors.py +56 -0
- lorebase_keeper/reading/diff.py +408 -0
- lorebase_keeper/reading/graph.py +268 -0
- lorebase_keeper/reading/history.py +152 -0
- lorebase_keeper/reading/points.py +410 -0
- lorebase_keeper/reading/problems.py +74 -0
- lorebase_keeper/reading/record_history.py +722 -0
- lorebase_keeper/reading/records.py +780 -0
- lorebase_keeper/reading/requests.py +107 -0
- lorebase_keeper/reading/runtime.py +145 -0
- lorebase_keeper/reading/search.py +708 -0
- lorebase_keeper/reading/sensitivity.py +882 -0
- lorebase_keeper/reading/settings.py +339 -0
- lorebase_keeper/reading/snapshots.py +148 -0
- lorebase_keeper/reading/tree.py +98 -0
- lorebase_keeper/reading/views.py +264 -0
- lorebase_keeper/reading/visibility.py +93 -0
- lorebase_keeper/routes/__init__.py +1 -0
- lorebase_keeper/routes/admin.py +597 -0
- lorebase_keeper/routes/agents.py +160 -0
- lorebase_keeper/routes/branches.py +438 -0
- lorebase_keeper/routes/derivations.py +215 -0
- lorebase_keeper/routes/documents.py +497 -0
- lorebase_keeper/routes/domain_api.py +59 -0
- lorebase_keeper/routes/domains.py +220 -0
- lorebase_keeper/routes/events.py +187 -0
- lorebase_keeper/routes/kind_health.py +166 -0
- lorebase_keeper/routes/operations.py +392 -0
- lorebase_keeper/routes/proposal_items.py +563 -0
- lorebase_keeper/routes/proposals.py +1107 -0
- lorebase_keeper/routes/read.py +493 -0
- lorebase_keeper/routes/revisions.py +523 -0
- lorebase_keeper/routes/schema.py +682 -0
- lorebase_keeper/routes/schema_inference.py +437 -0
- lorebase_keeper/routes/schema_suggestions.py +328 -0
- lorebase_keeper/routes/sessions.py +286 -0
- lorebase_keeper/routes/source_preview.py +232 -0
- lorebase_keeper/routes/sources.py +200 -0
- lorebase_keeper/routes/trace.py +506 -0
- lorebase_keeper/routes/vault.py +332 -0
- lorebase_keeper/routes/webhooks.py +298 -0
- lorebase_keeper/routes/whoami.py +88 -0
- lorebase_keeper/services/__init__.py +1 -0
- lorebase_keeper/services/agent_writes.py +1035 -0
- lorebase_keeper/services/branches.py +337 -0
- lorebase_keeper/services/config.py +431 -0
- lorebase_keeper/services/derivation_preview.py +279 -0
- lorebase_keeper/services/document_urls.py +114 -0
- lorebase_keeper/services/domains.py +118 -0
- lorebase_keeper/services/errors.py +196 -0
- lorebase_keeper/services/operation_catalog.py +806 -0
- lorebase_keeper/services/operation_events.py +199 -0
- lorebase_keeper/services/operations.py +1229 -0
- lorebase_keeper/services/pii_import.py +222 -0
- lorebase_keeper/services/proposal_reads.py +158 -0
- lorebase_keeper/services/proposals.py +850 -0
- lorebase_keeper/services/reads.py +286 -0
- lorebase_keeper/services/schema.py +601 -0
- lorebase_keeper/services/schema_inference.py +390 -0
- lorebase_keeper/services/schema_suggestions.py +418 -0
- lorebase_keeper/services/source_preview.py +127 -0
- lorebase_keeper/settings.py +313 -0
- lorebase_keeper/tracing/__init__.py +1 -0
- lorebase_keeper/tracing/capture.py +350 -0
- lorebase_keeper/tracing/config.py +157 -0
- lorebase_keeper/tracing/explain.py +97 -0
- lorebase_keeper/tracing/middleware.py +359 -0
- lorebase_keeper/tracing/notes.py +80 -0
- lorebase_keeper/tracing/recorder.py +180 -0
- lorebase_keeper-0.2.3.dist-info/METADATA +21 -0
- lorebase_keeper-0.2.3.dist-info/RECORD +126 -0
- lorebase_keeper-0.2.3.dist-info/WHEEL +4 -0
- lorebase_keeper-0.2.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""Settings of the Domain, branch, schema and Operation endpoints (`keeper.domains`, ...).
|
|
2
|
+
|
|
3
|
+
They are sub-sections of the `keeper` section; see `config/lorebase.example.yaml`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Literal, Self
|
|
9
|
+
|
|
10
|
+
from pydantic import Field, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from lorebase_core.expr import ExprLimits
|
|
13
|
+
from lorebase_core.infer import InferenceOptions
|
|
14
|
+
from lorebase_core.model import SECRET_REFERENCE_PREFIX, DomainSlug
|
|
15
|
+
from lorebase_core.tree import DEFAULT_LEAF_SIZE
|
|
16
|
+
from lorebase_platform.settings import SectionModel
|
|
17
|
+
from lorebase_store.models.enums import PrincipalType
|
|
18
|
+
|
|
19
|
+
#: How an uploaded file is read for schema inference.
|
|
20
|
+
UploadFormat = Literal["csv", "json", "jsonl", "parquet"]
|
|
21
|
+
DEFAULT_UPLOAD_FORMATS: dict[str, UploadFormat] = {
|
|
22
|
+
".json": "json",
|
|
23
|
+
".jsonl": "jsonl",
|
|
24
|
+
".ndjson": "jsonl",
|
|
25
|
+
".parquet": "parquet",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
DEFAULT_REF_NAME_PATTERN = r"^[A-Za-z0-9][A-Za-z0-9._-]*(/[A-Za-z0-9][A-Za-z0-9._-]*)*$"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DomainSettings(SectionModel):
|
|
32
|
+
"""Domain creation."""
|
|
33
|
+
|
|
34
|
+
leaf_size: int = Field(
|
|
35
|
+
default=DEFAULT_LEAF_SIZE,
|
|
36
|
+
ge=2,
|
|
37
|
+
description="Tree leaf size of new Domains; fixed for the life of a Domain.",
|
|
38
|
+
)
|
|
39
|
+
creator_role: str | None = Field(
|
|
40
|
+
default="admin",
|
|
41
|
+
description="Role granted in a new Domain to the principal that created it; null "
|
|
42
|
+
"grants nothing (installation administrators then grant roles).",
|
|
43
|
+
)
|
|
44
|
+
protect_default_branch: bool = Field(
|
|
45
|
+
default=True, description="Whether the default branch of a new Domain is protected."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BranchSettings(SectionModel):
|
|
50
|
+
"""Branch and tag management."""
|
|
51
|
+
|
|
52
|
+
materialization: Literal["full"] = Field(
|
|
53
|
+
default="full",
|
|
54
|
+
description="Materialization of new branches. M1 supports `full` only: rows are copied "
|
|
55
|
+
"from the source branch when the branch starts at its head, rebuilt from the tree "
|
|
56
|
+
"otherwise.",
|
|
57
|
+
)
|
|
58
|
+
max_materialized_per_domain: int = Field(
|
|
59
|
+
default=20, ge=1, description="Fully materialized branches allowed per Domain."
|
|
60
|
+
)
|
|
61
|
+
name_pattern: str = Field(
|
|
62
|
+
default=DEFAULT_REF_NAME_PATTERN,
|
|
63
|
+
min_length=1,
|
|
64
|
+
description="Regular expression branch and tag names must match.",
|
|
65
|
+
)
|
|
66
|
+
max_name_length: int = Field(default=200, ge=1)
|
|
67
|
+
lock_retry_after_s: int = Field(
|
|
68
|
+
default=1,
|
|
69
|
+
ge=0,
|
|
70
|
+
description="Retry-After sent when a branch stays locked by another writer (503).",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SchemaChangeSettings(SectionModel):
|
|
75
|
+
"""Schema change sets."""
|
|
76
|
+
|
|
77
|
+
max_issues: int = Field(default=200, ge=1, description="Issues listed in one response.")
|
|
78
|
+
max_listed_changes: int = Field(
|
|
79
|
+
default=1000, ge=1, description="Element changes listed in one response."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SchemaSuggestionSettings(SectionModel):
|
|
84
|
+
"""Review of schema suggestions (design 9.2 step 10). How suggestions are aggregated and
|
|
85
|
+
listed (support, count, name matching) is set in `schema.suggestions`."""
|
|
86
|
+
|
|
87
|
+
branch_prefix: str = Field(
|
|
88
|
+
default="schema-suggestions/",
|
|
89
|
+
description="Accepted suggestions are applied as one schema change set on a new "
|
|
90
|
+
"branch named with this prefix and the first suggestion's id, proposed into the "
|
|
91
|
+
"default branch.",
|
|
92
|
+
)
|
|
93
|
+
max_decisions: int = Field(
|
|
94
|
+
default=100, ge=1, description="Suggestions one `schema/suggestions:decide` may decide."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class OperationSettings(SectionModel):
|
|
99
|
+
"""Operation execution (design 10.1)."""
|
|
100
|
+
|
|
101
|
+
max_bulk_rows: int = Field(
|
|
102
|
+
default=10000, ge=1, description="Rows accepted by one import_records or import_links."
|
|
103
|
+
)
|
|
104
|
+
import_batch_rows: int = Field(
|
|
105
|
+
default=1000, ge=1, description="Rows committed per Revision by bulk imports."
|
|
106
|
+
)
|
|
107
|
+
max_bulk_delete_keys: int = Field(
|
|
108
|
+
default=1000,
|
|
109
|
+
ge=1,
|
|
110
|
+
description="Keys accepted by one delete_records call (all deleted in one Revision).",
|
|
111
|
+
)
|
|
112
|
+
dry_run_max_edits: int = Field(
|
|
113
|
+
default=1000, ge=1, description="Edits listed in a response; longer lists are cut."
|
|
114
|
+
)
|
|
115
|
+
max_issues: int = Field(default=200, ge=1, description="Violations listed in one response.")
|
|
116
|
+
side_effect_max_edits: int = Field(
|
|
117
|
+
default=100,
|
|
118
|
+
ge=0,
|
|
119
|
+
description="Edits listed in the payload of an Operation side effect event; longer "
|
|
120
|
+
"lists are cut (edits_truncated).",
|
|
121
|
+
)
|
|
122
|
+
direct_commit_on_protected: bool = Field(
|
|
123
|
+
default=False,
|
|
124
|
+
description="Whether Operations whose review policy is `never` commit directly to "
|
|
125
|
+
"protected branches. When false they are refused and must go through a Proposal.",
|
|
126
|
+
)
|
|
127
|
+
skip_expression_preconditions: bool = Field(
|
|
128
|
+
default=False,
|
|
129
|
+
description="When expressions are not enabled: true skips CEL preconditions (reported "
|
|
130
|
+
"as warnings) instead of refusing the execution.",
|
|
131
|
+
)
|
|
132
|
+
expressions_enabled: bool = Field(
|
|
133
|
+
default=True,
|
|
134
|
+
description="Evaluate the CEL preconditions, expression review policies and steps of "
|
|
135
|
+
"stored Operations with the expression engine (sandboxed: no I/O, deterministic, "
|
|
136
|
+
"bounded by expression_limits). When false, Operations that need CEL are refused "
|
|
137
|
+
"(expressions-not-enabled). Whether schemas may hold CEL at all is "
|
|
138
|
+
"`schema.expressions_enabled`.",
|
|
139
|
+
)
|
|
140
|
+
expression_limits: ExprLimits = Field(
|
|
141
|
+
default_factory=ExprLimits,
|
|
142
|
+
description="Budget of one expression evaluated by an Operation: source length, "
|
|
143
|
+
"nesting depth, evaluation steps and time, sizes of built strings and lists, Record "
|
|
144
|
+
"and Link lookups, decimal precision.",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@model_validator(mode="after")
|
|
148
|
+
def _batch_within_bulk(self) -> Self:
|
|
149
|
+
if self.import_batch_rows > self.max_bulk_rows:
|
|
150
|
+
raise ValueError("import_batch_rows cannot exceed max_bulk_rows")
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ProposalSettings(SectionModel):
|
|
155
|
+
"""Proposals, reviews and merges (design 6.10, 10.3).
|
|
156
|
+
|
|
157
|
+
A Domain overrides `required_approvals` with its `required_approvals` setting; its
|
|
158
|
+
`merge_policy` setting holds the merge policy options of new Proposals.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
required_approvals: int = Field(
|
|
162
|
+
default=1,
|
|
163
|
+
ge=0,
|
|
164
|
+
description="Approvals a Proposal needs before it merges into a protected branch.",
|
|
165
|
+
)
|
|
166
|
+
allow_self_approval: bool = Field(
|
|
167
|
+
default=False,
|
|
168
|
+
description="Whether authors may approve their own Proposals into protected branches; "
|
|
169
|
+
"when false their approvals are refused and never counted.",
|
|
170
|
+
)
|
|
171
|
+
changes_requested_blocks: bool = Field(
|
|
172
|
+
default=True,
|
|
173
|
+
description="Whether an outstanding `request_changes` review blocks merges into "
|
|
174
|
+
"protected branches.",
|
|
175
|
+
)
|
|
176
|
+
auto_close_source_branch: bool = Field(
|
|
177
|
+
default=False,
|
|
178
|
+
description="Delete the source branch once its Proposal merged (never the default "
|
|
179
|
+
"branch or a protected one).",
|
|
180
|
+
)
|
|
181
|
+
attempt_retention: int = Field(
|
|
182
|
+
default=5,
|
|
183
|
+
ge=1,
|
|
184
|
+
description="Merge attempts kept per Proposal; older superseded ones are deleted "
|
|
185
|
+
"(merged attempts are always kept).",
|
|
186
|
+
)
|
|
187
|
+
preview_on_create: bool = Field(
|
|
188
|
+
default=True,
|
|
189
|
+
description="Compute the merge preview (Conflict count) when a Proposal is created.",
|
|
190
|
+
)
|
|
191
|
+
max_resolutions: int = Field(
|
|
192
|
+
default=1000, ge=1, description="Resolutions accepted by one bulk request."
|
|
193
|
+
)
|
|
194
|
+
max_item_decisions: int = Field(
|
|
195
|
+
default=5000,
|
|
196
|
+
ge=1,
|
|
197
|
+
description="Extraction items one `items:decide` request may decide (listed or "
|
|
198
|
+
"selected by filter).",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class SchemaInferenceSettings(SectionModel):
|
|
203
|
+
"""Schema inference from uploaded files or Postgres, MySQL, MariaDB and SQL Server tables
|
|
204
|
+
(design 9.1, tier 0).
|
|
205
|
+
|
|
206
|
+
Uploads are CSV or TSV text, JSON documents, JSON Lines or Parquet files (Parquet needs the
|
|
207
|
+
`lorebase-keeper[parquet]` extra); they are also bounded by
|
|
208
|
+
`keeper.limits.max_request_body_bytes`. A preview returns the first rows of the uploads
|
|
209
|
+
instead of a proposal, under the same limits.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
max_files: int = Field(default=50, ge=1, description="Files accepted by one request.")
|
|
213
|
+
max_file_bytes: int = Field(
|
|
214
|
+
default=1_048_576, ge=1, description="Largest file accepted, in bytes (413 above)."
|
|
215
|
+
)
|
|
216
|
+
max_total_bytes: int = Field(
|
|
217
|
+
default=1_048_576, ge=1, description="Largest sum of file sizes, in bytes (413 above)."
|
|
218
|
+
)
|
|
219
|
+
file_suffixes: tuple[str, ...] = Field(
|
|
220
|
+
default=(".csv", ".tsv", ".txt", ".json", ".jsonl", ".ndjson", ".parquet"),
|
|
221
|
+
min_length=1,
|
|
222
|
+
description="Accepted file name suffixes.",
|
|
223
|
+
)
|
|
224
|
+
file_formats: dict[str, UploadFormat] = Field(
|
|
225
|
+
default_factory=DEFAULT_UPLOAD_FORMATS.copy,
|
|
226
|
+
description="How files are read, by lowercase name suffix: json (an array of objects), "
|
|
227
|
+
"jsonl (one object per line) or parquet; any other accepted suffix is read as CSV or "
|
|
228
|
+
"TSV text (csv).",
|
|
229
|
+
)
|
|
230
|
+
allowed_sources: tuple[str, ...] = Field(
|
|
231
|
+
default=(),
|
|
232
|
+
description="Secret references (secret://env/NAME, secret://file/PATH) holding Postgres, "
|
|
233
|
+
"MySQL, MariaDB or SQL Server URLs that callers may name as sources. The default of every "
|
|
234
|
+
"Domain not listed in `domain_sources`. Empty: database inference is disabled for those "
|
|
235
|
+
"Domains.",
|
|
236
|
+
)
|
|
237
|
+
domain_sources: dict[DomainSlug, tuple[str, ...]] = Field(
|
|
238
|
+
default_factory=dict,
|
|
239
|
+
description="Per-Domain source lists keyed by Domain id. A listed Domain may name only "
|
|
240
|
+
"its own secret references (an empty list disables database inference for it); "
|
|
241
|
+
"`allowed_sources` does not apply to it.",
|
|
242
|
+
)
|
|
243
|
+
preview_rows: int = Field(
|
|
244
|
+
default=20, ge=1, description="Rows returned by a preview when the request names none."
|
|
245
|
+
)
|
|
246
|
+
preview_max_rows: int = Field(
|
|
247
|
+
default=200, ge=1, description="Most rows a preview may ask for (422 above)."
|
|
248
|
+
)
|
|
249
|
+
preview_max_cell_chars: int = Field(
|
|
250
|
+
default=1000, ge=1, description="Longest text returned per value of a preview."
|
|
251
|
+
)
|
|
252
|
+
allowed_schemas: dict[str, tuple[str, ...]] = Field(
|
|
253
|
+
default_factory=dict,
|
|
254
|
+
description="Schemas a MySQL, MariaDB or SQL Server source may be profiled in, keyed by "
|
|
255
|
+
"its secret reference (a database name for MySQL). A source not listed may name any "
|
|
256
|
+
"schema of its database; a listed one only these (the connection's own schema counts "
|
|
257
|
+
"when none is named).",
|
|
258
|
+
)
|
|
259
|
+
max_issues: int = Field(default=500, ge=1, description="Issues listed in one response.")
|
|
260
|
+
options: InferenceOptions = Field(
|
|
261
|
+
default_factory=InferenceOptions,
|
|
262
|
+
description="Thresholds and naming rules; a request may override some of them.",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
@field_validator("file_formats")
|
|
266
|
+
@classmethod
|
|
267
|
+
def _lowercase_suffixes(cls, value: dict[str, UploadFormat]) -> dict[str, UploadFormat]:
|
|
268
|
+
return {suffix.lower(): fmt for suffix, fmt in value.items()}
|
|
269
|
+
|
|
270
|
+
def format_of(self, file_name: str) -> UploadFormat:
|
|
271
|
+
"""How a file is read: by the longest matching suffix of `file_formats`, else csv."""
|
|
272
|
+
name = file_name.lower()
|
|
273
|
+
matches = [suffix for suffix in self.file_formats if name.endswith(suffix)]
|
|
274
|
+
return self.file_formats[max(matches, key=len)] if matches else "csv"
|
|
275
|
+
|
|
276
|
+
@field_validator("domain_sources")
|
|
277
|
+
@classmethod
|
|
278
|
+
def _secret_references(cls, value: dict[str, tuple[str, ...]]) -> dict[str, tuple[str, ...]]:
|
|
279
|
+
for domain_id, sources in value.items():
|
|
280
|
+
for source in sources:
|
|
281
|
+
if not source.startswith(SECRET_REFERENCE_PREFIX):
|
|
282
|
+
raise ValueError(
|
|
283
|
+
f"sources of Domain {domain_id!r} must be secret references "
|
|
284
|
+
f"such as {SECRET_REFERENCE_PREFIX}env/NAME"
|
|
285
|
+
)
|
|
286
|
+
return value
|
|
287
|
+
|
|
288
|
+
def sources_for(self, domain_id: str) -> tuple[str, ...]:
|
|
289
|
+
"""The secret references a Domain may name: its own list, or the default."""
|
|
290
|
+
return self.domain_sources.get(domain_id, self.allowed_sources)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class AgentWriteSettings(SectionModel):
|
|
294
|
+
"""Writes of external agents through governed Operations (design 10.1, 11.1, 13.2).
|
|
295
|
+
|
|
296
|
+
When an Operation needs review (protected target branch, or its review policy) and the
|
|
297
|
+
caller named a `task`, Keeper runs it on the task branch `{branch_prefix}/{principal}/
|
|
298
|
+
{task}` and opens or updates a Proposal instead of answering `review-required`. A Domain
|
|
299
|
+
overrides any member here with the same name in its `agent_writes` setting.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
enabled: bool = Field(
|
|
303
|
+
default=True, description="Turn Operations that need review into task branches."
|
|
304
|
+
)
|
|
305
|
+
principal_types: tuple[PrincipalType, ...] = Field(
|
|
306
|
+
default=(PrincipalType.SERVICE,),
|
|
307
|
+
description="Principal types that are agents: for them `auto_propose` applies when a "
|
|
308
|
+
"request leaves it out; other principals must ask with `auto_propose: true`.",
|
|
309
|
+
)
|
|
310
|
+
auto_propose: bool = Field(
|
|
311
|
+
default=True,
|
|
312
|
+
description="Open a Proposal when a request of an agent that names a `task` needs "
|
|
313
|
+
"review and does not set `auto_propose`.",
|
|
314
|
+
)
|
|
315
|
+
auto_merge: bool = Field(
|
|
316
|
+
default=False,
|
|
317
|
+
description="Merge the Proposal at once when a request does not set `auto_merge`. "
|
|
318
|
+
"Needs a role in `allowed_roles_for_auto_merge`.",
|
|
319
|
+
)
|
|
320
|
+
allowed_roles_for_auto_merge: tuple[str, ...] = Field(
|
|
321
|
+
default=(),
|
|
322
|
+
description="Roles (granted in the Domain, covering the task branch) whose agents may "
|
|
323
|
+
"have their Proposals merged automatically; empty: nobody. Approvals the target "
|
|
324
|
+
"branch requires are never skipped.",
|
|
325
|
+
)
|
|
326
|
+
max_open_branches_per_principal: int = Field(
|
|
327
|
+
default=10, ge=1, description="Task branches one principal may hold at a time."
|
|
328
|
+
)
|
|
329
|
+
branch_prefix: str = Field(
|
|
330
|
+
default="agent",
|
|
331
|
+
pattern=DEFAULT_REF_NAME_PATTERN,
|
|
332
|
+
description="First part of task branch names.",
|
|
333
|
+
)
|
|
334
|
+
branch_mode: Literal["copy", "overlay"] = Field(
|
|
335
|
+
default="overlay",
|
|
336
|
+
description="How task branches are materialized: overlay (only the rows the task "
|
|
337
|
+
"changes, reading the target as of the fork point; a copy when the target is not "
|
|
338
|
+
"fully materialized) or copy (full rows).",
|
|
339
|
+
)
|
|
340
|
+
delete_branch_on_merge: bool = Field(
|
|
341
|
+
default=True,
|
|
342
|
+
description="Delete a task branch when its Proposal merged or was closed and no other "
|
|
343
|
+
"open Proposal uses it.",
|
|
344
|
+
)
|
|
345
|
+
max_task_length: int = Field(
|
|
346
|
+
default=60, ge=1, description="Longest `task` (after normalization) accepted."
|
|
347
|
+
)
|
|
348
|
+
title_template: str = Field(
|
|
349
|
+
default="Agent {principal}: {task}",
|
|
350
|
+
min_length=1,
|
|
351
|
+
description="Title of the Proposal; `{principal}` and `{task}` are replaced.",
|
|
352
|
+
)
|
|
353
|
+
summary_parameter_values: bool = Field(
|
|
354
|
+
default=True,
|
|
355
|
+
description="List parameter values in the Proposal description; false lists names only "
|
|
356
|
+
"(values may be sensitive).",
|
|
357
|
+
)
|
|
358
|
+
summary_value_chars: int = Field(
|
|
359
|
+
default=80, ge=1, description="Characters of a parameter value kept in the description."
|
|
360
|
+
)
|
|
361
|
+
max_summary_operations: int = Field(
|
|
362
|
+
default=20, ge=1, description="Executions listed in the description of one Proposal."
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
@field_validator("title_template")
|
|
366
|
+
@classmethod
|
|
367
|
+
def _known_placeholders(cls, value: str) -> str:
|
|
368
|
+
try:
|
|
369
|
+
value.format(principal="p", task="t")
|
|
370
|
+
except (KeyError, IndexError, ValueError):
|
|
371
|
+
raise ValueError("only {principal} and {task} may be used as placeholders") from None
|
|
372
|
+
return value
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
DOCX_MEDIA_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class DocumentSettings(SectionModel):
|
|
379
|
+
"""Document intake for extraction (design 9.2 step 1): uploads and allowlisted URLs.
|
|
380
|
+
|
|
381
|
+
The upload route accepts bodies up to `max_bytes` plus `multipart_overhead_bytes`,
|
|
382
|
+
whatever `keeper.limits.max_request_body_bytes` says.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
max_bytes: int = Field(
|
|
386
|
+
default=20 * 1024 * 1024, ge=1, description="Largest document, in bytes (413 above)."
|
|
387
|
+
)
|
|
388
|
+
multipart_overhead_bytes: int = Field(
|
|
389
|
+
default=64 * 1024, ge=0, description="Room for multipart framing and form fields."
|
|
390
|
+
)
|
|
391
|
+
media_types: dict[str, str] = Field(
|
|
392
|
+
default_factory=lambda: {
|
|
393
|
+
".md": "text/markdown",
|
|
394
|
+
".markdown": "text/markdown",
|
|
395
|
+
".txt": "text/plain",
|
|
396
|
+
".pdf": "application/pdf",
|
|
397
|
+
".docx": DOCX_MEDIA_TYPE,
|
|
398
|
+
},
|
|
399
|
+
min_length=1,
|
|
400
|
+
description="Accepted file name suffixes and the media type each one is stored as.",
|
|
401
|
+
)
|
|
402
|
+
url_hosts: tuple[str, ...] = Field(
|
|
403
|
+
default=(),
|
|
404
|
+
description="Host names documents may be fetched from (exact match); empty disables "
|
|
405
|
+
"URL intake. Keeper fetches them itself, without following redirects, from addresses "
|
|
406
|
+
"checked as for webhook targets.",
|
|
407
|
+
)
|
|
408
|
+
url_schemes: tuple[str, ...] = Field(
|
|
409
|
+
default=("https",), min_length=1, description="URL schemes allowed for URL intake."
|
|
410
|
+
)
|
|
411
|
+
url_allow_private_targets: bool = Field(
|
|
412
|
+
default=False,
|
|
413
|
+
description="Fetch from hosts resolving to loopback, private, link-local or other "
|
|
414
|
+
"non-global addresses, and resolve the host again for the connection. Off: every "
|
|
415
|
+
"address must be global unicast and the connection is pinned to the checked ones.",
|
|
416
|
+
)
|
|
417
|
+
url_timeout_s: float = Field(default=30.0, gt=0, description="Time budget of one fetch.")
|
|
418
|
+
extract_on_upload: bool = Field(
|
|
419
|
+
default=True,
|
|
420
|
+
description="Queue an extraction job for every uploaded document unless the request "
|
|
421
|
+
"says otherwise.",
|
|
422
|
+
)
|
|
423
|
+
jobs_listed: int = Field(
|
|
424
|
+
default=20, ge=1, description="Extraction jobs listed with a document, newest first."
|
|
425
|
+
)
|
|
426
|
+
max_text_chars: int = Field(
|
|
427
|
+
default=2_000_000,
|
|
428
|
+
ge=1,
|
|
429
|
+
description="Characters of parsed text one `GET .../documents/{id}/text` returns; a "
|
|
430
|
+
"longer text is cut there and the answer says `truncated`.",
|
|
431
|
+
)
|