@openlucaskaka/kagent 0.1.7

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 (117) hide show
  1. package/README.md +353 -0
  2. package/npm/bin/kagent-serve.js +6 -0
  3. package/npm/bin/kagent.js +6 -0
  4. package/npm/lib/App.js +524 -0
  5. package/npm/lib/app-state.js +224 -0
  6. package/npm/lib/approval-choice.js +25 -0
  7. package/npm/lib/commands.js +59 -0
  8. package/npm/lib/editor.js +188 -0
  9. package/npm/lib/ink-runner.js +41 -0
  10. package/npm/lib/kagent-home.js +39 -0
  11. package/npm/lib/launcher.js +221 -0
  12. package/npm/lib/protocol.js +33 -0
  13. package/npm/lib/provider-setup.js +139 -0
  14. package/npm/lib/python-runner.js +892 -0
  15. package/npm/lib/runtime-client.js +390 -0
  16. package/npm/lib/terminal-input.js +127 -0
  17. package/npm/lib/terminal-text.js +19 -0
  18. package/npm/lib/terminal-width.js +14 -0
  19. package/npm/lib/transcript.js +227 -0
  20. package/npm/lib/ui-components.js +247 -0
  21. package/npm/lib/update-manager.js +334 -0
  22. package/package.json +39 -0
  23. package/pyproject.toml +55 -0
  24. package/src/kagent/__init__.py +90 -0
  25. package/src/kagent/cli/__init__.py +5 -0
  26. package/src/kagent/cli/__main__.py +6 -0
  27. package/src/kagent/cli/commands.py +112 -0
  28. package/src/kagent/cli/conversation.py +127 -0
  29. package/src/kagent/cli/interactive.py +841 -0
  30. package/src/kagent/cli/main.py +685 -0
  31. package/src/kagent/cli/memory.py +460 -0
  32. package/src/kagent/cli/pending_approval.py +169 -0
  33. package/src/kagent/cli/provider.py +27 -0
  34. package/src/kagent/cli/session_commands.py +401 -0
  35. package/src/kagent/cli/stdio_runtime.py +784 -0
  36. package/src/kagent/cli/trace.py +67 -0
  37. package/src/kagent/cli/ui.py +931 -0
  38. package/src/kagent/core/__init__.py +0 -0
  39. package/src/kagent/core/agent.py +296 -0
  40. package/src/kagent/core/faults.py +11 -0
  41. package/src/kagent/core/invariants.py +47 -0
  42. package/src/kagent/core/normalization.py +81 -0
  43. package/src/kagent/core/planning.py +48 -0
  44. package/src/kagent/core/state.py +73 -0
  45. package/src/kagent/core/summary.py +64 -0
  46. package/src/kagent/core/tools.py +196 -0
  47. package/src/kagent/core/trace.py +57 -0
  48. package/src/kagent/eval/__init__.py +11 -0
  49. package/src/kagent/eval/cases.py +229 -0
  50. package/src/kagent/eval/evaluator.py +216 -0
  51. package/src/kagent/integrations/__init__.py +3 -0
  52. package/src/kagent/integrations/audit.py +95 -0
  53. package/src/kagent/integrations/backends.py +131 -0
  54. package/src/kagent/integrations/memory.py +301 -0
  55. package/src/kagent/ops/__init__.py +0 -0
  56. package/src/kagent/ops/batch.py +156 -0
  57. package/src/kagent/ops/doctor.py +255 -0
  58. package/src/kagent/ops/metrics.py +214 -0
  59. package/src/kagent/ops/release_evidence.py +877 -0
  60. package/src/kagent/ops/release_manifest.py +142 -0
  61. package/src/kagent/ops/trace_replay.py +285 -0
  62. package/src/kagent/providers/__init__.py +7 -0
  63. package/src/kagent/providers/embeddings.py +187 -0
  64. package/src/kagent/providers/llm.py +770 -0
  65. package/src/kagent/py.typed +1 -0
  66. package/src/kagent/runtime/__init__.py +28 -0
  67. package/src/kagent/runtime/action_graph.py +543 -0
  68. package/src/kagent/runtime/agent.py +2089 -0
  69. package/src/kagent/runtime/approval.py +64 -0
  70. package/src/kagent/runtime/cancellation.py +64 -0
  71. package/src/kagent/runtime/checkpoint_state.py +146 -0
  72. package/src/kagent/runtime/checkpoint_storage.py +270 -0
  73. package/src/kagent/runtime/context.py +65 -0
  74. package/src/kagent/runtime/file_transaction.py +195 -0
  75. package/src/kagent/runtime/hooks.py +74 -0
  76. package/src/kagent/runtime/metadata.py +116 -0
  77. package/src/kagent/runtime/patch_checkpoints.py +385 -0
  78. package/src/kagent/runtime/policy.py +50 -0
  79. package/src/kagent/runtime/presentation.py +205 -0
  80. package/src/kagent/runtime/redaction.py +130 -0
  81. package/src/kagent/runtime/sandbox.py +331 -0
  82. package/src/kagent/runtime/skills.py +66 -0
  83. package/src/kagent/runtime/steering.py +56 -0
  84. package/src/kagent/runtime/steps.py +255 -0
  85. package/src/kagent/runtime/task_state.py +40 -0
  86. package/src/kagent/runtime/tools.py +3532 -0
  87. package/src/kagent/runtime/types.py +240 -0
  88. package/src/kagent/runtime/workspace.py +597 -0
  89. package/src/kagent/service/__init__.py +26 -0
  90. package/src/kagent/service/__main__.py +6 -0
  91. package/src/kagent/service/active_runs.py +178 -0
  92. package/src/kagent/service/cli.py +737 -0
  93. package/src/kagent/service/contract.py +2571 -0
  94. package/src/kagent/service/errors.py +71 -0
  95. package/src/kagent/service/idempotency.py +584 -0
  96. package/src/kagent/service/router.py +884 -0
  97. package/src/kagent/service/run.py +150 -0
  98. package/src/kagent/service/runtime.py +1915 -0
  99. package/src/kagent/service/runtime_approval.py +20 -0
  100. package/src/kagent/service/runtime_cancel.py +192 -0
  101. package/src/kagent/service/runtime_lifecycle.py +229 -0
  102. package/src/kagent/service/runtime_metadata.py +21 -0
  103. package/src/kagent/service/runtime_policy.py +115 -0
  104. package/src/kagent/service/runtime_recovery.py +573 -0
  105. package/src/kagent/service/runtime_resume.py +382 -0
  106. package/src/kagent/service/runtime_resume_claim.py +116 -0
  107. package/src/kagent/service/runtime_run.py +350 -0
  108. package/src/kagent/service/runtime_status.py +2007 -0
  109. package/src/kagent/service/safety.py +139 -0
  110. package/src/kagent/service/server.py +114 -0
  111. package/src/kagent/service/status.py +233 -0
  112. package/src/kagent/service/trace_store.py +322 -0
  113. package/src/kagent/service/transport.py +24 -0
  114. package/src/kagent/utils/__init__.py +0 -0
  115. package/src/kagent/utils/config_validation.py +21 -0
  116. package/src/kagent/utils/json_output.py +23 -0
  117. package/src/kagent/utils/paths.py +473 -0
