@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.gemini/settings.json +11 -0
- package/.kimi/mcp.json +11 -0
- package/CHANGELOG.md +10 -0
- package/OMG-setup.sh +1 -1
- package/OMG_COMPAT_CONTRACT.md +10 -4
- package/README.md +1 -0
- package/build/lib/commands/OMG:forge.md +92 -0
- package/build/lib/commands/OMG:mode.md +13 -13
- package/build/lib/commands/OMG:session-branch.md +17 -1
- package/build/lib/commands/OMG:session-fork.md +5 -1
- package/build/lib/commands/OMG:session-merge.md +5 -1
- package/build/lib/control_plane/server.py +4 -0
- package/build/lib/control_plane/service.py +55 -0
- package/build/lib/hooks/setup_wizard.py +21 -1
- package/build/lib/hooks/shadow_manager.py +25 -2
- package/build/lib/hooks/state_migration.py +3 -0
- package/build/lib/plugins/dephealth/cve_scanner.py +91 -0
- package/build/lib/plugins/dephealth/vuln_analyzer.py +7 -0
- package/build/lib/registry/omg-capability.schema.json +83 -1
- package/build/lib/runtime/adoption.py +12 -4
- package/build/lib/runtime/artifact_parsers.py +161 -0
- package/build/lib/runtime/background_verification.py +48 -0
- package/build/lib/runtime/claim_judge.py +184 -7
- package/build/lib/runtime/contract_compiler.py +118 -9
- package/build/lib/runtime/evidence_query.py +203 -0
- package/build/lib/runtime/omg_mcp_server.py +19 -0
- package/build/lib/runtime/playwright_adapter.py +39 -0
- package/build/lib/runtime/proof_chain.py +136 -8
- package/build/lib/runtime/proof_gate.py +102 -0
- package/build/lib/runtime/providers/gemini_provider.py +7 -0
- package/build/lib/runtime/providers/kimi_provider.py +7 -0
- package/build/lib/runtime/repro_pack.py +292 -0
- package/build/lib/runtime/runtime_profile.py +87 -15
- package/build/lib/runtime/security_check.py +86 -3
- package/build/lib/runtime/test_intent_lock.py +47 -0
- package/build/lib/runtime/tracebank.py +33 -3
- package/build/lib/runtime/verification_loop.py +73 -0
- package/commands/OMG:forge.md +92 -0
- package/commands/OMG:mode.md +13 -13
- package/commands/OMG:session-branch.md +17 -1
- package/commands/OMG:session-fork.md +5 -1
- package/commands/OMG:session-merge.md +5 -1
- package/control_plane/server.py +4 -0
- package/control_plane/service.py +55 -0
- package/dist/enterprise/bundle/.gemini/settings.json +11 -0
- package/dist/enterprise/bundle/.kimi/mcp.json +11 -0
- package/dist/enterprise/bundle/OMG_COMPAT_CONTRACT.md +9 -3
- package/dist/enterprise/bundle/registry/omg-capability.schema.json +83 -1
- package/dist/enterprise/bundle/settings.json +1 -0
- package/dist/enterprise/manifest.json +17 -3
- package/dist/public/bundle/.agents/skills/omg/incident-replay/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/incident-replay/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/lsp-pack/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/lsp-pack/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/mcp-fabric/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/mcp-fabric/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/plan-council/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/plan-council/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/preflight/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/preflight/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/proof-gate/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/proof-gate/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/remote-supervisor/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/remote-supervisor/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/robotics/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/robotics/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/secure-worktree-pipeline/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/security-check/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/security-check/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/test-intent-lock/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/test-intent-lock/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/tracebank/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/tracebank/openai.yaml +1 -1
- package/dist/public/bundle/.agents/skills/omg/vision/SKILL.md +1 -1
- package/dist/public/bundle/.agents/skills/omg/vision/openai.yaml +1 -1
- package/dist/public/bundle/.gemini/settings.json +11 -0
- package/dist/public/bundle/.kimi/mcp.json +11 -0
- package/dist/public/bundle/OMG_COMPAT_CONTRACT.md +9 -3
- package/dist/public/bundle/registry/omg-capability.schema.json +83 -1
- package/dist/public/bundle/settings.json +2 -1
- package/dist/public/manifest.json +43 -29
- package/docs/proof.md +1 -0
- package/hooks/setup_wizard.py +21 -1
- package/hooks/shadow_manager.py +25 -2
- package/hooks/state_migration.py +3 -0
- package/hud/omg-hud.mjs +66 -3
- package/package.json +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +1 -1
- package/plugins/dephealth/cve_scanner.py +91 -0
- package/plugins/dephealth/vuln_analyzer.py +7 -0
- package/pyproject.toml +1 -1
- package/registry/omg-capability.schema.json +83 -1
- package/runtime/adoption.py +13 -5
- package/runtime/artifact_parsers.py +161 -0
- package/runtime/background_verification.py +48 -0
- package/runtime/claim_judge.py +184 -7
- package/runtime/contract_compiler.py +118 -9
- package/runtime/evidence_query.py +203 -0
- package/runtime/omg_mcp_server.py +19 -0
- package/runtime/playwright_adapter.py +39 -0
- package/runtime/proof_chain.py +136 -8
- package/runtime/proof_gate.py +102 -0
- package/runtime/providers/gemini_provider.py +7 -0
- package/runtime/providers/kimi_provider.py +7 -0
- package/runtime/repro_pack.py +292 -0
- package/runtime/runtime_profile.py +87 -15
- package/runtime/security_check.py +86 -3
- package/runtime/test_intent_lock.py +47 -0
- package/runtime/tracebank.py +33 -3
- package/runtime/verification_loop.py +73 -0
- package/scripts/omg.py +30 -3
- package/settings.json +4 -3
- package/tools/python_sandbox.py +9 -6
- package/tools/session_snapshot.py +146 -40
|
@@ -1,15 +1,68 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
3
5
|
from typing import Any
|
|
4
6
|
|
|
7
|
+
from runtime import artifact_parsers
|
|
8
|
+
from runtime.evidence_query import get_evidence_pack
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def judge_claims(project_dir: str, claims: list[dict[str, Any]]) -> dict[str, Any]:
|
|
12
|
+
root = Path(project_dir)
|
|
13
|
+
evidence_dir = root / ".omg" / "evidence"
|
|
14
|
+
evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
|
|
16
|
+
results: list[dict[str, Any]] = []
|
|
17
|
+
aggregate_tokens: list[str] = []
|
|
18
|
+
|
|
19
|
+
for index, claim in enumerate(claims):
|
|
20
|
+
run_id = str(claim.get("run_id", "")).strip()
|
|
21
|
+
resolved_claim = dict(claim)
|
|
22
|
+
|
|
23
|
+
if run_id:
|
|
24
|
+
evidence_pack = get_evidence_pack(project_dir, run_id)
|
|
25
|
+
trace_ids: list[str] = []
|
|
26
|
+
if isinstance(evidence_pack, dict):
|
|
27
|
+
trace_ids = _as_non_empty_str_list(evidence_pack.get("trace_ids"))
|
|
28
|
+
resolved_claim = {
|
|
29
|
+
**claim,
|
|
30
|
+
"artifacts": [f".omg/evidence/{run_id}.json"],
|
|
31
|
+
"trace_ids": trace_ids,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
result = judge_claim(resolved_claim)
|
|
35
|
+
result_with_run = {**result, "run_id": run_id}
|
|
36
|
+
results.append(result_with_run)
|
|
37
|
+
aggregate_tokens.append(str(result.get("verdict", "")).strip().lower())
|
|
38
|
+
|
|
39
|
+
artifact_run_id = run_id or f"unknown-{index + 1}"
|
|
40
|
+
artifact_path = evidence_dir / f"claim-judge-{_sanitize_run_id(artifact_run_id)}.json"
|
|
41
|
+
artifact_payload = {
|
|
42
|
+
"schema": "ClaimJudgeResult",
|
|
43
|
+
"run_id": run_id,
|
|
44
|
+
"claim": claim,
|
|
45
|
+
"result": result,
|
|
46
|
+
}
|
|
47
|
+
artifact_path.write_text(json.dumps(artifact_payload, indent=2, sort_keys=True), encoding="utf-8")
|
|
48
|
+
|
|
49
|
+
verdict = "pass"
|
|
50
|
+
if any(token == "fail" for token in aggregate_tokens):
|
|
51
|
+
verdict = "fail"
|
|
52
|
+
elif any(token == "block" for token in aggregate_tokens):
|
|
53
|
+
verdict = "insufficient"
|
|
54
|
+
|
|
55
|
+
return {"schema": "ClaimJudgeResults", "verdict": verdict, "results": results}
|
|
56
|
+
|
|
5
57
|
|
|
6
58
|
def judge_claim(claim: dict[str, Any]) -> dict[str, Any]:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
59
|
+
normalized_claim = _normalize_claim(claim)
|
|
60
|
+
claim_type = str(normalized_claim.get("claim_type", "")).strip()
|
|
61
|
+
subject = str(normalized_claim.get("subject", "")).strip()
|
|
62
|
+
artifacts = _as_non_empty_str_list(normalized_claim.get("artifacts"))
|
|
63
|
+
trace_ids = _as_non_empty_str_list(normalized_claim.get("trace_ids"))
|
|
64
|
+
security_scans = normalized_claim.get("security_scans")
|
|
65
|
+
browser_evidence = normalized_claim.get("browser_evidence")
|
|
13
66
|
|
|
14
67
|
reasons: list[dict[str, Any]] = []
|
|
15
68
|
|
|
@@ -49,6 +102,22 @@ def judge_claim(claim: dict[str, Any]) -> dict[str, Any]:
|
|
|
49
102
|
}
|
|
50
103
|
)
|
|
51
104
|
|
|
105
|
+
raw_artifacts = _extract_artifact_dicts(claim)
|
|
106
|
+
project_dir = str(claim.get("project_dir", "."))
|
|
107
|
+
for artifact in raw_artifacts:
|
|
108
|
+
parse_result = parse_artifact_content(artifact=artifact, project_dir=project_dir)
|
|
109
|
+
if parse_result.get("parsed"):
|
|
110
|
+
continue
|
|
111
|
+
kind = str(parse_result.get("kind", "artifact")).strip().lower() or "artifact"
|
|
112
|
+
error = str(parse_result.get("error", "parse_error")).strip() or "parse_error"
|
|
113
|
+
reasons.append(
|
|
114
|
+
{
|
|
115
|
+
"code": f"artifact_parse_failed_{kind}",
|
|
116
|
+
"message": f"Artifact content parse failed for {kind}: {error}",
|
|
117
|
+
"field": "evidence.artifacts",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
52
121
|
hard_fail_codes = {"missing_artifacts", "missing_trace_ids"}
|
|
53
122
|
if any(reason.get("code") in hard_fail_codes for reason in reasons):
|
|
54
123
|
verdict = "fail"
|
|
@@ -66,19 +135,105 @@ def judge_claim(claim: dict[str, Any]) -> dict[str, Any]:
|
|
|
66
135
|
"evidence": {
|
|
67
136
|
"artifacts": artifacts,
|
|
68
137
|
"trace_ids": trace_ids,
|
|
69
|
-
"lineage":
|
|
138
|
+
"lineage": normalized_claim.get("lineage") if isinstance(normalized_claim.get("lineage"), dict) else {},
|
|
70
139
|
"security_scans": security_scans if isinstance(security_scans, list) else [],
|
|
71
140
|
"browser_evidence": browser_evidence if isinstance(browser_evidence, list) else [],
|
|
72
141
|
},
|
|
73
142
|
}
|
|
74
143
|
|
|
75
144
|
|
|
145
|
+
def _normalize_claim(claim: dict[str, Any]) -> dict[str, Any]:
|
|
146
|
+
evidence = _as_dict(claim.get("evidence"))
|
|
147
|
+
artifact_refs = _as_non_empty_str_list(claim.get("artifacts"))
|
|
148
|
+
artifact_refs.extend(_normalize_artifact_records(evidence.get("artifacts")))
|
|
149
|
+
|
|
150
|
+
trace_ids = _as_non_empty_str_list(evidence.get("trace_ids"))
|
|
151
|
+
if not trace_ids:
|
|
152
|
+
trace_ids = _as_non_empty_str_list(claim.get("trace_ids"))
|
|
153
|
+
|
|
154
|
+
claim_lineage = claim.get("lineage")
|
|
155
|
+
lineage = claim_lineage if isinstance(claim_lineage, dict) else _as_dict(evidence.get("lineage"))
|
|
156
|
+
|
|
157
|
+
claim_security_scans = claim.get("security_scans")
|
|
158
|
+
security_scans = claim_security_scans if isinstance(claim_security_scans, list) else _as_non_empty_dict_list(evidence.get("security_scans"))
|
|
159
|
+
|
|
160
|
+
claim_browser_evidence = claim.get("browser_evidence")
|
|
161
|
+
browser_evidence = claim_browser_evidence if isinstance(claim_browser_evidence, list) else _as_non_empty_dict_list(evidence.get("browser_evidence"))
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
"schema_version": claim.get("schema_version", 1),
|
|
165
|
+
"claim_type": claim.get("claim_type", ""),
|
|
166
|
+
"subject": claim.get("subject", ""),
|
|
167
|
+
"artifacts": artifact_refs,
|
|
168
|
+
"trace_ids": trace_ids,
|
|
169
|
+
"lineage": lineage,
|
|
170
|
+
"security_scans": security_scans,
|
|
171
|
+
"browser_evidence": browser_evidence,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _normalize_artifact_records(value: Any) -> list[str]:
|
|
176
|
+
if not isinstance(value, list):
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
refs: list[str] = []
|
|
180
|
+
for item in value:
|
|
181
|
+
if isinstance(item, str):
|
|
182
|
+
cleaned = item.strip()
|
|
183
|
+
if cleaned:
|
|
184
|
+
refs.append(cleaned)
|
|
185
|
+
continue
|
|
186
|
+
if not isinstance(item, dict):
|
|
187
|
+
continue
|
|
188
|
+
for field in ("kind", "path", "sha256", "parser", "summary", "trace_id"):
|
|
189
|
+
field_value = str(item.get(field, "")).strip()
|
|
190
|
+
if not field_value:
|
|
191
|
+
raise ValueError(f"claim_artifact_missing_{field}")
|
|
192
|
+
refs.append(str(item.get("path", "")).strip())
|
|
193
|
+
return refs
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def parse_artifact_content(artifact: dict[str, Any], project_dir: str) -> dict[str, Any]:
|
|
197
|
+
kind = str(artifact.get("kind", "")).strip().lower()
|
|
198
|
+
path_value = str(artifact.get("path", "")).strip()
|
|
199
|
+
if not kind or not path_value:
|
|
200
|
+
return {"parsed": False, "kind": kind or "unknown", "summary": {}, "error": "missing_kind_or_path"}
|
|
201
|
+
|
|
202
|
+
parser = _PARSERS.get(kind)
|
|
203
|
+
if parser is None:
|
|
204
|
+
return {"parsed": False, "kind": kind, "summary": {}, "error": "unsupported_artifact_kind"}
|
|
205
|
+
|
|
206
|
+
file_path = Path(path_value)
|
|
207
|
+
if not file_path.is_absolute():
|
|
208
|
+
file_path = Path(project_dir) / file_path
|
|
209
|
+
|
|
210
|
+
parsed = parser(str(file_path))
|
|
211
|
+
return {
|
|
212
|
+
"parsed": bool(parsed.get("valid")),
|
|
213
|
+
"kind": kind,
|
|
214
|
+
"summary": parsed.get("summary", {}),
|
|
215
|
+
"error": parsed.get("error"),
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
76
219
|
def _as_non_empty_str_list(value: Any) -> list[str]:
|
|
77
220
|
if not isinstance(value, list):
|
|
78
221
|
return []
|
|
79
222
|
return [str(item).strip() for item in value if str(item).strip()]
|
|
80
223
|
|
|
81
224
|
|
|
225
|
+
def _as_non_empty_dict_list(value: Any) -> list[dict[str, Any]]:
|
|
226
|
+
if not isinstance(value, list):
|
|
227
|
+
return []
|
|
228
|
+
return [item for item in value if isinstance(item, dict)]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _as_dict(value: Any) -> dict[str, Any]:
|
|
232
|
+
if not isinstance(value, dict):
|
|
233
|
+
return {}
|
|
234
|
+
return value
|
|
235
|
+
|
|
236
|
+
|
|
82
237
|
def _has_failed_scan(value: Any) -> bool:
|
|
83
238
|
if not isinstance(value, list):
|
|
84
239
|
return False
|
|
@@ -93,3 +248,25 @@ def _has_failed_scan(value: Any) -> bool:
|
|
|
93
248
|
if isinstance(unresolved_risks, list) and unresolved_risks:
|
|
94
249
|
return True
|
|
95
250
|
return False
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _extract_artifact_dicts(claim: dict[str, Any]) -> list[dict[str, Any]]:
|
|
254
|
+
evidence = _as_dict(claim.get("evidence"))
|
|
255
|
+
raw_artifacts = evidence.get("artifacts")
|
|
256
|
+
if not isinstance(raw_artifacts, list):
|
|
257
|
+
return []
|
|
258
|
+
return [item for item in raw_artifacts if isinstance(item, dict)]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
_PARSERS: dict[str, Any] = {
|
|
262
|
+
"junit": artifact_parsers.parse_junit,
|
|
263
|
+
"sarif": artifact_parsers.parse_sarif,
|
|
264
|
+
"coverage": artifact_parsers.parse_coverage,
|
|
265
|
+
"browser_trace": artifact_parsers.parse_browser_trace,
|
|
266
|
+
"diff_hunk": artifact_parsers.parse_diff_hunk,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _sanitize_run_id(value: str) -> str:
|
|
271
|
+
cleaned = "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "-" for ch in value.strip())
|
|
272
|
+
return cleaned or "unknown"
|
|
@@ -19,6 +19,7 @@ import zipfile
|
|
|
19
19
|
import yaml
|
|
20
20
|
|
|
21
21
|
from runtime.asset_loader import resolve_asset, resolve_assets
|
|
22
|
+
from runtime.proof_chain import _normalize_evidence_pack
|
|
22
23
|
from runtime.adoption import (
|
|
23
24
|
CANONICAL_MARKETPLACE_ID,
|
|
24
25
|
CANONICAL_PACKAGE_NAME,
|
|
@@ -31,7 +32,7 @@ from runtime.adoption import (
|
|
|
31
32
|
CONTRACT_DOC_PATH = Path("OMG_COMPAT_CONTRACT.md")
|
|
32
33
|
SCHEMA_PATH = Path("registry") / "omg-capability.schema.json"
|
|
33
34
|
BUNDLES_DIR = Path("registry") / "bundles"
|
|
34
|
-
SUPPORTED_HOSTS = ("claude", "codex")
|
|
35
|
+
SUPPORTED_HOSTS = ("claude", "codex", "gemini", "kimi")
|
|
35
36
|
SUPPORTED_CHANNELS = ("public", "enterprise")
|
|
36
37
|
DEFAULT_REQUIRED_BUNDLES = (
|
|
37
38
|
"control-plane",
|
|
@@ -120,6 +121,25 @@ REQUIRED_CODEX_OUTPUTS = (
|
|
|
120
121
|
"codex-rules.md",
|
|
121
122
|
"codex-mcp.toml",
|
|
122
123
|
)
|
|
124
|
+
HOST_COMPILED_ARTIFACTS = {
|
|
125
|
+
"claude": (
|
|
126
|
+
".claude-plugin/plugin.json",
|
|
127
|
+
".claude-plugin/marketplace.json",
|
|
128
|
+
".mcp.json",
|
|
129
|
+
"settings.json",
|
|
130
|
+
),
|
|
131
|
+
"codex": (
|
|
132
|
+
".agents/skills/omg/AGENTS.fragment.md",
|
|
133
|
+
".agents/skills/omg/codex-rules.md",
|
|
134
|
+
".agents/skills/omg/codex-mcp.toml",
|
|
135
|
+
),
|
|
136
|
+
"gemini": (
|
|
137
|
+
".gemini/settings.json",
|
|
138
|
+
),
|
|
139
|
+
"kimi": (
|
|
140
|
+
".kimi/mcp.json",
|
|
141
|
+
),
|
|
142
|
+
}
|
|
123
143
|
|
|
124
144
|
|
|
125
145
|
def _ensure_list(
|
|
@@ -170,7 +190,12 @@ def _validate_host_rule(
|
|
|
170
190
|
)
|
|
171
191
|
|
|
172
192
|
|
|
173
|
-
def _validate_policy_model(
|
|
193
|
+
def _validate_policy_model(
|
|
194
|
+
bundle_id: str,
|
|
195
|
+
policy_model: Any,
|
|
196
|
+
*,
|
|
197
|
+
bundle_hosts: Iterable[str] = (),
|
|
198
|
+
) -> list[str]:
|
|
174
199
|
errors: list[str] = []
|
|
175
200
|
payload = _ensure_dict(bundle_id=bundle_id, path="policy_model", value=policy_model, errors=errors)
|
|
176
201
|
if not payload:
|
|
@@ -289,6 +314,8 @@ def _validate_policy_model(bundle_id: str, policy_model: Any) -> list[str]:
|
|
|
289
314
|
value=payload.get("host_rules", {}),
|
|
290
315
|
errors=errors,
|
|
291
316
|
)
|
|
317
|
+
declared_hosts = {str(host).strip() for host in bundle_hosts if str(host).strip()}
|
|
318
|
+
|
|
292
319
|
_validate_host_rule(
|
|
293
320
|
bundle_id=bundle_id,
|
|
294
321
|
host_name="claude",
|
|
@@ -303,6 +330,15 @@ def _validate_policy_model(bundle_id: str, policy_model: Any) -> list[str]:
|
|
|
303
330
|
required_fields=("compilation_targets", "skills", "agents_fragments", "rules", "automations"),
|
|
304
331
|
errors=errors,
|
|
305
332
|
)
|
|
333
|
+
for host_name in ("gemini", "kimi"):
|
|
334
|
+
if host_name in host_rules or host_name in declared_hosts:
|
|
335
|
+
_validate_host_rule(
|
|
336
|
+
bundle_id=bundle_id,
|
|
337
|
+
host_name=host_name,
|
|
338
|
+
host_rule=host_rules.get(host_name),
|
|
339
|
+
required_fields=("compilation_targets", "mcp", "skills", "automations"),
|
|
340
|
+
errors=errors,
|
|
341
|
+
)
|
|
306
342
|
return errors
|
|
307
343
|
|
|
308
344
|
|
|
@@ -461,7 +497,7 @@ def validate_contract_registry(root_dir: str | Path | None = None) -> dict[str,
|
|
|
461
497
|
if bad_hosts:
|
|
462
498
|
errors.append(f"{bundle_id}: unsupported hosts {bad_hosts}")
|
|
463
499
|
if "policy_model" in bundle:
|
|
464
|
-
errors.extend(_validate_policy_model(bundle_id, bundle.get("policy_model")))
|
|
500
|
+
errors.extend(_validate_policy_model(bundle_id, bundle.get("policy_model"), bundle_hosts=hosts))
|
|
465
501
|
|
|
466
502
|
missing_bundles = [bundle_id for bundle_id in DEFAULT_REQUIRED_BUNDLES if bundle_id not in bundle_ids]
|
|
467
503
|
for bundle_id in missing_bundles:
|
|
@@ -1145,6 +1181,34 @@ def _compile_codex_outputs(
|
|
|
1145
1181
|
return artifacts
|
|
1146
1182
|
|
|
1147
1183
|
|
|
1184
|
+
def _compile_gemini_outputs(output_root: Path, channel: str) -> dict[str, Any]:
|
|
1185
|
+
del channel
|
|
1186
|
+
from runtime.mcp_config_writers import write_gemini_mcp_stdio_config
|
|
1187
|
+
|
|
1188
|
+
config_path = output_root / ".gemini" / "settings.json"
|
|
1189
|
+
write_gemini_mcp_stdio_config(
|
|
1190
|
+
command="python3",
|
|
1191
|
+
args=["-m", "runtime.omg_mcp_server"],
|
|
1192
|
+
server_name="omg-control",
|
|
1193
|
+
config_path=config_path,
|
|
1194
|
+
)
|
|
1195
|
+
return {"host": "gemini", "artifacts": [config_path]}
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def _compile_kimi_outputs(output_root: Path, channel: str) -> dict[str, Any]:
|
|
1199
|
+
del channel
|
|
1200
|
+
from runtime.mcp_config_writers import write_kimi_mcp_stdio_config
|
|
1201
|
+
|
|
1202
|
+
config_path = output_root / ".kimi" / "mcp.json"
|
|
1203
|
+
write_kimi_mcp_stdio_config(
|
|
1204
|
+
command="python3",
|
|
1205
|
+
args=["-m", "runtime.omg_mcp_server"],
|
|
1206
|
+
server_name="omg-control",
|
|
1207
|
+
config_path=config_path,
|
|
1208
|
+
)
|
|
1209
|
+
return {"host": "kimi", "artifacts": [config_path]}
|
|
1210
|
+
|
|
1211
|
+
|
|
1148
1212
|
def _copy_release_bundle(
|
|
1149
1213
|
*,
|
|
1150
1214
|
output_root: Path,
|
|
@@ -1164,11 +1228,12 @@ def _copy_release_bundle(
|
|
|
1164
1228
|
return copied
|
|
1165
1229
|
|
|
1166
1230
|
|
|
1167
|
-
def _build_dist_manifest(output_root: Path, *, channel: str, artifacts: list[Path]) -> Path:
|
|
1231
|
+
def _build_dist_manifest(output_root: Path, *, channel: str, hosts: list[str], artifacts: list[Path]) -> Path:
|
|
1168
1232
|
dist_root = output_root / "dist" / channel
|
|
1169
1233
|
payload = {
|
|
1170
1234
|
"schema": "OmgCompiledArtifactManifest",
|
|
1171
1235
|
"channel": channel,
|
|
1236
|
+
"hosts": list(hosts),
|
|
1172
1237
|
"contract_version": CANONICAL_VERSION,
|
|
1173
1238
|
"artifacts": [
|
|
1174
1239
|
{
|
|
@@ -1269,8 +1334,14 @@ def compile_contract_outputs(
|
|
|
1269
1334
|
"artifacts": [],
|
|
1270
1335
|
}
|
|
1271
1336
|
|
|
1337
|
+
if "gemini" in selected_hosts:
|
|
1338
|
+
artifacts.extend(_compile_gemini_outputs(output, channel)["artifacts"])
|
|
1339
|
+
|
|
1340
|
+
if "kimi" in selected_hosts:
|
|
1341
|
+
artifacts.extend(_compile_kimi_outputs(output, channel)["artifacts"])
|
|
1342
|
+
|
|
1272
1343
|
bundled_artifacts = _copy_release_bundle(output_root=output, channel=channel, artifacts=artifacts)
|
|
1273
|
-
manifest_path = _build_dist_manifest(output, channel=channel, artifacts=bundled_artifacts)
|
|
1344
|
+
manifest_path = _build_dist_manifest(output, channel=channel, hosts=selected_hosts, artifacts=bundled_artifacts)
|
|
1274
1345
|
artifacts.append(manifest_path)
|
|
1275
1346
|
|
|
1276
1347
|
return {
|
|
@@ -1291,7 +1362,7 @@ def _provider_statuses() -> dict[str, dict[str, Any]]:
|
|
|
1291
1362
|
}
|
|
1292
1363
|
statuses: dict[str, dict[str, Any]] = {}
|
|
1293
1364
|
|
|
1294
|
-
for provider_name in
|
|
1365
|
+
for provider_name in SUPPORTED_HOSTS:
|
|
1295
1366
|
if provider_name in ready_override:
|
|
1296
1367
|
statuses[provider_name] = {"ready": True, "source": "env"}
|
|
1297
1368
|
continue
|
|
@@ -1307,10 +1378,15 @@ def _provider_statuses() -> dict[str, dict[str, Any]]:
|
|
|
1307
1378
|
}
|
|
1308
1379
|
continue
|
|
1309
1380
|
|
|
1310
|
-
|
|
1381
|
+
if provider_name == "gemini":
|
|
1382
|
+
import runtime.providers.gemini_provider # noqa: F401
|
|
1383
|
+
elif provider_name == "kimi":
|
|
1384
|
+
import runtime.providers.kimi_provider # noqa: F401
|
|
1385
|
+
else:
|
|
1386
|
+
import runtime.providers.codex_provider # noqa: F401
|
|
1311
1387
|
from runtime.cli_provider import get_provider
|
|
1312
1388
|
|
|
1313
|
-
provider = get_provider(
|
|
1389
|
+
provider = get_provider(provider_name)
|
|
1314
1390
|
ready = bool(provider and provider.detect())
|
|
1315
1391
|
statuses[provider_name] = {"ready": ready, "source": "provider"}
|
|
1316
1392
|
|
|
@@ -1557,6 +1633,12 @@ def _check_provider_host_parity(output_root: Path, providers: dict[str, dict[str
|
|
|
1557
1633
|
output_root / ".agents" / "skills" / "omg" / "AGENTS.fragment.md",
|
|
1558
1634
|
output_root / ".agents" / "skills" / "omg" / "codex-mcp.toml",
|
|
1559
1635
|
),
|
|
1636
|
+
"gemini": (
|
|
1637
|
+
output_root / ".gemini" / "settings.json",
|
|
1638
|
+
),
|
|
1639
|
+
"kimi": (
|
|
1640
|
+
output_root / ".kimi" / "mcp.json",
|
|
1641
|
+
),
|
|
1560
1642
|
}
|
|
1561
1643
|
for provider, status in providers.items():
|
|
1562
1644
|
if not status.get("ready"):
|
|
@@ -1628,6 +1710,7 @@ def build_release_readiness(
|
|
|
1628
1710
|
output = _resolve_output_root(root, output_root)
|
|
1629
1711
|
blockers: list[str] = []
|
|
1630
1712
|
checks: dict[str, Any] = {}
|
|
1713
|
+
required_provider_hosts: set[str] = set()
|
|
1631
1714
|
|
|
1632
1715
|
validation = validate_contract_registry(root)
|
|
1633
1716
|
checks["contract_validation"] = validation
|
|
@@ -1655,6 +1738,17 @@ def build_release_readiness(
|
|
|
1655
1738
|
if _sha256_file(artifact_path) != expected_sha:
|
|
1656
1739
|
manifest_errors.append(f"{required_channel}: sha mismatch for {rel_path}")
|
|
1657
1740
|
manifest_paths = {str(a.get("path", "")) for a in manifest.get("artifacts", []) if isinstance(a, dict)}
|
|
1741
|
+
declared_hosts = [str(host) for host in manifest.get("hosts", []) if str(host).strip()]
|
|
1742
|
+
if not declared_hosts:
|
|
1743
|
+
declared_hosts = ["claude", "codex"]
|
|
1744
|
+
required_provider_hosts.update(declared_hosts)
|
|
1745
|
+
for host_name in declared_hosts:
|
|
1746
|
+
for host_path in HOST_COMPILED_ARTIFACTS.get(host_name, ()):
|
|
1747
|
+
bundled_host_path = f"bundle/{host_path}"
|
|
1748
|
+
if bundled_host_path not in manifest_paths:
|
|
1749
|
+
manifest_errors.append(
|
|
1750
|
+
f"{required_channel}: host_parity_missing {host_name} {bundled_host_path}"
|
|
1751
|
+
)
|
|
1658
1752
|
for req_path in REQUIRED_ADVANCED_PLUGIN_ARTIFACTS:
|
|
1659
1753
|
if req_path not in manifest_paths:
|
|
1660
1754
|
manifest_errors.append(f"{required_channel}: advanced_plugin_missing {req_path}")
|
|
@@ -1739,10 +1833,17 @@ def build_release_readiness(
|
|
|
1739
1833
|
providers = _provider_statuses()
|
|
1740
1834
|
checks["providers"] = providers
|
|
1741
1835
|
for provider_name, status in providers.items():
|
|
1836
|
+
if provider_name not in required_provider_hosts:
|
|
1837
|
+
continue
|
|
1742
1838
|
if not status.get("ready"):
|
|
1743
1839
|
blockers.append(f"provider not ready: {provider_name}")
|
|
1744
1840
|
|
|
1745
|
-
|
|
1841
|
+
required_providers = {
|
|
1842
|
+
provider_name: status
|
|
1843
|
+
for provider_name, status in providers.items()
|
|
1844
|
+
if provider_name in required_provider_hosts
|
|
1845
|
+
}
|
|
1846
|
+
provider_parity = _check_provider_host_parity(output, required_providers)
|
|
1746
1847
|
checks["provider_host_parity"] = provider_parity
|
|
1747
1848
|
blockers.extend(provider_parity.get("blockers", []))
|
|
1748
1849
|
|
|
@@ -1781,6 +1882,14 @@ def _check_recent_evidence(output_root: Path) -> dict[str, Any]:
|
|
|
1781
1882
|
except Exception:
|
|
1782
1883
|
continue
|
|
1783
1884
|
if payload.get("schema") == "EvidencePack":
|
|
1885
|
+
try:
|
|
1886
|
+
payload = _normalize_evidence_pack(payload)
|
|
1887
|
+
except ValueError as exc:
|
|
1888
|
+
return {
|
|
1889
|
+
"status": "error",
|
|
1890
|
+
"evidence_file": str(path.relative_to(output_root)),
|
|
1891
|
+
"blockers": [f"invalid evidence pack: {exc}"],
|
|
1892
|
+
}
|
|
1784
1893
|
evidence_payloads.append((path, payload))
|
|
1785
1894
|
|
|
1786
1895
|
if not evidence_payloads:
|
|
@@ -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
|