okstra 0.168.0 → 0.169.1

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