okstra 0.167.0 → 0.169.0
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.
- package/README.md +6 -5
- package/docs/architecture/storage-model.md +57 -1
- package/docs/architecture.md +70 -2
- package/docs/cli.md +8 -4
- package/docs/for-ai/skills/okstra-code-review.md +3 -2
- package/docs/for-ai/skills/okstra-schedule-gen.md +3 -1
- package/docs/pr-template-usage.md +10 -6
- package/docs/project-structure-overview.md +14 -11
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -5
- package/runtime/agents/workers/report-writer-worker.md +9 -4
- package/runtime/agents/workers/translator-worker.md +6 -4
- package/runtime/prompts/coding-preflight/architectures/hexagonal.md +27 -1
- package/runtime/prompts/coding-preflight/clean-code.md +13 -0
- package/runtime/prompts/duties/acceptance-critic.md +24 -0
- package/runtime/prompts/duties/acceptance-verifier.md +24 -0
- package/runtime/prompts/duties/analysis-worker.md +24 -0
- package/runtime/prompts/duties/code-reviewer.md +24 -0
- package/runtime/prompts/duties/common.md +35 -0
- package/runtime/prompts/duties/implementation-executor.md +24 -0
- package/runtime/prompts/duties/implementation-verifier.md +24 -0
- package/runtime/prompts/duties/lead.md +24 -0
- package/runtime/prompts/duties/report-writer.md +24 -0
- package/runtime/prompts/duties/reverification-worker.md +24 -0
- package/runtime/prompts/duties/schedule-verifier.md +24 -0
- package/runtime/prompts/duties/scope-critic.md +24 -0
- package/runtime/prompts/duties/translator.md +24 -0
- package/runtime/prompts/lead/convergence.md +51 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -20
- package/runtime/prompts/lead/plan-body-verification.md +16 -1
- package/runtime/prompts/lead/report-writer.md +20 -5
- package/runtime/prompts/lead/team-contract.md +13 -13
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-executor.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation.md +4 -2
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +33 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +13 -12
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +1502 -0
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +788 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +2 -107
- package/runtime/python/okstra_ctl/context_cost.py +46 -5
- package/runtime/python/okstra_ctl/dispatch_core.py +312 -37
- package/runtime/python/okstra_ctl/dispatch_state.py +461 -36
- package/runtime/python/okstra_ctl/doctor.py +150 -16
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +87 -9
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +214 -23
- package/runtime/python/okstra_ctl/path_hints.py +26 -0
- package/runtime/python/okstra_ctl/paths.py +20 -0
- package/runtime/python/okstra_ctl/ports/__init__.py +8 -0
- package/runtime/python/okstra_ctl/ports/host.py +3 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +60 -0
- package/runtime/python/okstra_ctl/pr_template.py +3 -6
- package/runtime/python/okstra_ctl/registry/host_registry.py +5 -0
- package/runtime/python/okstra_ctl/render.py +217 -12
- package/runtime/python/okstra_ctl/report_finalize.py +44 -0
- package/runtime/python/okstra_ctl/run.py +368 -51
- package/runtime/python/okstra_ctl/session.py +16 -12
- package/runtime/python/okstra_ctl/team.py +11 -11
- package/runtime/python/okstra_ctl/worker_dispatch.py +104 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +5 -38
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +32 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +38 -1
- package/runtime/skills/okstra-code-review/SKILL.md +22 -3
- package/runtime/skills/okstra-run/SKILL.md +16 -1
- package/runtime/skills/okstra-schedule-gen/SKILL.md +15 -1
- package/runtime/templates/implementation-worker-preamble.md +0 -10
- package/runtime/templates/report-writer-prompt-preamble.md +0 -9
- package/runtime/templates/reports/settings.template.json +0 -11
- package/runtime/templates/worker-prompt-preamble.md +0 -10
- package/runtime/validators/lib/fixtures.sh +93 -0
- package/runtime/validators/lib/validate-assets.sh +0 -8
- package/runtime/validators/validate-run.py +182 -0
- package/src/cli-registry.mjs +14 -0
- package/src/commands/execute/agent-prompt.mjs +25 -0
- package/src/commands/execute/codex-dispatch.mjs +6 -63
- package/src/commands/execute/worker-dispatch.mjs +76 -0
- package/src/commands/lifecycle/doctor.mjs +18 -3
- package/src/commands/lifecycle/install.mjs +33 -15
- package/src/commands/lifecycle/uninstall.mjs +4 -3
- package/src/lib/install-assets.mjs +9 -0
- package/runtime/agents/workers/antigravity-worker.md +0 -259
- package/runtime/agents/workers/codex-worker.md +0 -259
- package/runtime/agents/workers/grok-worker.md +0 -259
- package/runtime/agents/workers/kimi-worker.md +0 -259
- package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +0 -79
- package/runtime/templates/operating-standard.md +0 -22
- package/src/lib/worker-agent-render.mjs +0 -50
- /package/runtime/templates/{prd → pr}/pr-body.template.md +0 -0
|
@@ -0,0 +1,1502 @@
|
|
|
1
|
+
"""Compose and verify auditable LLM invocation specifications."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import contextlib
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import fcntl
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from pathlib import PurePosixPath
|
|
13
|
+
import re
|
|
14
|
+
import tempfile
|
|
15
|
+
from typing import Iterator, Literal, Mapping, get_args
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
AgentAudience = Literal[
|
|
19
|
+
"lead",
|
|
20
|
+
"analysis-worker",
|
|
21
|
+
"implementation-executor",
|
|
22
|
+
"implementation-verifier",
|
|
23
|
+
"acceptance-verifier",
|
|
24
|
+
"reverification-worker",
|
|
25
|
+
"scope-critic",
|
|
26
|
+
"acceptance-critic",
|
|
27
|
+
"report-writer",
|
|
28
|
+
"translator",
|
|
29
|
+
"code-reviewer",
|
|
30
|
+
"schedule-verifier",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
_SUPPORTED_AUDIENCES = frozenset(get_args(AgentAudience))
|
|
34
|
+
_SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
35
|
+
_TOP_LEVEL_KEYS = {
|
|
36
|
+
"schemaVersion",
|
|
37
|
+
"invocationId",
|
|
38
|
+
"audience",
|
|
39
|
+
"dispatchKind",
|
|
40
|
+
"workerId",
|
|
41
|
+
"assignmentRef",
|
|
42
|
+
"contractSource",
|
|
43
|
+
"modelAssignment",
|
|
44
|
+
"dutyContract",
|
|
45
|
+
"instruction",
|
|
46
|
+
"prompt",
|
|
47
|
+
"digests",
|
|
48
|
+
}
|
|
49
|
+
_NESTED_KEYS = {
|
|
50
|
+
"contractSource": {"mode", "runManifestPath", "dutyRootPath"},
|
|
51
|
+
"modelAssignment": {
|
|
52
|
+
"provider",
|
|
53
|
+
"model",
|
|
54
|
+
"modelExecutionValue",
|
|
55
|
+
"runner",
|
|
56
|
+
"hostRuntime",
|
|
57
|
+
"hostModelValue",
|
|
58
|
+
},
|
|
59
|
+
"dutyContract": {"id", "version"},
|
|
60
|
+
"instruction": {"sourcePaths"},
|
|
61
|
+
"prompt": {"path"},
|
|
62
|
+
"digests": {
|
|
63
|
+
"catalogDigest",
|
|
64
|
+
"assignmentDigest",
|
|
65
|
+
"dutyDigest",
|
|
66
|
+
"instructionDigest",
|
|
67
|
+
"promptDigest",
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AgentInvocationError(RuntimeError):
|
|
73
|
+
"""Raised when an invocation contract cannot be safely materialized."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, message: str, *, reason: str = "invalid_agent_invocation"):
|
|
76
|
+
super().__init__(message)
|
|
77
|
+
self.reason = reason
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class DutyContract:
|
|
82
|
+
id: str
|
|
83
|
+
version: int
|
|
84
|
+
kind: Literal["common", "role"]
|
|
85
|
+
applies_to: AgentAudience | None
|
|
86
|
+
body: str
|
|
87
|
+
source_path: Path
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class AgentModelAssignment:
|
|
92
|
+
provider: str
|
|
93
|
+
model: str
|
|
94
|
+
model_execution_value: str
|
|
95
|
+
runner: str
|
|
96
|
+
host_runtime: str
|
|
97
|
+
host_model_value: str | None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class AgentInstructionSource:
|
|
102
|
+
kind: Literal["project", "runtime"]
|
|
103
|
+
path: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class AgentInstruction:
|
|
108
|
+
anchor_lines: tuple[str, ...]
|
|
109
|
+
body: str
|
|
110
|
+
source_paths: tuple[AgentInstructionSource, ...]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class AgentInvocationRequest:
|
|
115
|
+
invocation_id: str
|
|
116
|
+
worker_id: str | None
|
|
117
|
+
audience: AgentAudience
|
|
118
|
+
assignment_ref: str | None
|
|
119
|
+
purpose: str | None
|
|
120
|
+
assignment: AgentModelAssignment
|
|
121
|
+
instruction: AgentInstruction
|
|
122
|
+
project_root: Path
|
|
123
|
+
run_manifest_path: Path | None
|
|
124
|
+
duty_root: Path
|
|
125
|
+
prompt_path: Path
|
|
126
|
+
metadata_path: Path
|
|
127
|
+
dispatch_kind: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class PreparedAgentInvocation:
|
|
132
|
+
invocation_id: str
|
|
133
|
+
worker_id: str | None
|
|
134
|
+
assignment_ref: str | None
|
|
135
|
+
prompt_path: Path
|
|
136
|
+
metadata_path: Path
|
|
137
|
+
assignment: AgentModelAssignment
|
|
138
|
+
duty_id: str
|
|
139
|
+
duty_version: int
|
|
140
|
+
catalog_digest: str
|
|
141
|
+
assignment_digest: str
|
|
142
|
+
duty_digest: str
|
|
143
|
+
instruction_digest: str
|
|
144
|
+
prompt_digest: str
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class MaterializedStandaloneResult:
|
|
149
|
+
purpose: str
|
|
150
|
+
invocation_id: str
|
|
151
|
+
prompt_metadata_path: Path
|
|
152
|
+
result_path: Path
|
|
153
|
+
result_envelope_digest: str
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True)
|
|
157
|
+
class VerifiedStandaloneResult:
|
|
158
|
+
purpose: str
|
|
159
|
+
invocation_id: str
|
|
160
|
+
prompt_metadata_path: Path
|
|
161
|
+
result_path: Path
|
|
162
|
+
completion_path: Path
|
|
163
|
+
result_envelope_digest: str
|
|
164
|
+
returned_body: str
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def agent_model_assignment_from_payload(payload: object) -> AgentModelAssignment:
|
|
168
|
+
"""Parse the exact persisted six-field model assignment contract."""
|
|
169
|
+
if not isinstance(payload, Mapping) or set(payload) != _NESTED_KEYS["modelAssignment"]:
|
|
170
|
+
raise AgentInvocationError("model assignment payload is invalid")
|
|
171
|
+
string_fields = (
|
|
172
|
+
"provider",
|
|
173
|
+
"model",
|
|
174
|
+
"modelExecutionValue",
|
|
175
|
+
"runner",
|
|
176
|
+
"hostRuntime",
|
|
177
|
+
)
|
|
178
|
+
if any(not isinstance(payload[key], str) for key in string_fields):
|
|
179
|
+
raise AgentInvocationError("model assignment payload is invalid")
|
|
180
|
+
host_model_value = payload["hostModelValue"]
|
|
181
|
+
if host_model_value is not None and not isinstance(host_model_value, str):
|
|
182
|
+
raise AgentInvocationError("model assignment payload is invalid")
|
|
183
|
+
assignment = AgentModelAssignment(
|
|
184
|
+
provider=payload["provider"],
|
|
185
|
+
model=payload["model"],
|
|
186
|
+
model_execution_value=payload["modelExecutionValue"],
|
|
187
|
+
runner=payload["runner"],
|
|
188
|
+
host_runtime=payload["hostRuntime"],
|
|
189
|
+
host_model_value=host_model_value,
|
|
190
|
+
)
|
|
191
|
+
_validate_assignment(assignment)
|
|
192
|
+
return assignment
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class _MaterializedInvocation:
|
|
197
|
+
prepared: PreparedAgentInvocation
|
|
198
|
+
prompt_bytes: bytes
|
|
199
|
+
metadata: dict[str, object]
|
|
200
|
+
metadata_bytes: bytes
|
|
201
|
+
reservation_root: Path | None
|
|
202
|
+
reservation: dict[str, object] | None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def load_common_duty_contract(duty_root: Path) -> DutyContract:
|
|
206
|
+
"""Load the common fragment, which is deliberately outside the role catalog."""
|
|
207
|
+
path = duty_root / "common.md"
|
|
208
|
+
fields, body = _parse_duty_file(path)
|
|
209
|
+
if set(fields) != {"id", "version", "kind"}:
|
|
210
|
+
raise AgentInvocationError(f"invalid common duty frontmatter: {path}")
|
|
211
|
+
if fields["id"] != "common" or fields["kind"] != "common":
|
|
212
|
+
raise AgentInvocationError(f"invalid common duty frontmatter: {path}")
|
|
213
|
+
return DutyContract(
|
|
214
|
+
id="common",
|
|
215
|
+
version=_parse_version(fields["version"], path),
|
|
216
|
+
kind="common",
|
|
217
|
+
applies_to=None,
|
|
218
|
+
body=body,
|
|
219
|
+
source_path=path,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def load_duty_catalog(duty_root: Path) -> dict[AgentAudience, DutyContract]:
|
|
224
|
+
"""Load one role contract for every supported invocation audience."""
|
|
225
|
+
catalog: dict[AgentAudience, DutyContract] = {}
|
|
226
|
+
seen_ids: set[str] = set()
|
|
227
|
+
for path in sorted(duty_root.glob("*.md")):
|
|
228
|
+
if path.name == "common.md":
|
|
229
|
+
continue
|
|
230
|
+
duty = _load_role_duty(path)
|
|
231
|
+
if duty.id in seen_ids:
|
|
232
|
+
raise AgentInvocationError(f"duplicate duty id: {duty.id}")
|
|
233
|
+
if duty.applies_to in catalog:
|
|
234
|
+
raise AgentInvocationError(f"duplicate duty audience: {duty.applies_to}")
|
|
235
|
+
seen_ids.add(duty.id)
|
|
236
|
+
catalog[duty.applies_to] = duty
|
|
237
|
+
missing = sorted(_SUPPORTED_AUDIENCES - set(catalog))
|
|
238
|
+
if missing:
|
|
239
|
+
raise AgentInvocationError(f"missing duty audiences: {', '.join(missing)}")
|
|
240
|
+
return catalog
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def digest_duty_catalog(duty_root: Path) -> str:
|
|
244
|
+
"""Return the canonical digest of every duty file in a snapshot."""
|
|
245
|
+
names = [path.relative_to(duty_root).as_posix() for path in duty_root.glob("*.md")]
|
|
246
|
+
return _digest_framed_files(duty_root, names)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def prepare_agent_invocation(
|
|
250
|
+
request: AgentInvocationRequest,
|
|
251
|
+
) -> PreparedAgentInvocation:
|
|
252
|
+
"""Materialize one immutable prompt and its adjacent completion metadata."""
|
|
253
|
+
_validate_request(request)
|
|
254
|
+
materialized = _materialize(request)
|
|
255
|
+
if materialized.reservation_root is None:
|
|
256
|
+
with _exclusive_lock(_prompt_lock_path(request.prompt_path)):
|
|
257
|
+
_publish_or_reuse(materialized)
|
|
258
|
+
else:
|
|
259
|
+
reservation_lock = materialized.reservation_root / "publish.lock"
|
|
260
|
+
with _exclusive_lock(reservation_lock):
|
|
261
|
+
_publish_or_reuse_reservation(materialized)
|
|
262
|
+
with _exclusive_lock(_prompt_lock_path(request.prompt_path)):
|
|
263
|
+
_publish_or_reuse(materialized)
|
|
264
|
+
errors = verify_agent_invocation(
|
|
265
|
+
request.metadata_path,
|
|
266
|
+
project_root=request.project_root,
|
|
267
|
+
expected_run_manifest_path=request.run_manifest_path,
|
|
268
|
+
expected_assignment=request.assignment,
|
|
269
|
+
expected_invocation_id=request.invocation_id,
|
|
270
|
+
expected_worker_id=request.worker_id,
|
|
271
|
+
expected_assignment_ref=request.assignment_ref,
|
|
272
|
+
expected_audience=request.audience,
|
|
273
|
+
)
|
|
274
|
+
if errors:
|
|
275
|
+
raise AgentInvocationError("; ".join(errors))
|
|
276
|
+
return materialized.prepared
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def compose_agent_prompt(request: AgentInvocationRequest) -> str:
|
|
280
|
+
"""Render and validate a prompt candidate without publishing artifacts."""
|
|
281
|
+
_validate_request(request)
|
|
282
|
+
return _materialize(request).prompt_bytes.decode("utf-8")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def verify_agent_invocation(
|
|
286
|
+
metadata_path: Path,
|
|
287
|
+
*,
|
|
288
|
+
project_root: Path,
|
|
289
|
+
expected_run_manifest_path: Path | None,
|
|
290
|
+
expected_assignment: AgentModelAssignment | None = None,
|
|
291
|
+
expected_invocation_id: str | None = None,
|
|
292
|
+
expected_worker_id: str | None = None,
|
|
293
|
+
expected_assignment_ref: str | None = None,
|
|
294
|
+
expected_audience: AgentAudience | None = None,
|
|
295
|
+
) -> list[str]:
|
|
296
|
+
"""Return every deterministic violation found in a published invocation."""
|
|
297
|
+
metadata = _read_metadata(metadata_path)
|
|
298
|
+
if metadata is None or not _metadata_schema_is_exact(metadata):
|
|
299
|
+
return ["agent invocation metadata schema is invalid"]
|
|
300
|
+
errors = _verify_expected_identity(
|
|
301
|
+
metadata,
|
|
302
|
+
expected_invocation_id=expected_invocation_id,
|
|
303
|
+
expected_worker_id=expected_worker_id,
|
|
304
|
+
expected_assignment_ref=expected_assignment_ref,
|
|
305
|
+
expected_audience=expected_audience,
|
|
306
|
+
)
|
|
307
|
+
errors.extend(_verify_prompt_file(metadata, project_root))
|
|
308
|
+
errors.extend(_verify_assignment(metadata, expected_assignment))
|
|
309
|
+
errors.extend(
|
|
310
|
+
_verify_contract_source(
|
|
311
|
+
metadata,
|
|
312
|
+
metadata_path=metadata_path,
|
|
313
|
+
project_root=project_root,
|
|
314
|
+
expected_run_manifest_path=expected_run_manifest_path,
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
return _deduplicate(errors)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def materialize_standalone_result(
|
|
321
|
+
*,
|
|
322
|
+
project_root: Path,
|
|
323
|
+
purpose: str,
|
|
324
|
+
metadata_path: Path,
|
|
325
|
+
returned_body: bytes,
|
|
326
|
+
) -> MaterializedStandaloneResult:
|
|
327
|
+
"""Publish one immutable raw-return envelope for a standalone invocation."""
|
|
328
|
+
try:
|
|
329
|
+
returned_text = returned_body.decode("utf-8")
|
|
330
|
+
except UnicodeDecodeError as exc:
|
|
331
|
+
raise AgentInvocationError("returned body must be valid UTF-8") from exc
|
|
332
|
+
invocation_id, root = _standalone_lock_identity(
|
|
333
|
+
project_root=project_root, purpose=purpose, metadata_path=metadata_path,
|
|
334
|
+
)
|
|
335
|
+
with _exclusive_lock(root / f"{invocation_id}.result.publish.lock"):
|
|
336
|
+
invocation_id, root, canonical_metadata = _standalone_authority(
|
|
337
|
+
project_root=project_root,
|
|
338
|
+
purpose=purpose,
|
|
339
|
+
metadata_path=metadata_path,
|
|
340
|
+
)
|
|
341
|
+
result_path = root / f"{invocation_id}.result.json"
|
|
342
|
+
envelope = {
|
|
343
|
+
"schemaVersion": 1,
|
|
344
|
+
"purpose": purpose,
|
|
345
|
+
"invocationId": invocation_id,
|
|
346
|
+
"promptMetadataPath": _project_relative(
|
|
347
|
+
canonical_metadata,
|
|
348
|
+
project_root,
|
|
349
|
+
must_exist=True,
|
|
350
|
+
),
|
|
351
|
+
"returnedBody": returned_text,
|
|
352
|
+
}
|
|
353
|
+
envelope_bytes = _canonical_json(envelope)
|
|
354
|
+
_publish_exclusive(result_path, envelope_bytes)
|
|
355
|
+
return MaterializedStandaloneResult(
|
|
356
|
+
purpose=purpose,
|
|
357
|
+
invocation_id=invocation_id,
|
|
358
|
+
prompt_metadata_path=canonical_metadata,
|
|
359
|
+
result_path=result_path,
|
|
360
|
+
result_envelope_digest=_sha256(envelope_bytes),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def publish_standalone_completion(
|
|
365
|
+
*,
|
|
366
|
+
project_root: Path,
|
|
367
|
+
purpose: str,
|
|
368
|
+
metadata_path: Path,
|
|
369
|
+
completed_at: datetime,
|
|
370
|
+
) -> Path:
|
|
371
|
+
"""Publish the completion marker last, after re-verifying prompt and result."""
|
|
372
|
+
completed = _utc_seconds(completed_at)
|
|
373
|
+
invocation_id, root = _standalone_lock_identity(
|
|
374
|
+
project_root=project_root, purpose=purpose, metadata_path=metadata_path,
|
|
375
|
+
)
|
|
376
|
+
with _exclusive_lock(root / f"{invocation_id}.result.publish.lock"):
|
|
377
|
+
invocation_id, root, canonical_metadata = _standalone_authority(
|
|
378
|
+
project_root=project_root,
|
|
379
|
+
purpose=purpose,
|
|
380
|
+
metadata_path=metadata_path,
|
|
381
|
+
)
|
|
382
|
+
result_path = root / f"{invocation_id}.result.json"
|
|
383
|
+
completion_path = root / f"{invocation_id}.prompt.md.completion.json"
|
|
384
|
+
envelope_bytes, _returned_body = _verified_result_envelope(
|
|
385
|
+
result_path,
|
|
386
|
+
project_root=project_root,
|
|
387
|
+
purpose=purpose,
|
|
388
|
+
invocation_id=invocation_id,
|
|
389
|
+
metadata_path=canonical_metadata,
|
|
390
|
+
)
|
|
391
|
+
completion = {
|
|
392
|
+
"schemaVersion": 1,
|
|
393
|
+
"purpose": purpose,
|
|
394
|
+
"invocationId": invocation_id,
|
|
395
|
+
"status": "completed",
|
|
396
|
+
"promptMetadataPath": _project_relative(
|
|
397
|
+
canonical_metadata,
|
|
398
|
+
project_root,
|
|
399
|
+
must_exist=True,
|
|
400
|
+
),
|
|
401
|
+
"resultPath": _project_relative(
|
|
402
|
+
result_path,
|
|
403
|
+
project_root,
|
|
404
|
+
must_exist=True,
|
|
405
|
+
),
|
|
406
|
+
"resultEnvelopeDigest": _sha256(envelope_bytes),
|
|
407
|
+
"completedAt": completed,
|
|
408
|
+
}
|
|
409
|
+
_publish_exclusive(completion_path, _canonical_json(completion))
|
|
410
|
+
return completion_path
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def verify_standalone_completion(
|
|
414
|
+
completion_path: Path,
|
|
415
|
+
*,
|
|
416
|
+
project_root: Path,
|
|
417
|
+
expected_purpose: str,
|
|
418
|
+
) -> VerifiedStandaloneResult:
|
|
419
|
+
"""Verify a completion marker and return its already-read immutable body."""
|
|
420
|
+
_validate_slug(expected_purpose, "standalone purpose")
|
|
421
|
+
completion = _load_json_object(completion_path, "standalone completion")
|
|
422
|
+
expected_keys = {
|
|
423
|
+
"schemaVersion",
|
|
424
|
+
"purpose",
|
|
425
|
+
"invocationId",
|
|
426
|
+
"status",
|
|
427
|
+
"promptMetadataPath",
|
|
428
|
+
"resultPath",
|
|
429
|
+
"resultEnvelopeDigest",
|
|
430
|
+
"completedAt",
|
|
431
|
+
}
|
|
432
|
+
if set(completion) != expected_keys or completion.get("schemaVersion") != 1:
|
|
433
|
+
raise AgentInvocationError("standalone completion schema is invalid")
|
|
434
|
+
if completion.get("purpose") != expected_purpose:
|
|
435
|
+
raise AgentInvocationError("standalone completion purpose does not match")
|
|
436
|
+
invocation_id = completion.get("invocationId")
|
|
437
|
+
if not isinstance(invocation_id, str):
|
|
438
|
+
raise AgentInvocationError("standalone completion invocation ID is invalid")
|
|
439
|
+
_validate_slug(invocation_id, "standalone invocation ID")
|
|
440
|
+
root = project_root / ".okstra" / "agent-invocations" / expected_purpose
|
|
441
|
+
canonical_completion = root / f"{invocation_id}.prompt.md.completion.json"
|
|
442
|
+
if completion_path.resolve(strict=True) != canonical_completion.resolve(strict=True):
|
|
443
|
+
raise AgentInvocationError("standalone completion path is not canonical")
|
|
444
|
+
metadata_path = root / f"{invocation_id}.prompt.md.meta.json"
|
|
445
|
+
result_path = root / f"{invocation_id}.result.json"
|
|
446
|
+
if completion.get("status") != "completed":
|
|
447
|
+
raise AgentInvocationError("standalone completion status is invalid")
|
|
448
|
+
if completion.get("promptMetadataPath") != _project_relative(
|
|
449
|
+
metadata_path,
|
|
450
|
+
project_root,
|
|
451
|
+
must_exist=True,
|
|
452
|
+
):
|
|
453
|
+
raise AgentInvocationError("standalone completion metadata path does not match")
|
|
454
|
+
if completion.get("resultPath") != _project_relative(
|
|
455
|
+
result_path,
|
|
456
|
+
project_root,
|
|
457
|
+
must_exist=True,
|
|
458
|
+
):
|
|
459
|
+
raise AgentInvocationError("standalone completion result path does not match")
|
|
460
|
+
with _exclusive_lock(root / f"{invocation_id}.result.publish.lock"):
|
|
461
|
+
authority_id, _authority_root, canonical_metadata = _standalone_authority(
|
|
462
|
+
project_root=project_root,
|
|
463
|
+
purpose=expected_purpose,
|
|
464
|
+
metadata_path=metadata_path,
|
|
465
|
+
)
|
|
466
|
+
if authority_id != invocation_id:
|
|
467
|
+
raise AgentInvocationError("standalone completion identity does not match")
|
|
468
|
+
envelope_bytes, returned_body = _verified_result_envelope(
|
|
469
|
+
result_path,
|
|
470
|
+
project_root=project_root,
|
|
471
|
+
purpose=expected_purpose,
|
|
472
|
+
invocation_id=invocation_id,
|
|
473
|
+
metadata_path=canonical_metadata,
|
|
474
|
+
)
|
|
475
|
+
digest = _sha256(envelope_bytes)
|
|
476
|
+
if completion.get("resultEnvelopeDigest") != digest:
|
|
477
|
+
raise AgentInvocationError("result envelope digest does not match completion")
|
|
478
|
+
return VerifiedStandaloneResult(
|
|
479
|
+
purpose=expected_purpose,
|
|
480
|
+
invocation_id=invocation_id,
|
|
481
|
+
prompt_metadata_path=metadata_path,
|
|
482
|
+
result_path=result_path,
|
|
483
|
+
completion_path=canonical_completion,
|
|
484
|
+
result_envelope_digest=digest,
|
|
485
|
+
returned_body=returned_body,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _standalone_authority(
|
|
490
|
+
*, project_root: Path, purpose: str, metadata_path: Path,
|
|
491
|
+
) -> tuple[str, Path, Path]:
|
|
492
|
+
_validate_slug(purpose, "standalone purpose")
|
|
493
|
+
root = project_root / ".okstra" / "agent-invocations" / purpose
|
|
494
|
+
metadata = _load_json_object(metadata_path, "agent invocation metadata")
|
|
495
|
+
source = metadata.get("contractSource")
|
|
496
|
+
if not isinstance(source, Mapping) or source.get("mode") != "standalone":
|
|
497
|
+
raise AgentInvocationError("result completion requires standalone metadata")
|
|
498
|
+
invocation_id = metadata.get("invocationId")
|
|
499
|
+
if not isinstance(invocation_id, str):
|
|
500
|
+
raise AgentInvocationError("standalone invocation ID is invalid")
|
|
501
|
+
_validate_slug(invocation_id, "standalone invocation ID")
|
|
502
|
+
canonical_metadata = root / f"{invocation_id}.prompt.md.meta.json"
|
|
503
|
+
if metadata_path.resolve(strict=True) != canonical_metadata.resolve(strict=True):
|
|
504
|
+
raise AgentInvocationError("standalone metadata path is not canonical")
|
|
505
|
+
errors = verify_agent_invocation(
|
|
506
|
+
canonical_metadata,
|
|
507
|
+
project_root=project_root,
|
|
508
|
+
expected_run_manifest_path=None,
|
|
509
|
+
expected_invocation_id=invocation_id,
|
|
510
|
+
)
|
|
511
|
+
if errors:
|
|
512
|
+
raise AgentInvocationError("; ".join(errors))
|
|
513
|
+
return invocation_id, root, canonical_metadata
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _standalone_lock_identity(
|
|
517
|
+
*, project_root: Path, purpose: str, metadata_path: Path,
|
|
518
|
+
) -> tuple[str, Path]:
|
|
519
|
+
"""Derive only the stable lock name; verify all authority after locking."""
|
|
520
|
+
_validate_slug(purpose, "standalone purpose")
|
|
521
|
+
suffix = ".prompt.md.meta.json"
|
|
522
|
+
if not metadata_path.name.endswith(suffix):
|
|
523
|
+
raise AgentInvocationError("standalone metadata path is not canonical")
|
|
524
|
+
invocation_id = metadata_path.name.removesuffix(suffix)
|
|
525
|
+
_validate_slug(invocation_id, "standalone invocation ID")
|
|
526
|
+
return invocation_id, project_root / ".okstra" / "agent-invocations" / purpose
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _verified_result_envelope(
|
|
530
|
+
result_path: Path,
|
|
531
|
+
*,
|
|
532
|
+
project_root: Path,
|
|
533
|
+
purpose: str,
|
|
534
|
+
invocation_id: str,
|
|
535
|
+
metadata_path: Path,
|
|
536
|
+
) -> tuple[bytes, str]:
|
|
537
|
+
try:
|
|
538
|
+
body = result_path.read_bytes()
|
|
539
|
+
envelope = json.loads(body.decode("utf-8"))
|
|
540
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
541
|
+
raise AgentInvocationError(
|
|
542
|
+
f"standalone result envelope is missing or invalid: {result_path}"
|
|
543
|
+
) from exc
|
|
544
|
+
expected_keys = {
|
|
545
|
+
"schemaVersion", "purpose", "invocationId", "promptMetadataPath", "returnedBody",
|
|
546
|
+
}
|
|
547
|
+
expected_metadata = _project_relative(
|
|
548
|
+
metadata_path,
|
|
549
|
+
project_root,
|
|
550
|
+
must_exist=True,
|
|
551
|
+
)
|
|
552
|
+
if (
|
|
553
|
+
not isinstance(envelope, dict)
|
|
554
|
+
or set(envelope) != expected_keys
|
|
555
|
+
or envelope.get("schemaVersion") != 1
|
|
556
|
+
or envelope.get("purpose") != purpose
|
|
557
|
+
or envelope.get("invocationId") != invocation_id
|
|
558
|
+
or envelope.get("promptMetadataPath") != expected_metadata
|
|
559
|
+
or not isinstance(envelope.get("returnedBody"), str)
|
|
560
|
+
):
|
|
561
|
+
raise AgentInvocationError("standalone result envelope contract does not match")
|
|
562
|
+
if body != _canonical_json(envelope):
|
|
563
|
+
raise AgentInvocationError("standalone result envelope is not canonical JSON")
|
|
564
|
+
return body, envelope["returnedBody"]
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _validate_slug(value: str, label: str) -> None:
|
|
568
|
+
if not _SLUG_RE.fullmatch(value):
|
|
569
|
+
raise AgentInvocationError(f"{label} must be a slug")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _utc_seconds(value: datetime) -> str:
|
|
573
|
+
if value.tzinfo is None or value.utcoffset() != timezone.utc.utcoffset(value):
|
|
574
|
+
raise AgentInvocationError("completedAt must be a UTC datetime")
|
|
575
|
+
return value.astimezone(timezone.utc).replace(microsecond=0).strftime(
|
|
576
|
+
"%Y-%m-%dT%H:%M:%SZ"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _materialize(request: AgentInvocationRequest) -> _MaterializedInvocation:
|
|
581
|
+
common = load_common_duty_contract(request.duty_root)
|
|
582
|
+
catalog = load_duty_catalog(request.duty_root)
|
|
583
|
+
duty = catalog[request.audience]
|
|
584
|
+
prompt_bytes = _render_prompt(request, common, duty).encode("utf-8")
|
|
585
|
+
digests = _invocation_digests(request, duty, prompt_bytes)
|
|
586
|
+
metadata = _metadata_payload(request, duty, digests)
|
|
587
|
+
prepared = PreparedAgentInvocation(
|
|
588
|
+
invocation_id=request.invocation_id,
|
|
589
|
+
worker_id=request.worker_id,
|
|
590
|
+
assignment_ref=request.assignment_ref,
|
|
591
|
+
prompt_path=request.prompt_path,
|
|
592
|
+
metadata_path=request.metadata_path,
|
|
593
|
+
assignment=request.assignment,
|
|
594
|
+
duty_id=duty.id,
|
|
595
|
+
duty_version=duty.version,
|
|
596
|
+
catalog_digest=digests["catalogDigest"],
|
|
597
|
+
assignment_digest=digests["assignmentDigest"],
|
|
598
|
+
duty_digest=digests["dutyDigest"],
|
|
599
|
+
instruction_digest=digests["instructionDigest"],
|
|
600
|
+
prompt_digest=digests["promptDigest"],
|
|
601
|
+
)
|
|
602
|
+
reservation_root, reservation = _reservation_spec(request)
|
|
603
|
+
return _MaterializedInvocation(
|
|
604
|
+
prepared=prepared,
|
|
605
|
+
prompt_bytes=prompt_bytes,
|
|
606
|
+
metadata=metadata,
|
|
607
|
+
metadata_bytes=_pretty_json(metadata),
|
|
608
|
+
reservation_root=reservation_root,
|
|
609
|
+
reservation=reservation,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _validate_request(request: AgentInvocationRequest) -> None:
|
|
614
|
+
if request.audience not in _SUPPORTED_AUDIENCES:
|
|
615
|
+
raise AgentInvocationError(f"unknown duty audience: {request.audience}")
|
|
616
|
+
adjacent = request.prompt_path.with_name(request.prompt_path.name + ".meta.json")
|
|
617
|
+
if request.metadata_path != adjacent:
|
|
618
|
+
raise AgentInvocationError("metadata path must be adjacent to prompt path")
|
|
619
|
+
_validate_assignment(request.assignment)
|
|
620
|
+
_validate_instruction(request.instruction)
|
|
621
|
+
_project_relative(request.duty_root, request.project_root, must_exist=True)
|
|
622
|
+
_project_relative(request.prompt_path, request.project_root, must_exist=False)
|
|
623
|
+
_project_relative(request.metadata_path, request.project_root, must_exist=False)
|
|
624
|
+
if request.run_manifest_path is None:
|
|
625
|
+
_validate_standalone_identity(request)
|
|
626
|
+
else:
|
|
627
|
+
_validate_run_identity(request)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _validate_assignment(assignment: AgentModelAssignment) -> None:
|
|
631
|
+
values = (
|
|
632
|
+
assignment.provider,
|
|
633
|
+
assignment.model,
|
|
634
|
+
assignment.model_execution_value,
|
|
635
|
+
assignment.runner,
|
|
636
|
+
assignment.host_runtime,
|
|
637
|
+
)
|
|
638
|
+
if any(not value.strip() for value in values):
|
|
639
|
+
raise AgentInvocationError("model assignment fields must be non-empty")
|
|
640
|
+
if assignment.runner == "native-session" and not assignment.host_model_value:
|
|
641
|
+
raise AgentInvocationError("native assignment requires a host model value")
|
|
642
|
+
if assignment.runner == "cli-wrapper" and assignment.host_model_value is not None:
|
|
643
|
+
raise AgentInvocationError("CLI assignment must not define a host model value")
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _validate_instruction(instruction: AgentInstruction) -> None:
|
|
647
|
+
if re.search(r"(?m)^## (?:Duty Contract|Task Instructions)\s*$", instruction.body):
|
|
648
|
+
raise AgentInvocationError("task instruction contains a reserved duty heading")
|
|
649
|
+
if any("\n" in line for line in instruction.anchor_lines):
|
|
650
|
+
raise AgentInvocationError("instruction anchorLines must be single lines")
|
|
651
|
+
normalized = [_source_payload(source) for source in instruction.source_paths]
|
|
652
|
+
if not normalized or len({_canonical_json(item) for item in normalized}) != len(normalized):
|
|
653
|
+
raise AgentInvocationError("instruction sourcePaths must be non-empty and unique")
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _validate_standalone_identity(request: AgentInvocationRequest) -> None:
|
|
657
|
+
if not request.purpose or not _SLUG_RE.fullmatch(request.purpose):
|
|
658
|
+
raise AgentInvocationError("standalone purpose must be a slug")
|
|
659
|
+
if not _SLUG_RE.fullmatch(request.invocation_id):
|
|
660
|
+
raise AgentInvocationError("standalone invocation ID must be a slug")
|
|
661
|
+
if request.worker_id is not None or request.assignment_ref is not None:
|
|
662
|
+
raise AgentInvocationError("standalone invocation cannot bind a run worker")
|
|
663
|
+
root = (
|
|
664
|
+
request.project_root
|
|
665
|
+
/ ".okstra"
|
|
666
|
+
/ "agent-invocations"
|
|
667
|
+
/ request.purpose
|
|
668
|
+
)
|
|
669
|
+
expected_prompt = root / f"{request.invocation_id}.prompt.md"
|
|
670
|
+
expected_duty = root / f"{request.invocation_id}.duty-contracts"
|
|
671
|
+
if request.prompt_path != expected_prompt or request.duty_root != expected_duty:
|
|
672
|
+
raise AgentInvocationError("prompt does not use canonical standalone path")
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _validate_run_identity(request: AgentInvocationRequest) -> None:
|
|
676
|
+
if request.purpose is not None:
|
|
677
|
+
raise AgentInvocationError("run invocation cannot define standalone purpose")
|
|
678
|
+
if not request.worker_id or not _SLUG_RE.fullmatch(request.worker_id):
|
|
679
|
+
raise AgentInvocationError("run worker ID must be a non-empty slug")
|
|
680
|
+
if not request.assignment_ref:
|
|
681
|
+
raise AgentInvocationError("run assignment reference is required")
|
|
682
|
+
_project_relative(
|
|
683
|
+
request.run_manifest_path,
|
|
684
|
+
request.project_root,
|
|
685
|
+
must_exist=True,
|
|
686
|
+
)
|
|
687
|
+
suffix = request.assignment_ref.partition("/")[2]
|
|
688
|
+
if suffix and request.assignment_ref.split("/", 1)[0] in {"initial", "reverify"}:
|
|
689
|
+
if suffix != request.worker_id:
|
|
690
|
+
raise AgentInvocationError("run worker ID does not match assignment reference")
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _render_prompt(
|
|
694
|
+
request: AgentInvocationRequest,
|
|
695
|
+
common: DutyContract,
|
|
696
|
+
duty: DutyContract,
|
|
697
|
+
) -> str:
|
|
698
|
+
assignment = request.assignment
|
|
699
|
+
header = [
|
|
700
|
+
*request.instruction.anchor_lines,
|
|
701
|
+
f"**Provider:** {assignment.provider}",
|
|
702
|
+
f"**Model:** {assignment.model}",
|
|
703
|
+
f"**Model execution value:** {assignment.model_execution_value}",
|
|
704
|
+
f"**Runner:** {assignment.runner}",
|
|
705
|
+
f"**Host runtime:** {assignment.host_runtime}",
|
|
706
|
+
]
|
|
707
|
+
if assignment.host_model_value is not None:
|
|
708
|
+
header.append(f"**Host model value:** {assignment.host_model_value}")
|
|
709
|
+
duty_body = f"{common.body.rstrip()}\n\n{duty.body.rstrip()}"
|
|
710
|
+
task_body = request.instruction.body.rstrip("\n")
|
|
711
|
+
return (
|
|
712
|
+
"\n".join(header)
|
|
713
|
+
+ "\n\n## Duty Contract\n\n"
|
|
714
|
+
+ duty_body
|
|
715
|
+
+ "\n\n## Task Instructions\n\n"
|
|
716
|
+
+ task_body
|
|
717
|
+
+ "\n"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _invocation_digests(
|
|
722
|
+
request: AgentInvocationRequest,
|
|
723
|
+
duty: DutyContract,
|
|
724
|
+
prompt_bytes: bytes,
|
|
725
|
+
) -> dict[str, str]:
|
|
726
|
+
assignment = _assignment_payload(request.assignment)
|
|
727
|
+
instruction = {
|
|
728
|
+
"anchorLines": list(request.instruction.anchor_lines),
|
|
729
|
+
"body": request.instruction.body,
|
|
730
|
+
"sourcePaths": [
|
|
731
|
+
_source_payload(source) for source in request.instruction.source_paths
|
|
732
|
+
],
|
|
733
|
+
}
|
|
734
|
+
selected_names = ["common.md", duty.source_path.name]
|
|
735
|
+
return {
|
|
736
|
+
"catalogDigest": digest_duty_catalog(request.duty_root),
|
|
737
|
+
"assignmentDigest": _sha256(_canonical_json(assignment)),
|
|
738
|
+
"dutyDigest": _digest_framed_files(request.duty_root, selected_names),
|
|
739
|
+
"instructionDigest": _sha256(_canonical_json(instruction)),
|
|
740
|
+
"promptDigest": _sha256(prompt_bytes),
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _metadata_payload(
|
|
745
|
+
request: AgentInvocationRequest,
|
|
746
|
+
duty: DutyContract,
|
|
747
|
+
digests: Mapping[str, str],
|
|
748
|
+
) -> dict[str, object]:
|
|
749
|
+
mode = "run" if request.run_manifest_path is not None else "standalone"
|
|
750
|
+
manifest_path = (
|
|
751
|
+
_project_relative(
|
|
752
|
+
request.run_manifest_path,
|
|
753
|
+
request.project_root,
|
|
754
|
+
must_exist=True,
|
|
755
|
+
)
|
|
756
|
+
if request.run_manifest_path is not None
|
|
757
|
+
else None
|
|
758
|
+
)
|
|
759
|
+
return {
|
|
760
|
+
"schemaVersion": 1,
|
|
761
|
+
"invocationId": request.invocation_id,
|
|
762
|
+
"audience": request.audience,
|
|
763
|
+
"dispatchKind": request.dispatch_kind,
|
|
764
|
+
"workerId": request.worker_id,
|
|
765
|
+
"assignmentRef": request.assignment_ref,
|
|
766
|
+
"contractSource": {
|
|
767
|
+
"mode": mode,
|
|
768
|
+
"runManifestPath": manifest_path,
|
|
769
|
+
"dutyRootPath": _project_relative(
|
|
770
|
+
request.duty_root,
|
|
771
|
+
request.project_root,
|
|
772
|
+
must_exist=True,
|
|
773
|
+
),
|
|
774
|
+
},
|
|
775
|
+
"modelAssignment": _assignment_payload(request.assignment),
|
|
776
|
+
"dutyContract": {"id": duty.id, "version": duty.version},
|
|
777
|
+
"instruction": {
|
|
778
|
+
"sourcePaths": [
|
|
779
|
+
_source_payload(source)
|
|
780
|
+
for source in request.instruction.source_paths
|
|
781
|
+
]
|
|
782
|
+
},
|
|
783
|
+
"prompt": {
|
|
784
|
+
"path": _project_relative(
|
|
785
|
+
request.prompt_path,
|
|
786
|
+
request.project_root,
|
|
787
|
+
must_exist=False,
|
|
788
|
+
)
|
|
789
|
+
},
|
|
790
|
+
"digests": dict(digests),
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _reservation_spec(
|
|
795
|
+
request: AgentInvocationRequest,
|
|
796
|
+
) -> tuple[Path | None, dict[str, object] | None]:
|
|
797
|
+
if request.run_manifest_path is None:
|
|
798
|
+
return None, None
|
|
799
|
+
manifest = _load_json_object(request.run_manifest_path, "run manifest")
|
|
800
|
+
contract = manifest.get("agentContract")
|
|
801
|
+
if not isinstance(contract, dict):
|
|
802
|
+
raise AgentInvocationError("run manifest has no agent contract")
|
|
803
|
+
_validate_manifest_contract(request, manifest, contract)
|
|
804
|
+
reservation_value = contract.get("invocationReservationRootPath")
|
|
805
|
+
reservation_root = _project_path(
|
|
806
|
+
request.project_root,
|
|
807
|
+
reservation_value,
|
|
808
|
+
must_exist=False,
|
|
809
|
+
)
|
|
810
|
+
reservation = {
|
|
811
|
+
"schemaVersion": 1,
|
|
812
|
+
"invocationId": request.invocation_id,
|
|
813
|
+
"workerId": request.worker_id,
|
|
814
|
+
"assignmentRef": request.assignment_ref,
|
|
815
|
+
"audience": request.audience,
|
|
816
|
+
"dispatchKind": request.dispatch_kind,
|
|
817
|
+
"promptPath": _project_relative(
|
|
818
|
+
request.prompt_path,
|
|
819
|
+
request.project_root,
|
|
820
|
+
must_exist=False,
|
|
821
|
+
),
|
|
822
|
+
"metadataPath": _project_relative(
|
|
823
|
+
request.metadata_path,
|
|
824
|
+
request.project_root,
|
|
825
|
+
must_exist=False,
|
|
826
|
+
),
|
|
827
|
+
}
|
|
828
|
+
return reservation_root, reservation
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _validate_manifest_contract(
|
|
832
|
+
request: AgentInvocationRequest,
|
|
833
|
+
manifest: Mapping[str, object],
|
|
834
|
+
contract: Mapping[str, object],
|
|
835
|
+
) -> None:
|
|
836
|
+
duty_path = _project_relative(
|
|
837
|
+
request.duty_root,
|
|
838
|
+
request.project_root,
|
|
839
|
+
must_exist=True,
|
|
840
|
+
)
|
|
841
|
+
if contract.get("dutyRootPath") != duty_path:
|
|
842
|
+
raise AgentInvocationError("duty root does not match run manifest")
|
|
843
|
+
if contract.get("catalogDigest") != digest_duty_catalog(request.duty_root):
|
|
844
|
+
raise AgentInvocationError("catalog digest does not match run duty snapshot")
|
|
845
|
+
allowed = contract.get("allowedAudiences")
|
|
846
|
+
if not isinstance(allowed, list) or request.audience not in allowed:
|
|
847
|
+
raise AgentInvocationError("audience is not allowed by run manifest")
|
|
848
|
+
assignments = manifest.get("invocationAssignments")
|
|
849
|
+
manifest_assignment = (
|
|
850
|
+
assignments.get(request.assignment_ref)
|
|
851
|
+
if isinstance(assignments, dict)
|
|
852
|
+
else None
|
|
853
|
+
)
|
|
854
|
+
if manifest_assignment != _assignment_payload(request.assignment):
|
|
855
|
+
raise AgentInvocationError("model assignment does not match run manifest")
|
|
856
|
+
_validate_authorized_prompt_path(request, contract)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _validate_authorized_prompt_path(
|
|
860
|
+
request: AgentInvocationRequest,
|
|
861
|
+
contract: Mapping[str, object],
|
|
862
|
+
) -> None:
|
|
863
|
+
authorized = contract.get("authorizedPaths")
|
|
864
|
+
roots = authorized.get("promptRoots") if isinstance(authorized, dict) else None
|
|
865
|
+
if not isinstance(roots, list) or not roots:
|
|
866
|
+
raise AgentInvocationError("run manifest has no authorized prompt roots")
|
|
867
|
+
prompt = request.prompt_path.resolve(strict=False)
|
|
868
|
+
allowed = [
|
|
869
|
+
_project_path(request.project_root, value, must_exist=True)
|
|
870
|
+
for value in roots
|
|
871
|
+
]
|
|
872
|
+
if not any(_is_relative_to(prompt, root) for root in allowed):
|
|
873
|
+
raise AgentInvocationError("prompt path is outside authorized prompt roots")
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _publish_or_reuse_reservation(materialized: _MaterializedInvocation) -> None:
|
|
877
|
+
root = materialized.reservation_root
|
|
878
|
+
reservation = materialized.reservation
|
|
879
|
+
if root is None or reservation is None:
|
|
880
|
+
return
|
|
881
|
+
path = root / f"{materialized.prepared.invocation_id}.json"
|
|
882
|
+
if path.exists():
|
|
883
|
+
if _read_json_if_object(path) != reservation:
|
|
884
|
+
raise AgentInvocationError(
|
|
885
|
+
"invocation reservation conflicts with existing invocation",
|
|
886
|
+
reason="invocation_reservation_conflict",
|
|
887
|
+
)
|
|
888
|
+
return
|
|
889
|
+
_publish_exclusive(path, _pretty_json(reservation))
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def _publish_or_reuse(materialized: _MaterializedInvocation) -> None:
|
|
893
|
+
prompt = materialized.prepared.prompt_path
|
|
894
|
+
metadata = materialized.prepared.metadata_path
|
|
895
|
+
prompt_exists = prompt.exists()
|
|
896
|
+
metadata_exists = metadata.exists()
|
|
897
|
+
if metadata_exists and not prompt_exists:
|
|
898
|
+
raise _existing_conflict("metadata exists without prompt")
|
|
899
|
+
if prompt_exists and prompt.read_bytes() != materialized.prompt_bytes:
|
|
900
|
+
raise _existing_conflict("existing prompt differs")
|
|
901
|
+
if prompt_exists and metadata_exists:
|
|
902
|
+
if metadata.read_bytes() != materialized.metadata_bytes:
|
|
903
|
+
raise _existing_conflict("existing metadata differs")
|
|
904
|
+
return
|
|
905
|
+
if not prompt_exists:
|
|
906
|
+
_publish_exclusive(prompt, materialized.prompt_bytes)
|
|
907
|
+
_publish_exclusive(metadata, materialized.metadata_bytes)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _existing_conflict(detail: str) -> AgentInvocationError:
|
|
911
|
+
return AgentInvocationError(
|
|
912
|
+
f"existing_invocation_conflict: {detail}",
|
|
913
|
+
reason="existing_invocation_conflict",
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
@contextlib.contextmanager
|
|
918
|
+
def _exclusive_lock(path: Path) -> Iterator[None]:
|
|
919
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
920
|
+
handle = path.open("a+")
|
|
921
|
+
try:
|
|
922
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
923
|
+
yield
|
|
924
|
+
finally:
|
|
925
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
926
|
+
handle.close()
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def _prompt_lock_path(prompt_path: Path) -> Path:
|
|
930
|
+
return prompt_path.with_name(prompt_path.name + ".publish.lock")
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _publish_exclusive(path: Path, body: bytes) -> None:
|
|
934
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
935
|
+
temp_path: Path | None = None
|
|
936
|
+
try:
|
|
937
|
+
with tempfile.NamedTemporaryFile(
|
|
938
|
+
dir=path.parent,
|
|
939
|
+
prefix=f".{path.name}.",
|
|
940
|
+
suffix=".tmp",
|
|
941
|
+
delete=False,
|
|
942
|
+
) as handle:
|
|
943
|
+
temp_path = Path(handle.name)
|
|
944
|
+
handle.write(body)
|
|
945
|
+
handle.flush()
|
|
946
|
+
os.fsync(handle.fileno())
|
|
947
|
+
os.link(temp_path, path)
|
|
948
|
+
except FileExistsError as exc:
|
|
949
|
+
raise _existing_conflict(f"published path already exists: {path}") from exc
|
|
950
|
+
finally:
|
|
951
|
+
if temp_path is not None:
|
|
952
|
+
temp_path.unlink(missing_ok=True)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _verify_expected_identity(
|
|
956
|
+
metadata: Mapping[str, object],
|
|
957
|
+
*,
|
|
958
|
+
expected_invocation_id: str | None,
|
|
959
|
+
expected_worker_id: str | None,
|
|
960
|
+
expected_assignment_ref: str | None,
|
|
961
|
+
expected_audience: AgentAudience | None,
|
|
962
|
+
) -> list[str]:
|
|
963
|
+
expected = {
|
|
964
|
+
"invocationId": expected_invocation_id,
|
|
965
|
+
"workerId": expected_worker_id,
|
|
966
|
+
"assignmentRef": expected_assignment_ref,
|
|
967
|
+
"audience": expected_audience,
|
|
968
|
+
}
|
|
969
|
+
errors = []
|
|
970
|
+
for key, value in expected.items():
|
|
971
|
+
if value is not None and metadata.get(key) != value:
|
|
972
|
+
errors.append(f"{key} does not match expected invocation identity")
|
|
973
|
+
return errors
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _verify_prompt_file(
|
|
977
|
+
metadata: Mapping[str, object],
|
|
978
|
+
project_root: Path,
|
|
979
|
+
) -> list[str]:
|
|
980
|
+
prompt = metadata["prompt"]
|
|
981
|
+
digests = metadata["digests"]
|
|
982
|
+
assert isinstance(prompt, dict)
|
|
983
|
+
assert isinstance(digests, dict)
|
|
984
|
+
try:
|
|
985
|
+
path = _project_path(project_root, prompt["path"], must_exist=True)
|
|
986
|
+
except AgentInvocationError:
|
|
987
|
+
return ["prompt path is invalid or missing"]
|
|
988
|
+
body = path.read_bytes()
|
|
989
|
+
if digests["promptDigest"] != _sha256(body):
|
|
990
|
+
return ["prompt digest does not match prompt file"]
|
|
991
|
+
errors = []
|
|
992
|
+
errors.extend(_verify_instruction_digest(metadata, body))
|
|
993
|
+
errors.extend(_verify_model_header(metadata, body))
|
|
994
|
+
return errors
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def _verify_instruction_digest(
|
|
998
|
+
metadata: Mapping[str, object],
|
|
999
|
+
prompt_bytes: bytes,
|
|
1000
|
+
) -> list[str]:
|
|
1001
|
+
try:
|
|
1002
|
+
prompt = prompt_bytes.decode("utf-8")
|
|
1003
|
+
prefix, task_body = _split_prompt(prompt)
|
|
1004
|
+
anchor_lines = _extract_anchor_lines(prefix)
|
|
1005
|
+
except (UnicodeDecodeError, ValueError):
|
|
1006
|
+
return ["prompt structure is invalid"]
|
|
1007
|
+
instruction = metadata["instruction"]
|
|
1008
|
+
digests = metadata["digests"]
|
|
1009
|
+
assert isinstance(instruction, dict)
|
|
1010
|
+
assert isinstance(digests, dict)
|
|
1011
|
+
base = {
|
|
1012
|
+
"anchorLines": anchor_lines,
|
|
1013
|
+
"sourcePaths": instruction["sourcePaths"],
|
|
1014
|
+
}
|
|
1015
|
+
candidates = (
|
|
1016
|
+
{**base, "body": task_body},
|
|
1017
|
+
{**base, "body": task_body.rstrip("\n")},
|
|
1018
|
+
)
|
|
1019
|
+
actual = digests["instructionDigest"]
|
|
1020
|
+
if not any(_sha256(_canonical_json(candidate)) == actual for candidate in candidates):
|
|
1021
|
+
return ["instruction digest does not match prompt instructions"]
|
|
1022
|
+
return []
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _verify_model_header(
|
|
1026
|
+
metadata: Mapping[str, object],
|
|
1027
|
+
prompt_bytes: bytes,
|
|
1028
|
+
) -> list[str]:
|
|
1029
|
+
assignment = metadata["modelAssignment"]
|
|
1030
|
+
assert isinstance(assignment, dict)
|
|
1031
|
+
prompt = prompt_bytes.decode("utf-8")
|
|
1032
|
+
required = {
|
|
1033
|
+
f"**Provider:** {assignment['provider']}",
|
|
1034
|
+
f"**Model:** {assignment['model']}",
|
|
1035
|
+
f"**Model execution value:** {assignment['modelExecutionValue']}",
|
|
1036
|
+
f"**Runner:** {assignment['runner']}",
|
|
1037
|
+
f"**Host runtime:** {assignment['hostRuntime']}",
|
|
1038
|
+
}
|
|
1039
|
+
host_model = assignment["hostModelValue"]
|
|
1040
|
+
if host_model is not None:
|
|
1041
|
+
required.add(f"**Host model value:** {host_model}")
|
|
1042
|
+
prefix = prompt.split("\n\n## Duty Contract\n\n", 1)[0]
|
|
1043
|
+
if not required.issubset(set(prefix.splitlines())):
|
|
1044
|
+
return ["prompt model header does not match model assignment"]
|
|
1045
|
+
return []
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _split_prompt(prompt: str) -> tuple[str, str]:
|
|
1049
|
+
duty_marker = "\n\n## Duty Contract\n\n"
|
|
1050
|
+
task_marker = "\n\n## Task Instructions\n\n"
|
|
1051
|
+
if prompt.count(duty_marker) != 1 or prompt.count(task_marker) != 1:
|
|
1052
|
+
raise ValueError("prompt markers are invalid")
|
|
1053
|
+
prefix, remainder = prompt.split(duty_marker, 1)
|
|
1054
|
+
_duty, task = remainder.split(task_marker, 1)
|
|
1055
|
+
return prefix, task
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _extract_anchor_lines(prefix: str) -> list[str]:
|
|
1059
|
+
lines = prefix.splitlines()
|
|
1060
|
+
try:
|
|
1061
|
+
provider_index = next(
|
|
1062
|
+
index for index, line in enumerate(lines) if line.startswith("**Provider:**")
|
|
1063
|
+
)
|
|
1064
|
+
except StopIteration as exc:
|
|
1065
|
+
raise ValueError("model header is missing") from exc
|
|
1066
|
+
return lines[:provider_index]
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def _verify_assignment(
|
|
1070
|
+
metadata: Mapping[str, object],
|
|
1071
|
+
expected: AgentModelAssignment | None,
|
|
1072
|
+
) -> list[str]:
|
|
1073
|
+
assignment = metadata["modelAssignment"]
|
|
1074
|
+
digests = metadata["digests"]
|
|
1075
|
+
assert isinstance(assignment, dict)
|
|
1076
|
+
assert isinstance(digests, dict)
|
|
1077
|
+
errors = []
|
|
1078
|
+
if digests["assignmentDigest"] != _sha256(_canonical_json(assignment)):
|
|
1079
|
+
errors.append("assignment digest does not match model assignment")
|
|
1080
|
+
if expected is not None and assignment != _assignment_payload(expected):
|
|
1081
|
+
errors.append("model assignment does not match expected assignment")
|
|
1082
|
+
return errors
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _verify_contract_source(
|
|
1086
|
+
metadata: Mapping[str, object],
|
|
1087
|
+
*,
|
|
1088
|
+
metadata_path: Path,
|
|
1089
|
+
project_root: Path,
|
|
1090
|
+
expected_run_manifest_path: Path | None,
|
|
1091
|
+
) -> list[str]:
|
|
1092
|
+
source = metadata["contractSource"]
|
|
1093
|
+
assert isinstance(source, dict)
|
|
1094
|
+
mode = source["mode"]
|
|
1095
|
+
if mode == "standalone":
|
|
1096
|
+
if expected_run_manifest_path is not None:
|
|
1097
|
+
return ["standalone invocation cannot use a run manifest"]
|
|
1098
|
+
errors = _verify_metadata_adjacency(metadata, metadata_path, project_root)
|
|
1099
|
+
errors.extend(_verify_duty_snapshot(metadata, project_root, None))
|
|
1100
|
+
return errors
|
|
1101
|
+
if mode != "run":
|
|
1102
|
+
return ["contract source mode is invalid"]
|
|
1103
|
+
if expected_run_manifest_path is None:
|
|
1104
|
+
return ["run invocation requires expected run manifest"]
|
|
1105
|
+
errors = _verify_metadata_adjacency(metadata, metadata_path, project_root)
|
|
1106
|
+
errors.extend(
|
|
1107
|
+
_verify_run_contract(
|
|
1108
|
+
metadata,
|
|
1109
|
+
project_root=project_root,
|
|
1110
|
+
expected_run_manifest_path=expected_run_manifest_path,
|
|
1111
|
+
)
|
|
1112
|
+
)
|
|
1113
|
+
return errors
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def _verify_metadata_adjacency(
|
|
1117
|
+
metadata: Mapping[str, object],
|
|
1118
|
+
metadata_path: Path,
|
|
1119
|
+
project_root: Path,
|
|
1120
|
+
) -> list[str]:
|
|
1121
|
+
prompt = metadata["prompt"]
|
|
1122
|
+
assert isinstance(prompt, dict)
|
|
1123
|
+
try:
|
|
1124
|
+
prompt_path = _project_path(project_root, prompt["path"], must_exist=False)
|
|
1125
|
+
expected = prompt_path.with_name(prompt_path.name + ".meta.json")
|
|
1126
|
+
if metadata_path.resolve(strict=True) != expected.resolve(strict=True):
|
|
1127
|
+
return ["metadata path is not adjacent to prompt path"]
|
|
1128
|
+
except (AgentInvocationError, OSError):
|
|
1129
|
+
return ["metadata path is invalid"]
|
|
1130
|
+
return []
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def _verify_run_contract(
|
|
1134
|
+
metadata: Mapping[str, object],
|
|
1135
|
+
*,
|
|
1136
|
+
project_root: Path,
|
|
1137
|
+
expected_run_manifest_path: Path,
|
|
1138
|
+
) -> list[str]:
|
|
1139
|
+
source = metadata["contractSource"]
|
|
1140
|
+
assert isinstance(source, dict)
|
|
1141
|
+
try:
|
|
1142
|
+
recorded = _project_path(
|
|
1143
|
+
project_root,
|
|
1144
|
+
source["runManifestPath"],
|
|
1145
|
+
must_exist=True,
|
|
1146
|
+
)
|
|
1147
|
+
expected = expected_run_manifest_path.resolve(strict=True)
|
|
1148
|
+
except (AgentInvocationError, OSError):
|
|
1149
|
+
return ["run manifest path is invalid"]
|
|
1150
|
+
if recorded != expected:
|
|
1151
|
+
return ["run manifest path does not match expected run manifest"]
|
|
1152
|
+
try:
|
|
1153
|
+
manifest = _load_json_object(expected, "run manifest")
|
|
1154
|
+
except AgentInvocationError:
|
|
1155
|
+
return ["run manifest is invalid"]
|
|
1156
|
+
errors = _verify_run_manifest_contract(metadata, manifest, project_root)
|
|
1157
|
+
errors.extend(_verify_duty_snapshot(metadata, project_root, manifest))
|
|
1158
|
+
errors.extend(_verify_reservation(metadata, manifest, project_root))
|
|
1159
|
+
return errors
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _verify_run_manifest_contract(
|
|
1163
|
+
metadata: Mapping[str, object],
|
|
1164
|
+
manifest: Mapping[str, object],
|
|
1165
|
+
project_root: Path,
|
|
1166
|
+
) -> list[str]:
|
|
1167
|
+
source = metadata["contractSource"]
|
|
1168
|
+
digests = metadata["digests"]
|
|
1169
|
+
assert isinstance(source, dict)
|
|
1170
|
+
assert isinstance(digests, dict)
|
|
1171
|
+
contract = manifest.get("agentContract")
|
|
1172
|
+
if not isinstance(contract, dict):
|
|
1173
|
+
return ["run manifest agent contract is missing"]
|
|
1174
|
+
errors = []
|
|
1175
|
+
if contract.get("dutyRootPath") != source["dutyRootPath"]:
|
|
1176
|
+
errors.append("duty root does not match run manifest")
|
|
1177
|
+
if contract.get("catalogDigest") != digests["catalogDigest"]:
|
|
1178
|
+
errors.append("catalog digest does not match run manifest")
|
|
1179
|
+
allowed = contract.get("allowedAudiences")
|
|
1180
|
+
if not isinstance(allowed, list) or metadata["audience"] not in allowed:
|
|
1181
|
+
errors.append("audience is not allowed by run manifest")
|
|
1182
|
+
errors.extend(_verify_manifest_assignment(metadata, manifest))
|
|
1183
|
+
return errors
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _verify_manifest_assignment(
|
|
1187
|
+
metadata: Mapping[str, object],
|
|
1188
|
+
manifest: Mapping[str, object],
|
|
1189
|
+
) -> list[str]:
|
|
1190
|
+
assignments = manifest.get("invocationAssignments")
|
|
1191
|
+
reference = metadata["assignmentRef"]
|
|
1192
|
+
if not isinstance(assignments, dict) or reference not in assignments:
|
|
1193
|
+
return ["assignment reference is missing from run manifest"]
|
|
1194
|
+
if assignments[reference] != metadata["modelAssignment"]:
|
|
1195
|
+
return ["model assignment does not match run manifest"]
|
|
1196
|
+
return []
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def _verify_duty_snapshot(
|
|
1200
|
+
metadata: Mapping[str, object],
|
|
1201
|
+
project_root: Path,
|
|
1202
|
+
manifest: Mapping[str, object] | None,
|
|
1203
|
+
) -> list[str]:
|
|
1204
|
+
source = metadata["contractSource"]
|
|
1205
|
+
duty_contract = metadata["dutyContract"]
|
|
1206
|
+
digests = metadata["digests"]
|
|
1207
|
+
assert isinstance(source, dict)
|
|
1208
|
+
assert isinstance(duty_contract, dict)
|
|
1209
|
+
assert isinstance(digests, dict)
|
|
1210
|
+
try:
|
|
1211
|
+
root = _project_path(project_root, source["dutyRootPath"], must_exist=True)
|
|
1212
|
+
common = load_common_duty_contract(root)
|
|
1213
|
+
catalog = load_duty_catalog(root)
|
|
1214
|
+
duty = catalog[duty_contract["id"]]
|
|
1215
|
+
except (AgentInvocationError, KeyError):
|
|
1216
|
+
return ["duty snapshot is invalid"]
|
|
1217
|
+
errors = _verify_duty_metadata(duty_contract, duty)
|
|
1218
|
+
actual_catalog = digest_duty_catalog(root)
|
|
1219
|
+
expected_catalog = (
|
|
1220
|
+
manifest["agentContract"]["catalogDigest"]
|
|
1221
|
+
if manifest is not None
|
|
1222
|
+
else digests["catalogDigest"]
|
|
1223
|
+
)
|
|
1224
|
+
if actual_catalog != expected_catalog or digests["catalogDigest"] != actual_catalog:
|
|
1225
|
+
errors.append("catalog digest does not match run duty snapshot")
|
|
1226
|
+
return errors
|
|
1227
|
+
actual_duty = _digest_framed_files(root, ["common.md", duty.source_path.name])
|
|
1228
|
+
if digests["dutyDigest"] != actual_duty:
|
|
1229
|
+
errors.append("duty digest does not match run duty snapshot")
|
|
1230
|
+
return errors
|
|
1231
|
+
errors.extend(_verify_prompt_duty_body(metadata, project_root, common, duty))
|
|
1232
|
+
return errors
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def _verify_duty_metadata(
|
|
1236
|
+
recorded: Mapping[str, object],
|
|
1237
|
+
duty: DutyContract,
|
|
1238
|
+
) -> list[str]:
|
|
1239
|
+
if recorded["id"] != duty.id or recorded["version"] != duty.version:
|
|
1240
|
+
return ["duty identity does not match duty snapshot"]
|
|
1241
|
+
return []
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def _verify_prompt_duty_body(
|
|
1245
|
+
metadata: Mapping[str, object],
|
|
1246
|
+
project_root: Path,
|
|
1247
|
+
common: DutyContract,
|
|
1248
|
+
duty: DutyContract,
|
|
1249
|
+
) -> list[str]:
|
|
1250
|
+
prompt_spec = metadata["prompt"]
|
|
1251
|
+
assert isinstance(prompt_spec, dict)
|
|
1252
|
+
try:
|
|
1253
|
+
prompt_path = _project_path(project_root, prompt_spec["path"], must_exist=True)
|
|
1254
|
+
prompt = prompt_path.read_text(encoding="utf-8")
|
|
1255
|
+
_prefix, remainder = prompt.split("\n\n## Duty Contract\n\n", 1)
|
|
1256
|
+
rendered_duty, _task = remainder.split("\n\n## Task Instructions\n\n", 1)
|
|
1257
|
+
except (AgentInvocationError, OSError, UnicodeDecodeError, ValueError):
|
|
1258
|
+
return ["prompt duty contract does not match duty snapshot"]
|
|
1259
|
+
expected = f"{common.body.rstrip()}\n\n{duty.body.rstrip()}"
|
|
1260
|
+
if rendered_duty != expected:
|
|
1261
|
+
return ["prompt duty contract does not match duty snapshot"]
|
|
1262
|
+
return []
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def _verify_reservation(
|
|
1266
|
+
metadata: Mapping[str, object],
|
|
1267
|
+
manifest: Mapping[str, object],
|
|
1268
|
+
project_root: Path,
|
|
1269
|
+
) -> list[str]:
|
|
1270
|
+
contract = manifest.get("agentContract")
|
|
1271
|
+
if not isinstance(contract, dict):
|
|
1272
|
+
return ["run manifest agent contract is missing"]
|
|
1273
|
+
try:
|
|
1274
|
+
root = _project_path(
|
|
1275
|
+
project_root,
|
|
1276
|
+
contract["invocationReservationRootPath"],
|
|
1277
|
+
must_exist=True,
|
|
1278
|
+
)
|
|
1279
|
+
path = root / f"{metadata['invocationId']}.json"
|
|
1280
|
+
actual = _load_json_object(path, "invocation reservation")
|
|
1281
|
+
except (AgentInvocationError, KeyError):
|
|
1282
|
+
return ["invocation reservation is missing or invalid"]
|
|
1283
|
+
prompt = metadata["prompt"]
|
|
1284
|
+
assert isinstance(prompt, dict)
|
|
1285
|
+
expected = {
|
|
1286
|
+
"schemaVersion": 1,
|
|
1287
|
+
"invocationId": metadata["invocationId"],
|
|
1288
|
+
"workerId": metadata["workerId"],
|
|
1289
|
+
"assignmentRef": metadata["assignmentRef"],
|
|
1290
|
+
"audience": metadata["audience"],
|
|
1291
|
+
"dispatchKind": metadata["dispatchKind"],
|
|
1292
|
+
"promptPath": prompt["path"],
|
|
1293
|
+
"metadataPath": f"{prompt['path']}.meta.json",
|
|
1294
|
+
}
|
|
1295
|
+
if actual != expected:
|
|
1296
|
+
return ["invocation reservation does not match invocation metadata"]
|
|
1297
|
+
return []
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _metadata_schema_is_exact(metadata: Mapping[str, object]) -> bool:
|
|
1301
|
+
if set(metadata) != _TOP_LEVEL_KEYS or metadata.get("schemaVersion") != 1:
|
|
1302
|
+
return False
|
|
1303
|
+
for key, expected_keys in _NESTED_KEYS.items():
|
|
1304
|
+
value = metadata.get(key)
|
|
1305
|
+
if not isinstance(value, dict) or set(value) != expected_keys:
|
|
1306
|
+
return False
|
|
1307
|
+
sources = metadata["instruction"]["sourcePaths"]
|
|
1308
|
+
if not isinstance(sources, list) or not sources:
|
|
1309
|
+
return False
|
|
1310
|
+
if any(not isinstance(item, dict) or set(item) != {"kind", "path"} for item in sources):
|
|
1311
|
+
return False
|
|
1312
|
+
return _digest_paths(metadata) == {
|
|
1313
|
+
"digests.catalogDigest",
|
|
1314
|
+
"digests.assignmentDigest",
|
|
1315
|
+
"digests.dutyDigest",
|
|
1316
|
+
"digests.instructionDigest",
|
|
1317
|
+
"digests.promptDigest",
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
def _digest_paths(value: object, prefix: str = "") -> set[str]:
|
|
1322
|
+
if not isinstance(value, dict):
|
|
1323
|
+
return set()
|
|
1324
|
+
found: set[str] = set()
|
|
1325
|
+
for key, child in value.items():
|
|
1326
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
1327
|
+
if key == "digest" or key.endswith("Digest"):
|
|
1328
|
+
found.add(path)
|
|
1329
|
+
found.update(_digest_paths(child, path))
|
|
1330
|
+
return found
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
def _assignment_payload(assignment: AgentModelAssignment) -> dict[str, object]:
|
|
1334
|
+
return {
|
|
1335
|
+
"provider": assignment.provider,
|
|
1336
|
+
"model": assignment.model,
|
|
1337
|
+
"modelExecutionValue": assignment.model_execution_value,
|
|
1338
|
+
"runner": assignment.runner,
|
|
1339
|
+
"hostRuntime": assignment.host_runtime,
|
|
1340
|
+
"hostModelValue": assignment.host_model_value,
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _source_payload(source: AgentInstructionSource) -> dict[str, str]:
|
|
1345
|
+
if source.kind not in {"project", "runtime"}:
|
|
1346
|
+
raise AgentInvocationError("instruction sourcePaths contains an invalid kind")
|
|
1347
|
+
path = source.path
|
|
1348
|
+
pure = PurePosixPath(path)
|
|
1349
|
+
if (
|
|
1350
|
+
not path
|
|
1351
|
+
or "\\" in path
|
|
1352
|
+
or pure.is_absolute()
|
|
1353
|
+
or any(part in {"", ".", ".."} for part in pure.parts)
|
|
1354
|
+
):
|
|
1355
|
+
raise AgentInvocationError("instruction sourcePaths must use relative POSIX paths")
|
|
1356
|
+
return {"kind": source.kind, "path": pure.as_posix()}
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def _project_relative(
|
|
1360
|
+
path: Path,
|
|
1361
|
+
project_root: Path,
|
|
1362
|
+
*,
|
|
1363
|
+
must_exist: bool,
|
|
1364
|
+
) -> str:
|
|
1365
|
+
root = project_root.resolve(strict=True)
|
|
1366
|
+
try:
|
|
1367
|
+
if must_exist:
|
|
1368
|
+
resolved = path.resolve(strict=True)
|
|
1369
|
+
else:
|
|
1370
|
+
resolved = path.parent.resolve(strict=True) / path.name
|
|
1371
|
+
return resolved.relative_to(root).as_posix()
|
|
1372
|
+
except (OSError, ValueError) as exc:
|
|
1373
|
+
raise AgentInvocationError(f"path escapes project root: {path}") from exc
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def _project_path(
|
|
1377
|
+
project_root: Path,
|
|
1378
|
+
value: object,
|
|
1379
|
+
*,
|
|
1380
|
+
must_exist: bool,
|
|
1381
|
+
) -> Path:
|
|
1382
|
+
if not isinstance(value, str) or not value:
|
|
1383
|
+
raise AgentInvocationError("project-relative path is invalid")
|
|
1384
|
+
pure = PurePosixPath(value)
|
|
1385
|
+
if pure.is_absolute() or any(part in {"", ".", ".."} for part in pure.parts):
|
|
1386
|
+
raise AgentInvocationError("project-relative path is invalid")
|
|
1387
|
+
candidate = project_root.joinpath(*pure.parts)
|
|
1388
|
+
_project_relative(candidate, project_root, must_exist=must_exist)
|
|
1389
|
+
return candidate.resolve(strict=must_exist)
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
def _is_relative_to(path: Path, root: Path) -> bool:
|
|
1393
|
+
try:
|
|
1394
|
+
path.relative_to(root)
|
|
1395
|
+
except ValueError:
|
|
1396
|
+
return False
|
|
1397
|
+
return True
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
def _digest_framed_files(root: Path, relative_names: list[str]) -> str:
|
|
1401
|
+
framed = bytearray(b"okstra-digest-v1\0")
|
|
1402
|
+
for name in sorted(relative_names):
|
|
1403
|
+
name_bytes = name.encode("utf-8")
|
|
1404
|
+
body = (root / name).read_bytes()
|
|
1405
|
+
framed.extend(len(name_bytes).to_bytes(8, "big"))
|
|
1406
|
+
framed.extend(name_bytes)
|
|
1407
|
+
framed.extend(len(body).to_bytes(8, "big"))
|
|
1408
|
+
framed.extend(body)
|
|
1409
|
+
return _sha256(bytes(framed))
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def _sha256(data: bytes) -> str:
|
|
1413
|
+
return f"sha256:{hashlib.sha256(data).hexdigest()}"
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def _canonical_json(value: object) -> bytes:
|
|
1417
|
+
return json.dumps(
|
|
1418
|
+
value,
|
|
1419
|
+
ensure_ascii=False,
|
|
1420
|
+
sort_keys=True,
|
|
1421
|
+
separators=(",", ":"),
|
|
1422
|
+
allow_nan=False,
|
|
1423
|
+
).encode("utf-8")
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def _pretty_json(value: object) -> bytes:
|
|
1427
|
+
return (
|
|
1428
|
+
json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n"
|
|
1429
|
+
).encode("utf-8")
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
def _load_json_object(path: Path, label: str) -> dict[str, object]:
|
|
1433
|
+
value = _read_json_if_object(path)
|
|
1434
|
+
if value is None:
|
|
1435
|
+
raise AgentInvocationError(f"{label} is missing or invalid: {path}")
|
|
1436
|
+
return value
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def _read_json_if_object(path: Path) -> dict[str, object] | None:
|
|
1440
|
+
try:
|
|
1441
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
1442
|
+
except (OSError, json.JSONDecodeError):
|
|
1443
|
+
return None
|
|
1444
|
+
return value if isinstance(value, dict) else None
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def _read_metadata(path: Path) -> dict[str, object] | None:
|
|
1448
|
+
return _read_json_if_object(path)
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def _deduplicate(errors: list[str]) -> list[str]:
|
|
1452
|
+
return list(dict.fromkeys(errors))
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
def _load_role_duty(path: Path) -> DutyContract:
|
|
1456
|
+
fields, body = _parse_duty_file(path)
|
|
1457
|
+
if set(fields) != {"id", "version", "kind", "appliesTo"}:
|
|
1458
|
+
raise AgentInvocationError(f"invalid role duty frontmatter: {path}")
|
|
1459
|
+
audience = fields["appliesTo"]
|
|
1460
|
+
if audience not in _SUPPORTED_AUDIENCES:
|
|
1461
|
+
raise AgentInvocationError(f"unknown duty audience: {audience}")
|
|
1462
|
+
if fields["kind"] != "role" or fields["id"] != audience:
|
|
1463
|
+
raise AgentInvocationError(f"invalid role duty frontmatter: {path}")
|
|
1464
|
+
return DutyContract(
|
|
1465
|
+
id=fields["id"],
|
|
1466
|
+
version=_parse_version(fields["version"], path),
|
|
1467
|
+
kind="role",
|
|
1468
|
+
applies_to=audience,
|
|
1469
|
+
body=body,
|
|
1470
|
+
source_path=path,
|
|
1471
|
+
)
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
def _parse_duty_file(path: Path) -> tuple[dict[str, str], str]:
|
|
1475
|
+
try:
|
|
1476
|
+
text = path.read_text(encoding="utf-8")
|
|
1477
|
+
except OSError as exc:
|
|
1478
|
+
raise AgentInvocationError(f"cannot read duty contract: {path}") from exc
|
|
1479
|
+
lines = text.splitlines(keepends=True)
|
|
1480
|
+
if not lines or lines[0].strip() != "---":
|
|
1481
|
+
raise AgentInvocationError(f"missing duty frontmatter: {path}")
|
|
1482
|
+
try:
|
|
1483
|
+
end = next(index for index, line in enumerate(lines[1:], 1) if line.strip() == "---")
|
|
1484
|
+
except StopIteration as exc:
|
|
1485
|
+
raise AgentInvocationError(f"unterminated duty frontmatter: {path}") from exc
|
|
1486
|
+
fields: dict[str, str] = {}
|
|
1487
|
+
for line in lines[1:end]:
|
|
1488
|
+
key, separator, value = line.partition(":")
|
|
1489
|
+
if not separator or not key.strip() or key.strip() in fields:
|
|
1490
|
+
raise AgentInvocationError(f"invalid duty frontmatter: {path}")
|
|
1491
|
+
fields[key.strip()] = value.strip()
|
|
1492
|
+
return fields, "".join(lines[end + 1 :]).lstrip("\n")
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def _parse_version(value: str, path: Path) -> int:
|
|
1496
|
+
try:
|
|
1497
|
+
version = int(value)
|
|
1498
|
+
except ValueError as exc:
|
|
1499
|
+
raise AgentInvocationError(f"invalid duty version: {path}") from exc
|
|
1500
|
+
if version < 1:
|
|
1501
|
+
raise AgentInvocationError(f"invalid duty version: {path}")
|
|
1502
|
+
return version
|