@@ -0,0 +1,877 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import re
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from kagent import __version__
12
+ from kagent.ops.release_manifest import verify_release_manifest
13
+
14
+ PACKAGE_NAME = "kagent"
15
+ SECRET_LIKE_KEYS = (
16
+ "api_key",
17
+ "apikey",
18
+ "authorization",
19
+ "bearer",
20
+ "password",
21
+ "secret",
22
+ "token",
23
+ )
24
+ SECRET_KEY_ALLOWLIST = {
25
+ "llm_api_key_configured",
26
+ }
27
+ SECRET_VALUE_PATTERNS = (
28
+ re.compile(r"sk-[A-Za-z0-9:_-]{6,}"),
29
+ re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._:/+=-]{6,}"),
30
+ re.compile(r"(?i)\bauthorization\s*:\s*[A-Za-z0-9._:/+=-]{6,}"),
31
+ )
32
+ SENSITIVE_URL_VALUE_PATTERN = re.compile(r"(?i)\bhttps?://[^\s\"'<>]+")
33
+ REQUIRED_PROVIDER_SMOKE_RUN_IDS = (
34
+ "approval_run_id",
35
+ "cli_run_id",
36
+ "http_run_id",
37
+ "resumed_run_id",
38
+ )
39
+ REQUIRED_PROVIDER_SMOKE_CAPABILITIES = (
40
+ "approval_resume",
41
+ "cli_runtime",
42
+ "http_runtime",
43
+ "metrics",
44
+ "timeline",
45
+ "trace_status",
46
+ )
47
+ REQUIRED_PROVIDER_SMOKE_SNAPSHOT_FIELDS = (
48
+ "llm_api_key_configured",
49
+ "llm_base_url_host",
50
+ "llm_model",
51
+ "llm_provider",
52
+ )
53
+ REQUIRED_STAGING_ACCEPTANCE_FIELDS = (
54
+ "auth_subject",
55
+ "base_url_host",
56
+ "runtime_policy_source",
57
+ "runtime_run_id",
58
+ )
59
+ REQUIRED_STAGING_ACCEPTANCE_VALUES = {
60
+ "health_status": "ok",
61
+ "metrics_trace_persistence": "enabled",
62
+ "ready_status": "ready",
63
+ "runtime_http_request_approval_required": "true",
64
+ "runtime_note_allowed": "true",
65
+ "runtime_run_status": "done",
66
+ }
67
+ REQUIRED_STAGING_ACCEPTANCE_POSITIVE_INTS = (
68
+ "metrics_runtime_runs_total",
69
+ "runtime_effective_tool_policy_count",
70
+ "runtime_summary_run_count",
71
+ "runtime_timeline_event_count",
72
+ )
73
+ REQUIRED_OBSERVABILITY_ACCEPTANCE_FIELDS = (
74
+ "base_url_host",
75
+ "metrics_endpoint",
76
+ )
77
+ REQUIRED_OBSERVABILITY_ACCEPTANCE_VALUES = {
78
+ "grafana_dashboard_status": "passed",
79
+ "metrics_status": "200",
80
+ "prometheus_rules_status": "passed",
81
+ "required_metrics_present": "true",
82
+ }
83
+ REQUIRED_OBSERVABILITY_ACCEPTANCE_METRICS = (
84
+ "kagent_build_info",
85
+ "kagent_request_duration_seconds_bucket",
86
+ "kagent_requests_total",
87
+ "kagent_responses_total",
88
+ "kagent_runtime_approval_required_total",
89
+ "kagent_runtime_progress_event_sink_failures_total",
90
+ "kagent_runtime_hook_failures_total",
91
+ "kagent_runtime_reconciliation_runs_total",
92
+ "kagent_runtime_reconciliation_traces_scanned_total",
93
+ "kagent_runtime_reconciliation_outcomes_total",
94
+ "kagent_runtime_reconciliation_errors_total",
95
+ "kagent_runtime_planner_attempts_total",
96
+ "kagent_runtime_planner_failures_total",
97
+ "kagent_runtime_llm_provider_requests_total",
98
+ "kagent_runtime_llm_provider_requests_by_status_total",
99
+ "kagent_runtime_approvals_by_auth_subject_total",
100
+ "kagent_runtime_run_duration_seconds_bucket",
101
+ "kagent_runtime_run_lifecycle_state_by_auth_subject_total",
102
+ "kagent_runtime_run_lifecycle_state_total",
103
+ "kagent_runtime_tool_executions_total",
104
+ "kagent_runtime_runs_by_auth_subject_total",
105
+ "kagent_runtime_runs_total",
106
+ "kagent_runtime_stale_pending_approvals_current",
107
+ )
108
+ REQUIRED_OBSERVABILITY_ACCEPTANCE_METRICS_SHA256 = hashlib.sha256(
109
+ "\n".join(sorted(REQUIRED_OBSERVABILITY_ACCEPTANCE_METRICS)).encode("utf-8")
110
+ ).hexdigest()
111
+ REQUIRED_OBSERVABILITY_ACCEPTANCE_SHA_FIELDS = (
112
+ "grafana_dashboard_sha256",
113
+ "metrics_sha256",
114
+ "prometheus_rules_sha256",
115
+ )
116
+ MIN_OBSERVABILITY_ACCEPTANCE_REQUIRED_METRIC_COUNT = len(
117
+ REQUIRED_OBSERVABILITY_ACCEPTANCE_METRICS
118
+ )
119
+ REQUIRED_INTERNAL_ROLLOUT_FIELDS = (
120
+ "environment",
121
+ "expected_environment",
122
+ "expected_release_version",
123
+ "release_version",
124
+ "rollout_id",
125
+ "signed_off_at_utc",
126
+ )
127
+ REQUIRED_INTERNAL_ROLLOUT_TRUE_FIELDS = (
128
+ "environment_matches",
129
+ "required_checks_passed",
130
+ "required_roles_present",
131
+ "version_matches",
132
+ )
133
+
134
+
135
+ def build_release_evidence(
136
+ *,
137
+ run_checks_exit_code: int,
138
+ readiness_audit_path: Path,
139
+ release_manifest_path: Optional[Path] = None,
140
+ provider_smoke_evidence_path: Optional[Path] = None,
141
+ staging_acceptance_evidence_path: Optional[Path] = None,
142
+ observability_acceptance_evidence_path: Optional[Path] = None,
143
+ internal_rollout_evidence_path: Optional[Path] = None,
144
+ require_provider_smoke: bool = False,
145
+ require_staging_acceptance: bool = False,
146
+ require_observability_acceptance: bool = False,
147
+ require_internal_rollout: bool = False,
148
+ ) -> Dict[str, Any]:
149
+ readiness_audit = _read_json_file(readiness_audit_path)
150
+ release_manifest = (
151
+ verify_release_manifest(release_manifest_path)
152
+ if release_manifest_path is not None
153
+ else {"status": "not_provided"}
154
+ )
155
+ provider_smoke = (
156
+ _provider_smoke_record(provider_smoke_evidence_path)
157
+ if provider_smoke_evidence_path is not None
158
+ else {"status": "not_provided"}
159
+ )
160
+ staging_acceptance = (
161
+ _staging_acceptance_record(staging_acceptance_evidence_path)
162
+ if staging_acceptance_evidence_path is not None
163
+ else {"status": "not_provided"}
164
+ )
165
+ observability_acceptance = (
166
+ _observability_acceptance_record(observability_acceptance_evidence_path)
167
+ if observability_acceptance_evidence_path is not None
168
+ else {"status": "not_provided"}
169
+ )
170
+ internal_rollout = (
171
+ _internal_rollout_record(internal_rollout_evidence_path)
172
+ if internal_rollout_evidence_path is not None
173
+ else {"status": "not_provided"}
174
+ )
175
+ evidence_secret_findings = _external_evidence_secret_findings(
176
+ {
177
+ "provider_smoke": provider_smoke_evidence_path,
178
+ "staging_acceptance": staging_acceptance_evidence_path,
179
+ "observability_acceptance": observability_acceptance_evidence_path,
180
+ "internal_rollout": internal_rollout_evidence_path,
181
+ }
182
+ )
183
+ runtime_policy_fingerprint_mismatch = _runtime_policy_fingerprint_mismatch(
184
+ {
185
+ "provider_smoke": provider_smoke,
186
+ "staging_acceptance": staging_acceptance,
187
+ "internal_rollout": internal_rollout,
188
+ }
189
+ )
190
+ failed_checks = _failed_checks(
191
+ run_checks_exit_code=run_checks_exit_code,
192
+ readiness_audit=readiness_audit,
193
+ release_manifest=release_manifest,
194
+ provider_smoke=provider_smoke,
195
+ staging_acceptance=staging_acceptance,
196
+ observability_acceptance=observability_acceptance,
197
+ internal_rollout=internal_rollout,
198
+ evidence_secret_findings=evidence_secret_findings,
199
+ runtime_policy_fingerprint_mismatch=runtime_policy_fingerprint_mismatch,
200
+ require_provider_smoke=require_provider_smoke,
201
+ require_staging_acceptance=require_staging_acceptance,
202
+ require_observability_acceptance=require_observability_acceptance,
203
+ require_internal_rollout=require_internal_rollout,
204
+ )
205
+ evidence_files = {
206
+ "readiness_audit": _file_record(readiness_audit_path),
207
+ }
208
+ if release_manifest_path is not None:
209
+ evidence_files["release_manifest"] = _file_record(release_manifest_path)
210
+ if provider_smoke_evidence_path is not None:
211
+ evidence_files["provider_smoke"] = _file_record(provider_smoke_evidence_path)
212
+ if staging_acceptance_evidence_path is not None:
213
+ evidence_files["staging_acceptance"] = _file_record(
214
+ staging_acceptance_evidence_path
215
+ )
216
+ if observability_acceptance_evidence_path is not None:
217
+ evidence_files["observability_acceptance"] = _file_record(
218
+ observability_acceptance_evidence_path
219
+ )
220
+ if internal_rollout_evidence_path is not None:
221
+ evidence_files["internal_rollout"] = _file_record(
222
+ internal_rollout_evidence_path
223
+ )
224
+
225
+ return {
226
+ "package": PACKAGE_NAME,
227
+ "version": __version__,
228
+ "generated_at_utc": _utc_timestamp(),
229
+ "status": "blocked" if failed_checks else "ready",
230
+ "summary": {
231
+ "failed_checks": failed_checks,
232
+ "evidence_file_count": str(len(evidence_files)),
233
+ "evidence_secret_findings": evidence_secret_findings,
234
+ "runtime_policy_fingerprint_mismatch": (
235
+ runtime_policy_fingerprint_mismatch
236
+ ),
237
+ },
238
+ "run_checks": _run_checks_record(run_checks_exit_code),
239
+ "readiness_audit": _readiness_audit_record(readiness_audit),
240
+ "release_manifest": release_manifest,
241
+ "provider_smoke": provider_smoke,
242
+ "staging_acceptance": staging_acceptance,
243
+ "observability_acceptance": observability_acceptance,
244
+ "internal_rollout": internal_rollout,
245
+ "evidence_files": evidence_files,
246
+ }
247
+
248
+
249
+ def main(argv: Optional[List[str]] = None) -> None:
250
+ parser = argparse.ArgumentParser(
251
+ description="Generate a redacted JSON release evidence bundle."
252
+ )
253
+ parser.add_argument(
254
+ "--run-checks-exit-code",
255
+ required=True,
256
+ type=int,
257
+ help="Exit code from scripts/run_checks.sh.",
258
+ )
259
+ parser.add_argument(
260
+ "--readiness-audit",
261
+ required=True,
262
+ metavar="PATH",
263
+ help="JSON output from scripts/production_readiness_audit.py.",
264
+ )
265
+ parser.add_argument(
266
+ "--release-manifest",
267
+ default="",
268
+ metavar="PATH",
269
+ help="Verified release manifest JSON generated by release_manifest.",
270
+ )
271
+ parser.add_argument(
272
+ "--provider-smoke-evidence",
273
+ default="",
274
+ metavar="PATH",
275
+ help="Redacted JSON output from scripts/smoke_real_llm_runtime.sh.",
276
+ )
277
+ parser.add_argument(
278
+ "--require-provider-smoke",
279
+ action="store_true",
280
+ help="Block release evidence when provider smoke evidence is missing.",
281
+ )
282
+ parser.add_argument(
283
+ "--staging-acceptance-evidence",
284
+ default="",
285
+ metavar="PATH",
286
+ help="Redacted JSON output from scripts/staging_acceptance.sh.",
287
+ )
288
+ parser.add_argument(
289
+ "--require-staging-acceptance",
290
+ action="store_true",
291
+ help="Block release evidence when staging acceptance evidence is missing.",
292
+ )
293
+ parser.add_argument(
294
+ "--observability-acceptance-evidence",
295
+ default="",
296
+ metavar="PATH",
297
+ help="Redacted JSON output from scripts/observability_acceptance.sh.",
298
+ )
299
+ parser.add_argument(
300
+ "--require-observability-acceptance",
301
+ action="store_true",
302
+ help="Block release evidence when observability acceptance evidence is missing.",
303
+ )
304
+ parser.add_argument(
305
+ "--internal-rollout-evidence",
306
+ default="",
307
+ metavar="PATH",
308
+ help="Redacted JSON output from scripts/internal_rollout_acceptance.py.",
309
+ )
310
+ parser.add_argument(
311
+ "--require-internal-rollout",
312
+ action="store_true",
313
+ help="Block release evidence when internal rollout sign-off evidence is missing.",
314
+ )
315
+ parser.add_argument("--output", default="", metavar="PATH", help="Write bundle JSON to PATH.")
316
+ args = parser.parse_args(argv)
317
+
318
+ try:
319
+ payload = build_release_evidence(
320
+ run_checks_exit_code=args.run_checks_exit_code,
321
+ readiness_audit_path=Path(args.readiness_audit),
322
+ release_manifest_path=Path(args.release_manifest) if args.release_manifest else None,
323
+ provider_smoke_evidence_path=(
324
+ Path(args.provider_smoke_evidence) if args.provider_smoke_evidence else None
325
+ ),
326
+ staging_acceptance_evidence_path=(
327
+ Path(args.staging_acceptance_evidence)
328
+ if args.staging_acceptance_evidence
329
+ else None
330
+ ),
331
+ observability_acceptance_evidence_path=(
332
+ Path(args.observability_acceptance_evidence)
333
+ if args.observability_acceptance_evidence
334
+ else None
335
+ ),
336
+ internal_rollout_evidence_path=(
337
+ Path(args.internal_rollout_evidence)
338
+ if args.internal_rollout_evidence
339
+ else None
340
+ ),
341
+ require_provider_smoke=args.require_provider_smoke,
342
+ require_staging_acceptance=args.require_staging_acceptance,
343
+ require_observability_acceptance=args.require_observability_acceptance,
344
+ require_internal_rollout=args.require_internal_rollout,
345
+ )
346
+ output = json.dumps(payload, indent=2, sort_keys=True) + "\n"
347
+ if args.output:
348
+ Path(args.output).write_text(output, encoding="utf-8")
349
+ except (json.JSONDecodeError, OSError) as exc:
350
+ parser.error(str(exc))
351
+ print(output, end="")
352
+ if payload["status"] != "ready":
353
+ raise SystemExit(1)
354
+
355
+
356
+ def _failed_checks(
357
+ *,
358
+ run_checks_exit_code: int,
359
+ readiness_audit: Dict[str, Any],
360
+ release_manifest: Dict[str, Any],
361
+ provider_smoke: Dict[str, Any],
362
+ staging_acceptance: Dict[str, Any],
363
+ observability_acceptance: Dict[str, Any],
364
+ internal_rollout: Dict[str, Any],
365
+ evidence_secret_findings: List[Dict[str, str]],
366
+ runtime_policy_fingerprint_mismatch: Dict[str, str],
367
+ require_provider_smoke: bool,
368
+ require_staging_acceptance: bool,
369
+ require_observability_acceptance: bool,
370
+ require_internal_rollout: bool,
371
+ ) -> List[str]:
372
+ failed = []
373
+ if run_checks_exit_code != 0:
374
+ failed.append("run_checks_failed")
375
+ if str(readiness_audit.get("status", "")) != "passed":
376
+ failed.append("readiness_audit_failed")
377
+ if release_manifest["status"] not in {"verified", "not_provided"}:
378
+ failed.append("release_manifest_failed")
379
+ if provider_smoke["status"] not in {"passed", "not_provided"}:
380
+ failed.append("provider_smoke_failed")
381
+ if require_provider_smoke and provider_smoke["status"] != "passed":
382
+ failed.append(f"provider_smoke_{provider_smoke['status']}")
383
+ if staging_acceptance["status"] not in {"passed", "not_provided"}:
384
+ failed.append("staging_acceptance_failed")
385
+ if require_staging_acceptance and staging_acceptance["status"] != "passed":
386
+ failed.append(f"staging_acceptance_{staging_acceptance['status']}")
387
+ if observability_acceptance["status"] not in {"passed", "not_provided"}:
388
+ failed.append("observability_acceptance_failed")
389
+ if (
390
+ require_observability_acceptance
391
+ and observability_acceptance["status"] != "passed"
392
+ ):
393
+ failed.append(
394
+ f"observability_acceptance_{observability_acceptance['status']}"
395
+ )
396
+ if internal_rollout["status"] not in {"passed", "not_provided"}:
397
+ failed.append("internal_rollout_failed")
398
+ if require_internal_rollout and internal_rollout["status"] != "passed":
399
+ failed.append(f"internal_rollout_{internal_rollout['status']}")
400
+ if evidence_secret_findings:
401
+ failed.append("evidence_secret_detected")
402
+ if runtime_policy_fingerprint_mismatch:
403
+ failed.append("runtime_policy_fingerprint_mismatch")
404
+ return failed
405
+
406
+
407
+ def _run_checks_record(exit_code: int) -> Dict[str, str]:
408
+ return {
409
+ "status": "passed" if exit_code == 0 else "failed",
410
+ "exit_code": str(exit_code),
411
+ }
412
+
413
+
414
+ def _readiness_audit_record(payload: Dict[str, Any]) -> Dict[str, Any]:
415
+ summary = payload.get("summary", {})
416
+ return {
417
+ "status": str(payload.get("status", "unknown")),
418
+ "failed_checks": _string_list(summary.get("failed_checks", [])),
419
+ "missing_artifacts": _string_list(summary.get("missing_artifacts", [])),
420
+ }
421
+
422
+
423
+ def _provider_smoke_record(path: Path) -> Dict[str, Any]:
424
+ payload = _read_json_file(path)
425
+ status = str(payload.get("status", "unknown"))
426
+ missing_fields = (
427
+ _provider_smoke_missing_fields(payload) if status == "passed" else []
428
+ )
429
+ if missing_fields:
430
+ status = "invalid_evidence"
431
+ return {
432
+ "status": status,
433
+ "evidence_schema_version": str(
434
+ payload.get("evidence_schema_version", "")
435
+ ),
436
+ "provider_snapshot": _provider_snapshot_record(payload),
437
+ "capability_checks": _string_map(payload.get("capability_checks", {})),
438
+ "runtime_effective_tool_policy_sha256": str(
439
+ payload.get("runtime_effective_tool_policy_sha256", "")
440
+ ),
441
+ "run_ids": {
442
+ key: str(payload.get(key, ""))
443
+ for key in (
444
+ "approval_run_id",
445
+ "cli_run_id",
446
+ "http_run_id",
447
+ "resumed_run_id",
448
+ )
449
+ if str(payload.get(key, "")).strip()
450
+ },
451
+ "missing_fields": missing_fields,
452
+ }
453
+
454
+
455
+ def _staging_acceptance_record(path: Path) -> Dict[str, Any]:
456
+ payload = _read_json_file(path)
457
+ status = str(payload.get("status", "unknown"))
458
+ missing_fields = (
459
+ _staging_acceptance_missing_fields(payload) if status == "passed" else []
460
+ )
461
+ if missing_fields:
462
+ status = "invalid_evidence"
463
+ return {
464
+ "evidence_schema_version": str(
465
+ payload.get("evidence_schema_version", "")
466
+ ),
467
+ "status": status,
468
+ "base_url_host": str(payload.get("base_url_host", "")),
469
+ "health_status": str(payload.get("health_status", "")),
470
+ "ready_status": str(payload.get("ready_status", "")),
471
+ "runtime_run_id": str(payload.get("runtime_run_id", "")),
472
+ "auth_subject": str(payload.get("auth_subject", "")),
473
+ "runtime_policy_source": str(payload.get("runtime_policy_source", "")),
474
+ "runtime_effective_tool_policy_count": str(
475
+ payload.get("runtime_effective_tool_policy_count", "")
476
+ ),
477
+ "runtime_effective_tool_policy_sha256": str(
478
+ payload.get("runtime_effective_tool_policy_sha256", "")
479
+ ),
480
+ "runtime_note_allowed": str(payload.get("runtime_note_allowed", "")),
481
+ "runtime_http_request_approval_required": str(
482
+ payload.get("runtime_http_request_approval_required", "")
483
+ ),
484
+ "runtime_run_status": str(payload.get("runtime_run_status", "")),
485
+ "runtime_timeline_event_count": str(
486
+ payload.get("runtime_timeline_event_count", "")
487
+ ),
488
+ "runtime_summary_run_count": str(
489
+ payload.get("runtime_summary_run_count", "")
490
+ ),
491
+ "metrics_trace_persistence": str(
492
+ payload.get("metrics_trace_persistence", "")
493
+ ),
494
+ "metrics_runtime_runs_total": str(
495
+ payload.get("metrics_runtime_runs_total", "")
496
+ ),
497
+ "missing_fields": missing_fields,
498
+ }
499
+
500
+
501
+ def _observability_acceptance_record(path: Path) -> Dict[str, Any]:
502
+ payload = _read_json_file(path)
503
+ status = str(payload.get("status", "unknown"))
504
+ missing_fields = (
505
+ _observability_acceptance_missing_fields(payload)
506
+ if status == "passed"
507
+ else []
508
+ )
509
+ if missing_fields:
510
+ status = "invalid_evidence"
511
+ return {
512
+ "evidence_schema_version": str(
513
+ payload.get("evidence_schema_version", "")
514
+ ),
515
+ "status": status,
516
+ "base_url_host": str(payload.get("base_url_host", "")),
517
+ "metrics_endpoint": str(payload.get("metrics_endpoint", "")),
518
+ "metrics_status": str(payload.get("metrics_status", "")),
519
+ "required_metrics_present": str(payload.get("required_metrics_present", "")),
520
+ "required_metric_count": str(payload.get("required_metric_count", "")),
521
+ "missing_required_metrics": _string_list(
522
+ payload.get("missing_required_metrics")
523
+ ),
524
+ "required_metrics_sha256": str(payload.get("required_metrics_sha256", "")),
525
+ "metrics_sha256": str(payload.get("metrics_sha256", "")),
526
+ "grafana_dashboard_status": str(
527
+ payload.get("grafana_dashboard_status", "")
528
+ ),
529
+ "grafana_dashboard_sha256": str(
530
+ payload.get("grafana_dashboard_sha256", "")
531
+ ),
532
+ "prometheus_rules_status": str(payload.get("prometheus_rules_status", "")),
533
+ "prometheus_rules_sha256": str(
534
+ payload.get("prometheus_rules_sha256", "")
535
+ ),
536
+ "prometheus_query_status": str(
537
+ payload.get("prometheus_query_status", "")
538
+ ),
539
+ "prometheus_result_count": str(
540
+ payload.get("prometheus_result_count", "")
541
+ ),
542
+ "missing_fields": missing_fields,
543
+ }
544
+
545
+
546
+ def _internal_rollout_record(path: Path) -> Dict[str, Any]:
547
+ payload = _read_json_file(path)
548
+ status = str(payload.get("status", "unknown"))
549
+ missing_fields = (
550
+ _internal_rollout_missing_fields(payload) if status == "passed" else []
551
+ )
552
+ if missing_fields:
553
+ status = "invalid_evidence"
554
+ return {
555
+ "evidence_schema_version": str(
556
+ payload.get("evidence_schema_version", "")
557
+ ),
558
+ "status": status,
559
+ "rollout_id": str(payload.get("rollout_id", "")),
560
+ "release_version": str(payload.get("release_version", "")),
561
+ "environment": str(payload.get("environment", "")),
562
+ "signed_off_at_utc": str(payload.get("signed_off_at_utc", "")),
563
+ "runtime_effective_tool_policy_sha256": str(
564
+ payload.get("runtime_effective_tool_policy_sha256", "")
565
+ ),
566
+ "required_roles_present": str(payload.get("required_roles_present", "")),
567
+ "required_checks_passed": str(payload.get("required_checks_passed", "")),
568
+ "approver_role_count": str(payload.get("approver_role_count", "")),
569
+ "expected_release_version": str(
570
+ payload.get("expected_release_version", "")
571
+ ),
572
+ "version_matches": str(payload.get("version_matches", "")),
573
+ "expected_environment": str(payload.get("expected_environment", "")),
574
+ "environment_matches": str(payload.get("environment_matches", "")),
575
+ "sha256": str(payload.get("sha256", "")),
576
+ "missing_fields": missing_fields,
577
+ }
578
+
579
+
580
+ def _runtime_policy_fingerprint_mismatch(
581
+ records_by_label: Dict[str, Dict[str, Any]],
582
+ ) -> Dict[str, str]:
583
+ fingerprints = {
584
+ label: str(record.get("runtime_effective_tool_policy_sha256", ""))
585
+ for label, record in records_by_label.items()
586
+ if record.get("status") == "passed"
587
+ and _is_sha256(str(record.get("runtime_effective_tool_policy_sha256", "")))
588
+ }
589
+ if len(set(fingerprints.values())) <= 1:
590
+ return {}
591
+ return fingerprints
592
+
593
+
594
+ def _provider_snapshot_record(payload: Dict[str, Any]) -> Dict[str, str]:
595
+ provider_snapshot = payload.get("provider_snapshot", {})
596
+ if not isinstance(provider_snapshot, dict):
597
+ provider_snapshot = {}
598
+ return {
599
+ field: str(provider_snapshot.get(field, ""))
600
+ for field in REQUIRED_PROVIDER_SMOKE_SNAPSHOT_FIELDS
601
+ if str(provider_snapshot.get(field, "")).strip()
602
+ }
603
+
604
+
605
+ def _file_record(path: Path) -> Dict[str, str]:
606
+ data = path.read_bytes()
607
+ return {
608
+ "path": str(path),
609
+ "file_name": path.name,
610
+ "size_bytes": str(len(data)),
611
+ "sha256": hashlib.sha256(data).hexdigest(),
612
+ }
613
+
614
+
615
+ def _read_json_file(path: Path) -> Dict[str, Any]:
616
+ payload = json.loads(path.read_text(encoding="utf-8"))
617
+ if not isinstance(payload, dict):
618
+ raise OSError(f"{path} must contain a JSON object")
619
+ return payload
620
+
621
+
622
+ def _provider_smoke_missing_fields(payload: Dict[str, Any]) -> List[str]:
623
+ missing = []
624
+ if str(payload.get("evidence_schema_version", "")) != "1":
625
+ missing.append("evidence_schema_version")
626
+ for field in REQUIRED_PROVIDER_SMOKE_RUN_IDS:
627
+ if not str(payload.get(field, "")).strip():
628
+ missing.append(field)
629
+ provider_snapshot = payload.get("provider_snapshot", {})
630
+ if not isinstance(provider_snapshot, dict):
631
+ provider_snapshot = {}
632
+ for field in REQUIRED_PROVIDER_SMOKE_SNAPSHOT_FIELDS:
633
+ if not str(provider_snapshot.get(field, "")).strip():
634
+ missing.append(f"provider_snapshot.{field}")
635
+ capability_checks = payload.get("capability_checks", {})
636
+ if not isinstance(capability_checks, dict):
637
+ capability_checks = {}
638
+ for field in REQUIRED_PROVIDER_SMOKE_CAPABILITIES:
639
+ if str(capability_checks.get(field, "")) != "passed":
640
+ missing.append(f"capability_checks.{field}")
641
+ if not _is_sha256(str(payload.get("runtime_effective_tool_policy_sha256", ""))):
642
+ missing.append("runtime_effective_tool_policy_sha256")
643
+ return sorted(missing)
644
+
645
+
646
+ def _staging_acceptance_missing_fields(payload: Dict[str, Any]) -> List[str]:
647
+ missing = _schema_version_missing_fields(payload)
648
+ missing.extend(_missing_string_fields(payload, REQUIRED_STAGING_ACCEPTANCE_FIELDS))
649
+ missing.extend(_missing_value_fields(payload, REQUIRED_STAGING_ACCEPTANCE_VALUES))
650
+ missing.extend(
651
+ _missing_positive_int_fields(
652
+ payload,
653
+ REQUIRED_STAGING_ACCEPTANCE_POSITIVE_INTS,
654
+ )
655
+ )
656
+ if not _is_sha256(str(payload.get("runtime_effective_tool_policy_sha256", ""))):
657
+ missing.append("runtime_effective_tool_policy_sha256")
658
+ return sorted(missing)
659
+
660
+
661
+ def _observability_acceptance_missing_fields(payload: Dict[str, Any]) -> List[str]:
662
+ missing = _schema_version_missing_fields(payload)
663
+ missing.extend(_missing_string_fields(
664
+ payload,
665
+ REQUIRED_OBSERVABILITY_ACCEPTANCE_FIELDS,
666
+ ))
667
+ missing.extend(
668
+ _missing_value_fields(payload, REQUIRED_OBSERVABILITY_ACCEPTANCE_VALUES)
669
+ )
670
+ missing.extend(
671
+ _missing_positive_int_fields(
672
+ payload,
673
+ ("required_metric_count",),
674
+ )
675
+ )
676
+ try:
677
+ required_metric_count = int(str(payload.get("required_metric_count", "")))
678
+ except ValueError:
679
+ required_metric_count = 0
680
+ if (
681
+ required_metric_count < MIN_OBSERVABILITY_ACCEPTANCE_REQUIRED_METRIC_COUNT
682
+ and "required_metric_count" not in missing
683
+ ):
684
+ missing.append("required_metric_count")
685
+ missing_required_metrics = payload.get("missing_required_metrics")
686
+ if (
687
+ not isinstance(missing_required_metrics, list)
688
+ or len(missing_required_metrics) > 0
689
+ ):
690
+ missing.append("missing_required_metrics")
691
+ if (
692
+ str(payload.get("required_metrics_sha256", ""))
693
+ != REQUIRED_OBSERVABILITY_ACCEPTANCE_METRICS_SHA256
694
+ ):
695
+ missing.append("required_metrics_sha256")
696
+ for field in REQUIRED_OBSERVABILITY_ACCEPTANCE_SHA_FIELDS:
697
+ if not _is_sha256(str(payload.get(field, ""))):
698
+ missing.append(field)
699
+ prometheus_query_status = str(payload.get("prometheus_query_status", ""))
700
+ if prometheus_query_status not in {"passed", "not_configured"}:
701
+ missing.append("prometheus_query_status")
702
+ elif prometheus_query_status == "passed" and not _is_positive_int(
703
+ payload.get("prometheus_result_count", "")
704
+ ):
705
+ missing.append("prometheus_result_count")
706
+ return sorted(missing)
707
+
708
+
709
+ def _internal_rollout_missing_fields(payload: Dict[str, Any]) -> List[str]:
710
+ missing = _schema_version_missing_fields(payload)
711
+ missing.extend(_missing_string_fields(payload, REQUIRED_INTERNAL_ROLLOUT_FIELDS))
712
+ missing.extend(
713
+ _missing_value_fields(
714
+ payload,
715
+ {field: "true" for field in REQUIRED_INTERNAL_ROLLOUT_TRUE_FIELDS},
716
+ )
717
+ )
718
+ if not _is_positive_int(payload.get("approver_role_count", "")):
719
+ missing.append("approver_role_count")
720
+ if not _is_sha256(str(payload.get("runtime_effective_tool_policy_sha256", ""))):
721
+ missing.append("runtime_effective_tool_policy_sha256")
722
+ if not _is_sha256(str(payload.get("sha256", ""))):
723
+ missing.append("sha256")
724
+ return sorted(missing)
725
+
726
+
727
+ def _missing_string_fields(
728
+ payload: Dict[str, Any],
729
+ fields: tuple[str, ...],
730
+ ) -> List[str]:
731
+ return [field for field in fields if not str(payload.get(field, "")).strip()]
732
+
733
+
734
+ def _schema_version_missing_fields(payload: Dict[str, Any]) -> List[str]:
735
+ if str(payload.get("evidence_schema_version", "")) != "1":
736
+ return ["evidence_schema_version"]
737
+ return []
738
+
739
+
740
+ def _missing_value_fields(
741
+ payload: Dict[str, Any],
742
+ expected_values: Dict[str, str],
743
+ ) -> List[str]:
744
+ return [
745
+ field
746
+ for field, expected in expected_values.items()
747
+ if str(payload.get(field, "")) != expected
748
+ ]
749
+
750
+
751
+ def _missing_positive_int_fields(
752
+ payload: Dict[str, Any],
753
+ fields: tuple[str, ...],
754
+ ) -> List[str]:
755
+ return [field for field in fields if not _is_positive_int(payload.get(field, ""))]
756
+
757
+
758
+ def _is_positive_int(value: Any) -> bool:
759
+ try:
760
+ return int(str(value)) > 0
761
+ except ValueError:
762
+ return False
763
+
764
+
765
+ def _is_sha256(value: str) -> bool:
766
+ return bool(re.fullmatch(r"[0-9a-f]{64}", value))
767
+
768
+
769
+ def _external_evidence_secret_findings(
770
+ paths_by_label: Dict[str, Optional[Path]],
771
+ ) -> List[Dict[str, str]]:
772
+ findings: List[Dict[str, str]] = []
773
+ for label, path in paths_by_label.items():
774
+ if path is None or not path.is_file():
775
+ continue
776
+ try:
777
+ payload = json.loads(path.read_text(encoding="utf-8"))
778
+ except json.JSONDecodeError:
779
+ continue
780
+ findings.extend(_secret_findings(label, "$", payload))
781
+ return findings
782
+
783
+
784
+ def _secret_findings(label: str, path: str, value: Any) -> List[Dict[str, str]]:
785
+ findings: List[Dict[str, str]] = []
786
+ if isinstance(value, dict):
787
+ for key, item in value.items():
788
+ child_path = f"{path}.{_json_path_key(str(key))}"
789
+ if _is_secret_like_key(str(key), item):
790
+ findings.append(
791
+ {
792
+ "label": label,
793
+ "path": child_path,
794
+ "reason": "secret_like_key",
795
+ }
796
+ )
797
+ findings.extend(_secret_findings(label, child_path, item))
798
+ elif isinstance(value, list):
799
+ for index, item in enumerate(value):
800
+ findings.extend(_secret_findings(label, f"{path}[{index}]", item))
801
+ elif isinstance(value, str):
802
+ if _is_secret_like_value(value):
803
+ findings.append(
804
+ {
805
+ "label": label,
806
+ "path": path,
807
+ "reason": "secret_like_value",
808
+ }
809
+ )
810
+ if _is_sensitive_url_value(value):
811
+ findings.append(
812
+ {
813
+ "label": label,
814
+ "path": path,
815
+ "reason": "sensitive_url_value",
816
+ }
817
+ )
818
+ return findings
819
+
820
+
821
+ def _is_secret_like_key(key: str, value: Any) -> bool:
822
+ normalized = key.lower().replace("-", "_")
823
+ if normalized in SECRET_KEY_ALLOWLIST:
824
+ return False
825
+ if not _has_present_secret_like_key_value(value):
826
+ return False
827
+ return any(token in normalized for token in SECRET_LIKE_KEYS)
828
+
829
+
830
+ def _has_present_secret_like_key_value(value: Any) -> bool:
831
+ if value is None:
832
+ return False
833
+ if isinstance(value, str):
834
+ return bool(value.strip())
835
+ if isinstance(value, (list, dict)):
836
+ return bool(value)
837
+ if isinstance(value, bool):
838
+ return value
839
+ return True
840
+
841
+
842
+ def _is_secret_like_value(value: str) -> bool:
843
+ return any(pattern.search(value) for pattern in SECRET_VALUE_PATTERNS)
844
+
845
+
846
+ def _is_sensitive_url_value(value: str) -> bool:
847
+ return bool(SENSITIVE_URL_VALUE_PATTERN.search(value))
848
+
849
+
850
+ def _json_path_key(key: str) -> str:
851
+ if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key):
852
+ return key
853
+ return json.dumps(key)
854
+
855
+
856
+ def _string_list(value: Any) -> List[str]:
857
+ if not isinstance(value, list):
858
+ return []
859
+ return [str(item) for item in value]
860
+
861
+
862
+ def _string_map(value: Any) -> Dict[str, str]:
863
+ if not isinstance(value, dict):
864
+ return {}
865
+ return {
866
+ str(key): str(item)
867
+ for key, item in value.items()
868
+ if isinstance(key, str) and isinstance(item, (str, int, float, bool))
869
+ }
870
+
871
+
872
+ def _utc_timestamp() -> str:
873
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
874
+
875
+
876
+ if __name__ == "__main__":
877
+ main()