python-hwpx 2.24.1__py3-none-any.whl → 2.29.1__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/agent/__init__.py +99 -0
- hwpx/agent/blueprint/__init__.py +58 -0
- hwpx/agent/blueprint/bundle.py +282 -0
- hwpx/agent/blueprint/catalog.py +136 -0
- hwpx/agent/blueprint/dump.py +521 -0
- hwpx/agent/blueprint/mapping.py +312 -0
- hwpx/agent/blueprint/model.py +626 -0
- hwpx/agent/blueprint/native.py +622 -0
- hwpx/agent/blueprint/replay.py +452 -0
- hwpx/agent/catalog.py +167 -0
- hwpx/agent/cli.py +631 -0
- hwpx/agent/commands.py +1401 -0
- hwpx/agent/document.py +769 -0
- hwpx/agent/model.py +730 -0
- hwpx/agent/path.py +155 -0
- hwpx/agent/query.py +230 -0
- hwpx/benchmark/__init__.py +25 -0
- hwpx/benchmark/blind_eval.py +261 -0
- hwpx/document.py +17 -1
- hwpx/oxml/_document_impl.py +16 -14
- hwpx/practice/__init__.py +277 -0
- hwpx/practice/aggregate.py +947 -0
- hwpx/practice/campaign.py +296 -0
- hwpx/practice/domain.py +3137 -0
- hwpx/practice/dossier.py +78 -0
- hwpx/practice/evaluator.py +2359 -0
- hwpx/practice/forge.py +436 -0
- hwpx/practice/intake.py +230 -0
- hwpx/practice/lineage.py +113 -0
- hwpx/practice/mutations.py +168 -0
- hwpx/practice/registry.py +322 -0
- hwpx/practice/run.py +757 -0
- hwpx/practice/sanitize.py +154 -0
- hwpx/practice/scenario.py +146 -0
- hwpx/practice/split.py +249 -0
- hwpx/visual/__init__.py +38 -1
- hwpx/visual/_render_hwpx_mac.applescript +24 -1
- hwpx/visual/fixture_corpus.py +175 -0
- hwpx/visual/hancom_worker.py +288 -0
- hwpx/visual/page_qa.py +244 -0
- hwpx/visual/qa_contracts.py +293 -0
- hwpx/visual/qa_metrics.py +236 -0
- {python_hwpx-2.24.1.dist-info → python_hwpx-2.29.1.dist-info}/METADATA +3 -1
- {python_hwpx-2.24.1.dist-info → python_hwpx-2.29.1.dist-info}/RECORD +49 -11
- {python_hwpx-2.24.1.dist-info → python_hwpx-2.29.1.dist-info}/entry_points.txt +1 -0
- {python_hwpx-2.24.1.dist-info → python_hwpx-2.29.1.dist-info}/WHEEL +0 -0
- {python_hwpx-2.24.1.dist-info → python_hwpx-2.29.1.dist-info}/licenses/LICENSE +0 -0
- {python_hwpx-2.24.1.dist-info → python_hwpx-2.29.1.dist-info}/licenses/NOTICE +0 -0
- {python_hwpx-2.24.1.dist-info → python_hwpx-2.29.1.dist-info}/top_level.txt +0 -0
hwpx/agent/__init__.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Versioned semantic contracts for the HWPX agent document interface."""
|
|
2
|
+
|
|
3
|
+
from .model import (
|
|
4
|
+
AGENT_BATCH_RESULT_SCHEMA,
|
|
5
|
+
AGENT_BATCH_SCHEMA,
|
|
6
|
+
AGENT_CATALOG_SCHEMA,
|
|
7
|
+
AGENT_COMMAND_SCHEMA,
|
|
8
|
+
AGENT_ERROR_SCHEMA,
|
|
9
|
+
AGENT_NODE_SCHEMA,
|
|
10
|
+
AgentBatchResult,
|
|
11
|
+
AgentContractError,
|
|
12
|
+
AgentError,
|
|
13
|
+
AgentNode,
|
|
14
|
+
agent_contract_manifest,
|
|
15
|
+
validate_agent_batch,
|
|
16
|
+
validate_agent_command,
|
|
17
|
+
)
|
|
18
|
+
from .catalog import agent_catalog, agent_json_schemas, catalog_hash, human_help, node_help
|
|
19
|
+
from .commands import apply_document_commands
|
|
20
|
+
from .document import HwpxAgentDocument, NodeRecord
|
|
21
|
+
from .path import PathSegment, SemanticPath, canonicalize_path, parse_path
|
|
22
|
+
from .query import QueryResult, SemanticSelector, parse_selector
|
|
23
|
+
from .blueprint import (
|
|
24
|
+
BLUEPRINT_CATALOG_SCHEMA,
|
|
25
|
+
BLUEPRINT_REPLAY_RESULT_SCHEMA,
|
|
26
|
+
BLUEPRINT_REPLAY_SCHEMA,
|
|
27
|
+
BLUEPRINT_SCHEMA,
|
|
28
|
+
BlueprintBundle,
|
|
29
|
+
BlueprintDumpResult,
|
|
30
|
+
BlueprintReplayResult,
|
|
31
|
+
ReplayPlan,
|
|
32
|
+
blueprint_catalog,
|
|
33
|
+
blueprint_catalog_hash,
|
|
34
|
+
blueprint_hash,
|
|
35
|
+
blueprint_human_help,
|
|
36
|
+
blueprint_json_schemas,
|
|
37
|
+
blueprint_limits,
|
|
38
|
+
build_blueprint_bundle,
|
|
39
|
+
dump_document_blueprint,
|
|
40
|
+
read_blueprint_bundle,
|
|
41
|
+
repack_blueprint_bundle,
|
|
42
|
+
replay_document_blueprint,
|
|
43
|
+
validate_blueprint_manifest,
|
|
44
|
+
validate_replay_request,
|
|
45
|
+
write_blueprint_bundle,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"AGENT_BATCH_RESULT_SCHEMA",
|
|
50
|
+
"AGENT_BATCH_SCHEMA",
|
|
51
|
+
"AGENT_CATALOG_SCHEMA",
|
|
52
|
+
"AGENT_COMMAND_SCHEMA",
|
|
53
|
+
"AGENT_ERROR_SCHEMA",
|
|
54
|
+
"AGENT_NODE_SCHEMA",
|
|
55
|
+
"BLUEPRINT_CATALOG_SCHEMA",
|
|
56
|
+
"BLUEPRINT_REPLAY_RESULT_SCHEMA",
|
|
57
|
+
"BLUEPRINT_REPLAY_SCHEMA",
|
|
58
|
+
"BLUEPRINT_SCHEMA",
|
|
59
|
+
"AgentBatchResult",
|
|
60
|
+
"AgentContractError",
|
|
61
|
+
"AgentError",
|
|
62
|
+
"AgentNode",
|
|
63
|
+
"BlueprintReplayResult",
|
|
64
|
+
"ReplayPlan",
|
|
65
|
+
"BlueprintBundle",
|
|
66
|
+
"BlueprintDumpResult",
|
|
67
|
+
"HwpxAgentDocument",
|
|
68
|
+
"NodeRecord",
|
|
69
|
+
"PathSegment",
|
|
70
|
+
"QueryResult",
|
|
71
|
+
"SemanticPath",
|
|
72
|
+
"SemanticSelector",
|
|
73
|
+
"agent_catalog",
|
|
74
|
+
"agent_json_schemas",
|
|
75
|
+
"agent_contract_manifest",
|
|
76
|
+
"apply_document_commands",
|
|
77
|
+
"blueprint_catalog",
|
|
78
|
+
"blueprint_catalog_hash",
|
|
79
|
+
"blueprint_hash",
|
|
80
|
+
"blueprint_human_help",
|
|
81
|
+
"blueprint_json_schemas",
|
|
82
|
+
"blueprint_limits",
|
|
83
|
+
"build_blueprint_bundle",
|
|
84
|
+
"canonicalize_path",
|
|
85
|
+
"catalog_hash",
|
|
86
|
+
"human_help",
|
|
87
|
+
"node_help",
|
|
88
|
+
"parse_path",
|
|
89
|
+
"parse_selector",
|
|
90
|
+
"dump_document_blueprint",
|
|
91
|
+
"read_blueprint_bundle",
|
|
92
|
+
"repack_blueprint_bundle",
|
|
93
|
+
"replay_document_blueprint",
|
|
94
|
+
"validate_agent_batch",
|
|
95
|
+
"validate_agent_command",
|
|
96
|
+
"validate_blueprint_manifest",
|
|
97
|
+
"validate_replay_request",
|
|
98
|
+
"write_blueprint_bundle",
|
|
99
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Typed semantic HWPX blueprint contracts."""
|
|
2
|
+
|
|
3
|
+
from .catalog import (
|
|
4
|
+
blueprint_catalog,
|
|
5
|
+
blueprint_catalog_hash,
|
|
6
|
+
blueprint_human_help,
|
|
7
|
+
blueprint_json_schemas,
|
|
8
|
+
)
|
|
9
|
+
from .bundle import (
|
|
10
|
+
BlueprintBundle,
|
|
11
|
+
build_blueprint_bundle,
|
|
12
|
+
read_blueprint_bundle,
|
|
13
|
+
repack_blueprint_bundle,
|
|
14
|
+
write_blueprint_bundle,
|
|
15
|
+
)
|
|
16
|
+
from .dump import BlueprintDumpResult, dump_document_blueprint
|
|
17
|
+
from .mapping import ReplayPlan
|
|
18
|
+
from .replay import replay_document_blueprint
|
|
19
|
+
from .model import (
|
|
20
|
+
BLUEPRINT_CATALOG_SCHEMA,
|
|
21
|
+
BLUEPRINT_REPLAY_RESULT_SCHEMA,
|
|
22
|
+
BLUEPRINT_REPLAY_SCHEMA,
|
|
23
|
+
BLUEPRINT_SCHEMA,
|
|
24
|
+
BlueprintReplayResult,
|
|
25
|
+
blueprint_hash,
|
|
26
|
+
blueprint_limits,
|
|
27
|
+
canonical_manifest_bytes,
|
|
28
|
+
validate_blueprint_manifest,
|
|
29
|
+
validate_replay_request,
|
|
30
|
+
with_blueprint_hash,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"BLUEPRINT_CATALOG_SCHEMA",
|
|
35
|
+
"BLUEPRINT_REPLAY_RESULT_SCHEMA",
|
|
36
|
+
"BLUEPRINT_REPLAY_SCHEMA",
|
|
37
|
+
"BLUEPRINT_SCHEMA",
|
|
38
|
+
"BlueprintBundle",
|
|
39
|
+
"BlueprintDumpResult",
|
|
40
|
+
"BlueprintReplayResult",
|
|
41
|
+
"ReplayPlan",
|
|
42
|
+
"blueprint_catalog",
|
|
43
|
+
"blueprint_catalog_hash",
|
|
44
|
+
"blueprint_hash",
|
|
45
|
+
"blueprint_human_help",
|
|
46
|
+
"blueprint_json_schemas",
|
|
47
|
+
"blueprint_limits",
|
|
48
|
+
"build_blueprint_bundle",
|
|
49
|
+
"canonical_manifest_bytes",
|
|
50
|
+
"dump_document_blueprint",
|
|
51
|
+
"read_blueprint_bundle",
|
|
52
|
+
"repack_blueprint_bundle",
|
|
53
|
+
"replay_document_blueprint",
|
|
54
|
+
"validate_blueprint_manifest",
|
|
55
|
+
"validate_replay_request",
|
|
56
|
+
"with_blueprint_hash",
|
|
57
|
+
"write_blueprint_bundle",
|
|
58
|
+
]
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Deterministic and fail-closed ``.hwpxbp`` bundle I/O."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import stat
|
|
11
|
+
import tempfile
|
|
12
|
+
import zipfile
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path, PurePosixPath
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from ..model import AgentContractError
|
|
19
|
+
from .model import (
|
|
20
|
+
ASSET_PATH_PATTERN,
|
|
21
|
+
MAX_ASSETS,
|
|
22
|
+
MAX_ASSET_BYTES,
|
|
23
|
+
MAX_MANIFEST_BYTES,
|
|
24
|
+
MAX_TOTAL_ASSET_BYTES,
|
|
25
|
+
canonical_manifest_bytes,
|
|
26
|
+
validate_blueprint_manifest,
|
|
27
|
+
with_blueprint_hash,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
MANIFEST_PATH = "blueprint.json"
|
|
31
|
+
MAX_BUNDLE_BYTES = MAX_MANIFEST_BYTES + MAX_TOTAL_ASSET_BYTES + 2 * 1024 * 1024
|
|
32
|
+
MAX_COMPRESSION_RATIO = 100
|
|
33
|
+
|
|
34
|
+
ALLOWED_MEDIA_TYPES: dict[str, tuple[str, ...]] = {
|
|
35
|
+
"image/png": ("png",),
|
|
36
|
+
"image/jpeg": ("jpg", "jpeg"),
|
|
37
|
+
"image/gif": ("gif",),
|
|
38
|
+
"image/bmp": ("bmp",),
|
|
39
|
+
"image/tiff": ("tif", "tiff"),
|
|
40
|
+
"image/webp": ("webp",),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class BlueprintBundle:
|
|
46
|
+
"""A fully validated manifest and detached content-addressed assets."""
|
|
47
|
+
|
|
48
|
+
manifest: Mapping[str, Any]
|
|
49
|
+
assets: Mapping[str, bytes]
|
|
50
|
+
bundle_sha256: str
|
|
51
|
+
size: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _error(code: str, message: str, *, target: str = "bundle") -> AgentContractError:
|
|
55
|
+
return AgentContractError(code, message, target=target)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _json_without_duplicate_keys(data: bytes) -> dict[str, Any]:
|
|
59
|
+
def pairs_hook(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
60
|
+
result: dict[str, Any] = {}
|
|
61
|
+
for key, value in pairs:
|
|
62
|
+
if key in result:
|
|
63
|
+
raise _error("invariant_violation", f"duplicate JSON field: {key}", target=MANIFEST_PATH)
|
|
64
|
+
result[key] = value
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
value = json.loads(data.decode("utf-8"), object_pairs_hook=pairs_hook)
|
|
69
|
+
except AgentContractError:
|
|
70
|
+
raise
|
|
71
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
72
|
+
raise _error("invalid_syntax", "blueprint.json is not strict UTF-8 JSON", target=MANIFEST_PATH) from exc
|
|
73
|
+
if not isinstance(value, dict):
|
|
74
|
+
raise _error("invalid_syntax", "blueprint.json must contain one object", target=MANIFEST_PATH)
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validate_entry_name(name: str) -> None:
|
|
79
|
+
if not name or "\\" in name or "\x00" in name:
|
|
80
|
+
raise _error("verification_failed", "bundle entry path is not normalized", target=name or "bundle")
|
|
81
|
+
path = PurePosixPath(name)
|
|
82
|
+
if path.is_absolute() or name.startswith("/") or any(part in {"", ".", ".."} for part in path.parts):
|
|
83
|
+
raise _error("verification_failed", "bundle entry path is unsafe", target=name)
|
|
84
|
+
if name != MANIFEST_PATH and not ASSET_PATH_PATTERN.fullmatch(name):
|
|
85
|
+
raise _error("unsupported_content", "bundle contains an unknown or forbidden entry", target=name)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _validate_zip_info(info: zipfile.ZipInfo) -> None:
|
|
89
|
+
_validate_entry_name(info.filename)
|
|
90
|
+
if info.is_dir():
|
|
91
|
+
raise _error("unsupported_content", "directory entries are forbidden", target=info.filename)
|
|
92
|
+
mode = (info.external_attr >> 16) & 0xFFFF
|
|
93
|
+
if mode and stat.S_ISLNK(mode):
|
|
94
|
+
raise _error("unsupported_content", "symlink entries are forbidden", target=info.filename)
|
|
95
|
+
if info.flag_bits & 0x1:
|
|
96
|
+
raise _error("unsupported_content", "encrypted bundle entries are forbidden", target=info.filename)
|
|
97
|
+
if info.compress_type not in {zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED}:
|
|
98
|
+
raise _error("unsupported_content", "bundle compression method is forbidden", target=info.filename)
|
|
99
|
+
limit = MAX_MANIFEST_BYTES if info.filename == MANIFEST_PATH else MAX_ASSET_BYTES
|
|
100
|
+
if info.file_size > limit:
|
|
101
|
+
raise _error("resource_limit", "bundle entry exceeds its byte limit", target=info.filename)
|
|
102
|
+
if info.file_size and info.compress_size == 0:
|
|
103
|
+
raise _error("resource_limit", "bundle entry has an invalid compression ratio", target=info.filename)
|
|
104
|
+
if info.compress_size and info.file_size / info.compress_size > MAX_COMPRESSION_RATIO:
|
|
105
|
+
raise _error("resource_limit", "bundle entry exceeds the decompression-ratio limit", target=info.filename)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _sniff_media_type(data: bytes) -> str | None:
|
|
109
|
+
if data.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
110
|
+
return "image/png"
|
|
111
|
+
if data.startswith(b"\xff\xd8\xff"):
|
|
112
|
+
return "image/jpeg"
|
|
113
|
+
if data.startswith((b"GIF87a", b"GIF89a")):
|
|
114
|
+
return "image/gif"
|
|
115
|
+
if data.startswith(b"BM"):
|
|
116
|
+
return "image/bmp"
|
|
117
|
+
if data.startswith((b"II*\x00", b"MM\x00*")):
|
|
118
|
+
return "image/tiff"
|
|
119
|
+
if len(data) >= 12 and data[:4] == b"RIFF" and data[8:12] == b"WEBP":
|
|
120
|
+
return "image/webp"
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _validated_assets(
|
|
125
|
+
manifest: Mapping[str, Any], assets: Mapping[str, bytes]
|
|
126
|
+
) -> dict[str, bytes]:
|
|
127
|
+
declared = {str(item["assetPath"]): dict(item) for item in manifest["resources"]}
|
|
128
|
+
if set(assets) != set(declared):
|
|
129
|
+
raise _error(
|
|
130
|
+
"invariant_violation",
|
|
131
|
+
"bundle assets do not exactly match manifest resources",
|
|
132
|
+
target="resources",
|
|
133
|
+
)
|
|
134
|
+
if len(assets) > MAX_ASSETS:
|
|
135
|
+
raise _error("resource_limit", "asset count exceeds limit", target="resources")
|
|
136
|
+
total = 0
|
|
137
|
+
detached: dict[str, bytes] = {}
|
|
138
|
+
for path in sorted(assets):
|
|
139
|
+
_validate_entry_name(path)
|
|
140
|
+
payload = assets[path]
|
|
141
|
+
if not isinstance(payload, bytes):
|
|
142
|
+
raise _error("invalid_syntax", "asset payload must be bytes", target=path)
|
|
143
|
+
total += len(payload)
|
|
144
|
+
if len(payload) > MAX_ASSET_BYTES or total > MAX_TOTAL_ASSET_BYTES:
|
|
145
|
+
raise _error("resource_limit", "asset bytes exceed limit", target=path)
|
|
146
|
+
record = declared[path]
|
|
147
|
+
digest = "sha256:" + hashlib.sha256(payload).hexdigest()
|
|
148
|
+
if digest != record["sha256"] or len(payload) != record["size"]:
|
|
149
|
+
raise _error("verification_failed", "asset hash or size mismatch", target=path)
|
|
150
|
+
media_type = str(record["mediaType"])
|
|
151
|
+
suffix = path.rsplit(".", 1)[-1]
|
|
152
|
+
if media_type not in ALLOWED_MEDIA_TYPES or suffix not in ALLOWED_MEDIA_TYPES[media_type]:
|
|
153
|
+
raise _error("unsupported_content", "asset extension/MIME is not allow-listed", target=path)
|
|
154
|
+
if _sniff_media_type(payload) != media_type:
|
|
155
|
+
raise _error("verification_failed", "asset bytes do not match declared MIME", target=path)
|
|
156
|
+
detached[path] = bytes(payload)
|
|
157
|
+
return detached
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _zip_info(name: str) -> zipfile.ZipInfo:
|
|
161
|
+
info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
|
|
162
|
+
info.compress_type = zipfile.ZIP_STORED
|
|
163
|
+
info.create_system = 0
|
|
164
|
+
info.external_attr = 0
|
|
165
|
+
info.internal_attr = 0
|
|
166
|
+
info.flag_bits = 0
|
|
167
|
+
return info
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_blueprint_bundle(
|
|
171
|
+
manifest: Mapping[str, Any], assets: Mapping[str, bytes] | None = None
|
|
172
|
+
) -> bytes:
|
|
173
|
+
"""Return canonical bundle bytes for an already hashed manifest."""
|
|
174
|
+
|
|
175
|
+
checked = validate_blueprint_manifest(manifest)
|
|
176
|
+
checked_assets = _validated_assets(checked, assets or {})
|
|
177
|
+
manifest_bytes = canonical_manifest_bytes(checked, include_hash=True)
|
|
178
|
+
stream = io.BytesIO()
|
|
179
|
+
with zipfile.ZipFile(stream, "w", allowZip64=False) as archive:
|
|
180
|
+
archive.writestr(_zip_info(MANIFEST_PATH), manifest_bytes)
|
|
181
|
+
for name, payload in checked_assets.items():
|
|
182
|
+
archive.writestr(_zip_info(name), payload)
|
|
183
|
+
result = stream.getvalue()
|
|
184
|
+
if len(result) > MAX_BUNDLE_BYTES:
|
|
185
|
+
raise _error("resource_limit", "blueprint bundle exceeds total byte limit")
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def write_blueprint_bundle(
|
|
190
|
+
destination: str | os.PathLike[str],
|
|
191
|
+
manifest: Mapping[str, Any],
|
|
192
|
+
assets: Mapping[str, bytes] | None = None,
|
|
193
|
+
*,
|
|
194
|
+
overwrite: bool = False,
|
|
195
|
+
) -> BlueprintBundle:
|
|
196
|
+
"""Atomically write one deterministic bundle and return its validated view."""
|
|
197
|
+
|
|
198
|
+
path = Path(destination)
|
|
199
|
+
if path.suffix.casefold() != ".hwpxbp":
|
|
200
|
+
raise _error("invalid_syntax", "blueprint output must use .hwpxbp", target="output")
|
|
201
|
+
if path.exists() and not overwrite:
|
|
202
|
+
raise _error("identity_collision", "blueprint output already exists", target=str(path))
|
|
203
|
+
data = build_blueprint_bundle(manifest, assets)
|
|
204
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
descriptor, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
|
206
|
+
try:
|
|
207
|
+
with os.fdopen(descriptor, "wb") as handle:
|
|
208
|
+
handle.write(data)
|
|
209
|
+
handle.flush()
|
|
210
|
+
os.fsync(handle.fileno())
|
|
211
|
+
if path.exists() and not overwrite:
|
|
212
|
+
raise _error("identity_collision", "blueprint output already exists", target=str(path))
|
|
213
|
+
os.replace(temp_name, path)
|
|
214
|
+
except Exception:
|
|
215
|
+
try:
|
|
216
|
+
os.unlink(temp_name)
|
|
217
|
+
except FileNotFoundError:
|
|
218
|
+
pass
|
|
219
|
+
raise
|
|
220
|
+
return read_blueprint_bundle(data)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def read_blueprint_bundle(source: str | os.PathLike[str] | bytes) -> BlueprintBundle:
|
|
224
|
+
"""Validate a complete bundle in memory before returning any materialized data."""
|
|
225
|
+
|
|
226
|
+
data = bytes(source) if isinstance(source, bytes) else Path(source).read_bytes()
|
|
227
|
+
if len(data) > MAX_BUNDLE_BYTES:
|
|
228
|
+
raise _error("resource_limit", "blueprint bundle exceeds total byte limit")
|
|
229
|
+
try:
|
|
230
|
+
with zipfile.ZipFile(io.BytesIO(data), "r") as archive:
|
|
231
|
+
infos = archive.infolist()
|
|
232
|
+
names = [info.filename for info in infos]
|
|
233
|
+
if len(names) != len(set(names)):
|
|
234
|
+
raise _error("invariant_violation", "bundle contains duplicate entry paths")
|
|
235
|
+
if names.count(MANIFEST_PATH) != 1:
|
|
236
|
+
raise _error("invariant_violation", "bundle must contain exactly one blueprint.json")
|
|
237
|
+
if len(infos) > MAX_ASSETS + 1:
|
|
238
|
+
raise _error("resource_limit", "bundle entry count exceeds limit")
|
|
239
|
+
for info in infos:
|
|
240
|
+
_validate_zip_info(info)
|
|
241
|
+
if sum(info.file_size for info in infos if info.filename != MANIFEST_PATH) > MAX_TOTAL_ASSET_BYTES:
|
|
242
|
+
raise _error("resource_limit", "bundle assets exceed total byte limit", target="resources")
|
|
243
|
+
manifest_data = archive.read(MANIFEST_PATH)
|
|
244
|
+
manifest = validate_blueprint_manifest(_json_without_duplicate_keys(manifest_data))
|
|
245
|
+
asset_names = sorted(name for name in names if name != MANIFEST_PATH)
|
|
246
|
+
assets = {name: archive.read(name) for name in asset_names}
|
|
247
|
+
except AgentContractError:
|
|
248
|
+
raise
|
|
249
|
+
except (OSError, zipfile.BadZipFile, zipfile.LargeZipFile, RuntimeError) as exc:
|
|
250
|
+
raise _error("invalid_syntax", "invalid .hwpxbp ZIP container") from exc
|
|
251
|
+
checked_assets = _validated_assets(manifest, assets)
|
|
252
|
+
return BlueprintBundle(
|
|
253
|
+
manifest=manifest,
|
|
254
|
+
assets=checked_assets,
|
|
255
|
+
bundle_sha256="sha256:" + hashlib.sha256(data).hexdigest(),
|
|
256
|
+
size=len(data),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def repack_blueprint_bundle(
|
|
261
|
+
source: str | os.PathLike[str] | bytes,
|
|
262
|
+
destination: str | os.PathLike[str],
|
|
263
|
+
manifest: Mapping[str, Any],
|
|
264
|
+
*,
|
|
265
|
+
overwrite: bool = False,
|
|
266
|
+
) -> BlueprintBundle:
|
|
267
|
+
"""Safely repack edited typed JSON while preserving only validated assets."""
|
|
268
|
+
|
|
269
|
+
original = read_blueprint_bundle(source)
|
|
270
|
+
rehashed = with_blueprint_hash(manifest)
|
|
271
|
+
return write_blueprint_bundle(destination, rehashed, original.assets, overwrite=overwrite)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
__all__ = [
|
|
275
|
+
"ALLOWED_MEDIA_TYPES",
|
|
276
|
+
"BlueprintBundle",
|
|
277
|
+
"MANIFEST_PATH",
|
|
278
|
+
"build_blueprint_bundle",
|
|
279
|
+
"read_blueprint_bundle",
|
|
280
|
+
"repack_blueprint_bundle",
|
|
281
|
+
"write_blueprint_bundle",
|
|
282
|
+
]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Shared blueprint capability, limit, schema, and help catalog."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..model import AGENT_CATALOG_SCHEMA, NODE_KINDS
|
|
11
|
+
from .model import (
|
|
12
|
+
BLUEPRINT_CATALOG_SCHEMA,
|
|
13
|
+
BLUEPRINT_MODES,
|
|
14
|
+
BLUEPRINT_REPLAY_RESULT_SCHEMA,
|
|
15
|
+
BLUEPRINT_REPLAY_SCHEMA,
|
|
16
|
+
BLUEPRINT_SCHEMA,
|
|
17
|
+
FIDELITY_LEVELS,
|
|
18
|
+
blueprint_limits,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def blueprint_catalog() -> dict[str, Any]:
|
|
23
|
+
return {
|
|
24
|
+
"schemaVersion": BLUEPRINT_CATALOG_SCHEMA,
|
|
25
|
+
"agentCatalogVersion": AGENT_CATALOG_SCHEMA,
|
|
26
|
+
"schemas": {
|
|
27
|
+
"blueprint": BLUEPRINT_SCHEMA,
|
|
28
|
+
"replay": BLUEPRINT_REPLAY_SCHEMA,
|
|
29
|
+
"replayResult": BLUEPRINT_REPLAY_RESULT_SCHEMA,
|
|
30
|
+
},
|
|
31
|
+
"modes": list(BLUEPRINT_MODES),
|
|
32
|
+
"fidelity": list(FIDELITY_LEVELS),
|
|
33
|
+
"kinds": [kind for kind in NODE_KINDS if kind != "unsupported"],
|
|
34
|
+
"bundle": {
|
|
35
|
+
"extension": ".hwpxbp",
|
|
36
|
+
"manifest": "blueprint.json",
|
|
37
|
+
"assetPattern": "assets/<sha256>.<safe-extension>",
|
|
38
|
+
"allowedMediaPrefixes": ["image/"],
|
|
39
|
+
"forbidden": ["xml", "script", "symlink", "nested-archive", "absolute-path", "parent-path"],
|
|
40
|
+
},
|
|
41
|
+
"limits": blueprint_limits(),
|
|
42
|
+
"surfaces": {"cli": ["dump", "replay"], "mcpMaximumTools": 2},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def blueprint_catalog_hash() -> str:
|
|
47
|
+
encoded = json.dumps(
|
|
48
|
+
blueprint_catalog(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
49
|
+
).encode("utf-8")
|
|
50
|
+
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def blueprint_json_schemas() -> dict[str, Any]:
|
|
54
|
+
catalog = blueprint_catalog()
|
|
55
|
+
return {
|
|
56
|
+
"blueprint": {
|
|
57
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
58
|
+
"title": "HwpxBlueprint v1",
|
|
59
|
+
"type": "object",
|
|
60
|
+
"required": [
|
|
61
|
+
"schemaVersion",
|
|
62
|
+
"catalogVersion",
|
|
63
|
+
"catalogHash",
|
|
64
|
+
"source",
|
|
65
|
+
"mode",
|
|
66
|
+
"root",
|
|
67
|
+
"nodes",
|
|
68
|
+
"styles",
|
|
69
|
+
"numbering",
|
|
70
|
+
"resources",
|
|
71
|
+
"references",
|
|
72
|
+
"unsupported",
|
|
73
|
+
"capabilities",
|
|
74
|
+
"limits",
|
|
75
|
+
"fidelity",
|
|
76
|
+
"blueprintHash",
|
|
77
|
+
],
|
|
78
|
+
"additionalProperties": False,
|
|
79
|
+
"properties": {
|
|
80
|
+
"schemaVersion": {"const": BLUEPRINT_SCHEMA},
|
|
81
|
+
"catalogVersion": {"const": AGENT_CATALOG_SCHEMA},
|
|
82
|
+
"catalogHash": {"type": "string", "pattern": "^sha256:[a-f0-9]{64}$"},
|
|
83
|
+
"mode": {"enum": list(BLUEPRINT_MODES)},
|
|
84
|
+
"nodes": {"type": "array", "minItems": 1, "maxItems": catalog["limits"]["maxNodes"]},
|
|
85
|
+
"resources": {"type": "array", "maxItems": catalog["limits"]["maxAssets"]},
|
|
86
|
+
"blueprintHash": {"type": "string", "pattern": "^sha256:[a-f0-9]{64}$"},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"replay": {
|
|
90
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
91
|
+
"title": "HwpxBlueprintReplay v1",
|
|
92
|
+
"type": "object",
|
|
93
|
+
"required": [
|
|
94
|
+
"schemaVersion",
|
|
95
|
+
"bundle",
|
|
96
|
+
"target",
|
|
97
|
+
"targetParent",
|
|
98
|
+
"position",
|
|
99
|
+
"mode",
|
|
100
|
+
"mappingPolicy",
|
|
101
|
+
"expectedRevision",
|
|
102
|
+
"idempotencyKey",
|
|
103
|
+
"dryRun",
|
|
104
|
+
"quality",
|
|
105
|
+
"verificationRequirements",
|
|
106
|
+
],
|
|
107
|
+
"additionalProperties": False,
|
|
108
|
+
"properties": {
|
|
109
|
+
"schemaVersion": {"const": BLUEPRINT_REPLAY_SCHEMA},
|
|
110
|
+
"mode": {"enum": list(BLUEPRINT_MODES)},
|
|
111
|
+
"targetParent": {"type": "string"},
|
|
112
|
+
"dryRun": {"type": "boolean"},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def blueprint_human_help() -> str:
|
|
119
|
+
limits = blueprint_limits()
|
|
120
|
+
return (
|
|
121
|
+
"HWPX typed blueprint v1\n"
|
|
122
|
+
" dump: revision-bound document/subtree -> deterministic .hwpxbp\n"
|
|
123
|
+
" replay: source-bound|portable -> one atomic SavePipeline commit\n"
|
|
124
|
+
" fidelity: exact, mapped; strict replay rejects degraded/unsupported\n"
|
|
125
|
+
f" limits: nodes={limits['maxNodes']}, assets={limits['maxAssets']}, "
|
|
126
|
+
f"manifest={limits['maxManifestBytes']} bytes\n"
|
|
127
|
+
" raw XML, package parts, resident sessions, watch, and OfficeCLI are unavailable\n"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = [
|
|
132
|
+
"blueprint_catalog",
|
|
133
|
+
"blueprint_catalog_hash",
|
|
134
|
+
"blueprint_human_help",
|
|
135
|
+
"blueprint_json_schemas",
|
|
136
|
+
]
|