amkernel 0.1.0__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.
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env python3
2
+ """Fail closed unless pinned Appium skill sources and copies match the manifest."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import stat
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ SKILL_ROOT = Path(__file__).resolve().parents[1]
16
+ DEFAULT_MANIFEST = SKILL_ROOT / "references" / "official-skills.json"
17
+
18
+
19
+ def _digest(path: Path) -> str:
20
+ value = hashlib.sha256()
21
+ with path.open("rb") as stream:
22
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
23
+ value.update(chunk)
24
+ return value.hexdigest()
25
+
26
+
27
+ def _tree_entries(root: Path) -> tuple[set[str], set[str], set[str]]:
28
+ files: set[str] = set()
29
+ symlinks: set[str] = set()
30
+ invalid: set[str] = set()
31
+ if not root.is_dir():
32
+ return files, symlinks, invalid
33
+ for path in root.rglob("*"):
34
+ relative = path.relative_to(root)
35
+ if ".git" in relative.parts:
36
+ continue
37
+ name = relative.as_posix()
38
+ mode = path.lstat().st_mode
39
+ if stat.S_ISLNK(mode):
40
+ symlinks.add(name)
41
+ elif stat.S_ISREG(mode):
42
+ files.add(name)
43
+ elif not stat.S_ISDIR(mode):
44
+ invalid.add(name)
45
+ return files, symlinks, invalid
46
+
47
+
48
+ def _verify_tree(root: Path, expected: dict[str, str], label: str) -> list[str]:
49
+ errors: list[str] = []
50
+ if not root.is_dir() or root.is_symlink():
51
+ return [f"{label}: missing or linked directory: {root}"]
52
+
53
+ actual, symlinks, invalid = _tree_entries(root)
54
+ for relative in sorted(symlinks):
55
+ errors.append(f"{label}: symbolic links are forbidden: {relative}")
56
+ for relative in sorted(invalid):
57
+ errors.append(f"{label}: non-regular entries are forbidden: {relative}")
58
+ expected_paths = set(expected)
59
+ for relative in sorted(expected_paths - actual):
60
+ errors.append(f"{label}: missing file: {relative}")
61
+ for relative in sorted(actual - expected_paths):
62
+ errors.append(f"{label}: unexpected file: {relative}")
63
+ for relative in sorted(actual & expected_paths):
64
+ actual_digest = _digest(root / relative)
65
+ if actual_digest != expected[relative]:
66
+ errors.append(
67
+ f"{label}: sha256 mismatch: {relative}: "
68
+ f"expected {expected[relative]}, got {actual_digest}"
69
+ )
70
+ return errors
71
+
72
+
73
+ def _mapping(value: Any, label: str) -> dict[str, Any]:
74
+ if not isinstance(value, dict):
75
+ raise ValueError(f"{label} must be an object")
76
+ return value
77
+
78
+
79
+ def _file_map(value: Any, label: str) -> dict[str, str]:
80
+ mapping = _mapping(value, label)
81
+ result: dict[str, str] = {}
82
+ for path, digest in mapping.items():
83
+ if not isinstance(path, str) or not isinstance(digest, str):
84
+ raise ValueError(f"{label} keys and values must be strings")
85
+ if Path(path).is_absolute() or ".." in Path(path).parts:
86
+ raise ValueError(f"{label} contains unsafe path: {path!r}")
87
+ if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest):
88
+ raise ValueError(f"{label} contains invalid sha256 for {path!r}")
89
+ result[path] = digest
90
+ return result
91
+
92
+
93
+ def _location(value: Any, label: str) -> Path:
94
+ if not isinstance(value, str):
95
+ raise ValueError(f"{label} must be a string")
96
+ path = Path(value)
97
+ if path.is_absolute() or ".." in path.parts:
98
+ raise ValueError(f"{label} contains unsafe path: {value!r}")
99
+ return path
100
+
101
+
102
+ def _git(root: Path, *arguments: str) -> subprocess.CompletedProcess[str]:
103
+ environment = dict(os.environ)
104
+ environment["GIT_CONFIG_GLOBAL"] = os.devnull
105
+ environment["GIT_CONFIG_SYSTEM"] = os.devnull
106
+ return subprocess.run(
107
+ ["git", "-C", str(root), *arguments],
108
+ check=False,
109
+ capture_output=True,
110
+ text=True,
111
+ env=environment,
112
+ )
113
+
114
+
115
+ def _validated_root(
116
+ project_root: Path, location: Path, label: str
117
+ ) -> tuple[Path | None, list[str]]:
118
+ current = project_root
119
+ try:
120
+ project_mode = project_root.lstat().st_mode
121
+ except OSError as error:
122
+ return None, [f"{label}: inaccessible project root: {error}"]
123
+ if not stat.S_ISDIR(project_mode):
124
+ return None, [f"{label}: project root is not a real directory: {project_root}"]
125
+
126
+ for component in location.parts:
127
+ current = current / component
128
+ try:
129
+ mode = current.lstat().st_mode
130
+ except OSError as error:
131
+ return None, [f"{label}: inaccessible location component {current}: {error}"]
132
+ if stat.S_ISLNK(mode):
133
+ return None, [f"{label}: linked location component is forbidden: {current}"]
134
+ if not stat.S_ISDIR(mode):
135
+ return None, [f"{label}: location component is not a directory: {current}"]
136
+
137
+ resolved_project = project_root.resolve(strict=True)
138
+ resolved_current = current.resolve(strict=True)
139
+ try:
140
+ resolved_current.relative_to(resolved_project)
141
+ except ValueError:
142
+ return None, [f"{label}: location escapes project root: {current}"]
143
+ return current, []
144
+
145
+
146
+ def _canonical_repository(value: str) -> str:
147
+ return value.removesuffix(".git").rstrip("/")
148
+
149
+
150
+ def _verify_provenance(root: Path, repository: str, commit: str) -> list[str]:
151
+ errors: list[str] = []
152
+ head = _git(root, "rev-parse", "--verify", "HEAD")
153
+ if head.returncode != 0:
154
+ return [f"official source: not a Git checkout: {root}"]
155
+ if head.stdout.strip() != commit:
156
+ errors.append(
157
+ f"official source: HEAD mismatch: expected {commit}, got {head.stdout.strip()}"
158
+ )
159
+
160
+ branch = _git(root, "symbolic-ref", "-q", "HEAD")
161
+ if branch.returncode == 0:
162
+ errors.append(f"official source: HEAD must be detached, got {branch.stdout.strip()}")
163
+
164
+ origin = _git(root, "config", "--get", "remote.origin.url")
165
+ if origin.returncode != 0:
166
+ errors.append("official source: missing origin URL")
167
+ elif _canonical_repository(origin.stdout.strip()) != _canonical_repository(repository):
168
+ errors.append(
169
+ f"official source: origin mismatch: expected {repository}, got {origin.stdout.strip()}"
170
+ )
171
+ return errors
172
+
173
+
174
+ def verify(manifest_path: Path, project_root: Path) -> list[str]:
175
+ try:
176
+ document = json.loads(manifest_path.read_text(encoding="utf-8"))
177
+ manifest = _mapping(document, "manifest")
178
+ if manifest.get("schemaVersion") != 1:
179
+ raise ValueError("schemaVersion must be 1")
180
+
181
+ repository = manifest.get("repository")
182
+ commit = manifest.get("commit")
183
+ if not isinstance(repository, str):
184
+ raise ValueError("repository must be a string")
185
+ if (
186
+ not isinstance(commit, str)
187
+ or len(commit) != 40
188
+ or any(char not in "0123456789abcdef" for char in commit)
189
+ ):
190
+ raise ValueError("commit must be a full lowercase Git object ID")
191
+
192
+ source = _mapping(manifest.get("source"), "source")
193
+ source_location = _location(source.get("generatedLocation"), "source.generatedLocation")
194
+ source_root, errors = _validated_root(project_root, source_location, "official source")
195
+ source_files = _file_map(source.get("files"), "source.files")
196
+ if source_root is not None:
197
+ errors.extend(_verify_provenance(source_root, repository, commit))
198
+ errors.extend(_verify_tree(source_root, source_files, "official source"))
199
+
200
+ installed = manifest.get("installedSkills")
201
+ if not isinstance(installed, list) or not installed:
202
+ raise ValueError("installedSkills must be a non-empty array")
203
+ for index, item in enumerate(installed):
204
+ skill = _mapping(item, f"installedSkills[{index}]")
205
+ name = skill.get("name")
206
+ if not isinstance(name, str):
207
+ raise ValueError(f"installedSkills[{index}].name must be a string")
208
+ runtime_location = _location(
209
+ skill.get("generatedLocation"),
210
+ f"installedSkills[{index}].generatedLocation",
211
+ )
212
+ agent_location = _location(
213
+ skill.get("selectedAgentLocation"),
214
+ f"installedSkills[{index}].selectedAgentLocation",
215
+ )
216
+ if skill.get("selectedAgentMode") != "copy":
217
+ raise ValueError(f"installedSkills[{index}].selectedAgentMode must be copy")
218
+ files = _file_map(skill.get("files"), f"installedSkills[{index}].files")
219
+ runtime_root, runtime_errors = _validated_root(
220
+ project_root, runtime_location, f"runtime skill {name}"
221
+ )
222
+ errors.extend(runtime_errors)
223
+ if runtime_root is not None:
224
+ errors.extend(_verify_tree(runtime_root, files, f"runtime skill {name}"))
225
+ agent_root, agent_errors = _validated_root(
226
+ project_root, agent_location, f"selected-agent skill {name}"
227
+ )
228
+ errors.extend(agent_errors)
229
+ if agent_root is not None:
230
+ errors.extend(_verify_tree(agent_root, files, f"selected-agent skill {name}"))
231
+ return errors
232
+ except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as error:
233
+ return [f"manifest verification failed: {error}"]
234
+
235
+
236
+ def main() -> int:
237
+ parser = argparse.ArgumentParser(description=__doc__)
238
+ parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST)
239
+ parser.add_argument("--project-root", type=Path, default=Path.cwd())
240
+ arguments = parser.parse_args()
241
+
242
+ errors = verify(arguments.manifest.resolve(), arguments.project_root.resolve())
243
+ if errors:
244
+ for error in errors:
245
+ print(error)
246
+ return 1
247
+ print("Pinned Appium source and installed skill copies verified.")
248
+ return 0
249
+
250
+
251
+ if __name__ == "__main__":
252
+ raise SystemExit(main())
@@ -0,0 +1,328 @@
1
+ """Strict structured-output coercion and complete raster validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import json
7
+ import mmap
8
+ import struct
9
+ import warnings
10
+ import zlib
11
+ from dataclasses import dataclass
12
+ from typing import BinaryIO, Final, Literal, cast
13
+
14
+ from PIL import Image
15
+ from PIL.Image import DecompressionBombError
16
+
17
+ from amkernel.protocol import (
18
+ IMAGE_MIME_TYPES,
19
+ MAX_IMAGE_DIMENSION,
20
+ MAX_IMAGE_FRAMES,
21
+ MAX_IMAGE_PIXEL_FRAMES,
22
+ SAFE_MIME_TYPES,
23
+ JsonValue,
24
+ ProtocolViolation,
25
+ snapshot_json,
26
+ )
27
+
28
+ ImageMime = Literal["image/png", "image/jpeg", "image/gif", "image/webp"]
29
+ ImageData = bytes | mmap.mmap
30
+ _IMAGE_FORMATS: Final[dict[str, ImageMime]] = {
31
+ "PNG": "image/png",
32
+ "JPEG": "image/jpeg",
33
+ "GIF": "image/gif",
34
+ "WEBP": "image/webp",
35
+ }
36
+
37
+
38
+ class StructuredOutputError(ValueError):
39
+ """A user structured-output value violates the public helper contract."""
40
+
41
+
42
+ class DisplayError(StructuredOutputError):
43
+ """A display value is invalid or exceeds a display bound."""
44
+
45
+
46
+ class ArtifactError(StructuredOutputError):
47
+ """An explicit artifact value is invalid or exceeds an artifact bound."""
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class ImageInfo:
52
+ width: int
53
+ height: int
54
+ frames: int
55
+ pixel_frames: int
56
+
57
+
58
+ class _MmapImageReader:
59
+ """Expose one mmap with ordinary file seek semantics without copying it whole."""
60
+
61
+ __slots__ = ("_data", "_position")
62
+
63
+ def __init__(self, data: mmap.mmap) -> None:
64
+ self._data = data
65
+ self._position = 0
66
+
67
+ def read(self, size: int = -1) -> bytes:
68
+ if self._position >= len(self._data):
69
+ return b""
70
+ end = len(self._data) if size < 0 else min(len(self._data), self._position + size)
71
+ value = self._data[self._position : end]
72
+ self._position = end
73
+ return value
74
+
75
+ def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
76
+ if whence == io.SEEK_SET:
77
+ position = offset
78
+ elif whence == io.SEEK_CUR:
79
+ position = self._position + offset
80
+ elif whence == io.SEEK_END:
81
+ position = len(self._data) + offset
82
+ else:
83
+ raise ValueError("unsupported seek origin")
84
+ if position < 0:
85
+ raise ValueError("negative seek position")
86
+ self._position = position
87
+ return position
88
+
89
+ def tell(self) -> int:
90
+ return self._position
91
+
92
+
93
+ def strict_json(value: object, maximum_bytes: int) -> JsonValue:
94
+ """Detach a strict JSON value and translate protocol failures for user code."""
95
+ try:
96
+ return snapshot_json(cast(JsonValue, value), maximum_bytes)
97
+ except ProtocolViolation as error:
98
+ raise StructuredOutputError(str(error)) from error
99
+
100
+
101
+ def bytes_like(value: object) -> bytes:
102
+ """Copy one ordinary contiguous bytes-like value."""
103
+ if isinstance(value, bytes):
104
+ return value
105
+ if isinstance(value, (bytearray, memoryview)):
106
+ try:
107
+ return bytes(value)
108
+ except (TypeError, ValueError) as error:
109
+ raise StructuredOutputError("content must be a contiguous bytes-like value") from error
110
+ raise StructuredOutputError("content must be bytes-like")
111
+
112
+
113
+ def _reject_json_constant(value: str) -> None:
114
+ raise ValueError(f"invalid JSON constant: {value}")
115
+
116
+
117
+ def validate_safe_artifact(data: ImageData, mime_type: str) -> ImageInfo | None:
118
+ """Validate content before advertising one allowlisted serving MIME."""
119
+ if mime_type not in SAFE_MIME_TYPES:
120
+ return None
121
+ if mime_type in IMAGE_MIME_TYPES:
122
+ return validate_image(data, mime_type)
123
+ raw = data[:] if isinstance(data, mmap.mmap) else data
124
+ try:
125
+ text = raw.decode("utf-8", errors="strict")
126
+ if mime_type == "application/json":
127
+ value = json.loads(text, parse_constant=_reject_json_constant)
128
+ snapshot_json(cast(JsonValue, value), len(raw))
129
+ except (UnicodeError, ValueError, RecursionError, ProtocolViolation) as error:
130
+ raise StructuredOutputError("artifact content does not match its safe MIME type") from error
131
+ return None
132
+
133
+
134
+ def validate_image(data: ImageData, claimed_mime: str) -> ImageInfo:
135
+ """Prove exact container extent and fully decode every bounded raster frame."""
136
+ if claimed_mime not in IMAGE_MIME_TYPES:
137
+ raise StructuredOutputError("unsupported image MIME type")
138
+ try:
139
+ _validate_container(data, cast(ImageMime, claimed_mime))
140
+ with warnings.catch_warnings():
141
+ warnings.simplefilter("error")
142
+ source = (
143
+ cast(BinaryIO, _MmapImageReader(data))
144
+ if isinstance(data, mmap.mmap)
145
+ else io.BytesIO(data)
146
+ )
147
+ with Image.open(source) as image:
148
+ observed_mime = _IMAGE_FORMATS.get(image.format or "")
149
+ if observed_mime != claimed_mime:
150
+ raise StructuredOutputError("image MIME type does not match the decoded format")
151
+ frames = 0
152
+ pixel_frames = 0
153
+ first_width = 0
154
+ first_height = 0
155
+ while True:
156
+ try:
157
+ image.seek(frames)
158
+ except EOFError:
159
+ break
160
+ width, height = image.size
161
+ if not (
162
+ 1 <= width <= MAX_IMAGE_DIMENSION and 1 <= height <= MAX_IMAGE_DIMENSION
163
+ ):
164
+ raise StructuredOutputError("image dimensions exceed the supported bound")
165
+ if frames == 0:
166
+ first_width, first_height = width, height
167
+ frames += 1
168
+ if frames > MAX_IMAGE_FRAMES:
169
+ raise StructuredOutputError("image frame count exceeds the supported bound")
170
+ pixel_frames += width * height
171
+ if pixel_frames > MAX_IMAGE_PIXEL_FRAMES:
172
+ raise StructuredOutputError(
173
+ "image cumulative pixels exceed the supported bound"
174
+ )
175
+ image.load()
176
+ if frames == 0:
177
+ raise StructuredOutputError("image contains no decodable frame")
178
+ return ImageInfo(first_width, first_height, frames, pixel_frames)
179
+ except StructuredOutputError:
180
+ raise
181
+ except (OSError, ValueError, SyntaxError, Warning, DecompressionBombError) as error:
182
+ raise StructuredOutputError("image validation failed") from error
183
+
184
+
185
+ def _validate_container(data: ImageData, mime: ImageMime) -> None:
186
+ if mime == "image/png":
187
+ _validate_png(data)
188
+ elif mime == "image/jpeg":
189
+ _validate_jpeg(data)
190
+ elif mime == "image/gif":
191
+ _validate_gif(data)
192
+ else:
193
+ _validate_webp(data)
194
+
195
+
196
+ def _validate_png(data: ImageData) -> None:
197
+ if data[:8] != b"\x89PNG\r\n\x1a\n":
198
+ raise StructuredOutputError("invalid PNG container")
199
+ offset = 8
200
+ first = True
201
+ while offset + 12 <= len(data):
202
+ length = int.from_bytes(data[offset : offset + 4], "big")
203
+ kind = data[offset + 4 : offset + 8]
204
+ end = offset + 12 + length
205
+ if end > len(data):
206
+ raise StructuredOutputError("truncated PNG container")
207
+ expected_crc = int.from_bytes(data[offset + 8 + length : end], "big")
208
+ observed_crc = zlib.crc32(data[offset + 4 : offset + 8 + length]) & 0xFFFFFFFF
209
+ if observed_crc != expected_crc:
210
+ raise StructuredOutputError("PNG chunk checksum mismatch")
211
+ if first and kind != b"IHDR":
212
+ raise StructuredOutputError("PNG IHDR is not first")
213
+ first = False
214
+ offset = end
215
+ if kind == b"IEND":
216
+ if length != 0 or offset != len(data):
217
+ raise StructuredOutputError("PNG does not end exactly at IEND")
218
+ return
219
+ raise StructuredOutputError("PNG has no terminal IEND")
220
+
221
+
222
+ def _validate_jpeg(data: ImageData) -> None:
223
+ if len(data) < 4 or data[:2] != b"\xff\xd8":
224
+ raise StructuredOutputError("invalid JPEG container")
225
+ offset = 2
226
+ in_scan = False
227
+ while offset < len(data):
228
+ if not in_scan:
229
+ if data[offset] != 0xFF:
230
+ raise StructuredOutputError("invalid JPEG marker stream")
231
+ while offset < len(data) and data[offset] == 0xFF:
232
+ offset += 1
233
+ if offset >= len(data):
234
+ break
235
+ marker = data[offset]
236
+ offset += 1
237
+ else:
238
+ marker_start = data.find(b"\xff", offset)
239
+ if marker_start < 0:
240
+ break
241
+ offset = marker_start + 1
242
+ while offset < len(data) and data[offset] == 0xFF:
243
+ offset += 1
244
+ if offset >= len(data):
245
+ break
246
+ marker = data[offset]
247
+ offset += 1
248
+ if marker == 0x00 or 0xD0 <= marker <= 0xD7:
249
+ continue
250
+ in_scan = False
251
+ if marker == 0xD9:
252
+ if offset != len(data):
253
+ raise StructuredOutputError("JPEG does not end exactly at EOI")
254
+ return
255
+ if marker == 0xD8 or 0xD0 <= marker <= 0xD7 or marker == 0x01:
256
+ continue
257
+ if offset + 2 > len(data):
258
+ break
259
+ length = int.from_bytes(data[offset : offset + 2], "big")
260
+ if length < 2 or offset + length > len(data):
261
+ raise StructuredOutputError("truncated JPEG segment")
262
+ offset += length
263
+ if marker == 0xDA:
264
+ in_scan = True
265
+ raise StructuredOutputError("JPEG has no terminal EOI")
266
+
267
+
268
+ def _validate_gif(data: ImageData) -> None:
269
+ if len(data) < 13 or data[:6] not in {b"GIF87a", b"GIF89a"}:
270
+ raise StructuredOutputError("invalid GIF container")
271
+ packed = data[10]
272
+ offset = 13 + (3 * (2 << (packed & 7)) if packed & 0x80 else 0)
273
+ while offset < len(data):
274
+ introducer = data[offset]
275
+ offset += 1
276
+ if introducer == 0x3B:
277
+ if offset != len(data):
278
+ raise StructuredOutputError("GIF does not end exactly at trailer")
279
+ return
280
+ if introducer == 0x21:
281
+ if offset >= len(data):
282
+ break
283
+ offset += 1
284
+ offset = _skip_subblocks(data, offset)
285
+ elif introducer == 0x2C:
286
+ if offset + 9 > len(data):
287
+ break
288
+ local_packed = data[offset + 8]
289
+ offset += 9
290
+ if local_packed & 0x80:
291
+ offset += 3 * (2 << (local_packed & 7))
292
+ if offset >= len(data):
293
+ break
294
+ offset += 1
295
+ offset = _skip_subblocks(data, offset)
296
+ else:
297
+ raise StructuredOutputError("invalid GIF block")
298
+ raise StructuredOutputError("GIF has no terminal trailer")
299
+
300
+
301
+ def _skip_subblocks(data: ImageData, offset: int) -> int:
302
+ while offset < len(data):
303
+ size = data[offset]
304
+ offset += 1
305
+ if size == 0:
306
+ return offset
307
+ offset += size
308
+ if offset > len(data):
309
+ break
310
+ raise StructuredOutputError("truncated GIF sub-block")
311
+
312
+
313
+ def _validate_webp(data: ImageData) -> None:
314
+ if len(data) < 12 or data[:4] != b"RIFF" or data[8:12] != b"WEBP":
315
+ raise StructuredOutputError("invalid WebP container")
316
+ declared = struct.unpack_from("<I", data, 4)[0]
317
+ if declared + 8 != len(data):
318
+ raise StructuredOutputError("WebP RIFF length does not end exactly at EOF")
319
+ offset = 12
320
+ while offset < len(data):
321
+ if offset + 8 > len(data):
322
+ raise StructuredOutputError("truncated WebP chunk")
323
+ size = struct.unpack_from("<I", data, offset + 4)[0]
324
+ offset += 8 + size + (size & 1)
325
+ if offset > len(data):
326
+ raise StructuredOutputError("truncated WebP chunk")
327
+ if offset != len(data):
328
+ raise StructuredOutputError("invalid WebP chunk alignment")