@trac3er/oh-my-god 2.0.8 → 2.0.9

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 (118) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.gemini/settings.json +11 -0
  4. package/.kimi/mcp.json +11 -0
  5. package/CHANGELOG.md +10 -0
  6. package/OMG-setup.sh +1 -1
  7. package/OMG_COMPAT_CONTRACT.md +10 -4
  8. package/README.md +1 -0
  9. package/build/lib/commands/OMG:forge.md +92 -0
  10. package/build/lib/commands/OMG:mode.md +13 -13
  11. package/build/lib/commands/OMG:session-branch.md +17 -1
  12. package/build/lib/commands/OMG:session-fork.md +5 -1
  13. package/build/lib/commands/OMG:session-merge.md +5 -1
  14. package/build/lib/control_plane/server.py +4 -0
  15. package/build/lib/control_plane/service.py +55 -0
  16. package/build/lib/hooks/setup_wizard.py +21 -1
  17. package/build/lib/hooks/shadow_manager.py +25 -2
  18. package/build/lib/hooks/state_migration.py +3 -0
  19. package/build/lib/plugins/dephealth/cve_scanner.py +91 -0
  20. package/build/lib/plugins/dephealth/vuln_analyzer.py +7 -0
  21. package/build/lib/registry/omg-capability.schema.json +83 -1
  22. package/build/lib/runtime/adoption.py +12 -4
  23. package/build/lib/runtime/artifact_parsers.py +161 -0
  24. package/build/lib/runtime/background_verification.py +48 -0
  25. package/build/lib/runtime/claim_judge.py +184 -7
  26. package/build/lib/runtime/contract_compiler.py +118 -9
  27. package/build/lib/runtime/evidence_query.py +203 -0
  28. package/build/lib/runtime/omg_mcp_server.py +19 -0
  29. package/build/lib/runtime/playwright_adapter.py +39 -0
  30. package/build/lib/runtime/proof_chain.py +136 -8
  31. package/build/lib/runtime/proof_gate.py +102 -0
  32. package/build/lib/runtime/providers/gemini_provider.py +7 -0
  33. package/build/lib/runtime/providers/kimi_provider.py +7 -0
  34. package/build/lib/runtime/repro_pack.py +292 -0
  35. package/build/lib/runtime/runtime_profile.py +87 -15
  36. package/build/lib/runtime/security_check.py +86 -3
  37. package/build/lib/runtime/test_intent_lock.py +47 -0
  38. package/build/lib/runtime/tracebank.py +33 -3
  39. package/build/lib/runtime/verification_loop.py +73 -0
  40. package/commands/OMG:forge.md +92 -0
  41. package/commands/OMG:mode.md +13 -13
  42. package/commands/OMG:session-branch.md +17 -1
  43. package/commands/OMG:session-fork.md +5 -1
  44. package/commands/OMG:session-merge.md +5 -1
  45. package/control_plane/server.py +4 -0
  46. package/control_plane/service.py +55 -0
  47. package/dist/enterprise/bundle/.gemini/settings.json +11 -0
  48. package/dist/enterprise/bundle/.kimi/mcp.json +11 -0
  49. package/dist/enterprise/bundle/OMG_COMPAT_CONTRACT.md +9 -3
  50. package/dist/enterprise/bundle/registry/omg-capability.schema.json +83 -1
  51. package/dist/enterprise/bundle/settings.json +1 -0
  52. package/dist/enterprise/manifest.json +17 -3
  53. package/dist/public/bundle/.agents/skills/omg/incident-replay/SKILL.md +1 -1
  54. package/dist/public/bundle/.agents/skills/omg/incident-replay/openai.yaml +1 -1
  55. package/dist/public/bundle/.agents/skills/omg/lsp-pack/SKILL.md +1 -1
  56. package/dist/public/bundle/.agents/skills/omg/lsp-pack/openai.yaml +1 -1
  57. package/dist/public/bundle/.agents/skills/omg/mcp-fabric/SKILL.md +1 -1
  58. package/dist/public/bundle/.agents/skills/omg/mcp-fabric/openai.yaml +1 -1
  59. package/dist/public/bundle/.agents/skills/omg/plan-council/SKILL.md +1 -1
  60. package/dist/public/bundle/.agents/skills/omg/plan-council/openai.yaml +1 -1
  61. package/dist/public/bundle/.agents/skills/omg/preflight/SKILL.md +1 -1
  62. package/dist/public/bundle/.agents/skills/omg/preflight/openai.yaml +1 -1
  63. package/dist/public/bundle/.agents/skills/omg/proof-gate/SKILL.md +1 -1
  64. package/dist/public/bundle/.agents/skills/omg/proof-gate/openai.yaml +1 -1
  65. package/dist/public/bundle/.agents/skills/omg/remote-supervisor/SKILL.md +1 -1
  66. package/dist/public/bundle/.agents/skills/omg/remote-supervisor/openai.yaml +1 -1
  67. package/dist/public/bundle/.agents/skills/omg/robotics/SKILL.md +1 -1
  68. package/dist/public/bundle/.agents/skills/omg/robotics/openai.yaml +1 -1
  69. package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/SKILL.md +1 -1
  70. package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/openai.yaml +1 -1
  71. package/dist/public/bundle/.agents/skills/omg/security-check/SKILL.md +1 -1
  72. package/dist/public/bundle/.agents/skills/omg/security-check/openai.yaml +1 -1
  73. package/dist/public/bundle/.agents/skills/omg/test-intent-lock/SKILL.md +1 -1
  74. package/dist/public/bundle/.agents/skills/omg/test-intent-lock/openai.yaml +1 -1
  75. package/dist/public/bundle/.agents/skills/omg/tracebank/SKILL.md +1 -1
  76. package/dist/public/bundle/.agents/skills/omg/tracebank/openai.yaml +1 -1
  77. package/dist/public/bundle/.agents/skills/omg/vision/SKILL.md +1 -1
  78. package/dist/public/bundle/.agents/skills/omg/vision/openai.yaml +1 -1
  79. package/dist/public/bundle/.gemini/settings.json +11 -0
  80. package/dist/public/bundle/.kimi/mcp.json +11 -0
  81. package/dist/public/bundle/OMG_COMPAT_CONTRACT.md +9 -3
  82. package/dist/public/bundle/registry/omg-capability.schema.json +83 -1
  83. package/dist/public/bundle/settings.json +2 -1
  84. package/dist/public/manifest.json +43 -29
  85. package/docs/proof.md +1 -0
  86. package/hooks/setup_wizard.py +21 -1
  87. package/hooks/shadow_manager.py +25 -2
  88. package/hooks/state_migration.py +3 -0
  89. package/hud/omg-hud.mjs +66 -3
  90. package/package.json +1 -1
  91. package/plugins/advanced/plugin.json +1 -1
  92. package/plugins/core/plugin.json +1 -1
  93. package/plugins/dephealth/cve_scanner.py +91 -0
  94. package/plugins/dephealth/vuln_analyzer.py +7 -0
  95. package/pyproject.toml +1 -1
  96. package/registry/omg-capability.schema.json +83 -1
  97. package/runtime/adoption.py +13 -5
  98. package/runtime/artifact_parsers.py +161 -0
  99. package/runtime/background_verification.py +48 -0
  100. package/runtime/claim_judge.py +184 -7
  101. package/runtime/contract_compiler.py +118 -9
  102. package/runtime/evidence_query.py +203 -0
  103. package/runtime/omg_mcp_server.py +19 -0
  104. package/runtime/playwright_adapter.py +39 -0
  105. package/runtime/proof_chain.py +136 -8
  106. package/runtime/proof_gate.py +102 -0
  107. package/runtime/providers/gemini_provider.py +7 -0
  108. package/runtime/providers/kimi_provider.py +7 -0
  109. package/runtime/repro_pack.py +292 -0
  110. package/runtime/runtime_profile.py +87 -15
  111. package/runtime/security_check.py +86 -3
  112. package/runtime/test_intent_lock.py +47 -0
  113. package/runtime/tracebank.py +33 -3
  114. package/runtime/verification_loop.py +73 -0
  115. package/scripts/omg.py +30 -3
  116. package/settings.json +4 -3
  117. package/tools/python_sandbox.py +9 -6
  118. package/tools/session_snapshot.py +146 -40
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import cast
6
+
7
+ JsonPrimitive = str | int | float | bool | None
8
+ JsonValue = JsonPrimitive | dict[str, "JsonValue"] | list["JsonValue"]
9
+ JsonObject = dict[str, JsonValue]
10
+
11
+
12
+ _EVIDENCE_DIRS = (
13
+ Path(".omg") / "evidence",
14
+ Path(".omg") / "tracebank",
15
+ Path(".omg") / "evals",
16
+ Path(".omg") / "lineage",
17
+ Path(".omg") / "state",
18
+ )
19
+
20
+
21
+ def _iter_json_files(root: Path, rel_dir: Path) -> list[Path]:
22
+ directory = root / rel_dir
23
+ if not directory.exists():
24
+ return []
25
+ return sorted(path for path in directory.glob("*.json") if path.is_file())
26
+
27
+
28
+ def _load_json(path: Path) -> JsonObject | None:
29
+ try:
30
+ payload: object = json.loads(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
31
+ except (OSError, json.JSONDecodeError):
32
+ return None
33
+ if not isinstance(payload, dict):
34
+ return None
35
+ return cast(JsonObject, payload)
36
+
37
+
38
+ def _read_jsonl(path: Path) -> list[JsonObject]:
39
+ if not path.exists():
40
+ return []
41
+ rows: list[JsonObject] = []
42
+ try:
43
+ for line in path.read_text(encoding="utf-8").splitlines():
44
+ line = line.strip()
45
+ if not line:
46
+ continue
47
+ try:
48
+ item: object = json.loads(line) # pyright: ignore[reportAny]
49
+ except json.JSONDecodeError:
50
+ continue
51
+ if isinstance(item, dict):
52
+ rows.append(cast(JsonObject, item))
53
+ except OSError:
54
+ return []
55
+ return rows
56
+
57
+
58
+ def _record_string(record: JsonObject, key: str) -> str:
59
+ value = record.get(key)
60
+ return value if isinstance(value, str) else ""
61
+
62
+
63
+ def _record_string_list(record: JsonObject, key: str) -> list[str]:
64
+ value = record.get(key)
65
+ if not isinstance(value, list):
66
+ return []
67
+ return [item for item in value if isinstance(item, str)]
68
+
69
+
70
+ def _record_matches(
71
+ record: JsonObject,
72
+ *,
73
+ run_id: str | None,
74
+ trace_id: str | None,
75
+ schema: str | None,
76
+ kind: str | None,
77
+ ) -> bool:
78
+ if run_id is not None and _record_string(record, "run_id") != run_id:
79
+ return False
80
+ if trace_id is not None:
81
+ direct_trace_id = _record_string(record, "trace_id")
82
+ trace_ids = _record_string_list(record, "trace_ids")
83
+ if direct_trace_id != trace_id and trace_id not in trace_ids:
84
+ return False
85
+ if schema is not None and _record_string(record, "schema") != schema:
86
+ return False
87
+ if kind is not None:
88
+ direct_kind = _record_string(record, "kind")
89
+ artifacts = record.get("artifacts")
90
+ artifact_kinds: list[str] = []
91
+ if isinstance(artifacts, list):
92
+ for item in artifacts:
93
+ if isinstance(item, dict):
94
+ item_kind = item.get("kind")
95
+ if isinstance(item_kind, str):
96
+ artifact_kinds.append(item_kind)
97
+ if direct_kind != kind and kind not in artifact_kinds:
98
+ return False
99
+ return True
100
+
101
+
102
+ def get_evidence_pack(project_dir: str, run_id: str) -> JsonObject | None:
103
+ root = Path(project_dir)
104
+ evidence_files = _iter_json_files(root, Path(".omg") / "evidence")
105
+ for path in evidence_files:
106
+ payload = _load_json(path)
107
+ if payload is None:
108
+ continue
109
+ if payload.get("schema") != "EvidencePack":
110
+ continue
111
+ if _record_string(payload, "run_id") == run_id:
112
+ return payload
113
+ return None
114
+
115
+
116
+ def query_evidence(
117
+ project_dir: str,
118
+ *,
119
+ run_id: str | None = None,
120
+ trace_id: str | None = None,
121
+ schema: str | None = None,
122
+ kind: str | None = None,
123
+ ) -> list[JsonObject]:
124
+ root = Path(project_dir)
125
+ records: list[JsonObject] = []
126
+
127
+ for rel_dir in _EVIDENCE_DIRS:
128
+ for path in _iter_json_files(root, rel_dir):
129
+ payload = _load_json(path)
130
+ if payload is None:
131
+ continue
132
+ if _record_matches(
133
+ payload,
134
+ run_id=run_id,
135
+ trace_id=trace_id,
136
+ schema=schema,
137
+ kind=kind,
138
+ ):
139
+ records.append(payload)
140
+
141
+ for row in _read_jsonl(root / rel_dir / "events.jsonl"):
142
+ if _record_matches(
143
+ row,
144
+ run_id=run_id,
145
+ trace_id=trace_id,
146
+ schema=schema,
147
+ kind=kind,
148
+ ):
149
+ records.append(row)
150
+
151
+ return records
152
+
153
+
154
+ def list_evidence_packs(project_dir: str) -> list[JsonObject]:
155
+ root = Path(project_dir)
156
+ evidence_files = _iter_json_files(root, Path(".omg") / "evidence")
157
+ payloads: list[tuple[float, JsonObject]] = []
158
+
159
+ for path in evidence_files:
160
+ payload = _load_json(path)
161
+ if payload is None or payload.get("schema") != "EvidencePack":
162
+ continue
163
+ try:
164
+ mtime = path.stat().st_mtime
165
+ except OSError:
166
+ mtime = 0.0
167
+ payloads.append((mtime, payload))
168
+
169
+ payloads.sort(key=lambda item: item[0], reverse=True)
170
+ return [payload for _, payload in payloads]
171
+
172
+
173
+ def get_trace(project_dir: str, trace_id: str) -> JsonObject | None:
174
+ trace_rows = _read_jsonl(Path(project_dir) / ".omg" / "tracebank" / "events.jsonl")
175
+ for row in trace_rows:
176
+ if _record_string(row, "trace_id") == trace_id:
177
+ return row
178
+ return None
179
+
180
+
181
+ def get_eval(project_dir: str) -> JsonObject | None:
182
+ return _load_json(Path(project_dir) / ".omg" / "evals" / "latest.json")
183
+
184
+
185
+ def get_lineage(project_dir: str, lineage_id: str) -> JsonObject | None:
186
+ root = Path(project_dir)
187
+ lineage_files = _iter_json_files(root, Path(".omg") / "lineage")
188
+ for path in lineage_files:
189
+ payload = _load_json(path)
190
+ if payload is None:
191
+ continue
192
+ if _record_string(payload, "lineage_id") == lineage_id:
193
+ return payload
194
+ return None
195
+
196
+
197
+ def get_verification_state(project_dir: str) -> JsonObject | None:
198
+ payload = _load_json(Path(project_dir) / ".omg" / "state" / "background-verification.json")
199
+ if payload is None:
200
+ return None
201
+ if payload.get("schema") != "BackgroundVerificationState":
202
+ return None
203
+ return payload
@@ -174,6 +174,25 @@ def omg_security_check(scope: str = ".", include_live_enrichment: bool = False,
174
174
  return payload
175
175
 
176
176
 
177
+ @mcp.tool()
178
+ def omg_claim_judge(claims: list[dict[str, Any]]) -> dict[str, Any]:
179
+ _status, payload = _service().claim_judge({"claims": claims})
180
+ return payload
181
+
182
+
183
+ @mcp.tool()
184
+ def omg_test_intent_lock(
185
+ action: str,
186
+ intent: dict[str, Any] | None = None,
187
+ lock_id: str | None = None,
188
+ results: dict[str, Any] | None = None,
189
+ ) -> dict[str, Any]:
190
+ _status, payload = _service().test_intent_lock(
191
+ {"action": action, "intent": intent, "lock_id": lock_id, "results": results}
192
+ )
193
+ return payload
194
+
195
+
177
196
  @mcp.tool()
178
197
  def omg_guide_assert(candidate: str, rules: dict[str, Any]) -> dict[str, Any]:
179
198
  _status, payload = _service().guide_assert({"candidate": candidate, "rules": rules})
@@ -0,0 +1,39 @@
1
+ """Optional adapter for summarizing Playwright artifacts into proof-chain-friendly payloads."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ def summarize_playwright_artifacts(
9
+ trace_path: str | None = None,
10
+ junit_path: str | None = None,
11
+ screenshots: list[str] | None = None,
12
+ metadata: dict[str, Any] | None = None,
13
+ ) -> dict[str, Any]:
14
+ """Summarize browser artifacts into a proof-chain-friendly dict.
15
+
16
+ Returns a dict consumable by proof-gate / claim-judge:
17
+ status — "ok" or "error"
18
+ artifacts — {trace, junit, screenshots} (only provided paths)
19
+ metadata — provided metadata or {}
20
+ """
21
+ if not trace_path and not junit_path and not screenshots:
22
+ return {"status": "error", "reason": "no_artifacts_provided"}
23
+
24
+ artifacts: dict[str, Any] = {}
25
+
26
+ if trace_path:
27
+ artifacts["trace"] = str(Path(trace_path))
28
+
29
+ if junit_path:
30
+ artifacts["junit"] = str(Path(junit_path))
31
+
32
+ if screenshots:
33
+ artifacts["screenshots"] = [str(Path(s)) for s in screenshots]
34
+
35
+ return {
36
+ "status": "ok",
37
+ "artifacts": artifacts,
38
+ "metadata": metadata or {},
39
+ }
@@ -6,6 +6,9 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
 
9
+ _REQUIRED_ARTIFACT_FIELDS = ("kind", "path", "sha256", "parser", "summary", "trace_id")
10
+
11
+
9
12
  def _load_json(path: Path) -> dict[str, Any]:
10
13
  return json.loads(path.read_text(encoding="utf-8"))
11
14
 
@@ -26,6 +29,105 @@ def _read_jsonl(path: Path) -> list[dict[str, Any]]:
26
29
  return rows
27
30
 
28
31
 
32
+ def _hash_path(path: Path) -> str:
33
+ if not path.exists() or not path.is_file():
34
+ return ""
35
+ h = hashlib.sha256()
36
+ with path.open("rb") as handle:
37
+ while True:
38
+ chunk = handle.read(8192)
39
+ if not chunk:
40
+ break
41
+ h.update(chunk)
42
+ return h.hexdigest()
43
+
44
+
45
+ def _normalize_evidence_pack(payload: dict[str, Any]) -> dict[str, Any]:
46
+ if not isinstance(payload, dict):
47
+ raise ValueError("evidence_pack_invalid_payload")
48
+ schema_version = payload.get("schema_version")
49
+ if schema_version is None:
50
+ return payload
51
+ if schema_version != 2:
52
+ raise ValueError("evidence_pack_unsupported_schema_version")
53
+
54
+ artifacts = payload.get("artifacts", [])
55
+ if not isinstance(artifacts, list):
56
+ raise ValueError("evidence_pack_invalid_artifacts")
57
+
58
+ for index, artifact in enumerate(artifacts):
59
+ if not isinstance(artifact, dict):
60
+ raise ValueError(f"evidence_pack_artifact_invalid_type:{index}")
61
+ for field in _REQUIRED_ARTIFACT_FIELDS:
62
+ value = str(artifact.get(field, "")).strip()
63
+ if not value:
64
+ raise ValueError(f"evidence_pack_artifact_missing_{field}:{index}")
65
+ return payload
66
+
67
+
68
+ def _artifact_record(*, kind: str, path: str, parser: str, summary: str, trace_id: str, sha256: str = "") -> dict[str, str]:
69
+ return {
70
+ "kind": kind,
71
+ "path": path,
72
+ "sha256": sha256,
73
+ "parser": parser,
74
+ "summary": summary,
75
+ "trace_id": trace_id,
76
+ }
77
+
78
+
79
+ def _build_chain_artifacts(
80
+ *,
81
+ output_root: Path,
82
+ selected_path: str,
83
+ evidence_payload: dict[str, Any],
84
+ trace_payload: dict[str, Any],
85
+ eval_payload: dict[str, Any],
86
+ lineage: dict[str, Any] | Any,
87
+ trace_id: str,
88
+ ) -> list[dict[str, str]]:
89
+ artifacts: list[dict[str, str]] = []
90
+ raw_artifacts = evidence_payload.get("artifacts", [])
91
+ if isinstance(raw_artifacts, list):
92
+ for item in raw_artifacts:
93
+ if not isinstance(item, dict):
94
+ continue
95
+ path = str(item.get("path", "")).strip()
96
+ artifacts.append(
97
+ _artifact_record(
98
+ kind=str(item.get("kind", "")).strip() or "artifact",
99
+ path=path,
100
+ sha256=str(item.get("sha256", "")).strip(),
101
+ parser=str(item.get("parser", "")).strip() or "unknown",
102
+ summary=str(item.get("summary", "")).strip() or "evidence artifact",
103
+ trace_id=str(item.get("trace_id", "")).strip() or trace_id,
104
+ )
105
+ )
106
+
107
+ lineage_path = str((lineage or {}).get("path", "")).strip() if isinstance(lineage, dict) else ""
108
+ canonical_artifacts = [
109
+ ("trace", str(trace_payload.get("path", ".omg/tracebank/events.jsonl")).strip(), "jsonl", "tracebank event stream"),
110
+ ("eval", ".omg/evals/latest.json" if eval_payload else "", "json", "evaluation result"),
111
+ ("lineage", lineage_path, "json", "lineage manifest"),
112
+ ("evidence", selected_path, "json", "evidence pack"),
113
+ ]
114
+ for kind, path, parser, summary in canonical_artifacts:
115
+ if not path:
116
+ continue
117
+ file_path = output_root / path
118
+ artifacts.append(
119
+ _artifact_record(
120
+ kind=kind,
121
+ path=path,
122
+ sha256=_hash_path(file_path),
123
+ parser=parser,
124
+ summary=summary,
125
+ trace_id=trace_id,
126
+ )
127
+ )
128
+ return artifacts
129
+
130
+
29
131
  def _latest_evidence_pack(output_root: Path) -> tuple[str, dict[str, Any]]:
30
132
  evidence_dir = output_root / ".omg" / "evidence"
31
133
  if not evidence_dir.exists():
@@ -60,9 +162,10 @@ def assemble_proof_chain(project_dir: str, *, evidence_path: str | None = None)
60
162
 
61
163
  if evidence_path:
62
164
  selected_path = str(evidence_path)
63
- evidence_payload = _load_json(output_root / selected_path)
165
+ evidence_payload = _normalize_evidence_pack(_load_json(output_root / selected_path))
64
166
  else:
65
167
  selected_path, evidence_payload = _latest_evidence_pack(output_root)
168
+ evidence_payload = _normalize_evidence_pack(evidence_payload)
66
169
 
67
170
  trace_id = ""
68
171
  trace_ids = evidence_payload.get("trace_ids", [])
@@ -79,6 +182,7 @@ def assemble_proof_chain(project_dir: str, *, evidence_path: str | None = None)
79
182
 
80
183
  chain = {
81
184
  "schema": "ProofChain",
185
+ "schema_version": 2,
82
186
  "trace_id": trace_id,
83
187
  "eval_id": eval_id,
84
188
  "eval_trace_id": str(eval_payload.get("trace_id", "")),
@@ -91,16 +195,35 @@ def assemble_proof_chain(project_dir: str, *, evidence_path: str | None = None)
91
195
  "environment": evidence_payload.get("environment") or trace_payload.get("environment") or eval_payload.get("environment") or {"hostname": "unknown", "platform": "unknown"},
92
196
  "ci_job_url": evidence_payload.get("ci_job_url") or "",
93
197
  "external_inputs": evidence_payload.get("external_inputs", []),
94
- "artifacts": {
95
- "trace": trace_payload.get("path", ".omg/tracebank/events.jsonl"),
96
- "eval": ".omg/evals/latest.json" if eval_payload else "",
97
- "lineage": str((lineage or {}).get("path", "")) if isinstance(lineage, dict) else "",
98
- "evidence": selected_path,
99
- },
198
+ "artifacts": _build_chain_artifacts(
199
+ output_root=output_root,
200
+ selected_path=selected_path,
201
+ evidence_payload=evidence_payload,
202
+ trace_payload=trace_payload,
203
+ eval_payload=eval_payload,
204
+ lineage=lineage,
205
+ trace_id=trace_id,
206
+ ),
100
207
  }
101
208
  validation = validate_proof_chain(chain)
102
209
  chain["status"] = validation["status"]
103
210
  chain["blockers"] = validation["blockers"]
211
+
212
+ try:
213
+ from runtime.background_verification import publish_verification_state
214
+
215
+ evidence_links = [selected_path] if selected_path else []
216
+ publish_verification_state(
217
+ project_dir=project_dir,
218
+ run_id=str(chain.get("eval_id", "")),
219
+ status=str(validation["status"]),
220
+ blockers=list(validation.get("blockers", [])),
221
+ evidence_links=evidence_links,
222
+ progress={"phase": "proof_chain_assembled"},
223
+ )
224
+ except Exception:
225
+ pass
226
+
104
227
  return chain
105
228
 
106
229
 
@@ -162,9 +285,10 @@ def build_proof_gate_input(project_dir: str, *, evidence_path: str | None = None
162
285
 
163
286
  if evidence_path:
164
287
  selected_path = str(evidence_path)
165
- evidence_payload = _load_json(output_root / selected_path)
288
+ evidence_payload = _normalize_evidence_pack(_load_json(output_root / selected_path))
166
289
  else:
167
290
  selected_path, evidence_payload = _latest_evidence_pack(output_root)
291
+ evidence_payload = _normalize_evidence_pack(evidence_payload)
168
292
 
169
293
  security_evidence = _resolve_security_evidence(output_root=output_root, evidence_payload=evidence_payload)
170
294
  browser_evidence = _resolve_browser_evidence(output_root=output_root, evidence_payload=evidence_payload)
@@ -212,6 +336,10 @@ def _resolve_browser_evidence(*, output_root: Path, evidence_payload: dict[str,
212
336
  if path:
213
337
  candidates.append(path)
214
338
 
339
+ adapter_matches = sorted(output_root.glob(".omg/evidence/playwright-adapter-*.json"))
340
+ if adapter_matches:
341
+ candidates.append(adapter_matches[0].relative_to(output_root).as_posix())
342
+
215
343
  candidates.extend(
216
344
  [
217
345
  ".omg/evidence/browser-evidence.json",
@@ -1,7 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
4
+ from pathlib import Path
3
5
  from typing import Any
4
6
 
7
+ from runtime import artifact_parsers
8
+
9
+
10
+ _REQUIRED_ARTIFACT_FIELDS = ("kind", "path", "sha256", "parser", "summary", "trace_id")
11
+
5
12
 
6
13
  def evaluate_proof_gate(input: dict[str, Any]) -> dict[str, Any]:
7
14
  claims = _as_claims(input.get("claims"))
@@ -9,6 +16,7 @@ def evaluate_proof_gate(input: dict[str, Any]) -> dict[str, Any]:
9
16
  eval_output = _as_dict(input.get("eval_output"))
10
17
  security_evidence = _as_dict(input.get("security_evidence"))
11
18
  browser_evidence = _as_dict(input.get("browser_evidence"))
19
+ evidence_pack = _as_dict(input.get("evidence_pack"))
12
20
 
13
21
  blockers: list[str] = []
14
22
  if not claims:
@@ -27,6 +35,7 @@ def evaluate_proof_gate(input: dict[str, Any]) -> dict[str, Any]:
27
35
  blockers.extend(_validate_claim_artifacts(claims))
28
36
  blockers.extend(_validate_trace_linkage(claims=claims, trace_id=trace_id, eval_output=eval_output, browser_evidence=browser_evidence))
29
37
  blockers.extend(_validate_security_and_browser_artifacts(claims=claims, security_evidence=security_evidence, browser_evidence=browser_evidence))
38
+ blockers.extend(_validate_evidence_pack(evidence_pack))
30
39
 
31
40
  unique_blockers = list(dict.fromkeys(item for item in blockers if str(item).strip()))
32
41
  evidence_summary = {
@@ -93,8 +102,10 @@ def _collect_trace_ids(claim: dict[str, Any]) -> set[str]:
93
102
 
94
103
  def _validate_claim_artifacts(claims: list[dict[str, Any]]) -> list[str]:
95
104
  all_artifacts: list[str] = []
105
+ artifact_records: list[dict[str, Any]] = []
96
106
  for claim in claims:
97
107
  all_artifacts.extend(_collect_artifacts(claim))
108
+ artifact_records.extend(_extract_artifact_records(claim))
98
109
 
99
110
  blockers: list[str] = []
100
111
  required_tokens = {
@@ -106,6 +117,24 @@ def _validate_claim_artifacts(claims: list[dict[str, Any]]) -> list[str]:
106
117
  for key, tokens in required_tokens.items():
107
118
  if not any(any(token in artifact for token in tokens) for artifact in all_artifacts):
108
119
  blockers.append(f"proof_gate_missing_artifact_{key}")
120
+
121
+ for artifact in artifact_records:
122
+ kind = str(artifact.get("kind", "")).strip().lower()
123
+ path = str(artifact.get("path", "")).strip()
124
+ if not kind or not path:
125
+ continue
126
+
127
+ parse_result = _parse_artifact(kind=kind, path=path)
128
+ if not parse_result.get("valid"):
129
+ error = str(parse_result.get("error", "")).strip()
130
+ if error == "file_not_found":
131
+ blockers.append(f"proof_gate_artifact_file_missing_{kind}")
132
+ else:
133
+ blockers.append(f"proof_gate_artifact_parse_failed_{kind}")
134
+
135
+ hash_blocker = _validate_artifact_hash(artifact)
136
+ if hash_blocker:
137
+ blockers.append(hash_blocker)
109
138
  return blockers
110
139
 
111
140
 
@@ -161,3 +190,76 @@ def _validate_security_and_browser_artifacts(
161
190
  blockers.append("proof_gate_browser_trace_not_linked_by_claims")
162
191
 
163
192
  return blockers
193
+
194
+
195
+ def _validate_evidence_pack(payload: dict[str, Any]) -> list[str]:
196
+ if not payload:
197
+ return []
198
+ if str(payload.get("schema", "")).strip() != "EvidencePack":
199
+ return ["proof_gate_invalid_evidence_pack"]
200
+
201
+ schema_version = payload.get("schema_version")
202
+ if schema_version is None:
203
+ return []
204
+ if schema_version != 2:
205
+ return ["proof_gate_unsupported_evidence_schema_version"]
206
+
207
+ artifacts = payload.get("artifacts", [])
208
+ if not isinstance(artifacts, list):
209
+ return ["proof_gate_invalid_evidence_pack"]
210
+
211
+ blockers: list[str] = []
212
+ for artifact in artifacts:
213
+ if not isinstance(artifact, dict):
214
+ blockers.append("proof_gate_invalid_evidence_pack")
215
+ continue
216
+ for field in _REQUIRED_ARTIFACT_FIELDS:
217
+ value = str(artifact.get(field, "")).strip()
218
+ if not value:
219
+ blockers.append(f"proof_gate_evidence_artifact_missing_{field}")
220
+ return blockers
221
+
222
+
223
+ def _extract_artifact_records(claim: dict[str, Any]) -> list[dict[str, Any]]:
224
+ evidence = _as_dict(claim.get("evidence"))
225
+ raw_artifacts = evidence.get("artifacts", claim.get("artifacts", []))
226
+ if not isinstance(raw_artifacts, list):
227
+ return []
228
+ return [item for item in raw_artifacts if isinstance(item, dict)]
229
+
230
+
231
+ def _parse_artifact(*, kind: str, path: str) -> dict[str, Any]:
232
+ parser = _PARSERS.get(kind)
233
+ if parser is None:
234
+ return {"valid": False, "summary": {}, "error": "unsupported_artifact_kind"}
235
+ return parser(path)
236
+
237
+
238
+ def _validate_artifact_hash(artifact: dict[str, Any]) -> str | None:
239
+ sha256_value = str(artifact.get("sha256", "")).strip().lower()
240
+ path = str(artifact.get("path", "")).strip()
241
+ kind = str(artifact.get("kind", "artifact")).strip().lower() or "artifact"
242
+ if not sha256_value or not path:
243
+ return None
244
+ if len(sha256_value) != 64 or any(ch not in "0123456789abcdef" for ch in sha256_value):
245
+ return None
246
+
247
+ file_path = Path(path)
248
+ if not file_path.exists():
249
+ return f"proof_gate_artifact_file_missing_{kind}"
250
+ try:
251
+ digest = hashlib.sha256(file_path.read_bytes()).hexdigest()
252
+ except OSError:
253
+ return f"proof_gate_artifact_hash_unreadable_{kind}"
254
+ if digest != sha256_value:
255
+ return f"proof_gate_artifact_hash_mismatch_{kind}"
256
+ return None
257
+
258
+
259
+ _PARSERS: dict[str, Any] = {
260
+ "junit": artifact_parsers.parse_junit,
261
+ "sarif": artifact_parsers.parse_sarif,
262
+ "coverage": artifact_parsers.parse_coverage,
263
+ "browser_trace": artifact_parsers.parse_browser_trace,
264
+ "diff_hunk": artifact_parsers.parse_diff_hunk,
265
+ }
@@ -16,6 +16,13 @@ from runtime.tmux_session_manager import TmuxSessionManager
16
16
 
17
17
  _logger = logging.getLogger(__name__)
18
18
 
19
+ HOST_RULES = {
20
+ "compilation_targets": [".gemini/settings.json"],
21
+ "mcp": ["omg-control"],
22
+ "skills": ["omg/control-plane", "omg/mcp-fabric"],
23
+ "automations": ["contract-validate", "provider-routing"],
24
+ }
25
+
19
26
 
20
27
  class GeminiProvider(CLIProvider):
21
28
  """CLIProvider implementation for the Gemini CLI (``gemini``)."""
@@ -16,6 +16,13 @@ from runtime.tmux_session_manager import TmuxSessionManager
16
16
 
17
17
  _logger = logging.getLogger(__name__)
18
18
 
19
+ HOST_RULES = {
20
+ "compilation_targets": [".kimi/mcp.json"],
21
+ "mcp": ["omg-control"],
22
+ "skills": ["omg/control-plane", "omg/mcp-fabric"],
23
+ "automations": ["contract-validate", "provider-routing"],
24
+ }
25
+
19
26
 
20
27
  class KimiCodeProvider(CLIProvider):
21
28
  """CLIProvider implementation for the Kimi Code CLI (``kimi``)."""