platformforge 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (659) hide show
  1. platformforge/__init__.py +7 -0
  2. platformforge/__main__.py +5 -0
  3. platformforge/_forge_portal.py +913 -0
  4. platformforge/_graphstudio.py +376 -0
  5. platformforge/_graphview.py +181 -0
  6. platformforge/_installkit.py +1706 -0
  7. platformforge/agents/__init__.py +156 -0
  8. platformforge/agents/bench.py +61 -0
  9. platformforge/agents/contextpack.py +137 -0
  10. platformforge/agents/contracts.py +394 -0
  11. platformforge/agents/coordinators.py +98 -0
  12. platformforge/agents/critic.py +113 -0
  13. platformforge/agents/debate.py +161 -0
  14. platformforge/agents/executors.py +160 -0
  15. platformforge/agents/guardian.py +48 -0
  16. platformforge/agents/mirrors.py +240 -0
  17. platformforge/agents/orchestrator.py +295 -0
  18. platformforge/agents/planner.py +116 -0
  19. platformforge/agents/playbook.py +51 -0
  20. platformforge/agents/referee.py +132 -0
  21. platformforge/agents/reviewers.py +113 -0
  22. platformforge/agents/roster.py +893 -0
  23. platformforge/agents/runledger.py +85 -0
  24. platformforge/agents/runstore.py +74 -0
  25. platformforge/agents/specialists.py +73 -0
  26. platformforge/agents/taskspec.py +94 -0
  27. platformforge/agents/uniqueness.py +115 -0
  28. platformforge/agents/verifier.py +106 -0
  29. platformforge/aiplat/__init__.py +4 -0
  30. platformforge/aiplat/models.py +175 -0
  31. platformforge/analytics/__init__.py +6 -0
  32. platformforge/analytics/capacity.py +109 -0
  33. platformforge/analytics/debt.py +92 -0
  34. platformforge/analytics/dx.py +69 -0
  35. platformforge/analytics/finops_v4.py +171 -0
  36. platformforge/analytics/goldenpath.py +131 -0
  37. platformforge/analytics/history.py +233 -0
  38. platformforge/analytics/measurement.py +149 -0
  39. platformforge/analytics/models.py +127 -0
  40. platformforge/analytics/opsanalytics.py +119 -0
  41. platformforge/analytics/policyintel.py +139 -0
  42. platformforge/analytics/reliability.py +106 -0
  43. platformforge/analytics/soak.py +135 -0
  44. platformforge/analytics/store.py +254 -0
  45. platformforge/bench.py +174 -0
  46. platformforge/cases/__init__.py +13 -0
  47. platformforge/cases/casefile.py +106 -0
  48. platformforge/cases/context_audit.py +134 -0
  49. platformforge/cases/ledgers.py +174 -0
  50. platformforge/cases/replay.py +142 -0
  51. platformforge/cases/routing_audit.py +162 -0
  52. platformforge/caveman/__init__.py +11 -0
  53. platformforge/caveman/compress.py +156 -0
  54. platformforge/cicd/__init__.py +16 -0
  55. platformforge/cicd/backstage.py +51 -0
  56. platformforge/cicd/dockerfile.py +64 -0
  57. platformforge/cicd/github_actions.py +122 -0
  58. platformforge/cicd/gitlab_ci.py +56 -0
  59. platformforge/cicd/gitops.py +130 -0
  60. platformforge/cli/__init__.py +0 -0
  61. platformforge/cli/fleetcmd.py +457 -0
  62. platformforge/cli/main.py +3832 -0
  63. platformforge/cloud/__init__.py +8 -0
  64. platformforge/cloud/aws.py +254 -0
  65. platformforge/cloud/common.py +69 -0
  66. platformforge/cloud/multicloud.py +143 -0
  67. platformforge/collect/__init__.py +11 -0
  68. platformforge/collect/detect.py +254 -0
  69. platformforge/context/__init__.py +10 -0
  70. platformforge/context/capsule.py +60 -0
  71. platformforge/context/gateway.py +227 -0
  72. platformforge/context/refs.py +49 -0
  73. platformforge/context/sufficiency.py +50 -0
  74. platformforge/core/__init__.py +1 -0
  75. platformforge/core/hashing.py +39 -0
  76. platformforge/core/ownership.py +220 -0
  77. platformforge/core/receipts.py +74 -0
  78. platformforge/core/redaction.py +97 -0
  79. platformforge/core/store.py +119 -0
  80. platformforge/core/workspace.py +89 -0
  81. platformforge/data/contracts/agent-handoff.schema.json +26 -0
  82. platformforge/data/contracts/agent-run.schema.json +27 -0
  83. platformforge/data/contracts/agent-spec.schema.json +43 -0
  84. platformforge/data/contracts/capability.schema.json +23 -0
  85. platformforge/data/contracts/change-event.schema.json +22 -0
  86. platformforge/data/contracts/change-intent.schema.json +62 -0
  87. platformforge/data/contracts/change-plan.schema.json +43 -0
  88. platformforge/data/contracts/collector-receipt.schema.json +36 -0
  89. platformforge/data/contracts/context-pack.schema.json +29 -0
  90. platformforge/data/contracts/debate.schema.json +31 -0
  91. platformforge/data/contracts/distribution-manifest-v1.schema.json +81 -0
  92. platformforge/data/contracts/drift-event.schema.json +21 -0
  93. platformforge/data/contracts/fact.schema.json +35 -0
  94. platformforge/data/contracts/finding.schema.json +20 -0
  95. platformforge/data/contracts/graph-v2.schema.json +67 -0
  96. platformforge/data/contracts/graph.schema.json +40 -0
  97. platformforge/data/contracts/install-receipt-v1.schema.json +49 -0
  98. platformforge/data/contracts/observation.schema.json +93 -0
  99. platformforge/data/contracts/receipt.schema.json +21 -0
  100. platformforge/data/contracts/recommendation.schema.json +32 -0
  101. platformforge/data/contracts/refusal.schema.json +16 -0
  102. platformforge/data/contracts/slo-contract.schema.json +25 -0
  103. platformforge/data/contracts/task-spec.schema.json +37 -0
  104. platformforge/data/contracts/workspace-manifest-v1.schema.json +62 -0
  105. platformforge/data/evals/cases/agent-budget-exhaustion/case.yaml +6 -0
  106. platformforge/data/evals/cases/agent-conflicting-specialists/case.yaml +11 -0
  107. platformforge/data/evals/cases/agent-fleet-optimization/case.yaml +13 -0
  108. platformforge/data/evals/cases/agent-host-no-subagents/case.yaml +6 -0
  109. platformforge/data/evals/cases/agent-incident-multi/case.yaml +13 -0
  110. platformforge/data/evals/cases/agent-missing-evidence/case.yaml +6 -0
  111. platformforge/data/evals/cases/agent-multi-domain-audit/case.yaml +14 -0
  112. platformforge/data/evals/cases/agent-production-change/case.yaml +15 -0
  113. platformforge/data/evals/cases/agent-security-mandatory/case.yaml +12 -0
  114. platformforge/data/evals/cases/agent-simple-iac/case.yaml +14 -0
  115. platformforge/data/evals/cases/approval-integrity-seal/case.yaml +6 -0
  116. platformforge/data/evals/cases/capacity-hot/case.yaml +7 -0
  117. platformforge/data/evals/cases/capacity-hot/fixture/capacity.json +5 -0
  118. platformforge/data/evals/cases/catalog-noowner/case.yaml +7 -0
  119. platformforge/data/evals/cases/catalog-noowner/fixture/catalog-info.yaml +6 -0
  120. platformforge/data/evals/cases/contract-fact/case.yaml +5 -0
  121. platformforge/data/evals/cases/contract-fact/fixture/doc.json +2 -0
  122. platformforge/data/evals/cases/cosign-unverified/case.yaml +8 -0
  123. platformforge/data/evals/cases/cosign-unverified/fixture/sig.json +33 -0
  124. platformforge/data/evals/cases/expected-delta-refusal/case.yaml +6 -0
  125. platformforge/data/evals/cases/finops-noattr/case.yaml +9 -0
  126. platformforge/data/evals/cases/finops-noattr/fixture/costs.json +7 -0
  127. platformforge/data/evals/cases/fleet-inv-absent-allocation-unknown/case.yaml +7 -0
  128. platformforge/data/evals/cases/fleet-inv-centrality-not-criticality/case.yaml +7 -0
  129. platformforge/data/evals/cases/fleet-inv-correlation-not-causality/case.yaml +7 -0
  130. platformforge/data/evals/cases/fleet-inv-dx-no-person-metrics/case.yaml +7 -0
  131. platformforge/data/evals/cases/fleet-inv-federation-restricted-denied/case.yaml +7 -0
  132. platformforge/data/evals/cases/fleet-inv-federation-secret-denied/case.yaml +7 -0
  133. platformforge/data/evals/cases/fleet-inv-history-window-deterministic/case.yaml +7 -0
  134. platformforge/data/evals/cases/fleet-inv-low-util-not-deletable/case.yaml +7 -0
  135. platformforge/data/evals/cases/fleet-inv-member-key-deterministic/case.yaml +7 -0
  136. platformforge/data/evals/cases/fleet-inv-missing-denominator/case.yaml +7 -0
  137. platformforge/data/evals/cases/fleet-inv-partial-coverage-visible/case.yaml +7 -0
  138. platformforge/data/evals/cases/fleet-inv-plan-changeintent-only/case.yaml +7 -0
  139. platformforge/data/evals/cases/fleet-inv-report-all-cited/case.yaml +7 -0
  140. platformforge/data/evals/cases/fleet-inv-small-sample-confidence/case.yaml +7 -0
  141. platformforge/data/evals/cases/fleet-inv-uncertain-optimization-suppressed/case.yaml +7 -0
  142. platformforge/data/evals/cases/gha-dangerous/case.yaml +12 -0
  143. platformforge/data/evals/cases/gha-dangerous/fixture/.github/workflows/ci.yaml +11 -0
  144. platformforge/data/evals/cases/gitops-v2/case.yaml +11 -0
  145. platformforge/data/evals/cases/gitops-v2/fixture/argo.yaml +41 -0
  146. platformforge/data/evals/cases/graph-k8s-blast/case.yaml +7 -0
  147. platformforge/data/evals/cases/graph-k8s-blast/fixture/deploy.yaml +15 -0
  148. platformforge/data/evals/cases/iac-module-nopin/case.yaml +8 -0
  149. platformforge/data/evals/cases/iac-module-nopin/fixture/main.tf +3 -0
  150. platformforge/data/evals/cases/iac-plan-replace/case.yaml +8 -0
  151. platformforge/data/evals/cases/iac-plan-replace/fixture/plan.json +12 -0
  152. platformforge/data/evals/cases/iac-sg-open/case.yaml +8 -0
  153. platformforge/data/evals/cases/iac-sg-open/fixture/main.tf +9 -0
  154. platformforge/data/evals/cases/iam-oidc/case.yaml +8 -0
  155. platformforge/data/evals/cases/iam-oidc/fixture/policy.json +11 -0
  156. platformforge/data/evals/cases/iam-wildcard-positive/case.yaml +8 -0
  157. platformforge/data/evals/cases/iam-wildcard-positive/fixture/policy.json +3 -0
  158. platformforge/data/evals/cases/k8s-bad-actor/case.yaml +19 -0
  159. platformforge/data/evals/cases/k8s-bad-actor/fixture/app.yaml +97 -0
  160. platformforge/data/evals/cases/k8s-beta-api/case.yaml +8 -0
  161. platformforge/data/evals/cases/k8s-beta-api/fixture/cr.yaml +7 -0
  162. platformforge/data/evals/cases/k8s-dep-api-boundary/case.yaml +15 -0
  163. platformforge/data/evals/cases/k8s-dep-api-boundary/fixture/app.yaml +25 -0
  164. platformforge/data/evals/cases/k8s-dep-api-clean/case.yaml +12 -0
  165. platformforge/data/evals/cases/k8s-dep-api-clean/fixture/app.yaml +29 -0
  166. platformforge/data/evals/cases/k8s-dep-api-unknown/case.yaml +11 -0
  167. platformforge/data/evals/cases/k8s-dep-api-unknown/fixture/app.yaml +25 -0
  168. platformforge/data/evals/cases/k8s-dep-api-v120/case.yaml +12 -0
  169. platformforge/data/evals/cases/k8s-dep-api-v120/fixture/app.yaml +25 -0
  170. platformforge/data/evals/cases/k8s-dep-api-v129/case.yaml +9 -0
  171. platformforge/data/evals/cases/k8s-dep-api-v129/fixture/app.yaml +25 -0
  172. platformforge/data/evals/cases/k8s-hpa-noreq/case.yaml +8 -0
  173. platformforge/data/evals/cases/k8s-hpa-noreq/fixture/app.yaml +28 -0
  174. platformforge/data/evals/cases/k8s-limits-boundary/case.yaml +10 -0
  175. platformforge/data/evals/cases/k8s-limits-boundary/fixture/app.yaml +15 -0
  176. platformforge/data/evals/cases/k8s-limits-negative/case.yaml +8 -0
  177. platformforge/data/evals/cases/k8s-limits-negative/fixture/deploy.yaml +15 -0
  178. platformforge/data/evals/cases/k8s-limits-positive/case.yaml +8 -0
  179. platformforge/data/evals/cases/k8s-limits-positive/fixture/deploy.yaml +14 -0
  180. platformforge/data/evals/cases/k8s-nopdb/case.yaml +7 -0
  181. platformforge/data/evals/cases/k8s-nopdb/fixture/app.yaml +34 -0
  182. platformforge/data/evals/cases/knowledge-registry/case.yaml +10 -0
  183. platformforge/data/evals/cases/kyverno-dep/case.yaml +10 -0
  184. platformforge/data/evals/cases/kyverno-dep/fixture/pol.yaml +18 -0
  185. platformforge/data/evals/cases/kyverno-legacy/case.yaml +8 -0
  186. platformforge/data/evals/cases/kyverno-legacy/fixture/pol.yaml +7 -0
  187. platformforge/data/evals/cases/live-aws-collect/case.yaml +11 -0
  188. platformforge/data/evals/cases/live-aws-collect/fixture/aws/ec2_describe-vpcs.json +15 -0
  189. platformforge/data/evals/cases/live-aws-collect/fixture/aws/iam_list-users.denied.txt +1 -0
  190. platformforge/data/evals/cases/live-aws-collect/fixture/aws/sts_get-caller-identity.json +5 -0
  191. platformforge/data/evals/cases/live-drift/case.yaml +8 -0
  192. platformforge/data/evals/cases/live-drift/fixture/after.json +22 -0
  193. platformforge/data/evals/cases/live-drift/fixture/before.json +22 -0
  194. platformforge/data/evals/cases/live-incident/case.yaml +7 -0
  195. platformforge/data/evals/cases/live-incident/fixture/changes.json +8 -0
  196. platformforge/data/evals/cases/live-incident/fixture/incident.json +7 -0
  197. platformforge/data/evals/cases/live-k8s-collect/case.yaml +10 -0
  198. platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_configmaps.denied.txt +1 -0
  199. platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_deployments.apps.json +25 -0
  200. platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_pods.json +17 -0
  201. platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_secrets.json +13 -0
  202. platformforge/data/evals/cases/live-multicluster/case.yaml +7 -0
  203. platformforge/data/evals/cases/live-multicluster/fixture/clusters.json +6 -0
  204. platformforge/data/evals/cases/live-plan/case.yaml +7 -0
  205. platformforge/data/evals/cases/live-plan/fixture/drift.json +8 -0
  206. platformforge/data/evals/cases/live-reconcile/case.yaml +7 -0
  207. platformforge/data/evals/cases/live-reconcile/fixture/desired.json +7 -0
  208. platformforge/data/evals/cases/live-reconcile/fixture/observed.json +18 -0
  209. platformforge/data/evals/cases/live-secrets-boundary/case.yaml +8 -0
  210. platformforge/data/evals/cases/live-secrets-boundary/fixture/envelope.json +16 -0
  211. platformforge/data/evals/cases/live-temporal-graph/case.yaml +10 -0
  212. platformforge/data/evals/cases/live-temporal-graph/fixture/graph.json +22 -0
  213. platformforge/data/evals/cases/ops-approval/case.yaml +6 -0
  214. platformforge/data/evals/cases/ops-convergence/case.yaml +6 -0
  215. platformforge/data/evals/cases/ops-execution-safety/case.yaml +6 -0
  216. platformforge/data/evals/cases/ops-intent/case.yaml +6 -0
  217. platformforge/data/evals/cases/ops-planning/case.yaml +6 -0
  218. platformforge/data/evals/cases/ops-policy/case.yaml +6 -0
  219. platformforge/data/evals/cases/ops-rollback/case.yaml +6 -0
  220. platformforge/data/evals/cases/ops-verification/case.yaml +6 -0
  221. platformforge/data/evals/cases/rollback-argocd-revision/case.yaml +6 -0
  222. platformforge/data/evals/cases/rollback-blind-inverse/case.yaml +6 -0
  223. platformforge/data/evals/cases/rollback-git-commit/case.yaml +6 -0
  224. platformforge/data/evals/cases/rollback-impossible/case.yaml +6 -0
  225. platformforge/data/evals/cases/rollback-k8s-annotation-restore/case.yaml +6 -0
  226. platformforge/data/evals/cases/rollback-k8s-scale/case.yaml +6 -0
  227. platformforge/data/evals/cases/rollback-material-tamper/case.yaml +6 -0
  228. platformforge/data/evals/cases/rollback-terraform-replan/case.yaml +6 -0
  229. platformforge/data/evals/cases/rollback-unknown/case.yaml +6 -0
  230. platformforge/data/evals/cases/routing-specialist/case.yaml +9 -0
  231. platformforge/data/evals/cases/rtk-roundtrip/case.yaml +5 -0
  232. platformforge/data/evals/cases/rtk-roundtrip/fixture/verbose.txt +300 -0
  233. platformforge/data/evals/cases/sbom-vuln/case.yaml +9 -0
  234. platformforge/data/evals/cases/sbom-vuln/fixture/sbom.json +9 -0
  235. platformforge/data/evals/cases/secret-prose-mixed/fixture/app.env +1 -0
  236. platformforge/data/evals/cases/secret-prose-mixed/fixture/policy-notes.md +7 -0
  237. platformforge/data/evals/cases/secrets-noleak/case.yaml +5 -0
  238. platformforge/data/evals/cases/secrets-noleak/fixture/app.env +2 -0
  239. platformforge/data/evals/cases/slo-fastburn/case.yaml +8 -0
  240. platformforge/data/evals/cases/slo-fastburn/fixture/events.yaml +2 -0
  241. platformforge/data/evals/cases/slo-fastburn/fixture/slo.yaml +4 -0
  242. platformforge/data/evals/cases/slo-noinput/case.yaml +8 -0
  243. platformforge/data/evals/cases/slo-noinput/fixture/events.yaml +1 -0
  244. platformforge/data/evals/cases/slo-noinput/fixture/slo.yaml +4 -0
  245. platformforge/data/evals/cases/sot-conflict/case.yaml +6 -0
  246. platformforge/data/evals/cases/token-budget/case.yaml +6 -0
  247. platformforge/data/evals/cases/token-budget/fixture/big.py +50 -0
  248. platformforge/data/evals/cases/xr-delete/case.yaml +8 -0
  249. platformforge/data/evals/cases/xr-delete/fixture/xr.yaml +10 -0
  250. platformforge/data/evals/cases/xr-graph-chain/case.yaml +8 -0
  251. platformforge/data/evals/cases/xr-graph-chain/fixture/xrd.yaml +90 -0
  252. platformforge/data/evals/cases/xr-negative-crd/case.yaml +8 -0
  253. platformforge/data/evals/cases/xr-negative-crd/fixture/widget.yaml +19 -0
  254. platformforge/data/evals/cases/xr-upbound-mr/case.yaml +8 -0
  255. platformforge/data/evals/cases/xr-upbound-mr/fixture/bucket.yaml +12 -0
  256. platformforge/data/evals/qpt-bench/change-review/case.yaml +5 -0
  257. platformforge/data/evals/qpt-bench/change-review/fixture/plan.json +9 -0
  258. platformforge/data/evals/qpt-bench/gha-security/case.yaml +5 -0
  259. platformforge/data/evals/qpt-bench/gha-security/fixture/ci.yml +10 -0
  260. platformforge/data/evals/qpt-bench/iam-investigate/case.yaml +5 -0
  261. platformforge/data/evals/qpt-bench/iam-investigate/fixture/policy.json +6 -0
  262. platformforge/data/evals/qpt-bench/incident-analysis/case.yaml +5 -0
  263. platformforge/data/evals/qpt-bench/incident-analysis/fixture/query.json +5 -0
  264. platformforge/data/evals/qpt-bench/incident-analysis/fixture/rules.yaml +6 -0
  265. platformforge/data/evals/qpt-bench/k8s-review/case.yaml +6 -0
  266. platformforge/data/evals/qpt-bench/k8s-review/fixture/deploy.yaml +15 -0
  267. platformforge/data/evals/qpt-bench/tf-security/case.yaml +5 -0
  268. platformforge/data/evals/qpt-bench/tf-security/fixture/main.tf +17 -0
  269. platformforge/data/knowledge/aws/eks.yaml +20 -0
  270. platformforge/data/knowledge/aws/iam.yaml +28 -0
  271. platformforge/data/knowledge/aws/network-exposure.yaml +19 -0
  272. platformforge/data/knowledge/aws/organizations.yaml +19 -0
  273. platformforge/data/knowledge/crossplane/composition-providers.yaml +20 -0
  274. platformforge/data/knowledge/crossplane/managed-resources.yaml +26 -0
  275. platformforge/data/knowledge/crossplane/v1-v2.yaml +25 -0
  276. platformforge/data/knowledge/kubernetes/api-deprecations.yaml +33 -0
  277. platformforge/data/knowledge/kubernetes/autoscaling-rbac.yaml +27 -0
  278. platformforge/data/knowledge/kubernetes/gateway-api.yaml +20 -0
  279. platformforge/data/knowledge/kubernetes/pod-security.yaml +26 -0
  280. platformforge/data/knowledge/sources.yaml +516 -0
  281. platformforge/data/knowledge/terraform/lifecycle.yaml +18 -0
  282. platformforge/data/knowledge/terraform/plan-semantics.yaml +26 -0
  283. platformforge/data/lab/chaos-pod-kill/chaos.yaml +3 -0
  284. platformforge/data/lab/chaos-pod-kill/facts.json +13 -0
  285. platformforge/data/lab/fleets/acme/capacity.yaml +35 -0
  286. platformforge/data/lab/fleets/acme/costs.yaml +25 -0
  287. platformforge/data/lab/fleets/acme/events.yaml +41 -0
  288. platformforge/data/lab/fleets/acme/fleet.yaml +51 -0
  289. platformforge/data/lab/fleets/acme/graph.yaml +45 -0
  290. platformforge/data/lab/fleets/acme/policies.yaml +28 -0
  291. platformforge/data/lab/fleets/acme/requests.yaml +24 -0
  292. platformforge/data/lab/scenarios/argo-bad-rollout/expected.yaml +6 -0
  293. platformforge/data/lab/scenarios/argo-bad-rollout/fixture/app.yaml +9 -0
  294. platformforge/data/lab/scenarios/aws-dump/expected.yaml +19 -0
  295. platformforge/data/lab/scenarios/aws-dump/fixture/compute.json +4 -0
  296. platformforge/data/lab/scenarios/aws-dump/fixture/iam.json +7 -0
  297. platformforge/data/lab/scenarios/aws-dump/fixture/network.json +6 -0
  298. platformforge/data/lab/scenarios/aws-dump/fixture/org.json +4 -0
  299. platformforge/data/lab/scenarios/aws-dump/fixture/storage.json +4 -0
  300. platformforge/data/lab/scenarios/container-unsigned/expected.yaml +5 -0
  301. platformforge/data/lab/scenarios/container-unsigned/fixture/supply.json +11 -0
  302. platformforge/data/lab/scenarios/cost-idle-workload/expected.yaml +5 -0
  303. platformforge/data/lab/scenarios/cost-idle-workload/fixture/costs.json +12 -0
  304. platformforge/data/lab/scenarios/crossplane-platform/expected.yaml +16 -0
  305. platformforge/data/lab/scenarios/crossplane-platform/fixture/negative.yaml +18 -0
  306. platformforge/data/lab/scenarios/crossplane-platform/fixture/xrd.yaml +90 -0
  307. platformforge/data/lab/scenarios/demo-platform/expected.yaml +16 -0
  308. platformforge/data/lab/scenarios/demo-platform/fixture/.github/workflows/ci.yml +10 -0
  309. platformforge/data/lab/scenarios/demo-platform/fixture/deploy.yaml +16 -0
  310. platformforge/data/lab/scenarios/demo-platform/fixture/infra.tf +5 -0
  311. platformforge/data/lab/scenarios/fleet-ai/expected.yaml +12 -0
  312. platformforge/data/lab/scenarios/fleet-capacity/expected.yaml +12 -0
  313. platformforge/data/lab/scenarios/fleet-cost/expected.yaml +18 -0
  314. platformforge/data/lab/scenarios/fleet-federation/expected.yaml +11 -0
  315. platformforge/data/lab/scenarios/fleet-golden-path/expected.yaml +18 -0
  316. platformforge/data/lab/scenarios/fleet-op-hotspot/expected.yaml +11 -0
  317. platformforge/data/lab/scenarios/fleet-ownership/expected.yaml +17 -0
  318. platformforge/data/lab/scenarios/fleet-partial/expected.yaml +10 -0
  319. platformforge/data/lab/scenarios/fleet-policy-friction/expected.yaml +10 -0
  320. platformforge/data/lab/scenarios/fleet-recurring-incident/expected.yaml +12 -0
  321. platformforge/data/lab/scenarios/fleet-report/expected.yaml +13 -0
  322. platformforge/data/lab/scenarios/fleet-risk/expected.yaml +18 -0
  323. platformforge/data/lab/scenarios/gitops-drift/expected.yaml +6 -0
  324. platformforge/data/lab/scenarios/gitops-drift/fixture/app.yaml +12 -0
  325. platformforge/data/lab/scenarios/k8s-bad-readiness/expected.yaml +6 -0
  326. platformforge/data/lab/scenarios/k8s-bad-readiness/fixture/deploy.yaml +23 -0
  327. platformforge/data/lab/scenarios/k8s-bad-readiness/fixture/netpol.yaml +9 -0
  328. platformforge/data/lab/scenarios/k8s-missing-limits/expected.yaml +6 -0
  329. platformforge/data/lab/scenarios/k8s-missing-limits/fixture/deploy.yaml +26 -0
  330. platformforge/data/lab/scenarios/k8s-missing-limits/fixture/netpol.yaml +9 -0
  331. platformforge/data/lab/scenarios/k8s-single-replica/expected.yaml +5 -0
  332. platformforge/data/lab/scenarios/k8s-single-replica/fixture/deploy.yaml +26 -0
  333. platformforge/data/lab/scenarios/k8s-single-replica/fixture/netpol.yaml +4 -0
  334. platformforge/data/lab/scenarios/live-aws-collect/expected.yaml +4 -0
  335. platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/ec2_describe-vpcs.json +15 -0
  336. platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/iam_list-users.denied.txt +1 -0
  337. platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/sts_get-caller-identity.json +5 -0
  338. platformforge/data/lab/scenarios/live-k8s-collect/expected.yaml +4 -0
  339. platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_configmaps.denied.txt +1 -0
  340. platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_deployments.apps.json +25 -0
  341. platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_pods.json +17 -0
  342. platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_secrets.json +13 -0
  343. platformforge/data/lab/scenarios/live-kind/expected.yaml +14 -0
  344. platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_configmaps.denied.txt +1 -0
  345. platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_deployments.apps.json +25 -0
  346. platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_pods.json +17 -0
  347. platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_secrets.json +13 -0
  348. platformforge/data/lab/scenarios/network-policy-missing/expected.yaml +6 -0
  349. platformforge/data/lab/scenarios/network-policy-missing/fixture/deploy.yaml +26 -0
  350. platformforge/data/lab/scenarios/ops-bad-rollout/expected.yaml +3 -0
  351. platformforge/data/lab/scenarios/ops-bad-rollout/fixture/ops.yaml +33 -0
  352. platformforge/data/lab/scenarios/ops-break-glass/expected.yaml +3 -0
  353. platformforge/data/lab/scenarios/ops-break-glass/fixture/ops.yaml +21 -0
  354. platformforge/data/lab/scenarios/ops-broken-selector/expected.yaml +3 -0
  355. platformforge/data/lab/scenarios/ops-broken-selector/fixture/ops.yaml +21 -0
  356. platformforge/data/lab/scenarios/ops-changed-plan-hash/expected.yaml +3 -0
  357. platformforge/data/lab/scenarios/ops-changed-plan-hash/fixture/ops.yaml +20 -0
  358. platformforge/data/lab/scenarios/ops-conflicting-operation/expected.yaml +3 -0
  359. platformforge/data/lab/scenarios/ops-conflicting-operation/fixture/ops.yaml +21 -0
  360. platformforge/data/lab/scenarios/ops-expired-approval/expected.yaml +3 -0
  361. platformforge/data/lab/scenarios/ops-expired-approval/fixture/ops.yaml +20 -0
  362. platformforge/data/lab/scenarios/ops-partial-failure/expected.yaml +3 -0
  363. platformforge/data/lab/scenarios/ops-partial-failure/fixture/ops.yaml +24 -0
  364. platformforge/data/lab/scenarios/ops-policy-denial/expected.yaml +3 -0
  365. platformforge/data/lab/scenarios/ops-policy-denial/fixture/ops.yaml +17 -0
  366. platformforge/data/lab/scenarios/ops-replica-drift/expected.yaml +3 -0
  367. platformforge/data/lab/scenarios/ops-replica-drift/fixture/ops.yaml +28 -0
  368. platformforge/data/lab/scenarios/ops-rollback-annotate/expected.yaml +3 -0
  369. platformforge/data/lab/scenarios/ops-rollback-annotate/fixture/ops.yaml +41 -0
  370. platformforge/data/lab/scenarios/ops-rollback-gitops/expected.yaml +3 -0
  371. platformforge/data/lab/scenarios/ops-rollback-gitops/fixture/ops.yaml +38 -0
  372. platformforge/data/lab/scenarios/ops-rollback-impossible/expected.yaml +3 -0
  373. platformforge/data/lab/scenarios/ops-rollback-impossible/fixture/ops.yaml +34 -0
  374. platformforge/data/lab/scenarios/ops-rollback-scale/expected.yaml +3 -0
  375. platformforge/data/lab/scenarios/ops-rollback-scale/fixture/ops.yaml +40 -0
  376. platformforge/data/lab/scenarios/ops-rollback-terraform/expected.yaml +3 -0
  377. platformforge/data/lab/scenarios/ops-rollback-terraform/fixture/ops.yaml +38 -0
  378. platformforge/data/lab/scenarios/ops-rollback-terraform/fixture/tf/tfplan.bin +1 -0
  379. platformforge/data/lab/scenarios/ops-stale-observation/expected.yaml +3 -0
  380. platformforge/data/lab/scenarios/ops-stale-observation/fixture/ops.yaml +20 -0
  381. platformforge/data/lab/scenarios/secret-exposure/expected.yaml +5 -0
  382. platformforge/data/lab/scenarios/secret-exposure/fixture/app.env +2 -0
  383. platformforge/data/lab/scenarios/slo-burn/expected.yaml +5 -0
  384. platformforge/data/lab/scenarios/slo-burn/fixture/events.yaml +2 -0
  385. platformforge/data/lab/scenarios/slo-burn/fixture/slo.yaml +4 -0
  386. platformforge/data/lab/scenarios/terraform-iam-wildcard/expected.yaml +5 -0
  387. platformforge/data/lab/scenarios/terraform-iam-wildcard/fixture/policy.json +14 -0
  388. platformforge/data/lab/scenarios/terraform-public-bucket/expected.yaml +6 -0
  389. platformforge/data/lab/scenarios/terraform-public-bucket/fixture/main.tf +5 -0
  390. platformforge/data/portable_skills/platformforge-agents/SKILL.md +47 -0
  391. platformforge/data/portable_skills/platformforge-change/SKILL.md +41 -0
  392. platformforge/data/portable_skills/platformforge-core/SKILL.md +62 -0
  393. platformforge/data/portable_skills/platformforge-economy/SKILL.md +42 -0
  394. platformforge/data/portable_skills/platformforge-finops/SKILL.md +34 -0
  395. platformforge/data/portable_skills/platformforge-fleet/SKILL.md +39 -0
  396. platformforge/data/portable_skills/platformforge-governance/SKILL.md +76 -0
  397. platformforge/data/portable_skills/platformforge-graph/SKILL.md +46 -0
  398. platformforge/data/portable_skills/platformforge-lab/SKILL.md +39 -0
  399. platformforge/data/portable_skills/platformforge-sdd/SKILL.md +39 -0
  400. platformforge/data/portable_skills/platformforge-security/SKILL.md +38 -0
  401. platformforge/data/portable_skills/platformforge-sre/SKILL.md +41 -0
  402. platformforge/data/rules/catalog/finops.yaml +26 -0
  403. platformforge/data/rules/catalog/gitops.yaml +161 -0
  404. platformforge/data/rules/catalog/iac.yaml +84 -0
  405. platformforge/data/rules/catalog/k8s.yaml +322 -0
  406. platformforge/data/rules/catalog/orchestration.yaml +99 -0
  407. platformforge/data/rules/catalog/product.yaml +42 -0
  408. platformforge/data/rules/catalog/routing.yaml +122 -0
  409. platformforge/data/rules/catalog/security.yaml +159 -0
  410. platformforge/data/rules/catalog/sre.yaml +67 -0
  411. platformforge/diagnose/__init__.py +75 -0
  412. platformforge/distribution/__init__.py +18 -0
  413. platformforge/distribution/bundle.py +127 -0
  414. platformforge/distribution/hostflow.py +247 -0
  415. platformforge/distribution/models.py +80 -0
  416. platformforge/distribution/service.py +225 -0
  417. platformforge/economy/__init__.py +27 -0
  418. platformforge/economy/bench.py +66 -0
  419. platformforge/economy/budget.py +198 -0
  420. platformforge/economy/cache.py +287 -0
  421. platformforge/economy/checkpoint.py +112 -0
  422. platformforge/economy/collection.py +142 -0
  423. platformforge/economy/doctor.py +207 -0
  424. platformforge/economy/engine.py +158 -0
  425. platformforge/economy/envelope.py +61 -0
  426. platformforge/economy/fleetpack.py +86 -0
  427. platformforge/economy/ledger.py +232 -0
  428. platformforge/economy/plan.py +131 -0
  429. platformforge/economy/pricing.py +101 -0
  430. platformforge/economy/qpt.py +286 -0
  431. platformforge/economy/reconcile.py +86 -0
  432. platformforge/economy/verifyplan.py +107 -0
  433. platformforge/economy/waste.py +123 -0
  434. platformforge/evals/__init__.py +16 -0
  435. platformforge/evals/coverage.py +140 -0
  436. platformforge/evals/fleet_invariants.py +260 -0
  437. platformforge/evals/ops_invariants.py +158 -0
  438. platformforge/evals/runner.py +541 -0
  439. platformforge/federation/__init__.py +4 -0
  440. platformforge/federation/node.py +135 -0
  441. platformforge/finops/__init__.py +14 -0
  442. platformforge/finops/alloc.py +31 -0
  443. platformforge/finops/costs.py +79 -0
  444. platformforge/finops/focus.py +100 -0
  445. platformforge/finops/graph_cost.py +51 -0
  446. platformforge/finops/ingest.py +144 -0
  447. platformforge/finops/insights.py +193 -0
  448. platformforge/finops/unit.py +36 -0
  449. platformforge/fleet/__init__.py +5 -0
  450. platformforge/fleet/loader.py +73 -0
  451. platformforge/fleet/models.py +155 -0
  452. platformforge/fleet/orggraph.py +172 -0
  453. platformforge/fleet/query.py +154 -0
  454. platformforge/forge/__init__.py +9 -0
  455. platformforge/forge/collect.py +88 -0
  456. platformforge/forge/delegate.py +77 -0
  457. platformforge/forge/manifest.py +106 -0
  458. platformforge/freeze/__init__.py +22 -0
  459. platformforge/freeze/exceptions.py +56 -0
  460. platformforge/freeze/manifest.py +167 -0
  461. platformforge/freeze/snapshots.py +108 -0
  462. platformforge/graph/__init__.py +39 -0
  463. platformforge/graph/backend.py +174 -0
  464. platformforge/graph/bench.py +245 -0
  465. platformforge/graph/build.py +130 -0
  466. platformforge/graph/delivery.py +183 -0
  467. platformforge/graph/diff.py +292 -0
  468. platformforge/graph/events.py +116 -0
  469. platformforge/graph/migrate.py +44 -0
  470. platformforge/graph/model.py +264 -0
  471. platformforge/graph/persist.py +97 -0
  472. platformforge/graph/query.py +290 -0
  473. platformforge/graph/temporal.py +78 -0
  474. platformforge/graph/vocab.py +63 -0
  475. platformforge/graphview.py +94 -0
  476. platformforge/iac/__init__.py +7 -0
  477. platformforge/iac/drift.py +53 -0
  478. platformforge/iac/plan.py +84 -0
  479. platformforge/iac/terraform.py +123 -0
  480. platformforge/install/__init__.py +1 -0
  481. platformforge/install/service.py +421 -0
  482. platformforge/k8s/__init__.py +8 -0
  483. platformforge/k8s/helm.py +152 -0
  484. platformforge/k8s/hubble.py +87 -0
  485. platformforge/k8s/manifests.py +513 -0
  486. platformforge/knowledge/__init__.py +1 -0
  487. platformforge/knowledge/packs.py +157 -0
  488. platformforge/knowledge/registry.py +249 -0
  489. platformforge/knowledge/select.py +127 -0
  490. platformforge/lab/__init__.py +5 -0
  491. platformforge/lab/chaos.py +71 -0
  492. platformforge/lab/fleetlab.py +351 -0
  493. platformforge/lab/opslab.py +271 -0
  494. platformforge/lab/runner.py +219 -0
  495. platformforge/live/__init__.py +36 -0
  496. platformforge/live/budget.py +92 -0
  497. platformforge/live/capability.py +80 -0
  498. platformforge/live/collectors/__init__.py +6 -0
  499. platformforge/live/collectors/aws.py +370 -0
  500. platformforge/live/collectors/aws_transport.py +98 -0
  501. platformforge/live/collectors/k8s.py +389 -0
  502. platformforge/live/collectors/k8s_transport.py +85 -0
  503. platformforge/live/cursors.py +48 -0
  504. platformforge/live/drift.py +130 -0
  505. platformforge/live/envelope.py +85 -0
  506. platformforge/live/federation.py +113 -0
  507. platformforge/live/fixtures.py +168 -0
  508. platformforge/live/identity.py +258 -0
  509. platformforge/live/incident.py +200 -0
  510. platformforge/live/models.py +435 -0
  511. platformforge/live/reconcile.py +382 -0
  512. platformforge/live/remediate.py +169 -0
  513. platformforge/live/store.py +256 -0
  514. platformforge/live/topology.py +249 -0
  515. platformforge/mcp/__init__.py +7 -0
  516. platformforge/mcp/parity.py +69 -0
  517. platformforge/mcp/registry.py +427 -0
  518. platformforge/mcp/server.py +84 -0
  519. platformforge/mcp/tools.py +732 -0
  520. platformforge/models/__init__.py +21 -0
  521. platformforge/models/base.py +214 -0
  522. platformforge/observe/__init__.py +26 -0
  523. platformforge/observe/capacity.py +39 -0
  524. platformforge/observe/dr.py +67 -0
  525. platformforge/observe/grafana.py +76 -0
  526. platformforge/observe/incident.py +58 -0
  527. platformforge/observe/otel.py +135 -0
  528. platformforge/observe/postmortem.py +72 -0
  529. platformforge/observe/prometheus.py +88 -0
  530. platformforge/observe/slo.py +125 -0
  531. platformforge/ops/__init__.py +8 -0
  532. platformforge/ops/actions.py +215 -0
  533. platformforge/ops/analytics.py +83 -0
  534. platformforge/ops/approval.py +275 -0
  535. platformforge/ops/autorem.py +101 -0
  536. platformforge/ops/config.py +175 -0
  537. platformforge/ops/engine.py +758 -0
  538. platformforge/ops/envelope.py +135 -0
  539. platformforge/ops/executors/__init__.py +1 -0
  540. platformforge/ops/executors/base.py +211 -0
  541. platformforge/ops/executors/git.py +77 -0
  542. platformforge/ops/executors/gitops.py +134 -0
  543. platformforge/ops/executors/terraform.py +84 -0
  544. platformforge/ops/gates.py +135 -0
  545. platformforge/ops/goldenpath.py +180 -0
  546. platformforge/ops/material.py +181 -0
  547. platformforge/ops/models.py +421 -0
  548. platformforge/ops/operation.py +335 -0
  549. platformforge/ops/opgraph.py +233 -0
  550. platformforge/ops/policy.py +275 -0
  551. platformforge/ops/preconditions.py +109 -0
  552. platformforge/ops/registry.py +101 -0
  553. platformforge/ops/risk.py +216 -0
  554. platformforge/ops/rollback.py +287 -0
  555. platformforge/ops/rollback_builders.py +163 -0
  556. platformforge/ops/runbook.py +269 -0
  557. platformforge/ops/simulate.py +125 -0
  558. platformforge/ops/source_of_truth.py +198 -0
  559. platformforge/ops/store.py +157 -0
  560. platformforge/ops/verify.py +190 -0
  561. platformforge/optimize/__init__.py +4 -0
  562. platformforge/optimize/engine.py +109 -0
  563. platformforge/optimize/models.py +68 -0
  564. platformforge/optimize/scan.py +196 -0
  565. platformforge/plan/__init__.py +107 -0
  566. platformforge/portable_cli.py +199 -0
  567. platformforge/portal_adapter.py +740 -0
  568. platformforge/portal_assets/chunk-7_l-b4Tr.js +1 -0
  569. platformforge/portal_assets/chunk-B0Iad6pD.js +1 -0
  570. platformforge/portal_assets/chunk-B605XnCF.js +1 -0
  571. platformforge/portal_assets/chunk-B7N3TXcn.js +1 -0
  572. platformforge/portal_assets/chunk-BAj6yGem.js +1 -0
  573. platformforge/portal_assets/chunk-BGUitn2r.js +1 -0
  574. platformforge/portal_assets/chunk-BVKnCv85.js +1 -0
  575. platformforge/portal_assets/chunk-BYyM9V_w.js +1 -0
  576. platformforge/portal_assets/chunk-BaQMhq-G.js +1 -0
  577. platformforge/portal_assets/chunk-BavOrxVm.js +1 -0
  578. platformforge/portal_assets/chunk-Bb0C24YV.js +1 -0
  579. platformforge/portal_assets/chunk-BfCFGPzs.js +1 -0
  580. platformforge/portal_assets/chunk-C2t4OUVu.js +1 -0
  581. platformforge/portal_assets/chunk-C3pI4QjU.js +1 -0
  582. platformforge/portal_assets/chunk-C4gpbtbP.js +1 -0
  583. platformforge/portal_assets/chunk-CGWeHhKY.js +1 -0
  584. platformforge/portal_assets/chunk-CJCT86Dr.js +1 -0
  585. platformforge/portal_assets/chunk-CXcsLDPJ.js +1 -0
  586. platformforge/portal_assets/chunk-CcjxoLxw.js +1 -0
  587. platformforge/portal_assets/chunk-D-gBPXHa.js +1 -0
  588. platformforge/portal_assets/chunk-D3eZogIe.js +1 -0
  589. platformforge/portal_assets/chunk-DQU0VyoQ.js +1 -0
  590. platformforge/portal_assets/chunk-DqWNkF1O.js +1 -0
  591. platformforge/portal_assets/chunk-DuPPkSIE.js +3 -0
  592. platformforge/portal_assets/chunk-DyIHOW_a.js +1 -0
  593. platformforge/portal_assets/chunk-Fx4zSGL5.js +1 -0
  594. platformforge/portal_assets/chunk-UBKt05Rg.js +1 -0
  595. platformforge/portal_assets/chunk-htnJDQEY.js +1 -0
  596. platformforge/portal_assets/chunk-kXRFMGqe.js +1 -0
  597. platformforge/portal_assets/chunk-qe3u763a.js +1 -0
  598. platformforge/portal_assets/favicon.ico +0 -0
  599. platformforge/portal_assets/index.html +13 -0
  600. platformforge/portal_assets/main-QDFVZK3T.js +5 -0
  601. platformforge/portal_assets/styles-MQEBDFDX.css +1 -0
  602. platformforge/product/__init__.py +10 -0
  603. platformforge/product/backstage.py +49 -0
  604. platformforge/product/catalog.py +74 -0
  605. platformforge/product/crossplane.py +341 -0
  606. platformforge/product/golden_paths/__init__.py +5 -0
  607. platformforge/product/golden_paths/engine.py +97 -0
  608. platformforge/product/golden_paths/library.yaml +198 -0
  609. platformforge/product/golden_paths/model.py +84 -0
  610. platformforge/product/maturity.py +107 -0
  611. platformforge/product/scorecards.py +84 -0
  612. platformforge/resources.py +36 -0
  613. platformforge/risk/__init__.py +5 -0
  614. platformforge/risk/engine.py +95 -0
  615. platformforge/risk/signals.py +49 -0
  616. platformforge/routing/__init__.py +13 -0
  617. platformforge/routing/decision.py +191 -0
  618. platformforge/routing/router.py +328 -0
  619. platformforge/rtk/__init__.py +9 -0
  620. platformforge/rtk/compact.py +127 -0
  621. platformforge/rtk/parsers.py +199 -0
  622. platformforge/rules/__init__.py +5 -0
  623. platformforge/rules/engine.py +345 -0
  624. platformforge/sandbox/__init__.py +7 -0
  625. platformforge/sandbox/env.py +151 -0
  626. platformforge/sandbox/review.py +92 -0
  627. platformforge/sdd/__init__.py +8 -0
  628. platformforge/sdd/lifecycle.py +239 -0
  629. platformforge/security/__init__.py +19 -0
  630. platformforge/security/cosign.py +67 -0
  631. platformforge/security/iam.py +112 -0
  632. platformforge/security/identity.py +84 -0
  633. platformforge/security/kyverno.py +106 -0
  634. platformforge/security/sbom.py +59 -0
  635. platformforge/security/scan.py +63 -0
  636. platformforge/security/slsa.py +86 -0
  637. platformforge/security/supply.py +62 -0
  638. platformforge/tokensave/__init__.py +20 -0
  639. platformforge/tokensave/budget.py +62 -0
  640. platformforge/tokensave/estimate.py +21 -0
  641. platformforge/tokensave/index.py +172 -0
  642. platformforge/tokensave/ledger.py +121 -0
  643. platformforge/tokensave/packs.py +285 -0
  644. platformforge/ui/__init__.py +28 -0
  645. platformforge/ui/app.py +465 -0
  646. platformforge/ui/home.py +113 -0
  647. platformforge/ui/i18n.py +143 -0
  648. platformforge/ui/kit.py +689 -0
  649. platformforge/ui/screen.py +628 -0
  650. platformforge/ui/tui.py +94 -0
  651. platformforge/ui/wizard.py +309 -0
  652. platformforge/workspace/__init__.py +13 -0
  653. platformforge/workspace/models.py +35 -0
  654. platformforge/workspace/service.py +69 -0
  655. platformforge-0.1.0.dist-info/METADATA +212 -0
  656. platformforge-0.1.0.dist-info/RECORD +659 -0
  657. platformforge-0.1.0.dist-info/WHEEL +4 -0
  658. platformforge-0.1.0.dist-info/entry_points.txt +3 -0
  659. platformforge-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,159 @@
