plugin-scanner 3.4.2__py3-none-any.whl → 3.4.4__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 (34) hide show
  1. codex_plugin_scanner/guard/adapters/capability_report.py +476 -0
  2. codex_plugin_scanner/guard/adapters/cline_plugin.py +112 -50
  3. codex_plugin_scanner/guard/adapters/contract_models.py +220 -1
  4. codex_plugin_scanner/guard/adapters/contracts.py +525 -3
  5. codex_plugin_scanner/guard/daemon/hook_worker_responses.py +17 -1
  6. codex_plugin_scanner/guard/daemon/server.py +58 -3
  7. codex_plugin_scanner/guard/daemon/static/assets/chunks/about-workspace.js +1 -1
  8. codex_plugin_scanner/guard/daemon/static/assets/chunks/app-detail-workspace.js +1 -1
  9. codex_plugin_scanner/guard/daemon/static/assets/chunks/approval-proof-modal.js +1 -1
  10. codex_plugin_scanner/guard/daemon/static/assets/chunks/audit-workspace.js +113 -26
  11. codex_plugin_scanner/guard/daemon/static/assets/chunks/connect-guard-cloud-button.js +1 -1
  12. codex_plugin_scanner/guard/daemon/static/assets/chunks/extensions-workspace.js +1 -1
  13. codex_plugin_scanner/guard/daemon/static/assets/chunks/feed-health-workspace.js +1 -1
  14. codex_plugin_scanner/guard/daemon/static/assets/chunks/home-protection-module.js +1 -1
  15. codex_plugin_scanner/guard/daemon/static/assets/chunks/mcp-policy-request-panel.js +1 -1
  16. codex_plugin_scanner/guard/daemon/static/assets/chunks/policy-workspace-page.js +1 -1
  17. codex_plugin_scanner/guard/daemon/static/assets/chunks/settings-workspace.js +30 -12
  18. codex_plugin_scanner/guard/daemon/static/assets/chunks/supply-chain-hub-workspace.js +114 -13
  19. codex_plugin_scanner/guard/daemon/static/assets/chunks/supply-chain-workspace.js +1 -1
  20. codex_plugin_scanner/guard/daemon/static/assets/guard-dashboard.js +166 -80
  21. codex_plugin_scanner/guard/daemon/static/assets/index.css +10 -2
  22. codex_plugin_scanner/guard/evaluation_contracts.py +468 -0
  23. codex_plugin_scanner/guard/evaluation_evidence_package.py +218 -0
  24. codex_plugin_scanner/guard/evaluation_preflight.py +636 -0
  25. codex_plugin_scanner/guard/evaluation_witness.py +279 -0
  26. codex_plugin_scanner/guard/project_folder_picker.py +116 -0
  27. codex_plugin_scanner/guard/schemas/evaluation-profile-v1.schema.json +218 -0
  28. codex_plugin_scanner/guard/schemas/evaluation-result-v1.schema.json +205 -0
  29. codex_plugin_scanner/version.py +1 -1
  30. {plugin_scanner-3.4.2.dist-info → plugin_scanner-3.4.4.dist-info}/METADATA +7 -7
  31. {plugin_scanner-3.4.2.dist-info → plugin_scanner-3.4.4.dist-info}/RECORD +34 -26
  32. {plugin_scanner-3.4.2.dist-info → plugin_scanner-3.4.4.dist-info}/WHEEL +0 -0
  33. {plugin_scanner-3.4.2.dist-info → plugin_scanner-3.4.4.dist-info}/entry_points.txt +0 -0
  34. {plugin_scanner-3.4.2.dist-info → plugin_scanner-3.4.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,476 @@
1
+ """Versioned per-event harness capability reports.
2
+
3
+ The report is sourced only from :class:`HarnessProtectionContract` rows. It
4
+ does not probe a host, inspect a file, or convert a synthetic canary into a
5
+ live enforcement claim. Those observations can be supplied explicitly as
6
+ separate evidence fields and are rejected when incomplete or stale.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections.abc import Mapping, Sequence
13
+ from dataclasses import replace
14
+ from datetime import datetime, timezone
15
+ from html import escape as html_escape
16
+
17
+ from jsonschema import Draft202012Validator
18
+
19
+ from .contract_models import CAPABILITY_DECLARED_ACTIONS, HarnessCapabilityReport, HarnessEventCapability
20
+
21
+ CAPABILITY_REPORT_SCHEMA_VERSION = "harness-capability-report.v1"
22
+
23
+ CAPABILITY_REPORT_SCHEMA: dict[str, object] = {
24
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
25
+ "$id": "https://hol.org/schemas/guard/harness-capability-report.v1.schema.json",
26
+ "title": "HOL Guard harness capability report v1",
27
+ "type": "object",
28
+ "additionalProperties": False,
29
+ "required": ["schema_version", "build", "commit", "requested_host", "capabilities"],
30
+ "properties": {
31
+ "schema_version": {"const": CAPABILITY_REPORT_SCHEMA_VERSION},
32
+ "build": {"type": "string", "minLength": 1, "maxLength": 256},
33
+ "commit": {"type": "string", "minLength": 1, "maxLength": 256},
34
+ "requested_host": {"type": ["string", "null"], "maxLength": 256},
35
+ "capabilities": {
36
+ "type": "array",
37
+ "minItems": 1,
38
+ "items": {"$ref": "#/$defs/capability"},
39
+ },
40
+ },
41
+ "$defs": {
42
+ "capability": {
43
+ "type": "object",
44
+ "additionalProperties": False,
45
+ "required": [
46
+ "harness",
47
+ "adapter",
48
+ "host_version_scope",
49
+ "os_arch",
50
+ "local_hosted",
51
+ "event",
52
+ "transport",
53
+ "mode",
54
+ "declared_actions",
55
+ "error_behavior",
56
+ "mandatory_compatibility",
57
+ "known_blind_spots",
58
+ "source_reference",
59
+ "deployment_health",
60
+ "evidence_level",
61
+ "observed_at",
62
+ "expires_at",
63
+ "evidence_reference",
64
+ "evidence_build",
65
+ "evidence_host_version_scope",
66
+ "evidence_os_arch",
67
+ "denied_witness_reference",
68
+ "allowed_witness_reference",
69
+ "compatibility_verified",
70
+ ],
71
+ "properties": {
72
+ "harness": {"type": "string", "minLength": 1, "maxLength": 128},
73
+ "adapter": {"type": "string", "minLength": 1, "maxLength": 128},
74
+ "host_version_scope": {"type": "string", "minLength": 1, "maxLength": 256},
75
+ "os_arch": {"type": "string", "minLength": 1, "maxLength": 128},
76
+ "local_hosted": {"enum": ["local", "hosted", "unknown"]},
77
+ "event": {"type": "string", "minLength": 1, "maxLength": 128},
78
+ "transport": {"type": "string", "minLength": 1, "maxLength": 128},
79
+ "mode": {"type": "string", "minLength": 1, "maxLength": 128},
80
+ "declared_actions": {
81
+ "type": "array",
82
+ "minItems": 1,
83
+ "uniqueItems": True,
84
+ "items": {"enum": sorted(CAPABILITY_DECLARED_ACTIONS)},
85
+ },
86
+ "error_behavior": {"type": "string", "minLength": 1, "maxLength": 2_000},
87
+ "mandatory_compatibility": {"type": "string", "minLength": 1, "maxLength": 2_000},
88
+ "known_blind_spots": {
89
+ "type": "array",
90
+ "minItems": 1,
91
+ "uniqueItems": True,
92
+ "items": {"type": "string", "minLength": 1, "maxLength": 2_000},
93
+ },
94
+ "source_reference": {"type": "string", "minLength": 1, "maxLength": 512},
95
+ "deployment_health": {"enum": ["unverified", "healthy", "degraded", "unavailable", "unknown"]},
96
+ "evidence_level": {
97
+ "enum": [
98
+ "not_run",
99
+ "source_review",
100
+ "unit_test",
101
+ "synthetic_canary",
102
+ "live_block",
103
+ "independent_review",
104
+ ]
105
+ },
106
+ "observed_at": {"type": ["string", "null"], "maxLength": 64},
107
+ "expires_at": {"type": ["string", "null"], "maxLength": 64},
108
+ "evidence_reference": {"type": ["string", "null"], "maxLength": 512},
109
+ "evidence_build": {"type": ["string", "null"], "maxLength": 256},
110
+ "evidence_host_version_scope": {"type": ["string", "null"], "maxLength": 256},
111
+ "evidence_os_arch": {"type": ["string", "null"], "maxLength": 128},
112
+ "denied_witness_reference": {"type": ["string", "null"], "maxLength": 512},
113
+ "allowed_witness_reference": {"type": ["string", "null"], "maxLength": 512},
114
+ "compatibility_verified": {"type": "boolean"},
115
+ },
116
+ "allOf": [
117
+ {
118
+ "if": {"properties": {"evidence_level": {"const": "not_run"}}},
119
+ "then": {
120
+ "properties": {
121
+ "observed_at": {"type": "null"},
122
+ "expires_at": {"type": "null"},
123
+ "evidence_reference": {"type": "null"},
124
+ "evidence_build": {"type": "null"},
125
+ "evidence_host_version_scope": {"type": "null"},
126
+ "evidence_os_arch": {"type": "null"},
127
+ "denied_witness_reference": {"type": "null"},
128
+ "allowed_witness_reference": {"type": "null"},
129
+ "compatibility_verified": {"const": False},
130
+ }
131
+ },
132
+ },
133
+ {
134
+ "if": {
135
+ "properties": {
136
+ "evidence_level": {
137
+ "enum": [
138
+ "source_review",
139
+ "unit_test",
140
+ "synthetic_canary",
141
+ "live_block",
142
+ "independent_review",
143
+ ]
144
+ }
145
+ }
146
+ },
147
+ "then": {
148
+ "required": ["observed_at", "expires_at", "evidence_reference"],
149
+ "properties": {
150
+ "observed_at": {"type": "string", "minLength": 1},
151
+ "expires_at": {"type": "string", "minLength": 1},
152
+ "evidence_reference": {"type": "string", "minLength": 1},
153
+ },
154
+ },
155
+ },
156
+ ],
157
+ }
158
+ },
159
+ }
160
+
161
+
162
+ def _payload(value: HarnessCapabilityReport | Mapping[str, object]) -> dict[str, object]:
163
+ if isinstance(value, HarnessCapabilityReport):
164
+ return value.to_dict()
165
+ if isinstance(value, Mapping):
166
+ return dict(value)
167
+ raise TypeError("capability report must be a HarnessCapabilityReport or mapping")
168
+
169
+
170
+ def _parse_timestamp(value: object, *, field: str) -> datetime:
171
+ if not isinstance(value, str) or not value.strip():
172
+ raise ValueError(f"{field} must be an RFC3339 timestamp")
173
+ normalized = value.strip()
174
+ if normalized.endswith("Z"):
175
+ normalized = f"{normalized[:-1]}+00:00"
176
+ try:
177
+ parsed = datetime.fromisoformat(normalized)
178
+ except ValueError as exc:
179
+ raise ValueError(f"{field} must be an RFC3339 timestamp") from exc
180
+ if parsed.tzinfo is None:
181
+ raise ValueError(f"{field} must include a timezone")
182
+ return parsed.astimezone(timezone.utc)
183
+
184
+
185
+ def validate_capability_report(
186
+ report: HarnessCapabilityReport | Mapping[str, object],
187
+ *,
188
+ now: datetime | None = None,
189
+ ) -> dict[str, object]:
190
+ """Validate schema, evidence completeness, and evidence freshness.
191
+
192
+ ``synthetic_canary`` is a valid evidence level, but remains a distinct
193
+ value. It is never upgraded to ``live_block`` by this validator.
194
+ """
195
+
196
+ payload = _payload(report)
197
+ Draft202012Validator(CAPABILITY_REPORT_SCHEMA).validate(payload)
198
+ current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
199
+ capabilities = payload.get("capabilities")
200
+ if not isinstance(capabilities, list): # pragma: no cover - schema catches this
201
+ raise ValueError("capabilities must be a list")
202
+
203
+ def require_live_scope(index: int, capability: Mapping[str, object]) -> None:
204
+ actions = capability.get("declared_actions")
205
+ if (
206
+ not isinstance(actions, list)
207
+ or "block" not in actions
208
+ or capability.get("adapter") == "unsupported"
209
+ or capability.get("transport") == "none"
210
+ or capability.get("mode") == "unsupported"
211
+ or capability.get("local_hosted") == "unknown"
212
+ ):
213
+ raise ValueError(f"capabilities[{index}] live block proof requires a declared blocking boundary")
214
+ expected_scope = {
215
+ "evidence_build": payload["build"],
216
+ "evidence_host_version_scope": capability["host_version_scope"],
217
+ "evidence_os_arch": capability["os_arch"],
218
+ }
219
+ for field, expected in expected_scope.items():
220
+ if expected in {"unknown", ""}:
221
+ raise ValueError(f"capabilities[{index}] live proof requires a known declared {field} scope")
222
+ if capability.get(field) != expected:
223
+ raise ValueError(f"capabilities[{index}] live proof requires exact {field} matching the declared scope")
224
+ for field in ("denied_witness_reference", "allowed_witness_reference"):
225
+ value = capability.get(field)
226
+ if not isinstance(value, str) or not value.strip():
227
+ raise ValueError(f"capabilities[{index}] live proof requires {field}")
228
+
229
+ for index, capability in enumerate(capabilities):
230
+ if not isinstance(capability, Mapping): # pragma: no cover - schema catches this
231
+ continue
232
+ evidence_level = capability.get("evidence_level")
233
+ if evidence_level == "not_run":
234
+ if capability.get("deployment_health") == "healthy":
235
+ raise ValueError(f"capabilities[{index}] healthy deployment requires live_block evidence")
236
+ if capability.get("compatibility_verified") is True:
237
+ raise ValueError(f"capabilities[{index}] compatibility verification requires healthy live proof")
238
+ continue
239
+ observed_at = _parse_timestamp(capability.get("observed_at"), field=f"capabilities[{index}].observed_at")
240
+ expires_at = _parse_timestamp(capability.get("expires_at"), field=f"capabilities[{index}].expires_at")
241
+ if observed_at > current:
242
+ raise ValueError(f"capabilities[{index}] evidence observation is in the future")
243
+ if expires_at <= observed_at:
244
+ raise ValueError(f"capabilities[{index}] evidence expiry must be after observation")
245
+ if expires_at <= current:
246
+ raise ValueError(f"capabilities[{index}] evidence is stale")
247
+ if evidence_level == "live_block":
248
+ require_live_scope(index, capability)
249
+ if capability.get("deployment_health") == "unverified":
250
+ raise ValueError(f"capabilities[{index}] live_block evidence cannot have unverified deployment health")
251
+ if capability.get("deployment_health") == "healthy":
252
+ if evidence_level != "live_block":
253
+ raise ValueError(f"capabilities[{index}] healthy deployment requires live_block evidence")
254
+ require_live_scope(index, capability)
255
+ if capability.get("compatibility_verified") is not True:
256
+ raise ValueError(f"capabilities[{index}] healthy deployment requires verified compatibility")
257
+ if capability.get("compatibility_verified") is True and (
258
+ evidence_level != "live_block" or capability.get("deployment_health") != "healthy"
259
+ ):
260
+ raise ValueError(f"capabilities[{index}] compatibility verification requires healthy live proof")
261
+ return payload
262
+
263
+
264
+ def _bounded_host_label(host: str) -> str:
265
+ """Normalize an unknown host label before it enters report metadata."""
266
+
267
+ normalized = " ".join(host.strip().split())
268
+ return (normalized or "unknown-host")[:128]
269
+
270
+
271
+ def _unsupported_capability(host: str) -> HarnessEventCapability:
272
+ requested_input = _bounded_host_label(host)
273
+ requested = requested_input.lower()
274
+ return HarnessEventCapability(
275
+ harness=requested,
276
+ adapter="unsupported",
277
+ host_version_scope="unknown",
278
+ os_arch="unknown",
279
+ local_hosted="unknown",
280
+ event="unsupported",
281
+ transport="none",
282
+ mode="unsupported",
283
+ declared_actions=("unavailable",),
284
+ error_behavior="No adapter is registered, so Guard reports the host as unsupported and performs no hook claim.",
285
+ mandatory_compatibility=(
286
+ "A reviewed adapter and verified host event contract are required before support can be claimed."
287
+ ),
288
+ known_blind_spots=(
289
+ f"Requested host {requested_input!r} has no registered Guard adapter or declared event surface.",
290
+ ),
291
+ source_reference="src/codex_plugin_scanner/guard/adapters/__init__.py:_ADAPTER_SPECS",
292
+ )
293
+
294
+
295
+ def _apply_scope(
296
+ capabilities: Sequence[HarnessEventCapability],
297
+ *,
298
+ host_version_scope: str | None,
299
+ os_arch: str | None,
300
+ local_hosted: str | None,
301
+ ) -> tuple[HarnessEventCapability, ...]:
302
+ rows: list[HarnessEventCapability] = []
303
+ for capability in capabilities:
304
+ rows.append(
305
+ replace(
306
+ capability,
307
+ host_version_scope=host_version_scope or capability.host_version_scope,
308
+ os_arch=os_arch or capability.os_arch,
309
+ local_hosted=local_hosted or capability.local_hosted, # type: ignore[arg-type]
310
+ )
311
+ )
312
+ return tuple(rows)
313
+
314
+
315
+ def build_capability_report(
316
+ *,
317
+ build_id: str = "unknown",
318
+ commit: str = "unknown",
319
+ requested_host: str | None = None,
320
+ host_version_scope: str | None = None,
321
+ os_arch: str | None = None,
322
+ local_hosted: str | None = None,
323
+ ) -> HarnessCapabilityReport:
324
+ """Build a report from the registered contract authority.
325
+
326
+ Passing an unknown ``requested_host`` adds one explicit unsupported row;
327
+ it never mutates or extends the adapter registry.
328
+ """
329
+
330
+ if not isinstance(build_id, str) or not build_id.strip():
331
+ raise ValueError("build_id must be a non-empty string")
332
+ if not isinstance(commit, str) or not commit.strip():
333
+ raise ValueError("commit must be a non-empty string")
334
+ from .contracts import HARNESS_CONTRACTS, contract_for
335
+
336
+ requested_label = _bounded_host_label(requested_host) if requested_host is not None else None
337
+ if requested_label is None or requested_label.lower() in {"", "all", "*"}:
338
+ if any(value is not None for value in (host_version_scope, os_arch, local_hosted)):
339
+ raise ValueError("host scope overrides require one requested host")
340
+ capabilities = tuple(capability for contract in HARNESS_CONTRACTS for capability in contract.capability_events)
341
+ report_host = None
342
+ else:
343
+ contract = contract_for(requested_label.lower()) or contract_for(requested_label)
344
+ capabilities = (_unsupported_capability(requested_label),) if contract is None else contract.capability_events
345
+ report_host = requested_label
346
+ scoped = _apply_scope(
347
+ capabilities,
348
+ host_version_scope=host_version_scope,
349
+ os_arch=os_arch,
350
+ local_hosted=local_hosted,
351
+ )
352
+ report = HarnessCapabilityReport(
353
+ schema_version=CAPABILITY_REPORT_SCHEMA_VERSION,
354
+ build=build_id.strip(),
355
+ commit=commit.strip(),
356
+ requested_host=report_host,
357
+ capabilities=scoped,
358
+ )
359
+ validate_capability_report(report)
360
+ return report
361
+
362
+
363
+ def capability_report_for(
364
+ requested_host: str,
365
+ *,
366
+ build_id: str = "unknown",
367
+ commit: str = "unknown",
368
+ host_version_scope: str | None = None,
369
+ os_arch: str | None = None,
370
+ local_hosted: str | None = None,
371
+ ) -> HarnessCapabilityReport:
372
+ """Return one host report, including an explicit row for unknown hosts."""
373
+
374
+ return build_capability_report(
375
+ build_id=build_id,
376
+ commit=commit,
377
+ requested_host=requested_host,
378
+ host_version_scope=host_version_scope,
379
+ os_arch=os_arch,
380
+ local_hosted=local_hosted,
381
+ )
382
+
383
+
384
+ def render_capability_report_json(report: HarnessCapabilityReport | Mapping[str, object]) -> str:
385
+ """Render the validated report as deterministic machine JSON."""
386
+
387
+ payload = validate_capability_report(report)
388
+ return f"{json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(',', ':'))}\n"
389
+
390
+
391
+ def _markdown_cell(value: object) -> str:
392
+ if isinstance(value, (tuple, list)):
393
+ value = ", ".join(str(item) for item in value)
394
+ text = html_escape(str(value), quote=True).replace("`", "&#96;")
395
+ return text.replace("\\", "\\\\").replace("|", "\\|").replace("\r", " ").replace("\n", "<br>")
396
+
397
+
398
+ def render_capability_report_markdown(report: HarnessCapabilityReport | Mapping[str, object]) -> str:
399
+ """Render the same validated rows as deterministic Markdown."""
400
+
401
+ payload = validate_capability_report(report)
402
+ capabilities = payload["capabilities"]
403
+ assert isinstance(capabilities, list) # validated above
404
+ lines = [
405
+ "# Harness event capability report",
406
+ "",
407
+ f"- Schema: `{_markdown_cell(payload['schema_version'])}`",
408
+ f"- Build: `{_markdown_cell(payload['build'])}`",
409
+ f"- Commit: `{_markdown_cell(payload['commit'])}`",
410
+ f"- Requested host: `{_markdown_cell(payload['requested_host'] or 'all')}`",
411
+ "",
412
+ (
413
+ "Rows describe declared boundaries. `unverified` / `not_run` is the default proof state; "
414
+ "file presence and synthetic canaries do not establish a live block."
415
+ ),
416
+ "",
417
+ (
418
+ "| Harness | Adapter | Host/version scope | OS/arch | Local/hosted | Event | Transport | Mode | "
419
+ "Declared actions | Error behavior | Mandatory compatibility | Known blind spots | Source reference | "
420
+ "Deployment health | Evidence level | Observed at | Expires at | Evidence reference | Evidence build | "
421
+ "Evidence host/version scope | Evidence OS/arch | Denied witness | Allowed witness | "
422
+ "Compatibility verified |"
423
+ ),
424
+ ]
425
+ fields = (
426
+ "harness",
427
+ "adapter",
428
+ "host_version_scope",
429
+ "os_arch",
430
+ "local_hosted",
431
+ "event",
432
+ "transport",
433
+ "mode",
434
+ "declared_actions",
435
+ "error_behavior",
436
+ "mandatory_compatibility",
437
+ "known_blind_spots",
438
+ "source_reference",
439
+ "deployment_health",
440
+ "evidence_level",
441
+ "observed_at",
442
+ "expires_at",
443
+ "evidence_reference",
444
+ "evidence_build",
445
+ "evidence_host_version_scope",
446
+ "evidence_os_arch",
447
+ "denied_witness_reference",
448
+ "allowed_witness_reference",
449
+ "compatibility_verified",
450
+ )
451
+ lines.append("| " + " | ".join("---" for _ in fields) + " |")
452
+ for capability in capabilities:
453
+ if not isinstance(capability, Mapping): # pragma: no cover - schema catches this
454
+ continue
455
+ lines.append("| " + " | ".join(_markdown_cell(capability.get(field, "")) for field in fields) + " |")
456
+ return "\n".join(lines) + "\n"
457
+
458
+
459
+ # Naming aliases keep the report discoverable beside the legacy table helper.
460
+ render_harness_capability_report_json = render_capability_report_json
461
+ render_harness_capability_report_markdown = render_capability_report_markdown
462
+ harness_capability_report = build_capability_report
463
+
464
+
465
+ __all__ = [
466
+ "CAPABILITY_REPORT_SCHEMA",
467
+ "CAPABILITY_REPORT_SCHEMA_VERSION",
468
+ "build_capability_report",
469
+ "capability_report_for",
470
+ "harness_capability_report",
471
+ "render_capability_report_json",
472
+ "render_capability_report_markdown",
473
+ "render_harness_capability_report_json",
474
+ "render_harness_capability_report_markdown",
475
+ "validate_capability_report",
476
+ ]