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,392 @@
|
|
|
1
|
+
"""Operation execution (design 5.7, 10.1, 11.1): `POST /domains/{d}/operations/{op}/execute`.
|
|
2
|
+
|
|
3
|
+
Built-in Operations need `operation.execute` (role `editor`), stored ones
|
|
4
|
+
`operation.execute_custom` (role `operator`) and, when the Operation lists `permissions`, one
|
|
5
|
+
of the principals or groups named there. Grants narrowed to a Kind, Operation or branch
|
|
6
|
+
pattern apply. The response carries `status` (`committed` or `dry_run`), the computed `edits`,
|
|
7
|
+
`violations` and the new Revision(s); rejected executions are 422 problems with the same
|
|
8
|
+
members (`precondition-failed` when a CEL precondition does not hold), and executions that
|
|
9
|
+
need review (on a protected branch, or when a review policy expression is true) are 409
|
|
10
|
+
problems pointing to Proposals.
|
|
11
|
+
|
|
12
|
+
An agent that names a `task` gets a Proposal instead of that 409 (`services.agent_writes`): the
|
|
13
|
+
Operation runs on the branch `agent/{principal}/{task}` and the answer is 202 with the
|
|
14
|
+
Proposal and where it stands (`proposed`, `merged` or `conflicts`), `next` hints included.
|
|
15
|
+
`intent` goes into the Proposal description; `auto_propose` and `auto_merge` override the
|
|
16
|
+
defaults of `keeper.agent_writes` (merging only if the Domain allows it for the caller's role).
|
|
17
|
+
Requests of this kind are `writes` for rate limiting like any other POST.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import asdict
|
|
23
|
+
from typing import Any, Literal
|
|
24
|
+
|
|
25
|
+
from fastapi import APIRouter, Request, Response
|
|
26
|
+
from fastapi.responses import JSONResponse
|
|
27
|
+
from pydantic import BaseModel, Field, JsonValue
|
|
28
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
29
|
+
|
|
30
|
+
from lorebase_core.canonical import hex
|
|
31
|
+
from lorebase_keeper.auth.audit import annotate
|
|
32
|
+
from lorebase_keeper.auth.deps import AuthDep, PrincipalDep, authorize
|
|
33
|
+
from lorebase_keeper.auth.permissions import Permission
|
|
34
|
+
from lorebase_keeper.auth.rbac import AccessContext
|
|
35
|
+
from lorebase_keeper.context import StoreDep, parse_revision_id
|
|
36
|
+
from lorebase_keeper.deps import IdempotencyKeyDep, KeeperSettingsDep, get_idempotency_key
|
|
37
|
+
from lorebase_keeper.errors import VALIDATION_FAILED, Problem, ProblemError
|
|
38
|
+
from lorebase_keeper.reading.sensitivity import Masking, default_schema, masking_for
|
|
39
|
+
from lorebase_keeper.routes.domains import REPLAYED_HEADER
|
|
40
|
+
from lorebase_keeper.services import agent_writes
|
|
41
|
+
from lorebase_keeper.services import operations as service
|
|
42
|
+
from lorebase_keeper.services.errors import BRANCH_BUSY, commit_problem, finding_documents
|
|
43
|
+
from lorebase_store.auth import NewAuditEntry
|
|
44
|
+
from lorebase_store.commit import CommitError
|
|
45
|
+
from lorebase_store.repositories import LockTimeoutError
|
|
46
|
+
|
|
47
|
+
router = APIRouter(tags=["operations"])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ExecuteRequest(BaseModel):
|
|
51
|
+
"""An execution of an Operation."""
|
|
52
|
+
|
|
53
|
+
parameters: dict[str, JsonValue] = Field(default_factory=dict)
|
|
54
|
+
branch: str | None = Field(default=None, description="Default: the default branch.")
|
|
55
|
+
dry_run: bool = Field(
|
|
56
|
+
default=False, description="Compute edits and violations without committing."
|
|
57
|
+
)
|
|
58
|
+
idempotency_key: str | None = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
description="Same as the Idempotency-Key header; a retry with the same key and "
|
|
61
|
+
"parameters returns the original result.",
|
|
62
|
+
)
|
|
63
|
+
expected_head: str | None = Field(
|
|
64
|
+
default=None, description="Fail with 409 unless the branch head is this Revision."
|
|
65
|
+
)
|
|
66
|
+
message: str | None = Field(default=None, max_length=2000)
|
|
67
|
+
task: str | None = Field(
|
|
68
|
+
default=None,
|
|
69
|
+
min_length=1,
|
|
70
|
+
max_length=200,
|
|
71
|
+
description="Short slug naming the piece of work. When the Operation needs review "
|
|
72
|
+
"(protected branch, review policy), Keeper runs it on the branch "
|
|
73
|
+
"`agent/{principal}/{task}` and opens or updates a Proposal; without a task the "
|
|
74
|
+
"refusal is the 409 `review-required`. Ignored when the Operation commits directly.",
|
|
75
|
+
)
|
|
76
|
+
intent: str | None = Field(
|
|
77
|
+
default=None,
|
|
78
|
+
max_length=agent_writes.INTENT_MAX_CHARS,
|
|
79
|
+
description="What the agent is after, in a sentence or two; becomes the start of the "
|
|
80
|
+
"Proposal description.",
|
|
81
|
+
)
|
|
82
|
+
auto_propose: bool | None = Field(
|
|
83
|
+
default=None,
|
|
84
|
+
description="Open a Proposal when review is needed. Default: "
|
|
85
|
+
"`keeper.agent_writes.auto_propose` for agent principals, false for others. `true` "
|
|
86
|
+
"needs a `task`.",
|
|
87
|
+
)
|
|
88
|
+
auto_merge: bool | None = Field(
|
|
89
|
+
default=None,
|
|
90
|
+
description="Merge the Proposal at once. Default: `keeper.agent_writes.auto_merge`. "
|
|
91
|
+
"Honoured only when the Domain allows it for the caller's role, there is no open "
|
|
92
|
+
"Conflict and the required approvals are in.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ExecutionOut(BaseModel):
|
|
97
|
+
"""Result of an execution."""
|
|
98
|
+
|
|
99
|
+
status: Literal["committed", "dry_run"]
|
|
100
|
+
operation: str
|
|
101
|
+
branch: str
|
|
102
|
+
invocation_id: str
|
|
103
|
+
parameters_hash: str
|
|
104
|
+
head: str = Field(description="Branch head the Operation ran against.")
|
|
105
|
+
revision: str | None = Field(description="Last Revision written; null for dry runs.")
|
|
106
|
+
revisions: list[str] = Field(description="Every Revision written (imports run in batches).")
|
|
107
|
+
edits: list[dict[str, Any]]
|
|
108
|
+
edit_count: int
|
|
109
|
+
edits_truncated: bool
|
|
110
|
+
violations: list[dict[str, Any]]
|
|
111
|
+
violation_count: int
|
|
112
|
+
warnings: list[dict[str, Any]]
|
|
113
|
+
review_required: bool = Field(
|
|
114
|
+
description="True when a non-dry run would need a Proposal on this branch."
|
|
115
|
+
)
|
|
116
|
+
replayed: bool
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ProposalRefOut(BaseModel):
|
|
120
|
+
"""The Proposal carrying the edits of an agent write."""
|
|
121
|
+
|
|
122
|
+
id: str
|
|
123
|
+
title: str
|
|
124
|
+
status: str
|
|
125
|
+
source_ref: str
|
|
126
|
+
target_ref: str
|
|
127
|
+
url: str
|
|
128
|
+
created: bool = Field(description="False when an open Proposal of the branch was updated.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class AgentApprovalOut(BaseModel):
|
|
132
|
+
required: int
|
|
133
|
+
approvals: int
|
|
134
|
+
satisfied: bool
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class AgentAttemptOut(BaseModel):
|
|
138
|
+
"""The merge attempt: Conflicts between the task branch and the target."""
|
|
139
|
+
|
|
140
|
+
id: str
|
|
141
|
+
result: str | None
|
|
142
|
+
open_conflicts: int
|
|
143
|
+
resolved_conflicts: int
|
|
144
|
+
counts: dict[str, dict[str, int]]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AgentMergeOut(BaseModel):
|
|
148
|
+
requested: bool
|
|
149
|
+
allowed: bool = Field(description="The Domain allows automatic merges for the caller's role.")
|
|
150
|
+
performed: bool
|
|
151
|
+
blockers: list[str] = Field(
|
|
152
|
+
description="What keeps the Proposal from merging by itself: `approvals-pending` (the "
|
|
153
|
+
"target needs approvals), `review-policy-needs-approval` (the Operation's review "
|
|
154
|
+
"policy asks for a review and nobody approved)."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class NextHint(BaseModel):
|
|
159
|
+
"""A next step, in the style of problem details extensions."""
|
|
160
|
+
|
|
161
|
+
action: str
|
|
162
|
+
detail: str
|
|
163
|
+
method: str | None = None
|
|
164
|
+
href: str | None = None
|
|
165
|
+
permission: str | None = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class AgentWriteOut(BaseModel):
|
|
169
|
+
"""An Operation that needed review, run on a task branch and proposed."""
|
|
170
|
+
|
|
171
|
+
status: Literal["proposed", "merged", "conflicts"]
|
|
172
|
+
operation: str
|
|
173
|
+
branch: str = Field(description="The task branch.")
|
|
174
|
+
target: str = Field(description="The branch the Proposal merges into.")
|
|
175
|
+
invocation_id: str
|
|
176
|
+
parameters_hash: str
|
|
177
|
+
revision: str | None = Field(description="Last Revision written on the task branch.")
|
|
178
|
+
revisions: list[str]
|
|
179
|
+
edit_count: int
|
|
180
|
+
replayed: bool
|
|
181
|
+
proposal: ProposalRefOut
|
|
182
|
+
approval: AgentApprovalOut
|
|
183
|
+
attempt: AgentAttemptOut | None
|
|
184
|
+
merge: AgentMergeOut
|
|
185
|
+
merged_revision: str | None = Field(description="Head of the target after an automatic merge.")
|
|
186
|
+
branch_created: bool
|
|
187
|
+
branch_deleted: bool
|
|
188
|
+
warnings: list[dict[str, Any]]
|
|
189
|
+
next: list[NextHint]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
_ERRORS: dict[int | str, dict[str, Any]] = {
|
|
193
|
+
403: {"model": Problem, "description": "The caller lacks the permission named in the body."},
|
|
194
|
+
404: {"model": Problem, "description": "Domain, branch or Operation not found."},
|
|
195
|
+
202: {
|
|
196
|
+
"model": AgentWriteOut,
|
|
197
|
+
"description": "The Operation needs review and ran on a task branch; a Proposal "
|
|
198
|
+
"carries the edits (`status`: `proposed`, `merged` or `conflicts`).",
|
|
199
|
+
},
|
|
200
|
+
409: {
|
|
201
|
+
"model": Problem,
|
|
202
|
+
"description": "Review required (`review-required`, with `next` hints), head moved "
|
|
203
|
+
"(`stale-head`), idempotency key reused with other parameters (`idempotency-conflict`) "
|
|
204
|
+
"or too many task branches (`agent-branch-limit`).",
|
|
205
|
+
},
|
|
206
|
+
422: {
|
|
207
|
+
"model": Problem,
|
|
208
|
+
"description": "Invalid parameters or violations (`issues`, `edits`), a precondition "
|
|
209
|
+
"that does not hold (`precondition-failed`, with the expression and its position), or "
|
|
210
|
+
"expressions not enabled (`expressions-not-enabled`).",
|
|
211
|
+
},
|
|
212
|
+
503: {"model": Problem, "description": "The branch is locked by another writer."},
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _key(header: str | None, body: str | None) -> str | None:
|
|
217
|
+
if body is not None:
|
|
218
|
+
get_idempotency_key(body)
|
|
219
|
+
if header is not None and body is not None and header != body:
|
|
220
|
+
raise ProblemError(
|
|
221
|
+
VALIDATION_FAILED,
|
|
222
|
+
"The Idempotency-Key header and idempotency_key differ.",
|
|
223
|
+
)
|
|
224
|
+
return header or body
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@router.post(
|
|
228
|
+
"/domains/{domain_id}/operations/{operation}/execute",
|
|
229
|
+
summary="Execute an Operation",
|
|
230
|
+
response_model=ExecutionOut,
|
|
231
|
+
responses=_ERRORS,
|
|
232
|
+
)
|
|
233
|
+
async def execute_operation(
|
|
234
|
+
domain_id: str,
|
|
235
|
+
operation: str,
|
|
236
|
+
body: ExecuteRequest,
|
|
237
|
+
request: Request,
|
|
238
|
+
response: Response,
|
|
239
|
+
principal: PrincipalDep,
|
|
240
|
+
auth: AuthDep,
|
|
241
|
+
store: StoreDep,
|
|
242
|
+
keeper: KeeperSettingsDep,
|
|
243
|
+
header_key: IdempotencyKeyDep,
|
|
244
|
+
) -> ExecutionOut | JSONResponse:
|
|
245
|
+
"""Validate parameters, compute the edits and commit them as a Revision (or dry-run).
|
|
246
|
+
|
|
247
|
+
An Operation that needs review is a 409 `review-required` unless the request names a
|
|
248
|
+
`task`: then it runs on a task branch and the answer is 202 with the Proposal.
|
|
249
|
+
"""
|
|
250
|
+
settings = keeper.operations
|
|
251
|
+
if body.auto_propose and body.task is None:
|
|
252
|
+
raise ProblemError(
|
|
253
|
+
VALIDATION_FAILED,
|
|
254
|
+
"One or more request members are invalid.",
|
|
255
|
+
extensions={
|
|
256
|
+
"errors": [
|
|
257
|
+
{
|
|
258
|
+
"pointer": "/body/task",
|
|
259
|
+
"message": "is required with auto_propose",
|
|
260
|
+
"code": "required",
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
async def check(permission: Permission, context: AccessContext) -> None:
|
|
267
|
+
await authorize(request, auth, principal, permission, domain_id, context)
|
|
268
|
+
|
|
269
|
+
execution = service.ExecutionRequest(
|
|
270
|
+
operation=operation,
|
|
271
|
+
parameters=body.parameters,
|
|
272
|
+
branch=body.branch,
|
|
273
|
+
dry_run=body.dry_run,
|
|
274
|
+
idempotency_key=_key(header_key, body.idempotency_key),
|
|
275
|
+
expected_head=None
|
|
276
|
+
if body.expected_head is None
|
|
277
|
+
else parse_revision_id(body.expected_head, "/body/expected_head"),
|
|
278
|
+
message=body.message,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
async def emit(action: str, target: str, **details: Any) -> None:
|
|
282
|
+
if auth.audit_writer is not None:
|
|
283
|
+
await auth.audit_writer.write(
|
|
284
|
+
NewAuditEntry(
|
|
285
|
+
actor=principal.id,
|
|
286
|
+
action=action,
|
|
287
|
+
domain_id=domain_id,
|
|
288
|
+
target=target,
|
|
289
|
+
details={"outcome": "success", "via": "agent_write", **details},
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
async def roles_of(context: AccessContext) -> frozenset[str]:
|
|
294
|
+
return await auth.authorizer.roles_of(principal, domain_id, context)
|
|
295
|
+
|
|
296
|
+
async def reveal(session: AsyncSession) -> Masking:
|
|
297
|
+
reference = await default_schema(session, domain_id, store.runtime)
|
|
298
|
+
return await masking_for(
|
|
299
|
+
auth.authorizer, principal, domain_id, reference, keeper.reads.sensitivity
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
outcome: service.ExecutionOutcome | agent_writes.AgentWriteOutcome
|
|
303
|
+
try:
|
|
304
|
+
try:
|
|
305
|
+
outcome = await service.execute(
|
|
306
|
+
store,
|
|
307
|
+
domain_id,
|
|
308
|
+
execution,
|
|
309
|
+
principal,
|
|
310
|
+
settings,
|
|
311
|
+
check=check,
|
|
312
|
+
proposals_path=f"{keeper.api_prefix}/domains/{domain_id}/proposals",
|
|
313
|
+
reveal=reveal,
|
|
314
|
+
)
|
|
315
|
+
except ProblemError as refusal:
|
|
316
|
+
if body.dry_run or not agent_writes.diverts(refusal):
|
|
317
|
+
raise
|
|
318
|
+
outcome = await agent_writes.run(
|
|
319
|
+
store,
|
|
320
|
+
domain_id,
|
|
321
|
+
execution,
|
|
322
|
+
agent_writes.AgentRequest(
|
|
323
|
+
task=body.task,
|
|
324
|
+
intent=body.intent,
|
|
325
|
+
auto_propose=body.auto_propose,
|
|
326
|
+
auto_merge=body.auto_merge,
|
|
327
|
+
),
|
|
328
|
+
principal,
|
|
329
|
+
keeper,
|
|
330
|
+
original=refusal,
|
|
331
|
+
check=check,
|
|
332
|
+
roles_of=roles_of,
|
|
333
|
+
emit=emit,
|
|
334
|
+
)
|
|
335
|
+
except LockTimeoutError:
|
|
336
|
+
raise ProblemError(
|
|
337
|
+
BRANCH_BUSY,
|
|
338
|
+
"Another writer holds the branch; retry later.",
|
|
339
|
+
headers={"Retry-After": str(keeper.branches.lock_retry_after_s)},
|
|
340
|
+
) from None
|
|
341
|
+
except CommitError as error:
|
|
342
|
+
raise commit_problem(
|
|
343
|
+
error,
|
|
344
|
+
max_issues=settings.max_issues,
|
|
345
|
+
retry_after_s=keeper.branches.lock_retry_after_s,
|
|
346
|
+
) from None
|
|
347
|
+
if isinstance(outcome, agent_writes.AgentWriteOutcome):
|
|
348
|
+
annotate(
|
|
349
|
+
request,
|
|
350
|
+
action="operation.propose",
|
|
351
|
+
target=operation,
|
|
352
|
+
task=body.task,
|
|
353
|
+
**agent_writes.audit_details(outcome),
|
|
354
|
+
)
|
|
355
|
+
if outcome.replayed:
|
|
356
|
+
response.headers[REPLAYED_HEADER] = "true"
|
|
357
|
+
return JSONResponse(
|
|
358
|
+
status_code=202,
|
|
359
|
+
content=AgentWriteOut.model_validate(asdict(outcome)).model_dump(mode="json"),
|
|
360
|
+
headers=dict(response.headers),
|
|
361
|
+
)
|
|
362
|
+
revisions = [hex(r) for r in outcome.revisions]
|
|
363
|
+
annotate(
|
|
364
|
+
request,
|
|
365
|
+
action="operation.dry_run" if body.dry_run else "operation.execute",
|
|
366
|
+
target=operation,
|
|
367
|
+
branch=outcome.branch,
|
|
368
|
+
invocation_id=outcome.invocation_id,
|
|
369
|
+
parameters_hash=outcome.parameters_hash,
|
|
370
|
+
revisions=revisions,
|
|
371
|
+
replayed=outcome.replayed,
|
|
372
|
+
)
|
|
373
|
+
if outcome.replayed:
|
|
374
|
+
response.headers[REPLAYED_HEADER] = "true"
|
|
375
|
+
return ExecutionOut(
|
|
376
|
+
status=outcome.status.value,
|
|
377
|
+
operation=outcome.operation,
|
|
378
|
+
branch=outcome.branch,
|
|
379
|
+
invocation_id=outcome.invocation_id,
|
|
380
|
+
parameters_hash=outcome.parameters_hash,
|
|
381
|
+
head=hex(outcome.head),
|
|
382
|
+
revision=revisions[-1] if revisions else None,
|
|
383
|
+
revisions=revisions,
|
|
384
|
+
edits=outcome.edits,
|
|
385
|
+
edit_count=outcome.edit_count,
|
|
386
|
+
edits_truncated=len(outcome.edits) < outcome.edit_count,
|
|
387
|
+
violations=finding_documents(outcome.violations, settings.max_issues),
|
|
388
|
+
violation_count=len(outcome.violations),
|
|
389
|
+
warnings=finding_documents(outcome.warnings, settings.max_issues),
|
|
390
|
+
review_required=outcome.review_required,
|
|
391
|
+
replayed=outcome.replayed,
|
|
392
|
+
)
|