1
+ # DevSecOps rules — PF-SEC-*.
2
+ rules:
3
+ - rule_id: PF-SEC-001
4
+ domain: security
5
+ title: "Wildcard actions or admin grant in IAM policy"
6
+ severity: critical
7
+ applies_to: {fact_kind: security.iam_policy}
8
+ conditions:
9
+ any:
10
+ - {path: attrs.admin_grant, op: equals, value: true}
11
+ - {path: attrs.wildcard_actions, op: gt, value: 0}
12
+ message: "* actions widen compromise to everything the role can name"
13
+ sources: ['aws-iam-docs']
14
+ source_refs:
15
+ aws-iam-docs: "IAM/latest/UserGuide/access_policies_manage-attach-detach.html"
16
+
17
+ - rule_id: PF-SEC-002
18
+ domain: security
19
+ title: "Public principal in IAM policy"
20
+ severity: critical
21
+ applies_to: {fact_kind: security.iam_policy}
22
+ conditions:
23
+ all:
24
+ - {path: attrs.public_principals, op: gt, value: 0}
25
+ message: "Principal '*' grants the internet access"
26
+ sources: ['aws-iam-docs']
27
+ source_refs:
28
+ aws-iam-docs: "IAM/latest/UserGuide/reference_policies_elements_principal.html"
29
+
30
+ - rule_id: PF-SEC-003
31
+ domain: security
32
+ title: "Wildcard resource in IAM policy"
33
+ severity: medium
34
+ applies_to: {fact_kind: security.iam_policy}
35
+ conditions:
36
+ all:
37
+ - {path: attrs.wildcard_resources, op: gt, value: 0}
38
+ message: "Resource '*' applies the grant to every object"
39
+ sources: ['aws-iam-docs']
40
+ source_refs:
41
+ aws-iam-docs: "IAM/latest/UserGuide/reference_policies_elements_resource.html"
42
+
43
+ - rule_id: PF-SEC-010
44
+ domain: security
45
+ title: "Secret pattern in repository file"
46
+ severity: critical
47
+ applies_to: {fact_kind: security.secret_leak}
48
+ conditions:
49
+ all:
50
+ - {path: attrs.hit_count, op: gt, value: 0}
51
+ message: "rotate first, scrub second — history retains the value"
52
+ sources: ['github-security-docs']
53
+ source_refs:
54
+ github-security-docs: "en/code-security/secret-scanning/about-secret-scanning"
55
+
56
+ - rule_id: PF-SEC-020
57
+ domain: security
58
+ title: "Component with known CVE (offline vuln list)"
59
+ severity: high
60
+ applies_to: {fact_kind: security.sbom}
61
+ conditions:
62
+ all:
63
+ - {path: attrs.vuln_count, op: gt, value: 0}
64
+ message: "matched against caller-supplied vuln list — freshness matters"
65
+ sources: ['nvd', 'osv']
66
+ source_refs:
67
+ nvd: "vuln"
68
+
69
+ - rule_id: PF-SEC-021
70
+ domain: security
71
+ title: "Component without declared license"
72
+ severity: low
73
+ applies_to: {fact_kind: security.sbom}
74
+ conditions:
75
+ all:
76
+ - {path: attrs.unlicensed_components, op: not_empty}
77
+ message: "license posture unknown — legal exposure is unbounded"
78
+ sources: ['spdx-licenses']
79
+ source_refs:
80
+ spdx-licenses: "licenses"
81
+
82
+ - rule_id: PF-SEC-030
83
+ domain: security
84
+ title: "Artifact image not digest-pinned"
85
+ severity: high
86
+ applies_to: {fact_kind: security.artifact}
87
+ conditions:
88
+ all:
89
+ - {path: attrs.digest_pinned, op: equals, value: false}
90
+ message: "tag references are mutable — pin sha256 digest"
91
+ sources: ['kubernetes-docs']
92
+ source_refs:
93
+ kubernetes-docs: "docs/concepts/containers/images#image-pull-policy"
94
+
95
+ - rule_id: PF-SEC-031
96
+ domain: security
97
+ title: "Artifact without signature attestation"
98
+ severity: medium
99
+ applies_to: {fact_kind: security.artifact}
100
+ conditions:
101
+ all:
102
+ - {path: attrs.signed, op: equals, value: false}
103
+ message: "unsigned artifacts can't prove origin (cosign/SLSA)"
104
+ sources: ['slsa-spec', 'sigstore-docs']
105
+ source_refs:
106
+ slsa-spec: "spec/v1.0/levels"
107
+
108
+ - rule_id: PF-SEC-040
109
+ domain: security
110
+ title: "Legacy Kyverno policy type on deprecated engine version"
111
+ severity: medium
112
+ applies_to: {fact_kind: kyverno.policy}
113
+ conditions:
114
+ all:
115
+ - {path: attrs.legacy_type, op: equals, value: true}
116
+ - {path: attrs.legacy_deprecated, op: equals, value: true}
117
+ message: "Policy/ClusterPolicy are deprecated on kyverno ≥1.13 — migrate to CEL-based types"
118
+ sources: ['kyverno-docs']
119
+ source_refs:
120
+ kyverno-docs: "docs/"
121
+
122
+ - rule_id: PF-SEC-041
123
+ domain: security
124
+ title: "Kyverno policy with no rules"
125
+ severity: low
126
+ applies_to: {fact_kind: kyverno.policy}
127
+ conditions:
128
+ all:
129
+ - {path: attrs.rules, op: equals, value: 0}
130
+ - {path: attrs.policy_kind, op: in, value: [Policy, ClusterPolicy]}
131
+ message: "empty ruleset enforces nothing — declared intent without effect"
132
+ sources: ['kyverno-docs']
133
+ source_refs:
134
+ kyverno-docs: "docs/"
135
+
136
+ - rule_id: PF-SEC-042
137
+ domain: security
138
+ title: "Signature claimed but never verified"
139
+ severity: high
140
+ applies_to: {fact_kind: supply.signature}
141
+ conditions:
142
+ all:
143
+ - {path: attrs.claimed_signed, op: equals, value: true}
144
+ - {path: attrs.verified, op: equals, value: false}
145
+ message: "signed=true in a document is a claim — verification needs rekor/key lookup (host boundary)"
146
+ sources: ['sigstore-docs']
147
+
148
+ - rule_id: PF-SEC-043
149
+ domain: security
150
+ title: "OIDC-federated principal in trust policy"
151
+ severity: info
152
+ applies_to: {fact_kind: security.iam_policy}
153
+ conditions:
154
+ all:
155
+ - {path: attrs.federated_providers, op: not_empty}
156
+ message: "web-identity federation — check audience/sub conditions on the provider"
157
+ sources: ['aws-iam-docs']
158
+ source_refs:
159
+ aws-iam-docs: "IAM/latest/UserGuide/id_roles_providers_oidc.html"
@@ -0,0 +1,67 @@
1
+ # SRE rules — operate on sre.* facts emitted by observe verbs.
2
+ rules:
3
+ - rule_id: PF-SLO-001
4
+ domain: sre
5
+ title: "Error budget exhausted"
6
+ severity: critical
7
+ applies_to: {fact_kind: sre.error_budget}
8
+ conditions:
9
+ all:
10
+ - {path: attrs.status, op: equals, value: exhausted}
11
+ message: "freeze non-essential change; reliability is the feature now"
12
+ sources: ['google-sre-book']
13
+ source_refs:
14
+ google-sre-book: "sre-book/service-level-objectives"
15
+
16
+ - rule_id: PF-SLO-002
17
+ domain: sre
18
+ title: "Burn rate above 1 — budget consumed faster than window"
19
+ severity: high
20
+ applies_to: {fact_kind: sre.error_budget}
21
+ conditions:
22
+ all:
23
+ - {path: attrs.status, op: in, value: [at_risk, exhausted]}
24
+ - {path: attrs.burn_rate, op: gte, value: 2.0}
25
+ message: "sustained burn ≥2× empties the budget inside half the window"
26
+ sources: ['google-sre-book']
27
+ source_refs:
28
+ google-sre-book: "workbook/alerting-on-slos"
29
+
30
+ - rule_id: PF-SLO-003
31
+ domain: sre
32
+ title: "SLO inputs unresolved"
33
+ severity: info
34
+ applies_to: {fact_kind: sre.error_budget}
35
+ conditions:
36
+ all:
37
+ - {path: attrs.status, op: equals, value: unresolved}
38
+ message: "no SLI events — budget position cannot be claimed"
39
+ sources: ['google-sre-book']
40
+ source_refs:
41
+ google-sre-book: "sre-book/service-level-objectives"
42
+
43
+ - rule_id: PF-SRE-001
44
+ domain: sre
45
+ title: "Saturated resource"
46
+ severity: high
47
+ applies_to: {fact_kind: sre.capacity}
48
+ conditions:
49
+ all:
50
+ - {path: attrs.class, op: equals, value: saturated}
51
+ message: "≥95% utilization — headroom gone before the next spike"
52
+ sources: ['google-sre-book']
53
+ source_refs:
54
+ google-sre-book: "sre-book/monitoring-distributed-systems"
55
+
56
+ - rule_id: PF-SRE-002
57
+ domain: sre
58
+ title: "Hot resource"
59
+ severity: medium
60
+ applies_to: {fact_kind: sre.capacity}
61
+ conditions:
62
+ all:
63
+ - {path: attrs.class, op: equals, value: hot}
64
+ message: "≥85% utilization — plan capacity, don't wait for the page"
65
+ sources: ['google-sre-book']
66
+ source_refs:
67
+ google-sre-book: "workbook/implementing-slos"
@@ -0,0 +1,75 @@
1
+ """Compose graph + findings into a per-node diagnosis (§7 `diagnose`).
2
+
3
+ Answers "o que há de errado com X?" with evidence: the node's facts, the
4
+ findings touching it (by evidence location/source), its blast radius, and
5
+ what remains unresolved. Never infers — absent facts stay `unresolved`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from platformforge.graph.model import Graph
13
+ from platformforge.graph.model import node_id as mk_node_id
14
+ from platformforge.graph.query import blast_radius, dependencies, dependents
15
+
16
+
17
+ def _fact_nodes(fact: dict) -> set[str]:
18
+ """Node ids a fact contributes to, via its graph contribution."""
19
+ out = {fact.get("attrs", {}).get("node_id") or
20
+ fact.get("attrs", {}).get("id")}
21
+ for n in ((fact.get("attrs") or {}).get("graph") or {}).get("nodes", []):
22
+ out.add(mk_node_id(n["kind"], n["label"]))
23
+ return {x for x in out if x}
24
+
25
+
26
+ def _findings_for(findings: list[dict], node_id: str,
27
+ facts_by_id: dict[str, dict]) -> list[dict]:
28
+ out = []
29
+ for f in findings:
30
+ for e in f.get("evidence", []):
31
+ fact = facts_by_id.get(e, {})
32
+ loc = str(fact.get("location") or fact.get("source") or "")
33
+ if node_id in _fact_nodes(fact) or node_id in loc \
34
+ or e == node_id:
35
+ out.append({"rule_id": f.get("rule_id"),
36
+ "severity": f.get("severity"),
37
+ "title": f.get("title"),
38
+ "via": e})
39
+ break
40
+ return out
41
+
42
+
43
+ def diagnose(g: Graph, node_id: str, findings: list[dict],
44
+ facts: list[dict] | None = None,
45
+ criticality: dict | None = None) -> dict[str, Any]:
46
+ node = g.nodes.get(node_id)
47
+ if node is None:
48
+ return {"refusal": "platform.diagnose.unknown_node",
49
+ "node": node_id,
50
+ "unlock": "platformforge graph stats → pick a node id"}
51
+ facts_by_id = {f.get("fact_id"): f for f in facts or []}
52
+ node_facts = [f for f in facts_by_id.values()
53
+ if node_id in _fact_nodes(f)
54
+ or node_id in str(f.get("source", ""))]
55
+ blast = blast_radius(g, node_id)
56
+ deps = sorted(dependencies(g, node_id))
57
+ deps_of = sorted(dependents(g, node_id))
58
+
59
+ unresolved = []
60
+ for want in ("owner", "criticality", "env"):
61
+ if want not in node.attrs:
62
+ unresolved.append(f"node.attrs.{want}")
63
+ if node_facts == []:
64
+ unresolved.append("node_facts (no analyzer emitted facts for node)")
65
+
66
+ from platformforge.risk.engine import assess_change
67
+ from platformforge.risk.engine import criticality as crit
68
+ sig = {"blast_radius": len(blast.get("nodes", [])),
69
+ "declared_criticality": crit(node.attrs)}
70
+ sev = assess_change(sig)
71
+ return {"node": node_id, "kind": node.kind, "attrs": node.attrs,
72
+ "facts": node_facts,
73
+ "findings": _findings_for(findings, node_id, facts_by_id),
74
+ "dependencies": deps, "dependents": deps_of,
75
+ "blast_radius": blast, "risk": sev, "unresolved": unresolved}
@@ -0,0 +1,18 @@
1
+ """Portable distribution for Platform Forge."""
2
+ from .bundle import build_bundle, install_bundle, verify_bundle
3
+ from .models import DistributionManifest, InstallPlan, InstallReceipt
4
+ from .service import apply_install, doctor, plan_install, uninstall, upgrade
5
+
6
+ __all__ = [
7
+ "DistributionManifest",
8
+ "InstallPlan",
9
+ "InstallReceipt",
10
+ "apply_install",
11
+ "build_bundle",
12
+ "doctor",
13
+ "install_bundle",
14
+ "plan_install",
15
+ "uninstall",
16
+ "upgrade",
17
+ "verify_bundle",
18
+ ]
@@ -0,0 +1,127 @@
1
+ """Self-contained, relocatable portable bundles."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ from platformforge.distribution.service import _distribution, portable_assets
10
+
11
+
12
+ def _sha(b: bytes) -> str:
13
+ return hashlib.sha256(b).hexdigest()
14
+
15
+ def build_bundle(output: str | Path, profile: str = "agentic",
16
+ hosts: list[str] | None = None, offline: bool = True) -> dict:
17
+ hosts = hosts or ["agents"]
18
+ root = Path(output).resolve()
19
+ if root.exists():
20
+ shutil.rmtree(root)
21
+ root.mkdir(parents=True)
22
+ assets = portable_assets(hosts)
23
+ manifest = _distribution(profile, hosts, assets)
24
+ for rel, data in assets.items():
25
+ p = root / "assets" / rel
26
+ p.parent.mkdir(parents=True, exist_ok=True)
27
+ p.write_bytes(data)
28
+ (root / "manifest.json").write_text(json.dumps(manifest.to_dict(), indent=2, sort_keys=True) + "\n")
29
+ lines = []
30
+ for p in sorted((root / "assets").rglob("*")):
31
+ if p.is_file():
32
+ rel = p.relative_to(root)
33
+ lines.append(f"{_sha(p.read_bytes())} {rel.as_posix()}")
34
+ (root / "MANIFEST.sha256").write_text("\n".join(lines) + "\n")
35
+ return {"bundle": str(root), "distribution_id": manifest.distribution_id,
36
+ "assets": len(assets), "offline": offline}
37
+
38
+ def verify_bundle(bundle: str | Path) -> dict:
39
+ root = Path(bundle).resolve()
40
+ mp, cp = root / "manifest.json", root / "MANIFEST.sha256"
41
+ if not mp.is_file() or not cp.is_file():
42
+ return {"valid": False, "refusal": "PF-DIST-BUNDLE-INCOMPLETE"}
43
+ bad = []
44
+ for line in cp.read_text().splitlines():
45
+ if not line.strip():
46
+ continue
47
+ digest, rel = line.split(" ", 1)
48
+ p = (root / rel).resolve()
49
+ if root not in p.parents:
50
+ bad.append(rel + ":path-escape")
51
+ elif not p.is_file() or _sha(p.read_bytes()) != digest:
52
+ bad.append(rel)
53
+ return {"valid": not bad, "bad": bad,
54
+ "refusal": None if not bad else "PF-DIST-HASH-MISMATCH"}
55
+
56
+
57
+ def install_bundle(bundle: str | Path, target: str | Path) -> dict:
58
+ """Install a verified bundle without network access."""
59
+ import os
60
+
61
+ from platformforge.distribution.models import DIST_SCHEMA, DistributionManifest, InstallReceipt
62
+ from platformforge.distribution.service import DIST_MANIFEST, INSTALL_RECEIPT, PF_DIR, _safe_target, _sha
63
+ from platformforge.workspace.service import ensure_workspace
64
+
65
+ root = Path(bundle).resolve()
66
+ target = Path(target).resolve()
67
+ verified = verify_bundle(root)
68
+ if not verified["valid"]:
69
+ return verified
70
+ manifest = DistributionManifest.from_dict(json.loads((root / "manifest.json").read_text()))
71
+ if manifest.schema != DIST_SCHEMA:
72
+ return {"valid": False, "refusal": "PF-DIST-UNSUPPORTED-SCHEMA",
73
+ "schema": manifest.schema}
74
+
75
+ old = {}
76
+ rp = target / PF_DIR / INSTALL_RECEIPT
77
+ if rp.is_file():
78
+ try:
79
+ old = json.loads(rp.read_text()).get("files", {})
80
+ except (OSError, ValueError):
81
+ old = {}
82
+
83
+ conflicts, writes = [], []
84
+ for asset in manifest.assets:
85
+ src = (root / "assets" / asset.path).resolve()
86
+ if root not in src.parents or not src.is_file():
87
+ return {"valid": False, "refusal": "PF-DIST-BUNDLE-INCOMPLETE",
88
+ "asset": asset.path}
89
+ dest = _safe_target(target, asset.path)
90
+ if dest.exists():
91
+ cur = _sha(dest.read_bytes())
92
+ if cur == asset.sha256:
93
+ continue
94
+ if old.get(asset.path) != cur:
95
+ conflicts.append(asset.path)
96
+ continue
97
+ writes.append((src, dest))
98
+ if conflicts:
99
+ return {"refusal": "PF-DIST-CONFLICT", "conflicts": conflicts}
100
+
101
+ staged = []
102
+ try:
103
+ for src, dest in writes:
104
+ dest.parent.mkdir(parents=True, exist_ok=True)
105
+ tmp = dest.with_name(dest.name + ".platformforge-stage")
106
+ tmp.write_bytes(src.read_bytes())
107
+ staged.append((tmp, dest))
108
+ for tmp, dest in staged:
109
+ os.replace(tmp, dest)
110
+ ensure_workspace(target, profile=manifest.profile, hosts=manifest.hosts)
111
+ pf = target / PF_DIR
112
+ pf.mkdir(parents=True, exist_ok=True)
113
+ (pf / DIST_MANIFEST).write_text(json.dumps(manifest.to_dict(), indent=2, sort_keys=True) + "\n")
114
+ files = {a.path: a.sha256 for a in manifest.assets}
115
+ receipt = InstallReceipt(
116
+ manifest.distribution_id, manifest.platformforge_version,
117
+ manifest.source_sha, str(target), manifest.profile,
118
+ manifest.hosts, files,
119
+ )
120
+ (pf / INSTALL_RECEIPT).write_text(json.dumps(receipt.to_dict(), indent=2, sort_keys=True) + "\n")
121
+ return {"status": "committed", "offline": True,
122
+ "distribution_id": manifest.distribution_id,
123
+ "files": len(files), "target": str(target)}
124
+ except Exception:
125
+ for tmp, _ in staged:
126
+ tmp.unlink(missing_ok=True)
127
+ raise
@@ -0,0 +1,247 @@
1
+ """``platformforge hosts`` — observe the AI host, open it when supported.
2
+
3
+ Real per-host flow, every step a documented command verified against the
4
+ installed revisions (devin 3000.x, Claude Code 2.x, codex-cli 0.160.x,
5
+ read in 2026-10):
6
+
7
+ 1. detect the binary (``shutil.which``);
8
+ 2. read the version (``<bin> --version``);
9
+ 3. check authentication through the official status subcommand — the
10
+ receipt carries only the ``logged_in`` boolean, never the output (it
11
+ can list credential paths);
12
+ 4. read the repo's integration state (``install.service.status``);
13
+ 5. with ``--launch`` and a TTY, run ``<bin>`` in the repo directory — the
14
+ host inherits the terminal and returns its exit code to the receipt.
15
+ Without TTY or ``--launch``, the receipt carries ``launch_command``
16
+ for the operator to paste.
17
+
18
+ A signed-out host is never launched: the receipt points at the official
19
+ login command and never asks for a credential in a prompt. MCP exposes
20
+ observation only — launching stays a CLI+TTY human boundary.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ from collections.abc import Callable
29
+ from dataclasses import dataclass
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ Which = Callable[[str], str | None]
34
+ Runner = Callable[[list[str]], tuple[int, str]]
35
+
36
+ _TIMEOUT_S = 15
37
+ _OUT_MAX = 400
38
+
39
+ LOGIN_HINTS = {
40
+ "devin": "devin auth login",
41
+ "claude": "claude auth login",
42
+ "codex": "codex login",
43
+ }
44
+
45
+ #: Hosts the flow knows how to observe. ``copilot`` has no documented
46
+ #: status subcommand — ``logged_in`` stays ``None`` (honest unknown).
47
+ HOST_NAMES: tuple[str, ...] = ("claude", "devin", "codex", "copilot")
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class _HostCmd:
52
+ binary: str
53
+ auth_status: tuple[str, ...] | None
54
+
55
+
56
+ _HOST_CMDS = {
57
+ "claude": _HostCmd("claude", ("auth", "status")),
58
+ "devin": _HostCmd("devin", ("auth", "status")),
59
+ "codex": _HostCmd("codex", ("login", "status")),
60
+ "copilot": _HostCmd("copilot", None),
61
+ }
62
+
63
+
64
+ def _default_which(binary: str) -> str | None:
65
+ return shutil.which(binary)
66
+
67
+
68
+ def _default_runner(argv: list[str]) -> tuple[int, str]:
69
+ proc = subprocess.run(
70
+ argv, capture_output=True, text=True, timeout=_TIMEOUT_S,
71
+ errors="replace", check=False,
72
+ )
73
+ return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
74
+
75
+
76
+ def _logged_in(host: str, out: str) -> bool | None:
77
+ """Only the boolean. No legible positive/negative evidence -> ``None``."""
78
+ if host == "claude":
79
+ try:
80
+ data = json.loads(out)
81
+ except ValueError:
82
+ data = None
83
+ if isinstance(data, dict) and "loggedIn" in data:
84
+ return bool(data["loggedIn"])
85
+ low = out.lower()
86
+ # negative before positive: "Not logged in" contains "logged in"
87
+ if any(s in low for s in (
88
+ "not logged", "logged out", "no credentials", "unauthorized",
89
+ )):
90
+ return False
91
+ if "logged in" in low:
92
+ return True
93
+ return None
94
+
95
+
96
+ def _call(executable: str, argv: list[str], runner: Runner) -> tuple[int, str] | dict[str, Any]:
97
+ try:
98
+ return runner([executable, *argv])
99
+ except subprocess.TimeoutExpired as exc:
100
+ return {
101
+ "status": "refused",
102
+ "reason": "host_cli_timeout",
103
+ "command": " ".join(argv),
104
+ "timeout_s": exc.timeout,
105
+ }
106
+ except OSError as exc:
107
+ return {
108
+ "status": "refused",
109
+ "reason": "host_cli_failed",
110
+ "command": " ".join(argv),
111
+ "output": str(exc)[-_OUT_MAX:],
112
+ }
113
+
114
+
115
+ def _integration(repo: Path) -> dict[str, Any]:
116
+ """Repo install state, touching nothing — ``unverified`` when never installed."""
117
+ try:
118
+ from platformforge.install import service
119
+
120
+ st = service.status(scope="project", root=repo)
121
+ except Exception as exc: # noqa: BLE001 — status must never take the host flow down
122
+ return {"state": "unknown", "error": str(exc)[:200]}
123
+ return {
124
+ "state": st.get("status", "unknown"),
125
+ "checks": [
126
+ {"id": c.get("id"), "status": c.get("status")}
127
+ for c in (st.get("checks") or [])
128
+ ],
129
+ }
130
+
131
+
132
+ def host_state(
133
+ host: str,
134
+ *,
135
+ repo: Path,
136
+ which: Which | None = None,
137
+ runner: Runner | None = None,
138
+ ) -> dict[str, Any]:
139
+ """Observed state of one host: binary, version, auth, integration, launch cmd."""
140
+ if host not in _HOST_CMDS:
141
+ return {"host": host, "state": "refused",
142
+ "reason": "host_unknown", "known": list(_HOST_CMDS)}
143
+ which = which or _default_which
144
+ runner = runner or _default_runner
145
+ cmd = _HOST_CMDS[host]
146
+ state: dict[str, Any] = {
147
+ "host": host,
148
+ "binary": cmd.binary,
149
+ "launch_command": cmd.binary,
150
+ }
151
+ path = which(cmd.binary)
152
+ if not path:
153
+ state["state"] = "NOT_DETECTED"
154
+ return state
155
+ state["binary_path"] = path
156
+ state["state"] = "DETECTED"
157
+
158
+ version = _call(path, ["--version"], runner)
159
+ if isinstance(version, dict):
160
+ state["version_refused"] = version
161
+ else:
162
+ rc, out = version
163
+ first = next(
164
+ (line.strip() for line in out.splitlines() if line.strip()), "")
165
+ state["version"] = first or None
166
+ state["state"] = "SUPPORTED" if rc == 0 else "DETECTED"
167
+
168
+ if cmd.auth_status is None:
169
+ state["logged_in"] = None
170
+ state["auth_note"] = "no documented status subcommand in this revision"
171
+ else:
172
+ auth = _call(path, list(cmd.auth_status), runner)
173
+ if isinstance(auth, dict):
174
+ state["auth_refused"] = auth
175
+ state["logged_in"] = None
176
+ else:
177
+ state["logged_in"] = _logged_in(host, auth[1])
178
+ if state.get("logged_in") is False:
179
+ state["login_hint"] = LOGIN_HINTS.get(host)
180
+
181
+ state["integration"] = _integration(repo)
182
+ state["state"] = _final_state(state)
183
+ return state
184
+
185
+
186
+ def _final_state(state: dict[str, Any]) -> str:
187
+ if state.get("logged_in") is False:
188
+ return "NEEDS_LOGIN"
189
+ integ = (state.get("integration") or {}).get("state")
190
+ if integ == "healthy":
191
+ return "READY"
192
+ if integ in {"degraded", "broken"}:
193
+ return "INTEGRATION_" + integ.upper()
194
+ return "SUPPORTED"
195
+
196
+
197
+ def hosts_flow(
198
+ target: str,
199
+ *,
200
+ repo: Path,
201
+ launch: bool = False,
202
+ tty: bool | None = None,
203
+ which: Which | None = None,
204
+ runner: Runner | None = None,
205
+ exec_host: Callable[[str, Path], int] | None = None,
206
+ ) -> dict[str, Any]:
207
+ """The host flow: implicit selector via ``target`` (``all`` measures every
208
+ declared host), per-host state, and a real launch when ``--launch`` +
209
+ TTY + READY.
210
+
211
+ ``exec_host(path, repo)`` is the launch seam — the CLI injects
212
+ ``subprocess.call``; a test injects a spy. Without a TTY, ``launched``
213
+ is false and ``launch_refused`` says why — never silence.
214
+ """
215
+ repo = Path(repo)
216
+ names = list(HOST_NAMES) if target == "all" else [target]
217
+ states = [
218
+ host_state(name, repo=repo, which=which, runner=runner)
219
+ for name in names
220
+ ]
221
+ receipt: dict[str, Any] = {
222
+ "repo": repo.as_posix(),
223
+ "hosts": states,
224
+ "launched": False,
225
+ }
226
+ if target == "all" or not launch:
227
+ if launch:
228
+ receipt["launch_refused"] = "selector `all` does not launch: pick one host"
229
+ return receipt
230
+
231
+ state = states[0]
232
+ tty = sys.stdin.isatty() if tty is None else tty
233
+ if state["state"] == "NOT_DETECTED":
234
+ receipt["launch_refused"] = "host_absent"
235
+ elif state["state"] == "NEEDS_LOGIN":
236
+ receipt["launch_refused"] = "login_required"
237
+ receipt["login_hint"] = state.get("login_hint")
238
+ elif not tty:
239
+ receipt["launch_refused"] = "no_tty"
240
+ else:
241
+ # Path came from shutil.which — same trust level as any PATH exec.
242
+ exec_fn = exec_host or (
243
+ lambda path, cwd: subprocess.call(
244
+ [path], cwd=str(cwd)))
245
+ receipt["launched"] = True
246
+ receipt["host_exit_code"] = exec_fn(state["binary_path"], repo)
247
+ return receipt