immich-api 0.2.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.
Files changed (63) hide show
  1. immich_api/__init__.py +54 -0
  2. immich_api/__main__.py +4 -0
  3. immich_api/album_metadata.py +119 -0
  4. immich_api/api/__init__.py +3 -0
  5. immich_api/api/api_client_system.py +7 -0
  6. immich_api/auth.py +94 -0
  7. immich_api/connector_manifest.yml +212 -0
  8. immich_api/connectors/__init__.py +0 -0
  9. immich_api/connectors/mcp_source_presets.json +3 -0
  10. immich_api/connectors/tool_schema_fingerprints.json +1 -0
  11. immich_api/error_authority.py +248 -0
  12. immich_api/error_rendering.py +105 -0
  13. immich_api/immich_input_models.py +12 -0
  14. immich_api/immich_response_models.py +15 -0
  15. immich_api/kg_ingest.py +75 -0
  16. immich_api/mcp/__init__.py +21 -0
  17. immich_api/mcp/album_sample.py +38 -0
  18. immich_api/mcp_config.json +3 -0
  19. immich_api/mcp_server.py +50 -0
  20. immich_api/ontology/__init__.py +6 -0
  21. immich_api/ontology/immich.ttl +119 -0
  22. immich_api/ontology/schema-provenance.json +483 -0
  23. immich_api/ontology/shapes/immich.ttl +69 -0
  24. immich_api/openapi_auth_scheme.py +77 -0
  25. immich_api/openapi_contract.json +80122 -0
  26. immich_api/openapi_execution.py +30 -0
  27. immich_api/openapi_inventory.json +16154 -0
  28. immich_api/openapi_request.py +119 -0
  29. immich_api/openapi_response.py +76 -0
  30. immich_api/openapi_runtime.py +203 -0
  31. immich_api/parity/__init__.py +17 -0
  32. immich_api/parity/check_openapi_parity.py +173 -0
  33. immich_api/parity/openapi_discovery.py +95 -0
  34. immich_api/parity/openapi_parity.py +239 -0
  35. immich_api/parity/openapi_provider_evidence.py +143 -0
  36. immich_api/parity/openapi_provider_overlay.py +239 -0
  37. immich_api/parity/openapi_provider_responses.py +132 -0
  38. immich_api/parity/openapi_schema.py +206 -0
  39. immich_api/parity/openapi_schema_budget.py +104 -0
  40. immich_api/parity/openapi_schema_inputs.py +89 -0
  41. immich_api/parity/openapi_schema_types.py +163 -0
  42. immich_api/parity/openapi_security.py +157 -0
  43. immich_api/parity/openapi_source_parity.py +98 -0
  44. immich_api/parity/openapi_transport_parity.py +169 -0
  45. immich_api/parity/unicode_patterns.py +105 -0
  46. immich_api/parity/unicode_properties.json +4719 -0
  47. immich_api/prompts/__init__.py +0 -0
  48. immich_api/prompts/immich_album_review.json +18 -0
  49. immich_api/prompts/immich_asset_metadata.json +18 -0
  50. immich_api/prompts/immich_person_record.json +18 -0
  51. immich_api/prompts/immich_specialist.json +17 -0
  52. immich_api/prompts/main_agent.json +24 -0
  53. immich_api/skills/__init__.py +0 -0
  54. immich_api/skills/immich-album-review/SKILL.md +17 -0
  55. immich_api/skills/immich-asset-metadata/SKILL.md +17 -0
  56. immich_api/skills/immich-health/SKILL.md +17 -0
  57. immich_api/skills/immich-person-record/SKILL.md +17 -0
  58. immich_api-0.2.0.dist-info/METADATA +242 -0
  59. immich_api-0.2.0.dist-info/RECORD +63 -0
  60. immich_api-0.2.0.dist-info/WHEEL +5 -0
  61. immich_api-0.2.0.dist-info/entry_points.txt +14 -0
  62. immich_api-0.2.0.dist-info/licenses/LICENSE +21 -0
  63. immich_api-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,239 @@
