python-hwpx-automation 6.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Strict public contracts for typed HWPX blueprint dump and replay."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from collections.abc import Mapping, Sequence
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..model import (
|
|
14
|
+
AGENT_CATALOG_SCHEMA,
|
|
15
|
+
NODE_KINDS,
|
|
16
|
+
REVISION_PATTERN,
|
|
17
|
+
VERIFICATION_REQUIREMENTS,
|
|
18
|
+
AgentContractError,
|
|
19
|
+
AgentError,
|
|
20
|
+
_validate_position,
|
|
21
|
+
_validate_quality,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
BLUEPRINT_SCHEMA = "hwpx.agent-blueprint/v1"
|
|
25
|
+
BLUEPRINT_REPLAY_SCHEMA = "hwpx.agent-blueprint-replay/v1"
|
|
26
|
+
BLUEPRINT_REPLAY_RESULT_SCHEMA = "hwpx.agent-blueprint-replay-result/v1"
|
|
27
|
+
BLUEPRINT_CATALOG_SCHEMA = "hwpx.agent-blueprint-catalog/v1"
|
|
28
|
+
|
|
29
|
+
BLUEPRINT_MODES = ("source-bound", "portable")
|
|
30
|
+
FIDELITY_LEVELS = ("exact", "mapped", "degraded", "unsupported")
|
|
31
|
+
|
|
32
|
+
MAX_BLUEPRINT_NODES = 10_000
|
|
33
|
+
MAX_BLUEPRINT_DEPTH = 32
|
|
34
|
+
MAX_MANIFEST_BYTES = 16 * 1024 * 1024
|
|
35
|
+
MAX_ASSETS = 128
|
|
36
|
+
MAX_ASSET_BYTES = 16 * 1024 * 1024
|
|
37
|
+
MAX_TOTAL_ASSET_BYTES = 64 * 1024 * 1024
|
|
38
|
+
MAX_DEPENDENCIES = 4_096
|
|
39
|
+
MAX_REFERENCES = 50_000
|
|
40
|
+
MAX_BLUEPRINT_TEXT = 16_384
|
|
41
|
+
MAX_BLUEPRINT_JSON_DEPTH = 32
|
|
42
|
+
|
|
43
|
+
BLUEPRINT_ID_PATTERN = re.compile(r"^n[0-9]{6}$")
|
|
44
|
+
DEPENDENCY_KEY_PATTERN = re.compile(r"^(style|char|numbering|resource):[a-f0-9]{64}$")
|
|
45
|
+
SHA256_PATTERN = re.compile(r"^sha256:[a-f0-9]{64}$")
|
|
46
|
+
ASSET_PATH_PATTERN = re.compile(r"^assets/[a-f0-9]{64}\.[a-z0-9]{1,12}$")
|
|
47
|
+
|
|
48
|
+
_FORBIDDEN_KEYS = frozenset(
|
|
49
|
+
{
|
|
50
|
+
"xml",
|
|
51
|
+
"raw",
|
|
52
|
+
"rawxml",
|
|
53
|
+
"xpath",
|
|
54
|
+
"namespace",
|
|
55
|
+
"namespaceuri",
|
|
56
|
+
"packagepath",
|
|
57
|
+
"partpath",
|
|
58
|
+
"nativeobject",
|
|
59
|
+
"lxml",
|
|
60
|
+
"absolutepath",
|
|
61
|
+
"privatecoordinate",
|
|
62
|
+
"executable",
|
|
63
|
+
"script",
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
_MANIFEST_KEYS = frozenset(
|
|
68
|
+
{
|
|
69
|
+
"schemaVersion",
|
|
70
|
+
"catalogVersion",
|
|
71
|
+
"catalogHash",
|
|
72
|
+
"source",
|
|
73
|
+
"mode",
|
|
74
|
+
"root",
|
|
75
|
+
"nodes",
|
|
76
|
+
"styles",
|
|
77
|
+
"numbering",
|
|
78
|
+
"resources",
|
|
79
|
+
"references",
|
|
80
|
+
"unsupported",
|
|
81
|
+
"capabilities",
|
|
82
|
+
"limits",
|
|
83
|
+
"fidelity",
|
|
84
|
+
"blueprintHash",
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _object(value: object, name: str) -> dict[str, Any]:
|
|
90
|
+
if not isinstance(value, Mapping):
|
|
91
|
+
raise AgentContractError("invalid_syntax", f"{name} must be an object", target=name)
|
|
92
|
+
return dict(value)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _list(value: object, name: str) -> list[Any]:
|
|
96
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
|
|
97
|
+
raise AgentContractError("invalid_syntax", f"{name} must be an array", target=name)
|
|
98
|
+
return list(value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _exact_keys(
|
|
102
|
+
value: Mapping[str, Any],
|
|
103
|
+
*,
|
|
104
|
+
required: set[str] | frozenset[str],
|
|
105
|
+
optional: set[str] | frozenset[str] = frozenset(),
|
|
106
|
+
name: str,
|
|
107
|
+
) -> None:
|
|
108
|
+
missing = required - set(value)
|
|
109
|
+
extra = set(value) - required - optional
|
|
110
|
+
if missing or extra:
|
|
111
|
+
raise AgentContractError(
|
|
112
|
+
"invalid_syntax",
|
|
113
|
+
f"{name} fields mismatch (missing={sorted(missing)}, extra={sorted(extra)})",
|
|
114
|
+
target=name,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _validate_public_json(value: object, *, name: str, depth: int = 0) -> None:
|
|
119
|
+
if depth > MAX_BLUEPRINT_JSON_DEPTH:
|
|
120
|
+
raise AgentContractError("resource_limit", f"{name} exceeds JSON depth", target=name)
|
|
121
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
122
|
+
return
|
|
123
|
+
if isinstance(value, str):
|
|
124
|
+
if len(value) > MAX_BLUEPRINT_TEXT:
|
|
125
|
+
raise AgentContractError("resource_limit", f"{name} string is too long", target=name)
|
|
126
|
+
if value.startswith("/") and name.endswith(("source.label", "sourceHint.nativeId")):
|
|
127
|
+
raise AgentContractError(
|
|
128
|
+
"verification_failed", f"{name} must not contain an absolute path", target=name
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
if isinstance(value, Mapping):
|
|
132
|
+
for raw_key, child in value.items():
|
|
133
|
+
key = str(raw_key)
|
|
134
|
+
normalized = re.sub(r"[^a-z]", "", key.casefold())
|
|
135
|
+
if normalized in _FORBIDDEN_KEYS:
|
|
136
|
+
raise AgentContractError(
|
|
137
|
+
"unknown_property", f"{name} contains forbidden field {key!r}", target=f"{name}.{key}"
|
|
138
|
+
)
|
|
139
|
+
_validate_public_json(child, name=f"{name}.{key}", depth=depth + 1)
|
|
140
|
+
return
|
|
141
|
+
if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray, str)):
|
|
142
|
+
for index, child in enumerate(value):
|
|
143
|
+
_validate_public_json(child, name=f"{name}[{index}]", depth=depth + 1)
|
|
144
|
+
return
|
|
145
|
+
raise AgentContractError("invalid_syntax", f"{name} is not JSON-serializable", target=name)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def blueprint_limits() -> dict[str, int]:
|
|
149
|
+
return {
|
|
150
|
+
"maxNodes": MAX_BLUEPRINT_NODES,
|
|
151
|
+
"maxDepth": MAX_BLUEPRINT_DEPTH,
|
|
152
|
+
"maxManifestBytes": MAX_MANIFEST_BYTES,
|
|
153
|
+
"maxAssets": MAX_ASSETS,
|
|
154
|
+
"maxAssetBytes": MAX_ASSET_BYTES,
|
|
155
|
+
"maxTotalAssetBytes": MAX_TOTAL_ASSET_BYTES,
|
|
156
|
+
"maxDependencies": MAX_DEPENDENCIES,
|
|
157
|
+
"maxReferences": MAX_REFERENCES,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def canonical_manifest_bytes(value: Mapping[str, Any], *, include_hash: bool = False) -> bytes:
|
|
162
|
+
payload = dict(value)
|
|
163
|
+
if not include_hash:
|
|
164
|
+
payload["blueprintHash"] = None
|
|
165
|
+
try:
|
|
166
|
+
encoded = json.dumps(
|
|
167
|
+
payload,
|
|
168
|
+
ensure_ascii=False,
|
|
169
|
+
sort_keys=True,
|
|
170
|
+
separators=(",", ":"),
|
|
171
|
+
allow_nan=False,
|
|
172
|
+
).encode("utf-8")
|
|
173
|
+
except (TypeError, ValueError) as exc:
|
|
174
|
+
raise AgentContractError(
|
|
175
|
+
"invalid_syntax", "blueprint manifest is not canonical JSON", target="blueprint"
|
|
176
|
+
) from exc
|
|
177
|
+
if len(encoded) > MAX_MANIFEST_BYTES:
|
|
178
|
+
raise AgentContractError(
|
|
179
|
+
"resource_limit", "blueprint manifest exceeds byte limit", target="blueprint"
|
|
180
|
+
)
|
|
181
|
+
return encoded
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def blueprint_hash(value: Mapping[str, Any]) -> str:
|
|
185
|
+
return "sha256:" + hashlib.sha256(canonical_manifest_bytes(value)).hexdigest()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def with_blueprint_hash(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
189
|
+
result = dict(value)
|
|
190
|
+
result["blueprintHash"] = blueprint_hash(result)
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _validate_dependency_record(value: object, *, name: str, kind: str) -> dict[str, Any]:
|
|
195
|
+
record = _object(value, name)
|
|
196
|
+
required = {"key", "signature", "properties"}
|
|
197
|
+
optional = {"name", "kind"}
|
|
198
|
+
_exact_keys(record, required=required, optional=optional, name=name)
|
|
199
|
+
key = str(record["key"])
|
|
200
|
+
expected_prefix = "numbering" if kind == "numbering" else kind
|
|
201
|
+
if not DEPENDENCY_KEY_PATTERN.fullmatch(key) or not key.startswith(expected_prefix + ":"):
|
|
202
|
+
raise AgentContractError("invalid_syntax", f"{name}.key is invalid", target=f"{name}.key")
|
|
203
|
+
if not SHA256_PATTERN.fullmatch(str(record["signature"])):
|
|
204
|
+
raise AgentContractError(
|
|
205
|
+
"invalid_syntax", f"{name}.signature must be sha256", target=f"{name}.signature"
|
|
206
|
+
)
|
|
207
|
+
_validate_public_json(record["properties"], name=f"{name}.properties")
|
|
208
|
+
return record
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _validate_resource(value: object, *, name: str) -> dict[str, Any]:
|
|
212
|
+
record = _object(value, name)
|
|
213
|
+
_exact_keys(
|
|
214
|
+
record,
|
|
215
|
+
required={"key", "sha256", "mediaType", "size", "assetPath"},
|
|
216
|
+
optional={"name"},
|
|
217
|
+
name=name,
|
|
218
|
+
)
|
|
219
|
+
key = str(record["key"])
|
|
220
|
+
digest = str(record["sha256"])
|
|
221
|
+
if not DEPENDENCY_KEY_PATTERN.fullmatch(key) or not key.startswith("resource:"):
|
|
222
|
+
raise AgentContractError("invalid_syntax", f"{name}.key is invalid", target=f"{name}.key")
|
|
223
|
+
if not SHA256_PATTERN.fullmatch(digest):
|
|
224
|
+
raise AgentContractError("invalid_syntax", f"{name}.sha256 is invalid", target=f"{name}.sha256")
|
|
225
|
+
size = record["size"]
|
|
226
|
+
if isinstance(size, bool) or not isinstance(size, int) or not 0 <= size <= MAX_ASSET_BYTES:
|
|
227
|
+
raise AgentContractError("resource_limit", f"{name}.size is invalid", target=f"{name}.size")
|
|
228
|
+
asset_path = str(record["assetPath"])
|
|
229
|
+
if not ASSET_PATH_PATTERN.fullmatch(asset_path):
|
|
230
|
+
raise AgentContractError(
|
|
231
|
+
"verification_failed", f"{name}.assetPath is unsafe", target=f"{name}.assetPath"
|
|
232
|
+
)
|
|
233
|
+
if asset_path.split("/", 1)[1].split(".", 1)[0] != digest.removeprefix("sha256:"):
|
|
234
|
+
raise AgentContractError(
|
|
235
|
+
"verification_failed", f"{name}.assetPath does not match sha256", target=f"{name}.assetPath"
|
|
236
|
+
)
|
|
237
|
+
media_type = str(record["mediaType"])
|
|
238
|
+
if not media_type.startswith("image/"):
|
|
239
|
+
raise AgentContractError(
|
|
240
|
+
"unsupported_content", f"{name}.mediaType is not allow-listed", target=f"{name}.mediaType"
|
|
241
|
+
)
|
|
242
|
+
return record
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _validate_manifest_header(manifest: dict[str, Any]) -> None:
|
|
246
|
+
_exact_keys(manifest, required=_MANIFEST_KEYS, name="blueprint")
|
|
247
|
+
if manifest["schemaVersion"] != BLUEPRINT_SCHEMA:
|
|
248
|
+
raise AgentContractError("invalid_syntax", "unsupported blueprint schema", target="schemaVersion")
|
|
249
|
+
if manifest["catalogVersion"] != AGENT_CATALOG_SCHEMA:
|
|
250
|
+
raise AgentContractError("verification_failed", "agent catalog version skew", target="catalogVersion")
|
|
251
|
+
if not SHA256_PATTERN.fullmatch(str(manifest["catalogHash"])):
|
|
252
|
+
raise AgentContractError("invalid_syntax", "catalogHash must be sha256", target="catalogHash")
|
|
253
|
+
mode = str(manifest["mode"])
|
|
254
|
+
if mode not in BLUEPRINT_MODES:
|
|
255
|
+
raise AgentContractError("invalid_syntax", "unsupported blueprint mode", target="mode")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _validate_source(manifest: dict[str, Any]) -> None:
|
|
259
|
+
source = _object(manifest["source"], "source")
|
|
260
|
+
_exact_keys(source, required={"revision", "label"}, name="source")
|
|
261
|
+
if not REVISION_PATTERN.fullmatch(str(source["revision"])):
|
|
262
|
+
raise AgentContractError("invalid_syntax", "source.revision must be sha256", target="source.revision")
|
|
263
|
+
_validate_public_json(source, name="source")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _validate_root(manifest: dict[str, Any]) -> dict[str, Any]:
|
|
267
|
+
root = _object(manifest["root"], "root")
|
|
268
|
+
_exact_keys(
|
|
269
|
+
root,
|
|
270
|
+
required={"blueprintId", "kind", "sourcePath", "sourceStability"},
|
|
271
|
+
name="root",
|
|
272
|
+
)
|
|
273
|
+
if not BLUEPRINT_ID_PATTERN.fullmatch(str(root["blueprintId"])):
|
|
274
|
+
raise AgentContractError("invalid_syntax", "root blueprintId is invalid", target="root.blueprintId")
|
|
275
|
+
if root["kind"] not in NODE_KINDS:
|
|
276
|
+
raise AgentContractError("unknown_kind", "root kind is unknown", target="root.kind")
|
|
277
|
+
if not str(root["sourcePath"]).startswith("/"):
|
|
278
|
+
raise AgentContractError("invalid_syntax", "root sourcePath must be semantic", target="root.sourcePath")
|
|
279
|
+
return root
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _validate_nodes(
|
|
283
|
+
manifest: dict[str, Any],
|
|
284
|
+
) -> tuple[list[dict[str, Any]], set[str], dict[str, dict[str, Any]]]:
|
|
285
|
+
nodes = _list(manifest["nodes"], "nodes")
|
|
286
|
+
if not nodes or len(nodes) > MAX_BLUEPRINT_NODES:
|
|
287
|
+
raise AgentContractError("resource_limit", "node count is outside limits", target="nodes")
|
|
288
|
+
node_ids: set[str] = set()
|
|
289
|
+
nodes_by_id: dict[str, dict[str, Any]] = {}
|
|
290
|
+
detached_nodes: list[dict[str, Any]] = []
|
|
291
|
+
for index, item in enumerate(nodes):
|
|
292
|
+
name = f"nodes[{index}]"
|
|
293
|
+
node = _object(item, name)
|
|
294
|
+
_exact_keys(
|
|
295
|
+
node,
|
|
296
|
+
required={
|
|
297
|
+
"blueprintId",
|
|
298
|
+
"kind",
|
|
299
|
+
"properties",
|
|
300
|
+
"children",
|
|
301
|
+
"styleRefs",
|
|
302
|
+
"numberingRefs",
|
|
303
|
+
"resourceRefs",
|
|
304
|
+
"references",
|
|
305
|
+
"sourceHint",
|
|
306
|
+
"support",
|
|
307
|
+
},
|
|
308
|
+
name=name,
|
|
309
|
+
)
|
|
310
|
+
node_id = str(node["blueprintId"])
|
|
311
|
+
if not BLUEPRINT_ID_PATTERN.fullmatch(node_id) or node_id in node_ids:
|
|
312
|
+
raise AgentContractError("invariant_violation", f"{name}.blueprintId is invalid or duplicate", target=name)
|
|
313
|
+
node_ids.add(node_id)
|
|
314
|
+
if node["kind"] not in NODE_KINDS:
|
|
315
|
+
raise AgentContractError("unknown_kind", f"{name}.kind is unknown", target=f"{name}.kind")
|
|
316
|
+
for field_name in ("children", "styleRefs", "numberingRefs", "resourceRefs", "references"):
|
|
317
|
+
node[field_name] = [str(item) for item in _list(node[field_name], f"{name}.{field_name}")]
|
|
318
|
+
support = _object(node["support"], f"{name}.support")
|
|
319
|
+
_exact_keys(support, required={"replayable", "fidelity"}, name=f"{name}.support")
|
|
320
|
+
if not isinstance(support["replayable"], bool):
|
|
321
|
+
raise AgentContractError("invalid_syntax", f"{name}.support replayable must be boolean", target=name)
|
|
322
|
+
if support["fidelity"] not in FIDELITY_LEVELS:
|
|
323
|
+
raise AgentContractError("invalid_syntax", f"{name}.support fidelity is invalid", target=name)
|
|
324
|
+
node["properties"] = _object(node["properties"], f"{name}.properties")
|
|
325
|
+
node["sourceHint"] = _object(node["sourceHint"], f"{name}.sourceHint")
|
|
326
|
+
_validate_public_json(node["properties"], name=f"{name}.properties")
|
|
327
|
+
_validate_public_json(node["sourceHint"], name=f"{name}.sourceHint")
|
|
328
|
+
detached_nodes.append(node)
|
|
329
|
+
nodes_by_id[node_id] = node
|
|
330
|
+
return detached_nodes, node_ids, nodes_by_id
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _check_node_tree_shape(
|
|
334
|
+
root: dict[str, Any],
|
|
335
|
+
detached_nodes: list[dict[str, Any]],
|
|
336
|
+
node_ids: set[str],
|
|
337
|
+
nodes_by_id: dict[str, dict[str, Any]],
|
|
338
|
+
) -> str:
|
|
339
|
+
if str(root["blueprintId"]) not in node_ids:
|
|
340
|
+
raise AgentContractError("invariant_violation", "root does not reference a node", target="root")
|
|
341
|
+
for index, node in enumerate(detached_nodes):
|
|
342
|
+
for child in node["children"]:
|
|
343
|
+
if child not in node_ids:
|
|
344
|
+
raise AgentContractError(
|
|
345
|
+
"invariant_violation", "node child reference is missing", target=f"nodes[{index}].children"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
root_id = str(root["blueprintId"])
|
|
349
|
+
if nodes_by_id[root_id]["kind"] != root["kind"]:
|
|
350
|
+
raise AgentContractError("invariant_violation", "root kind does not match its node", target="root")
|
|
351
|
+
parent_counts: dict[str, int] = {node_id: 0 for node_id in node_ids}
|
|
352
|
+
for node in detached_nodes:
|
|
353
|
+
for child in node["children"]:
|
|
354
|
+
parent_counts[child] += 1
|
|
355
|
+
if parent_counts[root_id] != 0 or any(
|
|
356
|
+
count != 1 for node_id, count in parent_counts.items() if node_id != root_id
|
|
357
|
+
):
|
|
358
|
+
raise AgentContractError(
|
|
359
|
+
"invariant_violation", "blueprint nodes must form one rooted tree", target="nodes"
|
|
360
|
+
)
|
|
361
|
+
return root_id
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _check_node_reachability(
|
|
365
|
+
root_id: str,
|
|
366
|
+
node_ids: set[str],
|
|
367
|
+
nodes_by_id: dict[str, dict[str, Any]],
|
|
368
|
+
) -> None:
|
|
369
|
+
visited: set[str] = set()
|
|
370
|
+
visiting: set[str] = set()
|
|
371
|
+
|
|
372
|
+
def visit(node_id: str, depth: int) -> None:
|
|
373
|
+
if depth > MAX_BLUEPRINT_DEPTH:
|
|
374
|
+
raise AgentContractError("resource_limit", "blueprint node depth exceeds limit", target=node_id)
|
|
375
|
+
if node_id in visiting:
|
|
376
|
+
raise AgentContractError("invariant_violation", "blueprint node graph contains a cycle", target=node_id)
|
|
377
|
+
if node_id in visited:
|
|
378
|
+
return
|
|
379
|
+
visiting.add(node_id)
|
|
380
|
+
for child_id in nodes_by_id[node_id]["children"]:
|
|
381
|
+
visit(child_id, depth + 1)
|
|
382
|
+
visiting.remove(node_id)
|
|
383
|
+
visited.add(node_id)
|
|
384
|
+
|
|
385
|
+
visit(root_id, 0)
|
|
386
|
+
if visited != node_ids:
|
|
387
|
+
raise AgentContractError("invariant_violation", "blueprint contains orphan nodes", target="nodes")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _validate_dependency_records(
|
|
391
|
+
manifest: dict[str, Any],
|
|
392
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
|
393
|
+
styles = _list(manifest["styles"], "styles")
|
|
394
|
+
numbering = _list(manifest["numbering"], "numbering")
|
|
395
|
+
resources = _list(manifest["resources"], "resources")
|
|
396
|
+
if len(styles) + len(numbering) > MAX_DEPENDENCIES:
|
|
397
|
+
raise AgentContractError("resource_limit", "dependency count exceeds limit", target="styles")
|
|
398
|
+
if len(resources) > MAX_ASSETS:
|
|
399
|
+
raise AgentContractError("resource_limit", "resource count exceeds limit", target="resources")
|
|
400
|
+
detached_styles = [
|
|
401
|
+
_validate_dependency_record(item, name=f"styles[{index}]", kind=str(_object(item, "style").get("kind", "style")))
|
|
402
|
+
for index, item in enumerate(styles)
|
|
403
|
+
]
|
|
404
|
+
detached_numbering = [
|
|
405
|
+
_validate_dependency_record(item, name=f"numbering[{index}]", kind="numbering")
|
|
406
|
+
for index, item in enumerate(numbering)
|
|
407
|
+
]
|
|
408
|
+
detached_resources = [
|
|
409
|
+
_validate_resource(item, name=f"resources[{index}]") for index, item in enumerate(resources)
|
|
410
|
+
]
|
|
411
|
+
return detached_styles, detached_numbering, detached_resources
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _check_dependency_uniqueness(
|
|
415
|
+
detached_styles: list[dict[str, Any]],
|
|
416
|
+
detached_numbering: list[dict[str, Any]],
|
|
417
|
+
detached_resources: list[dict[str, Any]],
|
|
418
|
+
) -> list[str]:
|
|
419
|
+
style_keys = [str(item["key"]) for item in detached_styles]
|
|
420
|
+
numbering_keys = [str(item["key"]) for item in detached_numbering]
|
|
421
|
+
resource_keys = [str(item["key"]) for item in detached_resources]
|
|
422
|
+
dependency_keys = style_keys + numbering_keys + resource_keys
|
|
423
|
+
if len(dependency_keys) != len(set(dependency_keys)):
|
|
424
|
+
raise AgentContractError("invariant_violation", "dependency keys must be unique", target="styles")
|
|
425
|
+
asset_paths = [str(item["assetPath"]) for item in detached_resources]
|
|
426
|
+
if len(asset_paths) != len(set(asset_paths)):
|
|
427
|
+
raise AgentContractError("invariant_violation", "resource asset paths must be unique", target="resources")
|
|
428
|
+
if sum(int(item["size"]) for item in detached_resources) > MAX_TOTAL_ASSET_BYTES:
|
|
429
|
+
raise AgentContractError("resource_limit", "total asset bytes exceed limit", target="resources")
|
|
430
|
+
return dependency_keys
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _check_node_dependency_refs(
|
|
434
|
+
detached_nodes: list[dict[str, Any]],
|
|
435
|
+
detached_styles: list[dict[str, Any]],
|
|
436
|
+
detached_numbering: list[dict[str, Any]],
|
|
437
|
+
detached_resources: list[dict[str, Any]],
|
|
438
|
+
) -> None:
|
|
439
|
+
style_key_set = {str(item["key"]) for item in detached_styles}
|
|
440
|
+
numbering_key_set = {str(item["key"]) for item in detached_numbering}
|
|
441
|
+
resource_key_set = {str(item["key"]) for item in detached_resources}
|
|
442
|
+
for index, node in enumerate(detached_nodes):
|
|
443
|
+
if not set(node["styleRefs"]) <= style_key_set:
|
|
444
|
+
raise AgentContractError("invariant_violation", "node has an orphan style reference", target=f"nodes[{index}].styleRefs")
|
|
445
|
+
if not set(node["numberingRefs"]) <= numbering_key_set:
|
|
446
|
+
raise AgentContractError("invariant_violation", "node has an orphan numbering reference", target=f"nodes[{index}].numberingRefs")
|
|
447
|
+
if not set(node["resourceRefs"]) <= resource_key_set:
|
|
448
|
+
raise AgentContractError("invariant_violation", "node has an orphan resource reference", target=f"nodes[{index}].resourceRefs")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _validate_reference_records(
|
|
452
|
+
manifest: dict[str, Any],
|
|
453
|
+
node_ids: set[str],
|
|
454
|
+
dependency_keys: list[str],
|
|
455
|
+
) -> tuple[list[dict[str, Any]], dict[str, list[str]]]:
|
|
456
|
+
references = _list(manifest["references"], "references")
|
|
457
|
+
if len(references) > MAX_REFERENCES:
|
|
458
|
+
raise AgentContractError("resource_limit", "reference count exceeds limit", target="references")
|
|
459
|
+
reference_records: list[dict[str, Any]] = []
|
|
460
|
+
reference_edges: dict[str, list[str]] = {node_id: [] for node_id in node_ids}
|
|
461
|
+
for index, item in enumerate(references):
|
|
462
|
+
record = _object(item, f"references[{index}]")
|
|
463
|
+
_exact_keys(record, required={"from", "field", "to", "required"}, name=f"references[{index}]")
|
|
464
|
+
if str(record["from"]) not in node_ids:
|
|
465
|
+
raise AgentContractError("invariant_violation", "reference source is missing", target=f"references[{index}]")
|
|
466
|
+
if not isinstance(record["required"], bool):
|
|
467
|
+
raise AgentContractError("invalid_syntax", "reference required must be boolean", target=f"references[{index}]")
|
|
468
|
+
target = str(record["to"])
|
|
469
|
+
if target not in node_ids and target not in set(dependency_keys):
|
|
470
|
+
raise AgentContractError("invariant_violation", "reference target is missing", target=f"references[{index}]")
|
|
471
|
+
if target in node_ids:
|
|
472
|
+
reference_edges[str(record["from"])].append(target)
|
|
473
|
+
reference_records.append(record)
|
|
474
|
+
return reference_records, reference_edges
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _check_reference_inventory(
|
|
478
|
+
detached_nodes: list[dict[str, Any]],
|
|
479
|
+
reference_records: list[dict[str, Any]],
|
|
480
|
+
) -> None:
|
|
481
|
+
for index, node in enumerate(detached_nodes):
|
|
482
|
+
declared_fields = {
|
|
483
|
+
str(record["field"])
|
|
484
|
+
for record in reference_records
|
|
485
|
+
if str(record["from"]) == str(node["blueprintId"])
|
|
486
|
+
}
|
|
487
|
+
if set(node["references"]) != declared_fields:
|
|
488
|
+
raise AgentContractError("invariant_violation", "node reference inventory is inconsistent", target=f"nodes[{index}].references")
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _check_reference_acyclicity(
|
|
492
|
+
node_ids: set[str],
|
|
493
|
+
reference_edges: dict[str, list[str]],
|
|
494
|
+
) -> None:
|
|
495
|
+
reference_visited: set[str] = set()
|
|
496
|
+
reference_visiting: set[str] = set()
|
|
497
|
+
|
|
498
|
+
def visit_reference(node_id: str) -> None:
|
|
499
|
+
if node_id in reference_visiting:
|
|
500
|
+
raise AgentContractError("invariant_violation", "semantic reference graph contains a cycle", target=node_id)
|
|
501
|
+
if node_id in reference_visited:
|
|
502
|
+
return
|
|
503
|
+
reference_visiting.add(node_id)
|
|
504
|
+
for target_id in reference_edges[node_id]:
|
|
505
|
+
visit_reference(target_id)
|
|
506
|
+
reference_visiting.remove(node_id)
|
|
507
|
+
reference_visited.add(node_id)
|
|
508
|
+
|
|
509
|
+
for node_id in sorted(node_ids):
|
|
510
|
+
visit_reference(node_id)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _validate_metadata_sections(manifest: dict[str, Any]) -> list[Any]:
|
|
514
|
+
unsupported = _list(manifest["unsupported"], "unsupported")
|
|
515
|
+
for index, item in enumerate(unsupported):
|
|
516
|
+
record = _object(item, f"unsupported[{index}]")
|
|
517
|
+
_exact_keys(record, required={"path", "kind", "reason"}, name=f"unsupported[{index}]")
|
|
518
|
+
|
|
519
|
+
capabilities = _object(manifest["capabilities"], "capabilities")
|
|
520
|
+
_exact_keys(capabilities, required={"dump", "replay", "kinds"}, name="capabilities")
|
|
521
|
+
if not isinstance(capabilities["dump"], bool) or not isinstance(capabilities["replay"], bool):
|
|
522
|
+
raise AgentContractError("invalid_syntax", "capability flags must be boolean", target="capabilities")
|
|
523
|
+
capability_kinds = [str(item) for item in _list(capabilities["kinds"], "capabilities.kinds")]
|
|
524
|
+
if len(capability_kinds) != len(set(capability_kinds)) or any(kind not in NODE_KINDS for kind in capability_kinds):
|
|
525
|
+
raise AgentContractError("unknown_kind", "capability kinds are invalid", target="capabilities.kinds")
|
|
526
|
+
capabilities["kinds"] = capability_kinds
|
|
527
|
+
limits = _object(manifest["limits"], "limits")
|
|
528
|
+
if limits != blueprint_limits():
|
|
529
|
+
raise AgentContractError("verification_failed", "blueprint limits do not match v1", target="limits")
|
|
530
|
+
fidelity = _object(manifest["fidelity"], "fidelity")
|
|
531
|
+
_exact_keys(fidelity, required={"replayable", "ceiling", "reasons"}, name="fidelity")
|
|
532
|
+
if not isinstance(fidelity["replayable"], bool):
|
|
533
|
+
raise AgentContractError("invalid_syntax", "fidelity replayable must be boolean", target="fidelity.replayable")
|
|
534
|
+
if fidelity["ceiling"] not in FIDELITY_LEVELS:
|
|
535
|
+
raise AgentContractError("invalid_syntax", "fidelity ceiling is invalid", target="fidelity.ceiling")
|
|
536
|
+
fidelity["reasons"] = [str(item) for item in _list(fidelity["reasons"], "fidelity.reasons")]
|
|
537
|
+
return unsupported
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _verify_manifest_integrity(manifest: dict[str, Any], verify_hash: bool) -> None:
|
|
541
|
+
_validate_public_json(manifest, name="blueprint")
|
|
542
|
+
declared_hash = manifest["blueprintHash"]
|
|
543
|
+
if not SHA256_PATTERN.fullmatch(str(declared_hash)):
|
|
544
|
+
raise AgentContractError("invalid_syntax", "blueprintHash must be sha256", target="blueprintHash")
|
|
545
|
+
if verify_hash and declared_hash != blueprint_hash(manifest):
|
|
546
|
+
raise AgentContractError("verification_failed", "blueprintHash mismatch", target="blueprintHash")
|
|
547
|
+
canonical_manifest_bytes(manifest, include_hash=True)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def validate_blueprint_manifest(value: Mapping[str, Any], *, verify_hash: bool = True) -> dict[str, Any]:
|
|
551
|
+
"""Validate and detach one strict ``hwpx.agent-blueprint/v1`` manifest."""
|
|
552
|
+
|
|
553
|
+
manifest = _object(value, "blueprint")
|
|
554
|
+
_validate_manifest_header(manifest)
|
|
555
|
+
_validate_source(manifest)
|
|
556
|
+
root = _validate_root(manifest)
|
|
557
|
+
|
|
558
|
+
detached_nodes, node_ids, nodes_by_id = _validate_nodes(manifest)
|
|
559
|
+
root_id = _check_node_tree_shape(root, detached_nodes, node_ids, nodes_by_id)
|
|
560
|
+
_check_node_reachability(root_id, node_ids, nodes_by_id)
|
|
561
|
+
|
|
562
|
+
detached_styles, detached_numbering, detached_resources = _validate_dependency_records(manifest)
|
|
563
|
+
dependency_keys = _check_dependency_uniqueness(
|
|
564
|
+
detached_styles, detached_numbering, detached_resources
|
|
565
|
+
)
|
|
566
|
+
_check_node_dependency_refs(
|
|
567
|
+
detached_nodes, detached_styles, detached_numbering, detached_resources
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
reference_records, reference_edges = _validate_reference_records(
|
|
571
|
+
manifest, node_ids, dependency_keys
|
|
572
|
+
)
|
|
573
|
+
_check_reference_inventory(detached_nodes, reference_records)
|
|
574
|
+
_check_reference_acyclicity(node_ids, reference_edges)
|
|
575
|
+
|
|
576
|
+
unsupported = _validate_metadata_sections(manifest)
|
|
577
|
+
_verify_manifest_integrity(manifest, verify_hash)
|
|
578
|
+
return {
|
|
579
|
+
**manifest,
|
|
580
|
+
"nodes": detached_nodes,
|
|
581
|
+
"styles": detached_styles,
|
|
582
|
+
"numbering": detached_numbering,
|
|
583
|
+
"resources": detached_resources,
|
|
584
|
+
"references": reference_records,
|
|
585
|
+
"unsupported": [dict(item) for item in unsupported],
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def validate_replay_request(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
590
|
+
request = _object(value, "replay")
|
|
591
|
+
_exact_keys(
|
|
592
|
+
request,
|
|
593
|
+
required={
|
|
594
|
+
"schemaVersion",
|
|
595
|
+
"bundle",
|
|
596
|
+
"target",
|
|
597
|
+
"targetParent",
|
|
598
|
+
"position",
|
|
599
|
+
"mode",
|
|
600
|
+
"mappingPolicy",
|
|
601
|
+
"expectedRevision",
|
|
602
|
+
"idempotencyKey",
|
|
603
|
+
"dryRun",
|
|
604
|
+
"quality",
|
|
605
|
+
"verificationRequirements",
|
|
606
|
+
},
|
|
607
|
+
name="replay",
|
|
608
|
+
)
|
|
609
|
+
if request["schemaVersion"] != BLUEPRINT_REPLAY_SCHEMA:
|
|
610
|
+
raise AgentContractError("invalid_syntax", "unsupported replay schema", target="schemaVersion")
|
|
611
|
+
if request["mode"] not in BLUEPRINT_MODES:
|
|
612
|
+
raise AgentContractError("invalid_syntax", "unsupported replay mode", target="mode")
|
|
613
|
+
bundle = _object(request["bundle"], "bundle")
|
|
614
|
+
_exact_keys(bundle, required={"filename", "blueprintHash"}, name="bundle")
|
|
615
|
+
if not isinstance(bundle["filename"], str) or not bundle["filename"].strip():
|
|
616
|
+
raise AgentContractError("invalid_syntax", "bundle.filename cannot be empty", target="bundle.filename")
|
|
617
|
+
if not SHA256_PATTERN.fullmatch(str(bundle["blueprintHash"])):
|
|
618
|
+
raise AgentContractError("invalid_syntax", "bundle blueprintHash is invalid", target="bundle.blueprintHash")
|
|
619
|
+
target = _object(request["target"], "target")
|
|
620
|
+
_exact_keys(target, required={"input", "output", "overwrite"}, name="target")
|
|
621
|
+
for field_name in ("input", "output"):
|
|
622
|
+
if not isinstance(target[field_name], str) or not target[field_name].strip():
|
|
623
|
+
raise AgentContractError("invalid_syntax", f"target.{field_name} cannot be empty", target=f"target.{field_name}")
|
|
624
|
+
if not isinstance(target["overwrite"], bool):
|
|
625
|
+
raise AgentContractError("invalid_syntax", "target.overwrite must be boolean", target="target.overwrite")
|
|
626
|
+
if not str(request["targetParent"]).startswith("/"):
|
|
627
|
+
raise AgentContractError("invalid_syntax", "targetParent must be semantic", target="targetParent")
|
|
628
|
+
position = _validate_position(request["position"], "position")
|
|
629
|
+
policy = _object(request["mappingPolicy"], "mappingPolicy")
|
|
630
|
+
_exact_keys(policy, required={"strict"}, name="mappingPolicy")
|
|
631
|
+
if not isinstance(policy["strict"], bool):
|
|
632
|
+
raise AgentContractError("invalid_syntax", "mappingPolicy.strict must be boolean", target="mappingPolicy.strict")
|
|
633
|
+
expected = request["expectedRevision"]
|
|
634
|
+
if expected is not None and not REVISION_PATTERN.fullmatch(str(expected)):
|
|
635
|
+
raise AgentContractError("invalid_syntax", "expectedRevision must be sha256", target="expectedRevision")
|
|
636
|
+
if request["idempotencyKey"] is not None and not str(request["idempotencyKey"]).strip():
|
|
637
|
+
raise AgentContractError("invalid_syntax", "idempotencyKey cannot be empty", target="idempotencyKey")
|
|
638
|
+
if not isinstance(request["dryRun"], bool):
|
|
639
|
+
raise AgentContractError("invalid_syntax", "dryRun must be boolean", target="dryRun")
|
|
640
|
+
requirements = [str(item) for item in _list(request["verificationRequirements"], "verificationRequirements")]
|
|
641
|
+
unknown_requirements = sorted(set(requirements) - set(VERIFICATION_REQUIREMENTS))
|
|
642
|
+
if unknown_requirements:
|
|
643
|
+
raise AgentContractError(
|
|
644
|
+
"invalid_syntax",
|
|
645
|
+
f"unsupported verification requirements: {unknown_requirements}",
|
|
646
|
+
target="verificationRequirements",
|
|
647
|
+
)
|
|
648
|
+
quality = _validate_quality(request["quality"])
|
|
649
|
+
_validate_public_json(request, name="replay")
|
|
650
|
+
return {
|
|
651
|
+
**request,
|
|
652
|
+
"bundle": bundle,
|
|
653
|
+
"target": target,
|
|
654
|
+
"position": position,
|
|
655
|
+
"mappingPolicy": policy,
|
|
656
|
+
"quality": quality,
|
|
657
|
+
"verificationRequirements": requirements,
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@dataclass(frozen=True, slots=True)
|
|
662
|
+
class BlueprintReplayResult:
|
|
663
|
+
ok: bool
|
|
664
|
+
rolled_back: bool
|
|
665
|
+
dry_run: bool
|
|
666
|
+
input_revision: str
|
|
667
|
+
document_revision: str
|
|
668
|
+
output_filename: str
|
|
669
|
+
blueprint_hash: str
|
|
670
|
+
root_path: str | None = None
|
|
671
|
+
node_map: Mapping[str, str] = field(default_factory=dict)
|
|
672
|
+
dependency_maps: Mapping[str, Any] = field(default_factory=dict)
|
|
673
|
+
fidelity: Mapping[str, Any] = field(default_factory=dict)
|
|
674
|
+
semantic_diff: Mapping[str, Any] = field(default_factory=dict)
|
|
675
|
+
verification_report: Mapping[str, Any] = field(default_factory=dict)
|
|
676
|
+
error: AgentError | None = None
|
|
677
|
+
|
|
678
|
+
def to_dict(self) -> dict[str, Any]:
|
|
679
|
+
return {
|
|
680
|
+
"schemaVersion": BLUEPRINT_REPLAY_RESULT_SCHEMA,
|
|
681
|
+
"ok": self.ok,
|
|
682
|
+
"rolledBack": self.rolled_back,
|
|
683
|
+
"dryRun": self.dry_run,
|
|
684
|
+
"inputRevision": self.input_revision,
|
|
685
|
+
"documentRevision": self.document_revision,
|
|
686
|
+
"outputFilename": self.output_filename,
|
|
687
|
+
"blueprintHash": self.blueprint_hash,
|
|
688
|
+
"rootPath": self.root_path,
|
|
689
|
+
"nodeMap": dict(self.node_map),
|
|
690
|
+
"dependencyMaps": dict(self.dependency_maps),
|
|
691
|
+
"fidelity": dict(self.fidelity),
|
|
692
|
+
"semanticDiff": dict(self.semantic_diff),
|
|
693
|
+
"verificationReport": dict(self.verification_report),
|
|
694
|
+
"error": self.error.to_dict() if self.error is not None else None,
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
__all__ = [
|
|
699
|
+
"ASSET_PATH_PATTERN",
|
|
700
|
+
"BLUEPRINT_CATALOG_SCHEMA",
|
|
701
|
+
"BLUEPRINT_ID_PATTERN",
|
|
702
|
+
"BLUEPRINT_MODES",
|
|
703
|
+
"BLUEPRINT_REPLAY_RESULT_SCHEMA",
|
|
704
|
+
"BLUEPRINT_REPLAY_SCHEMA",
|
|
705
|
+
"BLUEPRINT_SCHEMA",
|
|
706
|
+
"BlueprintReplayResult",
|
|
707
|
+
"FIDELITY_LEVELS",
|
|
708
|
+
"MAX_ASSETS",
|
|
709
|
+
"MAX_ASSET_BYTES",
|
|
710
|
+
"MAX_BLUEPRINT_DEPTH",
|
|
711
|
+
"MAX_BLUEPRINT_NODES",
|
|
712
|
+
"MAX_DEPENDENCIES",
|
|
713
|
+
"MAX_MANIFEST_BYTES",
|
|
714
|
+
"MAX_REFERENCES",
|
|
715
|
+
"MAX_TOTAL_ASSET_BYTES",
|
|
716
|
+
"blueprint_hash",
|
|
717
|
+
"blueprint_limits",
|
|
718
|
+
"canonical_manifest_bytes",
|
|
719
|
+
"validate_blueprint_manifest",
|
|
720
|
+
"validate_replay_request",
|
|
721
|
+
"with_blueprint_hash",
|
|
722
|
+
]
|