1
+ """Complete source inventories and API/MCP contract parity, without provider calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import hashlib
7
+ import json
8
+
9
+ if __package__:
10
+ from .openapi_provider_overlay import apply_provider_overlay
11
+ from .openapi_schema import compile_contract, domain_schemas
12
+ from .openapi_security import validate_requirements, validate_security
13
+ from .openapi_source_parity import source_route, verify_source_routes
14
+ else:
15
+ from openapi_provider_overlay import apply_provider_overlay
16
+ from openapi_schema import compile_contract, domain_schemas
17
+ from openapi_security import validate_requirements, validate_security
18
+ from openapi_source_parity import source_route, verify_source_routes
19
+
20
+ HTTP_METHODS = frozenset(
21
+ {"get", "put", "post", "delete", "options", "head", "patch", "trace"}
22
+ )
23
+
24
+
25
+ def digest(value):
26
+ return hashlib.sha256(
27
+ json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
28
+ ).hexdigest()
29
+
30
+
31
+ def _operation_entry(document, path, *, method, operation):
32
+ return {
33
+ **source_route(document, path, operation),
34
+ "method": method.upper(),
35
+ "source_path": path,
36
+ "security": operation.get("security", document.get("security", [])),
37
+ "security_declaration": (
38
+ "operation"
39
+ if "security" in operation
40
+ else "root"
41
+ if "security" in document
42
+ else "unknown"
43
+ ),
44
+ "extensions": {
45
+ key: value for key, value in operation.items() if key.startswith("x-")
46
+ },
47
+ "deprecated": operation.get("deprecated", False),
48
+ "tags": operation.get("tags", []),
49
+ "operation_sha256": digest(operation),
50
+ }
51
+
52
+
53
+ def operation_inventory(document, source_sha256):
54
+ """Enumerate the entire pinned source, independently of compiler filtering."""
55
+ document = copy.deepcopy(document)
56
+ schemes = validate_security(document)
57
+ operations = {}
58
+ for path, item in sorted(document["paths"].items()):
59
+ for method, operation in sorted(item.items()):
60
+ if method not in HTTP_METHODS:
61
+ continue
62
+ action = operation["operationId"]
63
+ if action in operations:
64
+ raise ValueError(f"Duplicate source operation: {action}")
65
+ operations[action] = _operation_entry(
66
+ document, path, method=method, operation=operation
67
+ )
68
+ operations[action]["path_parameters_sha256"] = digest(
69
+ item.get("parameters", [])
70
+ )
71
+ if not operations:
72
+ raise ValueError("Source declares no HTTP operations")
73
+ return {
74
+ "version": 1,
75
+ "source_sha256": source_sha256,
76
+ "source_info": document.get("info", {}),
77
+ "openapi_version": document["openapi"],
78
+ "security_schemes": schemes,
79
+ "path_count": len(document["paths"]),
80
+ "operation_count": len(operations),
81
+ "operations": dict(sorted(operations.items())),
82
+ "scope": "All operations in this pinned OpenAPI source; upstream suite completeness requires separate evidence.",
83
+ "live_verified": False,
84
+ }
85
+
86
+
87
+ def require_declared_authorization(inventory):
88
+ """Schema omission is not proof that an operation is public."""
89
+ for operation in inventory["operations"].values():
90
+ validate_requirements(
91
+ operation["security"],
92
+ inventory["security_schemes"],
93
+ openapi_version=inventory["openapi_version"],
94
+ )
95
+ classified = {
96
+ item["operation_id"]
97
+ for item in inventory.get("provider_overlay", {}).get(
98
+ "authorization_classifications", []
99
+ )
100
+ }
101
+ unknown = [
102
+ action
103
+ for action, operation in inventory["operations"].items()
104
+ if operation["security_declaration"] == "unknown" and action not in classified
105
+ ]
106
+ if unknown:
107
+ raise ValueError(
108
+ f"Authorization classification requires reviewed provider evidence: {unknown}"
109
+ )
110
+
111
+
112
+ def require_complete_coverage(contract, api_actions, mcp_actions):
113
+ """Call with successful probe IDs, not discovery IDs, for transport coverage."""
114
+ expected = set(contract["operations"])
115
+ for label, actions in (("API", list(api_actions)), ("MCP", list(mcp_actions))):
116
+ if len(actions) != len(set(actions)):
117
+ raise ValueError(f"Duplicate {label} coverage IDs")
118
+ missing, extra = (
119
+ sorted(expected - set(actions)),
120
+ sorted(set(actions) - expected),
121
+ )
122
+ if missing or extra:
123
+ raise ValueError(
124
+ f"Incomplete {label} coverage: missing={missing}, extra={extra}"
125
+ )
126
+ return {"api_operations": len(expected), "mcp_operations": len(expected)}
127
+
128
+
129
+ def source_context(
130
+ document,
131
+ source_sha256,
132
+ *,
133
+ source_bytes=None,
134
+ overlay_bytes=None,
135
+ expected_overlay_sha256=None,
136
+ evidence_root=None,
137
+ ):
138
+ inventory = operation_inventory(document, source_sha256)
139
+ if overlay_bytes is None:
140
+ if expected_overlay_sha256 is not None or evidence_root is not None:
141
+ raise ValueError("Provider evidence options require an exact overlay")
142
+ return document, inventory
143
+ if (
144
+ not isinstance(source_bytes, bytes)
145
+ or hashlib.sha256(source_bytes).hexdigest() != source_sha256
146
+ or json.loads(source_bytes) != document
147
+ ):
148
+ raise ValueError("Provider correction requires the exact raw source bytes")
149
+ effective, receipt = apply_provider_overlay(
150
+ source_bytes,
151
+ overlay_bytes,
152
+ expected_overlay_sha256=expected_overlay_sha256,
153
+ evidence_root=evidence_root,
154
+ )
155
+ inventory["provider_overlay"] = receipt
156
+ return effective, inventory
157
+
158
+
159
+ def verify_contract(document, contract, source_sha256, **provider_options):
160
+ """Reject source omissions and exact method, route, parameter or schema drift."""
161
+ effective, inventory = source_context(document, source_sha256, **provider_options)
162
+ # A compiler must not be able to mutate the independent source inventory.
163
+ expected = compile_contract(
164
+ copy.deepcopy(effective),
165
+ response_corrections=inventory.get("provider_overlay", {}).get(
166
+ "response_corrections", []
167
+ ),
168
+ )
169
+ if set(inventory["operations"]) != set(expected["operations"]):
170
+ raise ValueError("Compiler omitted or invented source operations")
171
+ expected["source_sha256"] = source_sha256
172
+ if "provider_overlay" in inventory:
173
+ expected["provider_overlay"] = inventory["provider_overlay"]
174
+ verify_source_routes(inventory, expected)
175
+ verify_source_routes(inventory, contract)
176
+ expected["schemas"] = {
177
+ domain: domain_schemas(expected, domain) for domain in expected["domains"]
178
+ }
179
+ # JSON round-trip accounts only for tuples in the compiler's return value.
180
+ if contract != json.loads(json.dumps(expected)):
181
+ raise ValueError("API contract differs from complete source compilation")
182
+ mapped = [action for actions in contract["domains"].values() for action in actions]
183
+ require_complete_coverage(contract, mapped, mapped)
184
+ for domain, actions in contract["domains"].items():
185
+ if any(
186
+ contract["operations"][action]["domain"] != domain for action in actions
187
+ ):
188
+ raise ValueError("Operation is unreachable through its declared domain")
189
+ return inventory
190
+
191
+
192
+ def verify_mcp(contract, tools):
193
+ """Compare actual native tools/list results, including every action branch."""
194
+ try:
195
+ from agent_connector_sdk.manifest.tool_schema import read_field
196
+ except ImportError as exc:
197
+ raise RuntimeError("MCP parity requires the reviewed SDK environment") from exc
198
+ names = [read_field(tool, "name") for tool in tools]
199
+ if len(names) != len(set(names)):
200
+ raise ValueError("Duplicate MCP tool names")
201
+ by_name = dict(zip(names, tools, strict=True))
202
+ covered = []
203
+ for domain, actions in contract["domains"].items():
204
+ name = f"{domain}_operations"
205
+ if name not in by_name:
206
+ raise ValueError(f"Unreachable MCP domain: {domain}")
207
+ _verify_tool_schema(by_name[name], contract["schemas"][domain], name)
208
+ covered.extend(actions)
209
+ require_complete_coverage(contract, contract["operations"], covered)
210
+ return {"domains": len(contract["domains"]), "exposed_operations": len(covered)}
211
+
212
+
213
+ def _verify_tool_schema(tool, schemas, name):
214
+ from agent_connector_sdk.manifest.tool_schema import (
215
+ canonical_input_schema,
216
+ canonical_output_schema,
217
+ )
218
+ from fastmcp.utilities.json_schema import dereference_refs
219
+ from jsonschema import Draft202012Validator
220
+ from jsonschema.exceptions import SchemaError
221
+
222
+ inputs, outputs = schemas
223
+ expected = {"inputSchema": inputs, "outputSchema": outputs}
224
+ # Use FastMCP's actual bounded serve-time transformation. It preserves
225
+ # reference siblings and retains references for recursive/large graphs.
226
+ expanded = {
227
+ "inputSchema": dereference_refs(inputs),
228
+ "outputSchema": dereference_refs(outputs),
229
+ }
230
+ for canonical in (canonical_input_schema, canonical_output_schema):
231
+ actual = canonical(tool)
232
+ try:
233
+ Draft202012Validator.check_schema(actual)
234
+ except SchemaError as exc:
235
+ raise ValueError(
236
+ f"Native MCP schema is unsupported by client validation: {name}"
237
+ ) from exc
238
+ if actual != canonical(expected) and actual != canonical(expanded):
239
+ raise ValueError(f"MCP schema differs from API contract: {name}")
@@ -0,0 +1,143 @@
1
+ """Verify local provider evidence without following paths outside its capture root."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import stat
10
+ from contextlib import ExitStack
11
+ from pathlib import Path
12
+
13
+ _EVIDENCE = {"path": str, "sha256": str}
14
+ _EVIDENCE_OPTIONAL = {"git_blob": str, "source_commit": str}
15
+ _MAX_EVIDENCE_BYTES = 16 * 1024 * 1024
16
+
17
+
18
+ def _pairs(pairs):
19
+ result = {}
20
+ for key, value in pairs:
21
+ if key in result:
22
+ raise ValueError("Duplicate JSON key")
23
+ result[key] = value
24
+ return result
25
+
26
+
27
+ def _hash(value):
28
+ data = (
29
+ value
30
+ if isinstance(value, bytes)
31
+ else json.dumps(
32
+ value,
33
+ sort_keys=True,
34
+ separators=(",", ":"),
35
+ ensure_ascii=True,
36
+ allow_nan=False,
37
+ ).encode()
38
+ )
39
+ return hashlib.sha256(data).hexdigest()
40
+
41
+
42
+ def _fields(value, required, optional=None):
43
+ rules = required | (optional or {})
44
+ if (
45
+ not isinstance(value, dict)
46
+ or not required.keys() <= value.keys()
47
+ or value.keys() - rules.keys()
48
+ ):
49
+ raise ValueError("Unsupported or missing overlay fields")
50
+ for name, item in value.items():
51
+ if type(item) is not rules[name] or (
52
+ isinstance(item, str) and not item.strip()
53
+ ):
54
+ raise ValueError("Invalid overlay field type or empty string")
55
+
56
+
57
+ def _read_file(root, relative):
58
+ try:
59
+ return _safe_read(root, relative)
60
+ except OSError as exc:
61
+ raise ValueError("Evidence file is missing, unsafe, or unreadable") from exc
62
+
63
+
64
+ def _safe_read(root, relative):
65
+ if os.name != "posix":
66
+ raise ValueError(
67
+ "Secure evidence verification requires POSIX descriptor operations"
68
+ )
69
+ parts = relative.split("/")
70
+ if "\\" in relative or any(part in {"", ".", ".."} for part in parts):
71
+ raise ValueError("Evidence path must be a contained relative path")
72
+ parts = [*Path(root).absolute().parts[1:], *parts]
73
+ with ExitStack() as stack:
74
+ directory = os.open("/", os.O_RDONLY | os.O_DIRECTORY)
75
+ stack.callback(os.close, directory)
76
+ for part in parts[:-1]:
77
+ directory = os.open(
78
+ part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory
79
+ )
80
+ stack.callback(os.close, directory)
81
+ fd = os.open(
82
+ parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory
83
+ )
84
+ stream = stack.enter_context(os.fdopen(fd, "rb"))
85
+ if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
86
+ raise ValueError("Evidence must be a regular non-symlink file")
87
+ data = stream.read(_MAX_EVIDENCE_BYTES + 1)
88
+ if data[_MAX_EVIDENCE_BYTES:]:
89
+ raise ValueError("Evidence file exceeds the size limit")
90
+ return data
91
+
92
+
93
+ def _evidence(records, root):
94
+ verified = {}
95
+ for record in records:
96
+ _fields(record, _EVIDENCE, _EVIDENCE_OPTIONAL)
97
+ path = record["path"]
98
+ if path in verified:
99
+ raise ValueError("Duplicate evidence path")
100
+ data = _read_file(root, path)
101
+ if _hash(data) != record["sha256"]:
102
+ raise ValueError("Evidence SHA256 mismatch")
103
+ if "source_commit" in record and not re.fullmatch(
104
+ r"[0-9a-f]{40}(?:[0-9a-f]{24})?", record["source_commit"]
105
+ ):
106
+ raise ValueError("Evidence source commit must be an immutable Git ID")
107
+ blob = b"blob " + str(len(data)).encode() + b"\0" + data
108
+ blobs = {hashlib.sha1(blob, usedforsecurity=False).hexdigest(), _hash(blob)}
109
+ if "git_blob" in record and record["git_blob"] not in blobs:
110
+ raise ValueError("Evidence Git blob mismatch")
111
+ verified[path] = (record, len(data.splitlines()), blobs)
112
+ return verified
113
+
114
+
115
+ def _reference(reference, evidence):
116
+ _fields(
117
+ reference,
118
+ _EVIDENCE,
119
+ _EVIDENCE_OPTIONAL | {"line_start": int, "line_end": int, "url": str},
120
+ )
121
+ record, lines, blobs = evidence.get(reference["path"], ({}, 0, set()))
122
+ if reference["sha256"] != record.get("sha256"):
123
+ raise ValueError("Missing or stale authorization evidence")
124
+ if ("git_blob" in reference and reference["git_blob"] not in blobs) or (
125
+ "source_commit" in reference
126
+ and reference["source_commit"] != record.get("source_commit")
127
+ ):
128
+ raise ValueError("Authorization evidence Git identity mismatch")
129
+ start, end = reference.get("line_start", 1), reference.get("line_end", lines)
130
+ if not 1 <= start <= end <= lines:
131
+ raise ValueError("Authorization evidence line range is invalid")
132
+ return _hash(reference)
133
+
134
+
135
+ def _references(references, evidence):
136
+ if not references:
137
+ raise ValueError("Authorization classification requires provider evidence")
138
+ seen = set()
139
+ for reference in references:
140
+ digest = _reference(reference, evidence)
141
+ if digest in seen:
142
+ raise ValueError("Duplicate authorization evidence")
143
+ seen.add(digest)
@@ -0,0 +1,239 @@
1
+ """Apply reviewed additive schema corrections; retain auth evidence as a sidecar.
2
+
3
+ Version 1 supports named component object schemas and explicit public decorators.
4
+ Version 2 additionally permits a reviewed GET empty-200 to header-data 307 correction.
5
+ It cannot repair routes, remove constraints, create credentials, or grant readiness.
6
+ Component/operation hashes use compact sorted JSON; the effective document uses
7
+ sorted, indented UTF-8 JSON with a final newline. Both use ensure_ascii=True.
8
+ Evidence verification requires POSIX descriptor operations and fails closed elsewhere.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import copy
14
+ import json
15
+ import re
16
+
17
+ if __package__:
18
+ from .openapi_provider_evidence import (
19
+ _evidence,
20
+ _fields,
21
+ _hash,
22
+ _pairs,
23
+ _references,
24
+ )
25
+ from .openapi_provider_responses import apply_response_corrections
26
+ from .openapi_security import validate_security
27
+ else:
28
+ from openapi_provider_evidence import _evidence, _fields, _hash, _pairs, _references
29
+ from openapi_provider_responses import apply_response_corrections
30
+ from openapi_security import validate_security
31
+
32
+ _METHODS = {"get", "put", "post", "delete", "options", "head", "patch", "trace"}
33
+ _BINDING = dict.fromkeys(
34
+ ("operation_id", "method", "path", "source_operation_sha256"), str
35
+ )
36
+ _CORRECTION = dict.fromkeys(
37
+ (
38
+ "op",
39
+ "schema_pointer",
40
+ "property",
41
+ "before_sha256",
42
+ "after_sha256",
43
+ "source_commit",
44
+ "source_file",
45
+ "source_file_sha256",
46
+ ),
47
+ str,
48
+ )
49
+
50
+
51
+ def _target(document, correction):
52
+ _fields(correction, _CORRECTION)
53
+ pointer = correction["schema_pointer"]
54
+ if correction["op"] != "require-property" or not re.fullmatch(
55
+ r"/components/schemas/(?:[^~/]|~[01])+", pointer
56
+ ):
57
+ raise ValueError(
58
+ "Only named component require-property corrections are supported"
59
+ )
60
+ name = pointer.removeprefix("/components/schemas/")
61
+ name = name.replace("~1", "/").replace("~0", "~")
62
+ schemas = document.get("components", {}).get("schemas", {})
63
+ if not isinstance(schemas, dict):
64
+ raise ValueError("Schema components must be an object")
65
+ target = schemas.get(name)
66
+ if (
67
+ not isinstance(target, dict)
68
+ or target.get("type", "object") != "object"
69
+ or "$ref" in target
70
+ or not isinstance(target.get("properties"), dict)
71
+ ):
72
+ raise ValueError("Correction must target an existing object schema")
73
+ return target
74
+
75
+
76
+ def _require_property(document, correction, evidence):
77
+ target = _target(document, correction)
78
+ prop, required = correction["property"], target.get("required", [])
79
+ if not isinstance(required, list) or any(
80
+ not isinstance(item, str) for item in required
81
+ ):
82
+ raise ValueError("Malformed source required array")
83
+ if (
84
+ prop not in target["properties"]
85
+ or prop in required
86
+ or len(required) != len(set(required))
87
+ ):
88
+ raise ValueError(
89
+ "Correction must add one existing, previously optional property"
90
+ )
91
+ record = evidence.get(correction["source_file"], ({},))[0]
92
+ if (record.get("sha256"), record.get("source_commit")) != (
93
+ correction["source_file_sha256"],
94
+ correction["source_commit"],
95
+ ):
96
+ raise ValueError("Correction is not bound to verified provider evidence")
97
+ if _hash(target) != correction["before_sha256"]:
98
+ raise ValueError("Stale correction schema hash")
99
+ target["required"] = [*required, prop]
100
+ if _hash(target) != correction["after_sha256"]:
101
+ raise ValueError("Correction after schema hash mismatch")
102
+
103
+
104
+ def _operations(document):
105
+ result = {}
106
+ for path, item in document.get("paths", {}).items():
107
+ if "$ref" in item:
108
+ raise ValueError("Path references require an explicit adapter")
109
+ for method, operation in item.items():
110
+ if method not in _METHODS:
111
+ continue
112
+ action = operation.get("operationId")
113
+ if not isinstance(action, str) or not action or action in result:
114
+ raise ValueError("Missing or duplicate source operation ID")
115
+ result[action] = {
116
+ "operation_id": action,
117
+ "method": method.upper(),
118
+ "path": path,
119
+ "source_operation_sha256": _hash(operation),
120
+ "unknown": "security" not in operation and "security" not in document,
121
+ }
122
+ return result
123
+
124
+
125
+ def _classification(entry, operations, evidence):
126
+ _fields(
127
+ entry,
128
+ _BINDING
129
+ | {
130
+ "reviewed_guard_classification": str,
131
+ "precondition_category": str,
132
+ "preconditions": str,
133
+ "source_evidence": list,
134
+ },
135
+ )
136
+ source = operations.get(entry["operation_id"], {})
137
+ if not source.get("unknown") or any(entry[key] != source[key] for key in _BINDING):
138
+ raise ValueError(
139
+ "Authorization classification has a stale, extra, or already declared operation"
140
+ )
141
+ if entry[
142
+ "reviewed_guard_classification"
143
+ ] != "explicit-public-decorator" or not re.fullmatch(
144
+ r"[a-z][a-z0-9-]{0,63}", entry["precondition_category"]
145
+ ):
146
+ raise ValueError(
147
+ "Unsupported authorization classification or precondition category"
148
+ )
149
+ _references(entry["source_evidence"], evidence)
150
+
151
+
152
+ def _apply(document, overlay, evidence):
153
+ operations, corrections, classified = _operations(document), set(), set()
154
+ for correction in overlay["corrections"]:
155
+ _fields(correction, _CORRECTION)
156
+ key = (correction["schema_pointer"], correction["property"])
157
+ if key in corrections:
158
+ raise ValueError("Duplicate schema correction")
159
+ _require_property(document, correction, evidence)
160
+ corrections.add(key)
161
+ for entry in overlay["authorization_classifications"]:
162
+ _classification(entry, operations, evidence)
163
+ action = entry["operation_id"]
164
+ if action in classified:
165
+ raise ValueError("Duplicate authorization classification")
166
+ classified.add(action)
167
+ apply_response_corrections(
168
+ document,
169
+ overlay.get("response_corrections", []),
170
+ operations=operations,
171
+ evidence=evidence,
172
+ )
173
+ return sorted(
174
+ action
175
+ for action, operation in operations.items()
176
+ if operation["unknown"] and action not in classified
177
+ )
178
+
179
+
180
+ def _overlay_fields(overlay):
181
+ version = overlay.get("version") if isinstance(overlay, dict) else None
182
+ response_fields = {"response_corrections": list} if version == 2 else {}
183
+ _fields(
184
+ overlay,
185
+ {
186
+ "version": int,
187
+ "raw_source_sha256": str,
188
+ "effective_source_sha256": str,
189
+ "evidence_files": list,
190
+ "corrections": list,
191
+ "authorization_classifications": list,
192
+ }
193
+ | response_fields,
194
+ )
195
+ return overlay
196
+
197
+
198
+ def apply_provider_overlay(
199
+ raw_bytes, overlay_bytes, *, expected_overlay_sha256, evidence_root
200
+ ):
201
+ """Return ``(effective_document, receipt)``; a matching digest is not readiness.
202
+
203
+ The caller supplies the independently approved exact overlay digest. Evidence
204
+ files are read locally beneath evidence_root without following symlinks.
205
+ """
206
+ if (
207
+ not isinstance(raw_bytes, bytes)
208
+ or not isinstance(overlay_bytes, bytes)
209
+ or _hash(overlay_bytes) != expected_overlay_sha256
210
+ ):
211
+ raise ValueError("Expected reviewed overlay SHA256 mismatch or non-byte input")
212
+ document = json.loads(raw_bytes, object_pairs_hook=_pairs)
213
+ overlay = _overlay_fields(json.loads(overlay_bytes, object_pairs_hook=_pairs))
214
+ if (
215
+ overlay["version"] not in {1, 2}
216
+ or _hash(raw_bytes) != overlay["raw_source_sha256"]
217
+ ):
218
+ raise ValueError("Unsupported overlay version or raw source SHA256 mismatch")
219
+ validate_security(document)
220
+ effective = copy.deepcopy(document)
221
+ evidence = _evidence(overlay["evidence_files"], evidence_root)
222
+ unknown = _apply(effective, overlay, evidence)
223
+ encoded = (
224
+ json.dumps(
225
+ effective, sort_keys=True, indent=2, ensure_ascii=True, allow_nan=False
226
+ )
227
+ + "\n"
228
+ ).encode()
229
+ if _hash(encoded) != overlay["effective_source_sha256"]:
230
+ raise ValueError("Effective source SHA256 mismatch")
231
+ receipt = {
232
+ **overlay,
233
+ "overlay_sha256": _hash(overlay_bytes),
234
+ "remaining_unknown_operation_ids": unknown,
235
+ "ready": False,
236
+ "transport_parity_verified": False,
237
+ "live_verified": False,
238
+ }
239
+ return effective, receipt