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.
- platformforge/__init__.py +7 -0
- platformforge/__main__.py +5 -0
- platformforge/_forge_portal.py +913 -0
- platformforge/_graphstudio.py +376 -0
- platformforge/_graphview.py +181 -0
- platformforge/_installkit.py +1706 -0
- platformforge/agents/__init__.py +156 -0
- platformforge/agents/bench.py +61 -0
- platformforge/agents/contextpack.py +137 -0
- platformforge/agents/contracts.py +394 -0
- platformforge/agents/coordinators.py +98 -0
- platformforge/agents/critic.py +113 -0
- platformforge/agents/debate.py +161 -0
- platformforge/agents/executors.py +160 -0
- platformforge/agents/guardian.py +48 -0
- platformforge/agents/mirrors.py +240 -0
- platformforge/agents/orchestrator.py +295 -0
- platformforge/agents/planner.py +116 -0
- platformforge/agents/playbook.py +51 -0
- platformforge/agents/referee.py +132 -0
- platformforge/agents/reviewers.py +113 -0
- platformforge/agents/roster.py +893 -0
- platformforge/agents/runledger.py +85 -0
- platformforge/agents/runstore.py +74 -0
- platformforge/agents/specialists.py +73 -0
- platformforge/agents/taskspec.py +94 -0
- platformforge/agents/uniqueness.py +115 -0
- platformforge/agents/verifier.py +106 -0
- platformforge/aiplat/__init__.py +4 -0
- platformforge/aiplat/models.py +175 -0
- platformforge/analytics/__init__.py +6 -0
- platformforge/analytics/capacity.py +109 -0
- platformforge/analytics/debt.py +92 -0
- platformforge/analytics/dx.py +69 -0
- platformforge/analytics/finops_v4.py +171 -0
- platformforge/analytics/goldenpath.py +131 -0
- platformforge/analytics/history.py +233 -0
- platformforge/analytics/measurement.py +149 -0
- platformforge/analytics/models.py +127 -0
- platformforge/analytics/opsanalytics.py +119 -0
- platformforge/analytics/policyintel.py +139 -0
- platformforge/analytics/reliability.py +106 -0
- platformforge/analytics/soak.py +135 -0
- platformforge/analytics/store.py +254 -0
- platformforge/bench.py +174 -0
- platformforge/cases/__init__.py +13 -0
- platformforge/cases/casefile.py +106 -0
- platformforge/cases/context_audit.py +134 -0
- platformforge/cases/ledgers.py +174 -0
- platformforge/cases/replay.py +142 -0
- platformforge/cases/routing_audit.py +162 -0
- platformforge/caveman/__init__.py +11 -0
- platformforge/caveman/compress.py +156 -0
- platformforge/cicd/__init__.py +16 -0
- platformforge/cicd/backstage.py +51 -0
- platformforge/cicd/dockerfile.py +64 -0
- platformforge/cicd/github_actions.py +122 -0
- platformforge/cicd/gitlab_ci.py +56 -0
- platformforge/cicd/gitops.py +130 -0
- platformforge/cli/__init__.py +0 -0
- platformforge/cli/fleetcmd.py +457 -0
- platformforge/cli/main.py +3832 -0
- platformforge/cloud/__init__.py +8 -0
- platformforge/cloud/aws.py +254 -0
- platformforge/cloud/common.py +69 -0
- platformforge/cloud/multicloud.py +143 -0
- platformforge/collect/__init__.py +11 -0
- platformforge/collect/detect.py +254 -0
- platformforge/context/__init__.py +10 -0
- platformforge/context/capsule.py +60 -0
- platformforge/context/gateway.py +227 -0
- platformforge/context/refs.py +49 -0
- platformforge/context/sufficiency.py +50 -0
- platformforge/core/__init__.py +1 -0
- platformforge/core/hashing.py +39 -0
- platformforge/core/ownership.py +220 -0
- platformforge/core/receipts.py +74 -0
- platformforge/core/redaction.py +97 -0
- platformforge/core/store.py +119 -0
- platformforge/core/workspace.py +89 -0
- platformforge/data/contracts/agent-handoff.schema.json +26 -0
- platformforge/data/contracts/agent-run.schema.json +27 -0
- platformforge/data/contracts/agent-spec.schema.json +43 -0
- platformforge/data/contracts/capability.schema.json +23 -0
- platformforge/data/contracts/change-event.schema.json +22 -0
- platformforge/data/contracts/change-intent.schema.json +62 -0
- platformforge/data/contracts/change-plan.schema.json +43 -0
- platformforge/data/contracts/collector-receipt.schema.json +36 -0
- platformforge/data/contracts/context-pack.schema.json +29 -0
- platformforge/data/contracts/debate.schema.json +31 -0
- platformforge/data/contracts/distribution-manifest-v1.schema.json +81 -0
- platformforge/data/contracts/drift-event.schema.json +21 -0
- platformforge/data/contracts/fact.schema.json +35 -0
- platformforge/data/contracts/finding.schema.json +20 -0
- platformforge/data/contracts/graph-v2.schema.json +67 -0
- platformforge/data/contracts/graph.schema.json +40 -0
- platformforge/data/contracts/install-receipt-v1.schema.json +49 -0
- platformforge/data/contracts/observation.schema.json +93 -0
- platformforge/data/contracts/receipt.schema.json +21 -0
- platformforge/data/contracts/recommendation.schema.json +32 -0
- platformforge/data/contracts/refusal.schema.json +16 -0
- platformforge/data/contracts/slo-contract.schema.json +25 -0
- platformforge/data/contracts/task-spec.schema.json +37 -0
- platformforge/data/contracts/workspace-manifest-v1.schema.json +62 -0
- platformforge/data/evals/cases/agent-budget-exhaustion/case.yaml +6 -0
- platformforge/data/evals/cases/agent-conflicting-specialists/case.yaml +11 -0
- platformforge/data/evals/cases/agent-fleet-optimization/case.yaml +13 -0
- platformforge/data/evals/cases/agent-host-no-subagents/case.yaml +6 -0
- platformforge/data/evals/cases/agent-incident-multi/case.yaml +13 -0
- platformforge/data/evals/cases/agent-missing-evidence/case.yaml +6 -0
- platformforge/data/evals/cases/agent-multi-domain-audit/case.yaml +14 -0
- platformforge/data/evals/cases/agent-production-change/case.yaml +15 -0
- platformforge/data/evals/cases/agent-security-mandatory/case.yaml +12 -0
- platformforge/data/evals/cases/agent-simple-iac/case.yaml +14 -0
- platformforge/data/evals/cases/approval-integrity-seal/case.yaml +6 -0
- platformforge/data/evals/cases/capacity-hot/case.yaml +7 -0
- platformforge/data/evals/cases/capacity-hot/fixture/capacity.json +5 -0
- platformforge/data/evals/cases/catalog-noowner/case.yaml +7 -0
- platformforge/data/evals/cases/catalog-noowner/fixture/catalog-info.yaml +6 -0
- platformforge/data/evals/cases/contract-fact/case.yaml +5 -0
- platformforge/data/evals/cases/contract-fact/fixture/doc.json +2 -0
- platformforge/data/evals/cases/cosign-unverified/case.yaml +8 -0
- platformforge/data/evals/cases/cosign-unverified/fixture/sig.json +33 -0
- platformforge/data/evals/cases/expected-delta-refusal/case.yaml +6 -0
- platformforge/data/evals/cases/finops-noattr/case.yaml +9 -0
- platformforge/data/evals/cases/finops-noattr/fixture/costs.json +7 -0
- platformforge/data/evals/cases/fleet-inv-absent-allocation-unknown/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-centrality-not-criticality/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-correlation-not-causality/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-dx-no-person-metrics/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-federation-restricted-denied/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-federation-secret-denied/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-history-window-deterministic/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-low-util-not-deletable/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-member-key-deterministic/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-missing-denominator/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-partial-coverage-visible/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-plan-changeintent-only/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-report-all-cited/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-small-sample-confidence/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-uncertain-optimization-suppressed/case.yaml +7 -0
- platformforge/data/evals/cases/gha-dangerous/case.yaml +12 -0
- platformforge/data/evals/cases/gha-dangerous/fixture/.github/workflows/ci.yaml +11 -0
- platformforge/data/evals/cases/gitops-v2/case.yaml +11 -0
- platformforge/data/evals/cases/gitops-v2/fixture/argo.yaml +41 -0
- platformforge/data/evals/cases/graph-k8s-blast/case.yaml +7 -0
- platformforge/data/evals/cases/graph-k8s-blast/fixture/deploy.yaml +15 -0
- platformforge/data/evals/cases/iac-module-nopin/case.yaml +8 -0
- platformforge/data/evals/cases/iac-module-nopin/fixture/main.tf +3 -0
- platformforge/data/evals/cases/iac-plan-replace/case.yaml +8 -0
- platformforge/data/evals/cases/iac-plan-replace/fixture/plan.json +12 -0
- platformforge/data/evals/cases/iac-sg-open/case.yaml +8 -0
- platformforge/data/evals/cases/iac-sg-open/fixture/main.tf +9 -0
- platformforge/data/evals/cases/iam-oidc/case.yaml +8 -0
- platformforge/data/evals/cases/iam-oidc/fixture/policy.json +11 -0
- platformforge/data/evals/cases/iam-wildcard-positive/case.yaml +8 -0
- platformforge/data/evals/cases/iam-wildcard-positive/fixture/policy.json +3 -0
- platformforge/data/evals/cases/k8s-bad-actor/case.yaml +19 -0
- platformforge/data/evals/cases/k8s-bad-actor/fixture/app.yaml +97 -0
- platformforge/data/evals/cases/k8s-beta-api/case.yaml +8 -0
- platformforge/data/evals/cases/k8s-beta-api/fixture/cr.yaml +7 -0
- platformforge/data/evals/cases/k8s-dep-api-boundary/case.yaml +15 -0
- platformforge/data/evals/cases/k8s-dep-api-boundary/fixture/app.yaml +25 -0
- platformforge/data/evals/cases/k8s-dep-api-clean/case.yaml +12 -0
- platformforge/data/evals/cases/k8s-dep-api-clean/fixture/app.yaml +29 -0
- platformforge/data/evals/cases/k8s-dep-api-unknown/case.yaml +11 -0
- platformforge/data/evals/cases/k8s-dep-api-unknown/fixture/app.yaml +25 -0
- platformforge/data/evals/cases/k8s-dep-api-v120/case.yaml +12 -0
- platformforge/data/evals/cases/k8s-dep-api-v120/fixture/app.yaml +25 -0
- platformforge/data/evals/cases/k8s-dep-api-v129/case.yaml +9 -0
- platformforge/data/evals/cases/k8s-dep-api-v129/fixture/app.yaml +25 -0
- platformforge/data/evals/cases/k8s-hpa-noreq/case.yaml +8 -0
- platformforge/data/evals/cases/k8s-hpa-noreq/fixture/app.yaml +28 -0
- platformforge/data/evals/cases/k8s-limits-boundary/case.yaml +10 -0
- platformforge/data/evals/cases/k8s-limits-boundary/fixture/app.yaml +15 -0
- platformforge/data/evals/cases/k8s-limits-negative/case.yaml +8 -0
- platformforge/data/evals/cases/k8s-limits-negative/fixture/deploy.yaml +15 -0
- platformforge/data/evals/cases/k8s-limits-positive/case.yaml +8 -0
- platformforge/data/evals/cases/k8s-limits-positive/fixture/deploy.yaml +14 -0
- platformforge/data/evals/cases/k8s-nopdb/case.yaml +7 -0
- platformforge/data/evals/cases/k8s-nopdb/fixture/app.yaml +34 -0
- platformforge/data/evals/cases/knowledge-registry/case.yaml +10 -0
- platformforge/data/evals/cases/kyverno-dep/case.yaml +10 -0
- platformforge/data/evals/cases/kyverno-dep/fixture/pol.yaml +18 -0
- platformforge/data/evals/cases/kyverno-legacy/case.yaml +8 -0
- platformforge/data/evals/cases/kyverno-legacy/fixture/pol.yaml +7 -0
- platformforge/data/evals/cases/live-aws-collect/case.yaml +11 -0
- platformforge/data/evals/cases/live-aws-collect/fixture/aws/ec2_describe-vpcs.json +15 -0
- platformforge/data/evals/cases/live-aws-collect/fixture/aws/iam_list-users.denied.txt +1 -0
- platformforge/data/evals/cases/live-aws-collect/fixture/aws/sts_get-caller-identity.json +5 -0
- platformforge/data/evals/cases/live-drift/case.yaml +8 -0
- platformforge/data/evals/cases/live-drift/fixture/after.json +22 -0
- platformforge/data/evals/cases/live-drift/fixture/before.json +22 -0
- platformforge/data/evals/cases/live-incident/case.yaml +7 -0
- platformforge/data/evals/cases/live-incident/fixture/changes.json +8 -0
- platformforge/data/evals/cases/live-incident/fixture/incident.json +7 -0
- platformforge/data/evals/cases/live-k8s-collect/case.yaml +10 -0
- platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_configmaps.denied.txt +1 -0
- platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_deployments.apps.json +25 -0
- platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_pods.json +17 -0
- platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_secrets.json +13 -0
- platformforge/data/evals/cases/live-multicluster/case.yaml +7 -0
- platformforge/data/evals/cases/live-multicluster/fixture/clusters.json +6 -0
- platformforge/data/evals/cases/live-plan/case.yaml +7 -0
- platformforge/data/evals/cases/live-plan/fixture/drift.json +8 -0
- platformforge/data/evals/cases/live-reconcile/case.yaml +7 -0
- platformforge/data/evals/cases/live-reconcile/fixture/desired.json +7 -0
- platformforge/data/evals/cases/live-reconcile/fixture/observed.json +18 -0
- platformforge/data/evals/cases/live-secrets-boundary/case.yaml +8 -0
- platformforge/data/evals/cases/live-secrets-boundary/fixture/envelope.json +16 -0
- platformforge/data/evals/cases/live-temporal-graph/case.yaml +10 -0
- platformforge/data/evals/cases/live-temporal-graph/fixture/graph.json +22 -0
- platformforge/data/evals/cases/ops-approval/case.yaml +6 -0
- platformforge/data/evals/cases/ops-convergence/case.yaml +6 -0
- platformforge/data/evals/cases/ops-execution-safety/case.yaml +6 -0
- platformforge/data/evals/cases/ops-intent/case.yaml +6 -0
- platformforge/data/evals/cases/ops-planning/case.yaml +6 -0
- platformforge/data/evals/cases/ops-policy/case.yaml +6 -0
- platformforge/data/evals/cases/ops-rollback/case.yaml +6 -0
- platformforge/data/evals/cases/ops-verification/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-argocd-revision/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-blind-inverse/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-git-commit/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-impossible/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-k8s-annotation-restore/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-k8s-scale/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-material-tamper/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-terraform-replan/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-unknown/case.yaml +6 -0
- platformforge/data/evals/cases/routing-specialist/case.yaml +9 -0
- platformforge/data/evals/cases/rtk-roundtrip/case.yaml +5 -0
- platformforge/data/evals/cases/rtk-roundtrip/fixture/verbose.txt +300 -0
- platformforge/data/evals/cases/sbom-vuln/case.yaml +9 -0
- platformforge/data/evals/cases/sbom-vuln/fixture/sbom.json +9 -0
- platformforge/data/evals/cases/secret-prose-mixed/fixture/app.env +1 -0
- platformforge/data/evals/cases/secret-prose-mixed/fixture/policy-notes.md +7 -0
- platformforge/data/evals/cases/secrets-noleak/case.yaml +5 -0
- platformforge/data/evals/cases/secrets-noleak/fixture/app.env +2 -0
- platformforge/data/evals/cases/slo-fastburn/case.yaml +8 -0
- platformforge/data/evals/cases/slo-fastburn/fixture/events.yaml +2 -0
- platformforge/data/evals/cases/slo-fastburn/fixture/slo.yaml +4 -0
- platformforge/data/evals/cases/slo-noinput/case.yaml +8 -0
- platformforge/data/evals/cases/slo-noinput/fixture/events.yaml +1 -0
- platformforge/data/evals/cases/slo-noinput/fixture/slo.yaml +4 -0
- platformforge/data/evals/cases/sot-conflict/case.yaml +6 -0
- platformforge/data/evals/cases/token-budget/case.yaml +6 -0
- platformforge/data/evals/cases/token-budget/fixture/big.py +50 -0
- platformforge/data/evals/cases/xr-delete/case.yaml +8 -0
- platformforge/data/evals/cases/xr-delete/fixture/xr.yaml +10 -0
- platformforge/data/evals/cases/xr-graph-chain/case.yaml +8 -0
- platformforge/data/evals/cases/xr-graph-chain/fixture/xrd.yaml +90 -0
- platformforge/data/evals/cases/xr-negative-crd/case.yaml +8 -0
- platformforge/data/evals/cases/xr-negative-crd/fixture/widget.yaml +19 -0
- platformforge/data/evals/cases/xr-upbound-mr/case.yaml +8 -0
- platformforge/data/evals/cases/xr-upbound-mr/fixture/bucket.yaml +12 -0
- platformforge/data/evals/qpt-bench/change-review/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/change-review/fixture/plan.json +9 -0
- platformforge/data/evals/qpt-bench/gha-security/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/gha-security/fixture/ci.yml +10 -0
- platformforge/data/evals/qpt-bench/iam-investigate/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/iam-investigate/fixture/policy.json +6 -0
- platformforge/data/evals/qpt-bench/incident-analysis/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/incident-analysis/fixture/query.json +5 -0
- platformforge/data/evals/qpt-bench/incident-analysis/fixture/rules.yaml +6 -0
- platformforge/data/evals/qpt-bench/k8s-review/case.yaml +6 -0
- platformforge/data/evals/qpt-bench/k8s-review/fixture/deploy.yaml +15 -0
- platformforge/data/evals/qpt-bench/tf-security/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/tf-security/fixture/main.tf +17 -0
- platformforge/data/knowledge/aws/eks.yaml +20 -0
- platformforge/data/knowledge/aws/iam.yaml +28 -0
- platformforge/data/knowledge/aws/network-exposure.yaml +19 -0
- platformforge/data/knowledge/aws/organizations.yaml +19 -0
- platformforge/data/knowledge/crossplane/composition-providers.yaml +20 -0
- platformforge/data/knowledge/crossplane/managed-resources.yaml +26 -0
- platformforge/data/knowledge/crossplane/v1-v2.yaml +25 -0
- platformforge/data/knowledge/kubernetes/api-deprecations.yaml +33 -0
- platformforge/data/knowledge/kubernetes/autoscaling-rbac.yaml +27 -0
- platformforge/data/knowledge/kubernetes/gateway-api.yaml +20 -0
- platformforge/data/knowledge/kubernetes/pod-security.yaml +26 -0
- platformforge/data/knowledge/sources.yaml +516 -0
- platformforge/data/knowledge/terraform/lifecycle.yaml +18 -0
- platformforge/data/knowledge/terraform/plan-semantics.yaml +26 -0
- platformforge/data/lab/chaos-pod-kill/chaos.yaml +3 -0
- platformforge/data/lab/chaos-pod-kill/facts.json +13 -0
- platformforge/data/lab/fleets/acme/capacity.yaml +35 -0
- platformforge/data/lab/fleets/acme/costs.yaml +25 -0
- platformforge/data/lab/fleets/acme/events.yaml +41 -0
- platformforge/data/lab/fleets/acme/fleet.yaml +51 -0
- platformforge/data/lab/fleets/acme/graph.yaml +45 -0
- platformforge/data/lab/fleets/acme/policies.yaml +28 -0
- platformforge/data/lab/fleets/acme/requests.yaml +24 -0
- platformforge/data/lab/scenarios/argo-bad-rollout/expected.yaml +6 -0
- platformforge/data/lab/scenarios/argo-bad-rollout/fixture/app.yaml +9 -0
- platformforge/data/lab/scenarios/aws-dump/expected.yaml +19 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/compute.json +4 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/iam.json +7 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/network.json +6 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/org.json +4 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/storage.json +4 -0
- platformforge/data/lab/scenarios/container-unsigned/expected.yaml +5 -0
- platformforge/data/lab/scenarios/container-unsigned/fixture/supply.json +11 -0
- platformforge/data/lab/scenarios/cost-idle-workload/expected.yaml +5 -0
- platformforge/data/lab/scenarios/cost-idle-workload/fixture/costs.json +12 -0
- platformforge/data/lab/scenarios/crossplane-platform/expected.yaml +16 -0
- platformforge/data/lab/scenarios/crossplane-platform/fixture/negative.yaml +18 -0
- platformforge/data/lab/scenarios/crossplane-platform/fixture/xrd.yaml +90 -0
- platformforge/data/lab/scenarios/demo-platform/expected.yaml +16 -0
- platformforge/data/lab/scenarios/demo-platform/fixture/.github/workflows/ci.yml +10 -0
- platformforge/data/lab/scenarios/demo-platform/fixture/deploy.yaml +16 -0
- platformforge/data/lab/scenarios/demo-platform/fixture/infra.tf +5 -0
- platformforge/data/lab/scenarios/fleet-ai/expected.yaml +12 -0
- platformforge/data/lab/scenarios/fleet-capacity/expected.yaml +12 -0
- platformforge/data/lab/scenarios/fleet-cost/expected.yaml +18 -0
- platformforge/data/lab/scenarios/fleet-federation/expected.yaml +11 -0
- platformforge/data/lab/scenarios/fleet-golden-path/expected.yaml +18 -0
- platformforge/data/lab/scenarios/fleet-op-hotspot/expected.yaml +11 -0
- platformforge/data/lab/scenarios/fleet-ownership/expected.yaml +17 -0
- platformforge/data/lab/scenarios/fleet-partial/expected.yaml +10 -0
- platformforge/data/lab/scenarios/fleet-policy-friction/expected.yaml +10 -0
- platformforge/data/lab/scenarios/fleet-recurring-incident/expected.yaml +12 -0
- platformforge/data/lab/scenarios/fleet-report/expected.yaml +13 -0
- platformforge/data/lab/scenarios/fleet-risk/expected.yaml +18 -0
- platformforge/data/lab/scenarios/gitops-drift/expected.yaml +6 -0
- platformforge/data/lab/scenarios/gitops-drift/fixture/app.yaml +12 -0
- platformforge/data/lab/scenarios/k8s-bad-readiness/expected.yaml +6 -0
- platformforge/data/lab/scenarios/k8s-bad-readiness/fixture/deploy.yaml +23 -0
- platformforge/data/lab/scenarios/k8s-bad-readiness/fixture/netpol.yaml +9 -0
- platformforge/data/lab/scenarios/k8s-missing-limits/expected.yaml +6 -0
- platformforge/data/lab/scenarios/k8s-missing-limits/fixture/deploy.yaml +26 -0
- platformforge/data/lab/scenarios/k8s-missing-limits/fixture/netpol.yaml +9 -0
- platformforge/data/lab/scenarios/k8s-single-replica/expected.yaml +5 -0
- platformforge/data/lab/scenarios/k8s-single-replica/fixture/deploy.yaml +26 -0
- platformforge/data/lab/scenarios/k8s-single-replica/fixture/netpol.yaml +4 -0
- platformforge/data/lab/scenarios/live-aws-collect/expected.yaml +4 -0
- platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/ec2_describe-vpcs.json +15 -0
- platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/iam_list-users.denied.txt +1 -0
- platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/sts_get-caller-identity.json +5 -0
- platformforge/data/lab/scenarios/live-k8s-collect/expected.yaml +4 -0
- platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_configmaps.denied.txt +1 -0
- platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_deployments.apps.json +25 -0
- platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_pods.json +17 -0
- platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_secrets.json +13 -0
- platformforge/data/lab/scenarios/live-kind/expected.yaml +14 -0
- platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_configmaps.denied.txt +1 -0
- platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_deployments.apps.json +25 -0
- platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_pods.json +17 -0
- platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_secrets.json +13 -0
- platformforge/data/lab/scenarios/network-policy-missing/expected.yaml +6 -0
- platformforge/data/lab/scenarios/network-policy-missing/fixture/deploy.yaml +26 -0
- platformforge/data/lab/scenarios/ops-bad-rollout/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-bad-rollout/fixture/ops.yaml +33 -0
- platformforge/data/lab/scenarios/ops-break-glass/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-break-glass/fixture/ops.yaml +21 -0
- platformforge/data/lab/scenarios/ops-broken-selector/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-broken-selector/fixture/ops.yaml +21 -0
- platformforge/data/lab/scenarios/ops-changed-plan-hash/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-changed-plan-hash/fixture/ops.yaml +20 -0
- platformforge/data/lab/scenarios/ops-conflicting-operation/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-conflicting-operation/fixture/ops.yaml +21 -0
- platformforge/data/lab/scenarios/ops-expired-approval/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-expired-approval/fixture/ops.yaml +20 -0
- platformforge/data/lab/scenarios/ops-partial-failure/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-partial-failure/fixture/ops.yaml +24 -0
- platformforge/data/lab/scenarios/ops-policy-denial/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-policy-denial/fixture/ops.yaml +17 -0
- platformforge/data/lab/scenarios/ops-replica-drift/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-replica-drift/fixture/ops.yaml +28 -0
- platformforge/data/lab/scenarios/ops-rollback-annotate/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-annotate/fixture/ops.yaml +41 -0
- platformforge/data/lab/scenarios/ops-rollback-gitops/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-gitops/fixture/ops.yaml +38 -0
- platformforge/data/lab/scenarios/ops-rollback-impossible/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-impossible/fixture/ops.yaml +34 -0
- platformforge/data/lab/scenarios/ops-rollback-scale/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-scale/fixture/ops.yaml +40 -0
- platformforge/data/lab/scenarios/ops-rollback-terraform/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-terraform/fixture/ops.yaml +38 -0
- platformforge/data/lab/scenarios/ops-rollback-terraform/fixture/tf/tfplan.bin +1 -0
- platformforge/data/lab/scenarios/ops-stale-observation/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-stale-observation/fixture/ops.yaml +20 -0
- platformforge/data/lab/scenarios/secret-exposure/expected.yaml +5 -0
- platformforge/data/lab/scenarios/secret-exposure/fixture/app.env +2 -0
- platformforge/data/lab/scenarios/slo-burn/expected.yaml +5 -0
- platformforge/data/lab/scenarios/slo-burn/fixture/events.yaml +2 -0
- platformforge/data/lab/scenarios/slo-burn/fixture/slo.yaml +4 -0
- platformforge/data/lab/scenarios/terraform-iam-wildcard/expected.yaml +5 -0
- platformforge/data/lab/scenarios/terraform-iam-wildcard/fixture/policy.json +14 -0
- platformforge/data/lab/scenarios/terraform-public-bucket/expected.yaml +6 -0
- platformforge/data/lab/scenarios/terraform-public-bucket/fixture/main.tf +5 -0
- platformforge/data/portable_skills/platformforge-agents/SKILL.md +47 -0
- platformforge/data/portable_skills/platformforge-change/SKILL.md +41 -0
- platformforge/data/portable_skills/platformforge-core/SKILL.md +62 -0
- platformforge/data/portable_skills/platformforge-economy/SKILL.md +42 -0
- platformforge/data/portable_skills/platformforge-finops/SKILL.md +34 -0
- platformforge/data/portable_skills/platformforge-fleet/SKILL.md +39 -0
- platformforge/data/portable_skills/platformforge-governance/SKILL.md +76 -0
- platformforge/data/portable_skills/platformforge-graph/SKILL.md +46 -0
- platformforge/data/portable_skills/platformforge-lab/SKILL.md +39 -0
- platformforge/data/portable_skills/platformforge-sdd/SKILL.md +39 -0
- platformforge/data/portable_skills/platformforge-security/SKILL.md +38 -0
- platformforge/data/portable_skills/platformforge-sre/SKILL.md +41 -0
- platformforge/data/rules/catalog/finops.yaml +26 -0
- platformforge/data/rules/catalog/gitops.yaml +161 -0
- platformforge/data/rules/catalog/iac.yaml +84 -0
- platformforge/data/rules/catalog/k8s.yaml +322 -0
- platformforge/data/rules/catalog/orchestration.yaml +99 -0
- platformforge/data/rules/catalog/product.yaml +42 -0
- platformforge/data/rules/catalog/routing.yaml +122 -0
- platformforge/data/rules/catalog/security.yaml +159 -0
- platformforge/data/rules/catalog/sre.yaml +67 -0
- platformforge/diagnose/__init__.py +75 -0
- platformforge/distribution/__init__.py +18 -0
- platformforge/distribution/bundle.py +127 -0
- platformforge/distribution/hostflow.py +247 -0
- platformforge/distribution/models.py +80 -0
- platformforge/distribution/service.py +225 -0
- platformforge/economy/__init__.py +27 -0
- platformforge/economy/bench.py +66 -0
- platformforge/economy/budget.py +198 -0
- platformforge/economy/cache.py +287 -0
- platformforge/economy/checkpoint.py +112 -0
- platformforge/economy/collection.py +142 -0
- platformforge/economy/doctor.py +207 -0
- platformforge/economy/engine.py +158 -0
- platformforge/economy/envelope.py +61 -0
- platformforge/economy/fleetpack.py +86 -0
- platformforge/economy/ledger.py +232 -0
- platformforge/economy/plan.py +131 -0
- platformforge/economy/pricing.py +101 -0
- platformforge/economy/qpt.py +286 -0
- platformforge/economy/reconcile.py +86 -0
- platformforge/economy/verifyplan.py +107 -0
- platformforge/economy/waste.py +123 -0
- platformforge/evals/__init__.py +16 -0
- platformforge/evals/coverage.py +140 -0
- platformforge/evals/fleet_invariants.py +260 -0
- platformforge/evals/ops_invariants.py +158 -0
- platformforge/evals/runner.py +541 -0
- platformforge/federation/__init__.py +4 -0
- platformforge/federation/node.py +135 -0
- platformforge/finops/__init__.py +14 -0
- platformforge/finops/alloc.py +31 -0
- platformforge/finops/costs.py +79 -0
- platformforge/finops/focus.py +100 -0
- platformforge/finops/graph_cost.py +51 -0
- platformforge/finops/ingest.py +144 -0
- platformforge/finops/insights.py +193 -0
- platformforge/finops/unit.py +36 -0
- platformforge/fleet/__init__.py +5 -0
- platformforge/fleet/loader.py +73 -0
- platformforge/fleet/models.py +155 -0
- platformforge/fleet/orggraph.py +172 -0
- platformforge/fleet/query.py +154 -0
- platformforge/forge/__init__.py +9 -0
- platformforge/forge/collect.py +88 -0
- platformforge/forge/delegate.py +77 -0
- platformforge/forge/manifest.py +106 -0
- platformforge/freeze/__init__.py +22 -0
- platformforge/freeze/exceptions.py +56 -0
- platformforge/freeze/manifest.py +167 -0
- platformforge/freeze/snapshots.py +108 -0
- platformforge/graph/__init__.py +39 -0
- platformforge/graph/backend.py +174 -0
- platformforge/graph/bench.py +245 -0
- platformforge/graph/build.py +130 -0
- platformforge/graph/delivery.py +183 -0
- platformforge/graph/diff.py +292 -0
- platformforge/graph/events.py +116 -0
- platformforge/graph/migrate.py +44 -0
- platformforge/graph/model.py +264 -0
- platformforge/graph/persist.py +97 -0
- platformforge/graph/query.py +290 -0
- platformforge/graph/temporal.py +78 -0
- platformforge/graph/vocab.py +63 -0
- platformforge/graphview.py +94 -0
- platformforge/iac/__init__.py +7 -0
- platformforge/iac/drift.py +53 -0
- platformforge/iac/plan.py +84 -0
- platformforge/iac/terraform.py +123 -0
- platformforge/install/__init__.py +1 -0
- platformforge/install/service.py +421 -0
- platformforge/k8s/__init__.py +8 -0
- platformforge/k8s/helm.py +152 -0
- platformforge/k8s/hubble.py +87 -0
- platformforge/k8s/manifests.py +513 -0
- platformforge/knowledge/__init__.py +1 -0
- platformforge/knowledge/packs.py +157 -0
- platformforge/knowledge/registry.py +249 -0
- platformforge/knowledge/select.py +127 -0
- platformforge/lab/__init__.py +5 -0
- platformforge/lab/chaos.py +71 -0
- platformforge/lab/fleetlab.py +351 -0
- platformforge/lab/opslab.py +271 -0
- platformforge/lab/runner.py +219 -0
- platformforge/live/__init__.py +36 -0
- platformforge/live/budget.py +92 -0
- platformforge/live/capability.py +80 -0
- platformforge/live/collectors/__init__.py +6 -0
- platformforge/live/collectors/aws.py +370 -0
- platformforge/live/collectors/aws_transport.py +98 -0
- platformforge/live/collectors/k8s.py +389 -0
- platformforge/live/collectors/k8s_transport.py +85 -0
- platformforge/live/cursors.py +48 -0
- platformforge/live/drift.py +130 -0
- platformforge/live/envelope.py +85 -0
- platformforge/live/federation.py +113 -0
- platformforge/live/fixtures.py +168 -0
- platformforge/live/identity.py +258 -0
- platformforge/live/incident.py +200 -0
- platformforge/live/models.py +435 -0
- platformforge/live/reconcile.py +382 -0
- platformforge/live/remediate.py +169 -0
- platformforge/live/store.py +256 -0
- platformforge/live/topology.py +249 -0
- platformforge/mcp/__init__.py +7 -0
- platformforge/mcp/parity.py +69 -0
- platformforge/mcp/registry.py +427 -0
- platformforge/mcp/server.py +84 -0
- platformforge/mcp/tools.py +732 -0
- platformforge/models/__init__.py +21 -0
- platformforge/models/base.py +214 -0
- platformforge/observe/__init__.py +26 -0
- platformforge/observe/capacity.py +39 -0
- platformforge/observe/dr.py +67 -0
- platformforge/observe/grafana.py +76 -0
- platformforge/observe/incident.py +58 -0
- platformforge/observe/otel.py +135 -0
- platformforge/observe/postmortem.py +72 -0
- platformforge/observe/prometheus.py +88 -0
- platformforge/observe/slo.py +125 -0
- platformforge/ops/__init__.py +8 -0
- platformforge/ops/actions.py +215 -0
- platformforge/ops/analytics.py +83 -0
- platformforge/ops/approval.py +275 -0
- platformforge/ops/autorem.py +101 -0
- platformforge/ops/config.py +175 -0
- platformforge/ops/engine.py +758 -0
- platformforge/ops/envelope.py +135 -0
- platformforge/ops/executors/__init__.py +1 -0
- platformforge/ops/executors/base.py +211 -0
- platformforge/ops/executors/git.py +77 -0
- platformforge/ops/executors/gitops.py +134 -0
- platformforge/ops/executors/terraform.py +84 -0
- platformforge/ops/gates.py +135 -0
- platformforge/ops/goldenpath.py +180 -0
- platformforge/ops/material.py +181 -0
- platformforge/ops/models.py +421 -0
- platformforge/ops/operation.py +335 -0
- platformforge/ops/opgraph.py +233 -0
- platformforge/ops/policy.py +275 -0
- platformforge/ops/preconditions.py +109 -0
- platformforge/ops/registry.py +101 -0
- platformforge/ops/risk.py +216 -0
- platformforge/ops/rollback.py +287 -0
- platformforge/ops/rollback_builders.py +163 -0
- platformforge/ops/runbook.py +269 -0
- platformforge/ops/simulate.py +125 -0
- platformforge/ops/source_of_truth.py +198 -0
- platformforge/ops/store.py +157 -0
- platformforge/ops/verify.py +190 -0
- platformforge/optimize/__init__.py +4 -0
- platformforge/optimize/engine.py +109 -0
- platformforge/optimize/models.py +68 -0
- platformforge/optimize/scan.py +196 -0
- platformforge/plan/__init__.py +107 -0
- platformforge/portable_cli.py +199 -0
- platformforge/portal_adapter.py +740 -0
- platformforge/portal_assets/chunk-7_l-b4Tr.js +1 -0
- platformforge/portal_assets/chunk-B0Iad6pD.js +1 -0
- platformforge/portal_assets/chunk-B605XnCF.js +1 -0
- platformforge/portal_assets/chunk-B7N3TXcn.js +1 -0
- platformforge/portal_assets/chunk-BAj6yGem.js +1 -0
- platformforge/portal_assets/chunk-BGUitn2r.js +1 -0
- platformforge/portal_assets/chunk-BVKnCv85.js +1 -0
- platformforge/portal_assets/chunk-BYyM9V_w.js +1 -0
- platformforge/portal_assets/chunk-BaQMhq-G.js +1 -0
- platformforge/portal_assets/chunk-BavOrxVm.js +1 -0
- platformforge/portal_assets/chunk-Bb0C24YV.js +1 -0
- platformforge/portal_assets/chunk-BfCFGPzs.js +1 -0
- platformforge/portal_assets/chunk-C2t4OUVu.js +1 -0
- platformforge/portal_assets/chunk-C3pI4QjU.js +1 -0
- platformforge/portal_assets/chunk-C4gpbtbP.js +1 -0
- platformforge/portal_assets/chunk-CGWeHhKY.js +1 -0
- platformforge/portal_assets/chunk-CJCT86Dr.js +1 -0
- platformforge/portal_assets/chunk-CXcsLDPJ.js +1 -0
- platformforge/portal_assets/chunk-CcjxoLxw.js +1 -0
- platformforge/portal_assets/chunk-D-gBPXHa.js +1 -0
- platformforge/portal_assets/chunk-D3eZogIe.js +1 -0
- platformforge/portal_assets/chunk-DQU0VyoQ.js +1 -0
- platformforge/portal_assets/chunk-DqWNkF1O.js +1 -0
- platformforge/portal_assets/chunk-DuPPkSIE.js +3 -0
- platformforge/portal_assets/chunk-DyIHOW_a.js +1 -0
- platformforge/portal_assets/chunk-Fx4zSGL5.js +1 -0
- platformforge/portal_assets/chunk-UBKt05Rg.js +1 -0
- platformforge/portal_assets/chunk-htnJDQEY.js +1 -0
- platformforge/portal_assets/chunk-kXRFMGqe.js +1 -0
- platformforge/portal_assets/chunk-qe3u763a.js +1 -0
- platformforge/portal_assets/favicon.ico +0 -0
- platformforge/portal_assets/index.html +13 -0
- platformforge/portal_assets/main-QDFVZK3T.js +5 -0
- platformforge/portal_assets/styles-MQEBDFDX.css +1 -0
- platformforge/product/__init__.py +10 -0
- platformforge/product/backstage.py +49 -0
- platformforge/product/catalog.py +74 -0
- platformforge/product/crossplane.py +341 -0
- platformforge/product/golden_paths/__init__.py +5 -0
- platformforge/product/golden_paths/engine.py +97 -0
- platformforge/product/golden_paths/library.yaml +198 -0
- platformforge/product/golden_paths/model.py +84 -0
- platformforge/product/maturity.py +107 -0
- platformforge/product/scorecards.py +84 -0
- platformforge/resources.py +36 -0
- platformforge/risk/__init__.py +5 -0
- platformforge/risk/engine.py +95 -0
- platformforge/risk/signals.py +49 -0
- platformforge/routing/__init__.py +13 -0
- platformforge/routing/decision.py +191 -0
- platformforge/routing/router.py +328 -0
- platformforge/rtk/__init__.py +9 -0
- platformforge/rtk/compact.py +127 -0
- platformforge/rtk/parsers.py +199 -0
- platformforge/rules/__init__.py +5 -0
- platformforge/rules/engine.py +345 -0
- platformforge/sandbox/__init__.py +7 -0
- platformforge/sandbox/env.py +151 -0
- platformforge/sandbox/review.py +92 -0
- platformforge/sdd/__init__.py +8 -0
- platformforge/sdd/lifecycle.py +239 -0
- platformforge/security/__init__.py +19 -0
- platformforge/security/cosign.py +67 -0
- platformforge/security/iam.py +112 -0
- platformforge/security/identity.py +84 -0
- platformforge/security/kyverno.py +106 -0
- platformforge/security/sbom.py +59 -0
- platformforge/security/scan.py +63 -0
- platformforge/security/slsa.py +86 -0
- platformforge/security/supply.py +62 -0
- platformforge/tokensave/__init__.py +20 -0
- platformforge/tokensave/budget.py +62 -0
- platformforge/tokensave/estimate.py +21 -0
- platformforge/tokensave/index.py +172 -0
- platformforge/tokensave/ledger.py +121 -0
- platformforge/tokensave/packs.py +285 -0
- platformforge/ui/__init__.py +28 -0
- platformforge/ui/app.py +465 -0
- platformforge/ui/home.py +113 -0
- platformforge/ui/i18n.py +143 -0
- platformforge/ui/kit.py +689 -0
- platformforge/ui/screen.py +628 -0
- platformforge/ui/tui.py +94 -0
- platformforge/ui/wizard.py +309 -0
- platformforge/workspace/__init__.py +13 -0
- platformforge/workspace/models.py +35 -0
- platformforge/workspace/service.py +69 -0
- platformforge-0.1.0.dist-info/METADATA +212 -0
- platformforge-0.1.0.dist-info/RECORD +659 -0
- platformforge-0.1.0.dist-info/WHEEL +4 -0
- platformforge-0.1.0.dist-info/entry_points.txt +3 -0
- platformforge-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
"""Cycle 4 — operations orchestrator.
|
|
2
|
+
|
|
3
|
+
Drives the §8 pipeline end to end with *no implicit skips*:
|
|
4
|
+
|
|
5
|
+
intent → plan → simulate → risk → policy → approval →
|
|
6
|
+
envelope → preconditions → execute (typed) → verify →
|
|
7
|
+
converge | rollback → ledger receipts
|
|
8
|
+
|
|
9
|
+
Every stage emits receipts into the OperationLedger; every refusal is a
|
|
10
|
+
`PF-OPS-*` object. Executors run only via injected host transports —
|
|
11
|
+
`dry_run` is the default and `--execute` semantics live in the CLI.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from platformforge.ops.actions import spec_for
|
|
19
|
+
from platformforge.ops.approval import Approval, BreakGlass, check_approval
|
|
20
|
+
from platformforge.ops.envelope import ExecutionEnvelope
|
|
21
|
+
from platformforge.ops.models import ChangeIntent, ChangePlan, PlanStep
|
|
22
|
+
from platformforge.ops.operation import ExecutionStep, LockTable, Operation, OperationLedger
|
|
23
|
+
from platformforge.ops.policy import Policy, evaluate
|
|
24
|
+
from platformforge.ops.preconditions import check_preconditions
|
|
25
|
+
from platformforge.ops.risk import assess, classify_reversibility
|
|
26
|
+
from platformforge.ops.rollback import RollbackPlan, build_rollback_plan, compensate_for, derive_rollback
|
|
27
|
+
from platformforge.ops.source_of_truth import resolve
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def executor_for(action: str):
|
|
31
|
+
from platformforge.ops.executors.base import ObserveExecutor
|
|
32
|
+
from platformforge.ops.executors.git import GitExecutor
|
|
33
|
+
from platformforge.ops.executors.gitops import ArgoCDExecutor, KubernetesExecutor
|
|
34
|
+
from platformforge.ops.executors.terraform import TerraformExecutor, TofuExecutor
|
|
35
|
+
mapping = {"git": GitExecutor, "terraform": TerraformExecutor,
|
|
36
|
+
"tofu": TofuExecutor, "argocd": ArgoCDExecutor,
|
|
37
|
+
"kubernetes": KubernetesExecutor, "observe": ObserveExecutor}
|
|
38
|
+
spec = spec_for(action)
|
|
39
|
+
cls = mapping.get(spec.executor if spec else "")
|
|
40
|
+
return cls() if cls else None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def propose(intent: ChangeIntent,
|
|
44
|
+
resources: list[dict[str, Any]] | None = None,
|
|
45
|
+
sot_context: dict[str, Any] | None = None
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
"""§8 stage 1-3: validate intent, resolve source of truth."""
|
|
48
|
+
violations = intent.validate()
|
|
49
|
+
if violations:
|
|
50
|
+
return {"ok": False, "refusals": violations}
|
|
51
|
+
resolutions = resolve(resources or [], sot_context)
|
|
52
|
+
unresolved = [r.to_dict() for r in resolutions if not r.resolved]
|
|
53
|
+
return {"ok": True, "intent": intent.to_dict(),
|
|
54
|
+
"resolutions": [r.to_dict() for r in resolutions],
|
|
55
|
+
"unresolved_sources": unresolved,
|
|
56
|
+
"requires_human_review": bool(unresolved)}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def plan(intent: ChangeIntent, *,
|
|
60
|
+
plan_id: str = "",
|
|
61
|
+
steps: list[PlanStep] | None = None,
|
|
62
|
+
expected_delta=None,
|
|
63
|
+
affected: list[str] | None = None) -> ChangePlan:
|
|
64
|
+
p = ChangePlan(plan_id=plan_id or f"plan-{intent.intent_id}",
|
|
65
|
+
intent_id=intent.intent_id,
|
|
66
|
+
steps=list(steps or []),
|
|
67
|
+
affected_resources=list(affected
|
|
68
|
+
or intent.target_resources),
|
|
69
|
+
expected_delta=expected_delta
|
|
70
|
+
or __import__("platformforge.ops.models",
|
|
71
|
+
fromlist=["ExpectedDelta"]).ExpectedDelta())
|
|
72
|
+
return p
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def assess_risk(plan: ChangePlan, context: dict[str, Any] | None = None,
|
|
76
|
+
rollback_status: str | None = None
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
"""Per-step risk assessments + plan ceiling (max class wins).
|
|
79
|
+
Cycle 4.1: the material-built rollback status feeds both
|
|
80
|
+
rollback_confidence and the reversibility class (worst-of)."""
|
|
81
|
+
from platformforge.ops.risk import RISK_RANK, reversibility_for_status, rollback_confidence
|
|
82
|
+
ctx = context or {}
|
|
83
|
+
per_step = []
|
|
84
|
+
worst = "R0"
|
|
85
|
+
rb_conf = rollback_confidence(rollback_status)
|
|
86
|
+
for s in plan.steps:
|
|
87
|
+
rev = classify_reversibility(s.action, s.params, ctx)
|
|
88
|
+
rev = reversibility_for_status(rollback_status, rev)
|
|
89
|
+
spec = spec_for(s.action)
|
|
90
|
+
step_status = rollback_status
|
|
91
|
+
if spec and spec.rollback_strategy in ("manual-only",
|
|
92
|
+
"impossible", "unknown"):
|
|
93
|
+
step_status = spec.rollback_strategy if \
|
|
94
|
+
spec.rollback_strategy != "unknown" else "unresolved"
|
|
95
|
+
a = assess(s.action, s.params,
|
|
96
|
+
{**ctx, "reversibility": rev,
|
|
97
|
+
"rollback_confidence":
|
|
98
|
+
rollback_confidence(step_status)})
|
|
99
|
+
per_step.append({"step_id": s.step_id, "action": s.action,
|
|
100
|
+
"rollback_status": step_status or "unknown",
|
|
101
|
+
**a.to_dict()})
|
|
102
|
+
if RISK_RANK[a.risk_class] > RISK_RANK[worst]:
|
|
103
|
+
worst = a.risk_class
|
|
104
|
+
out = {"plan_id": plan.plan_id, "risk_class": worst,
|
|
105
|
+
"steps": per_step,
|
|
106
|
+
"rollback_confidence": rb_conf}
|
|
107
|
+
if rollback_status:
|
|
108
|
+
out["rollback_status"] = rollback_status
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def decide(plan: ChangePlan, policies: list[Policy],
|
|
113
|
+
inp: dict[str, Any] | None = None,
|
|
114
|
+
exceptions=None, at: str | None = None):
|
|
115
|
+
base_inp = {"plan_id": plan.plan_id, "plan_hash": plan.hash(),
|
|
116
|
+
"intent_id": plan.intent_id,
|
|
117
|
+
"environment": (inp or {}).get("environment", "unknown"),
|
|
118
|
+
**(inp or {})}
|
|
119
|
+
return evaluate(base_inp, policies, exceptions=exceptions, at=at)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def mint_envelope(plan: ChangePlan, *, execution_id: str,
|
|
123
|
+
decisions: list, approvals: list[Approval],
|
|
124
|
+
risk: dict[str, Any], rollback: RollbackPlan | None,
|
|
125
|
+
preconditions: dict[str, Any] | None = None,
|
|
126
|
+
verification: dict[str, Any] | None = None,
|
|
127
|
+
expires_at: str = ""
|
|
128
|
+
) -> ExecutionEnvelope | dict[str, Any]:
|
|
129
|
+
"""Envelopes mint only when every decision allows/requires-satisfied
|
|
130
|
+
— never self-minted."""
|
|
131
|
+
if not decisions:
|
|
132
|
+
return {"refusal": "PF-OPS-ENVELOPE-UNGOVERNED",
|
|
133
|
+
"unlock": "run policy evaluation first"}
|
|
134
|
+
for d in decisions:
|
|
135
|
+
eff = d.effective
|
|
136
|
+
if eff in ("deny", "would_deny", "unresolved"):
|
|
137
|
+
return {"refusal": "PF-OPS-POLICY-BLOCK",
|
|
138
|
+
"unlock": f"policy {d.policy_id} → {eff}; resolve "
|
|
139
|
+
"before minting an envelope"}
|
|
140
|
+
# Cycle 4.1 §E — a mutating plan with NO declared delta refuses to
|
|
141
|
+
# mint. `unknown_dimensions` counts as an honest declaration; a
|
|
142
|
+
# completely empty delta does not.
|
|
143
|
+
if delta_refusal(plan) is not None:
|
|
144
|
+
return delta_refusal(plan)
|
|
145
|
+
actions = [{"step_id": s.step_id, "action": s.action,
|
|
146
|
+
"params": s.params} for s in plan.steps]
|
|
147
|
+
env = ExecutionEnvelope(
|
|
148
|
+
execution_id=execution_id, intent_id=plan.intent_id,
|
|
149
|
+
change_plan_hash=plan.hash(),
|
|
150
|
+
executor=_dominant_executor(plan),
|
|
151
|
+
actions=actions, scope=list(plan.affected_resources),
|
|
152
|
+
preconditions=preconditions or {},
|
|
153
|
+
policy_decisions=[d.to_dict() for d in decisions],
|
|
154
|
+
approvals=[a.approval_id for a in approvals],
|
|
155
|
+
risk=risk, expected_delta=plan.expected_delta.to_dict(),
|
|
156
|
+
verification=verification or {},
|
|
157
|
+
rollback=(rollback or derive_rollback(actions)).to_dict(),
|
|
158
|
+
expires_at=expires_at)
|
|
159
|
+
violations = env.validate()
|
|
160
|
+
if violations:
|
|
161
|
+
return {"refusal": "PF-OPS-ENVELOPE-INVALID",
|
|
162
|
+
"unlock": "envelope validation failed",
|
|
163
|
+
"violations": violations}
|
|
164
|
+
return env
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def delta_refusal(plan: ChangePlan) -> dict[str, Any] | None:
|
|
168
|
+
"""Cycle 4.1 — mutating steps require an ExpectedDelta declaration
|
|
169
|
+
(adds/removes/changes OR explicit unknown_dimensions). Read-only
|
|
170
|
+
plans (validate/show/observe/diff) may omit it."""
|
|
171
|
+
ed = plan.expected_delta
|
|
172
|
+
declared = bool(getattr(ed, "adds", None)
|
|
173
|
+
or getattr(ed, "removes", None)
|
|
174
|
+
or getattr(ed, "changes", None)
|
|
175
|
+
or getattr(ed, "unknown_dimensions", None))
|
|
176
|
+
if declared:
|
|
177
|
+
return None
|
|
178
|
+
mutating = [s.action for s in plan.steps
|
|
179
|
+
if (spec_for(s.action) or _MISSING).mutating]
|
|
180
|
+
if mutating:
|
|
181
|
+
return {"refusal": "PF-OPS-NO-DELTA",
|
|
182
|
+
"unlock": "declare expected_delta (adds/removes/changes "
|
|
183
|
+
"or explicit unknown_dimensions) — mutating "
|
|
184
|
+
"plans cannot mint without an expected "
|
|
185
|
+
"outcome",
|
|
186
|
+
"mutating_steps": mutating}
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class _MissingSpec:
|
|
191
|
+
mutating = True # unknown action → treat as mutating (conservative)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
_MISSING = _MissingSpec()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _dominant_executor(plan: ChangePlan) -> str:
|
|
198
|
+
for s in plan.steps:
|
|
199
|
+
spec = spec_for(s.action)
|
|
200
|
+
if spec:
|
|
201
|
+
return spec.executor
|
|
202
|
+
return ""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_REQUIRE_DECISIONS = {
|
|
206
|
+
"require-dual-human": "dual-human",
|
|
207
|
+
"require-security-review": "security-review",
|
|
208
|
+
"require-owner": "resource-owner",
|
|
209
|
+
"require-platform-owner": "platform-owner",
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def required_approval_type(env: ExecutionEnvelope,
|
|
214
|
+
environment: str = "") -> str:
|
|
215
|
+
"""§43/§54 — `require-*` policy decisions and prod R4+ risk escalate
|
|
216
|
+
the approval type an envelope demands (break-glass still bypasses
|
|
217
|
+
with its own audit trail)."""
|
|
218
|
+
for d in env.policy_decisions or []:
|
|
219
|
+
eff = d.get("decision", "")
|
|
220
|
+
if eff in _REQUIRE_DECISIONS:
|
|
221
|
+
return _REQUIRE_DECISIONS[eff]
|
|
222
|
+
risk_class = (env.risk or {}).get("risk_class", "")
|
|
223
|
+
if environment == "prod" and risk_class in ("R4", "R5"):
|
|
224
|
+
return "dual-human"
|
|
225
|
+
if risk_class == "R5":
|
|
226
|
+
return "dual-human"
|
|
227
|
+
return ""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def execute(op: Operation, env: ExecutionEnvelope, *,
|
|
231
|
+
approvals: list[Approval],
|
|
232
|
+
transports: dict[str, Any] | None = None,
|
|
233
|
+
ledger: OperationLedger | None = None,
|
|
234
|
+
locks: LockTable | None = None,
|
|
235
|
+
dry_run: bool = True,
|
|
236
|
+
break_glass: BreakGlass | None = None,
|
|
237
|
+
preconditions: dict[str, Any] | None = None,
|
|
238
|
+
material_store=None,
|
|
239
|
+
at: str | None = None) -> dict[str, Any]:
|
|
240
|
+
"""§8 execution stages: approval → preconditions → locks → steps.
|
|
241
|
+
`dry_run=True` is the default; callers must opt out explicitly."""
|
|
242
|
+
ledger = ledger or OperationLedger()
|
|
243
|
+
locks = locks or LockTable()
|
|
244
|
+
transports = transports or {}
|
|
245
|
+
|
|
246
|
+
# 1. hash-bound approval re-check (TOCTOU)
|
|
247
|
+
# §61 — parameter bounds check against the *actual* step params:
|
|
248
|
+
# approving replicas 3→5 must not authorize 3→50.
|
|
249
|
+
merged_params: dict[str, Any] = {}
|
|
250
|
+
for a in env.actions:
|
|
251
|
+
merged_params.update(a.get("params") or {})
|
|
252
|
+
chk = check_approval(approvals, subject_hash=env.hash(),
|
|
253
|
+
scope=env.scope, params=merged_params,
|
|
254
|
+
required_type=required_approval_type(
|
|
255
|
+
env, (preconditions or {}).get(
|
|
256
|
+
"environment", "")),
|
|
257
|
+
break_glass=break_glass,
|
|
258
|
+
current_plan_hash=env.hash(), at=at)
|
|
259
|
+
if not chk.ok:
|
|
260
|
+
ledger.append("approval.denied", op.operation_id,
|
|
261
|
+
data={"refusal": chk.refusal})
|
|
262
|
+
return {"ok": False, "stage": "approval",
|
|
263
|
+
"refusal": chk.refusal}
|
|
264
|
+
|
|
265
|
+
# 2. preconditions — including TOCTOU resource-state comparison
|
|
266
|
+
# (§57–58) and owner drift when the caller supplies them.
|
|
267
|
+
pc = check_preconditions(
|
|
268
|
+
observation=(preconditions or {}).get("observation"),
|
|
269
|
+
max_observation_age_s=(preconditions or {}).get(
|
|
270
|
+
"max_observation_age_s", 900),
|
|
271
|
+
expected_resource_state=(preconditions or {}).get(
|
|
272
|
+
"expected_resource_state"),
|
|
273
|
+
current_resource_state=(preconditions or {}).get(
|
|
274
|
+
"current_resource_state"),
|
|
275
|
+
plan_hash=env.change_plan_hash,
|
|
276
|
+
current_plan_hash=(preconditions or {}).get(
|
|
277
|
+
"current_plan_hash", env.change_plan_hash),
|
|
278
|
+
owner=(preconditions or {}).get("owner", ""),
|
|
279
|
+
current_owner=(preconditions or {}).get("current_owner", ""),
|
|
280
|
+
policy_decision=(preconditions or {}).get("policy_decision"),
|
|
281
|
+
approval_valid=True,
|
|
282
|
+
maintenance_window=(preconditions or {}).get("maintenance_window"),
|
|
283
|
+
freeze_active=(preconditions or {}).get("freeze_active", False),
|
|
284
|
+
break_glass=chk.used_break_glass, at=at)
|
|
285
|
+
if not pc["ok"]:
|
|
286
|
+
ledger.append("precondition.failed", op.operation_id,
|
|
287
|
+
data=pc["refusal"])
|
|
288
|
+
return {"ok": False, "stage": "preconditions",
|
|
289
|
+
"refusal": pc["refusal"], "checks": pc["checks"]}
|
|
290
|
+
|
|
291
|
+
# 3. locks
|
|
292
|
+
lk = locks.acquire(op.resources or env.scope, op.operation_id, ledger)
|
|
293
|
+
if "refusal" in lk:
|
|
294
|
+
return {"ok": False, "stage": "locking", "refusal": lk}
|
|
295
|
+
|
|
296
|
+
# 4. FSM — walk the canonical chain to executing; resume-safe: a step
|
|
297
|
+
# whose completion is already legal-from-state is skipped.
|
|
298
|
+
for st in ("planned", "simulated", "policy-reviewed",
|
|
299
|
+
"awaiting-approval", "approved", "executing"):
|
|
300
|
+
if op.state == st:
|
|
301
|
+
continue
|
|
302
|
+
r = op.transition(st, ledger)
|
|
303
|
+
if "refusal" in r:
|
|
304
|
+
locks.release(op.operation_id, ledger)
|
|
305
|
+
return {"ok": False, "stage": f"transition→{st}",
|
|
306
|
+
"refusal": r, "state": op.state}
|
|
307
|
+
|
|
308
|
+
# 5. DAG execution — waves; mutating steps need transports.
|
|
309
|
+
# Cycle 4.1 §30: every mutating step captures RollbackMaterial
|
|
310
|
+
# (pre-state BEFORE the step, execution result AFTER) into the
|
|
311
|
+
# content-addressed store — never reused forward params.
|
|
312
|
+
from platformforge.ops.material import MaterialStore, capture_material
|
|
313
|
+
mstore = material_store or MaterialStore()
|
|
314
|
+
pre_state_ctx = (preconditions or {}).get("pre_state") or {}
|
|
315
|
+
sot_ctx = (preconditions or {}).get("sot") or {}
|
|
316
|
+
materials: dict[str, Any] = {}
|
|
317
|
+
plan_steps = {s["step_id"]: s for s in env.actions}
|
|
318
|
+
results: list[dict[str, Any]] = []
|
|
319
|
+
idem_seen: set[str] = set()
|
|
320
|
+
failed: list[str] = []
|
|
321
|
+
for wave in _waves(env):
|
|
322
|
+
for sid in wave:
|
|
323
|
+
a = plan_steps[sid]
|
|
324
|
+
spec = spec_for(a["action"])
|
|
325
|
+
ex = executor_for(a["action"])
|
|
326
|
+
step = ExecutionStep(step_id=sid, action=a["action"],
|
|
327
|
+
params=a.get("params", {}))
|
|
328
|
+
key = step.effect_key(env.change_plan_hash)
|
|
329
|
+
if key in idem_seen:
|
|
330
|
+
ledger.append("step.skipped", op.operation_id,
|
|
331
|
+
data={"step_id": sid,
|
|
332
|
+
"reason": "idempotent-duplicate"})
|
|
333
|
+
continue
|
|
334
|
+
idem_seen.add(key)
|
|
335
|
+
mat = None
|
|
336
|
+
if spec and spec.mutating:
|
|
337
|
+
mat = capture_material(
|
|
338
|
+
a["action"], sid, op.operation_id,
|
|
339
|
+
a.get("params", {}),
|
|
340
|
+
spec.pre_state_requirements,
|
|
341
|
+
pre_state=_capture_pre_state(
|
|
342
|
+
a["action"], a.get("params", {}),
|
|
343
|
+
pre_state_ctx.get(sid)),
|
|
344
|
+
source_of_truth=sot_ctx.get(sid) or
|
|
345
|
+
sot_ctx.get("__default__"),
|
|
346
|
+
provenance="observed" if pre_state_ctx.get(sid)
|
|
347
|
+
else "declared")
|
|
348
|
+
ledger.append("step.started", op.operation_id,
|
|
349
|
+
data={"step_id": sid, "action": a["action"],
|
|
350
|
+
"dry_run": dry_run,
|
|
351
|
+
"material": mat.hash() if mat else None})
|
|
352
|
+
if spec is None or ex is None:
|
|
353
|
+
r = {"ok": False, "refusal": {
|
|
354
|
+
"refusal": "PF-OPS-UNKNOWN-ACTION",
|
|
355
|
+
"unlock": f"{a['action']} not in vocabulary"}}
|
|
356
|
+
else:
|
|
357
|
+
t = transports.get(spec.executor)
|
|
358
|
+
rec = ex.run_step(sid, a["action"], a.get("params", {}),
|
|
359
|
+
transport=t, dry_run=dry_run)
|
|
360
|
+
r = rec.to_dict()
|
|
361
|
+
if mat is not None:
|
|
362
|
+
# post-result capture (§12/§31) + persist immutable
|
|
363
|
+
mat.execution_result.update(
|
|
364
|
+
{k: v for k, v in (r.get("outputs") or {}).items()
|
|
365
|
+
if k in spec.post_result_requirements
|
|
366
|
+
or k.endswith("commit") or k in ("pr", "output")})
|
|
367
|
+
for k in spec.post_result_requirements:
|
|
368
|
+
if k in r and k not in mat.execution_result:
|
|
369
|
+
mat.execution_result[k] = r[k]
|
|
370
|
+
mstore.put(mat)
|
|
371
|
+
r["material_hash"] = mat.hash()
|
|
372
|
+
materials[sid] = mat
|
|
373
|
+
results.append(r)
|
|
374
|
+
if r.get("ok"):
|
|
375
|
+
ledger.append("step.completed", op.operation_id,
|
|
376
|
+
data={"step_id": sid,
|
|
377
|
+
"receipt": r.get("receipt_hash")})
|
|
378
|
+
else:
|
|
379
|
+
failed.append(sid)
|
|
380
|
+
ledger.append("step.failed", op.operation_id,
|
|
381
|
+
data={"step_id": sid,
|
|
382
|
+
"refusal": r.get("refusal"),
|
|
383
|
+
"error": r.get("error", "")[:400]})
|
|
384
|
+
# stop the DAG — a failed dep blocks downstream
|
|
385
|
+
blocked = [s for w in _waves(env) for s in w
|
|
386
|
+
if s not in {x["step_id"] for x in results}]
|
|
387
|
+
for b in blocked:
|
|
388
|
+
ledger.append("step.skipped", op.operation_id,
|
|
389
|
+
data={"step_id": b,
|
|
390
|
+
"reason": f"dep-failed:{sid}"})
|
|
391
|
+
op.transition("failed", ledger)
|
|
392
|
+
locks.release(op.operation_id, ledger)
|
|
393
|
+
return {"ok": False, "stage": "execute",
|
|
394
|
+
"failed_step": sid, "results": results,
|
|
395
|
+
"state": op.state}
|
|
396
|
+
|
|
397
|
+
op.transition("verifying", ledger)
|
|
398
|
+
locks.release(op.operation_id, ledger)
|
|
399
|
+
# §30 — rebuild the rollback plan against captured materials so the
|
|
400
|
+
# post-execution plan is honest about executability.
|
|
401
|
+
fwd_steps = [{"step_id": a["step_id"], "action": a["action"],
|
|
402
|
+
"params": a.get("params", {})} for a in env.actions]
|
|
403
|
+
rb = build_rollback_plan(
|
|
404
|
+
fwd_steps, materials=materials,
|
|
405
|
+
context={"environment": (preconditions or {}).get(
|
|
406
|
+
"environment", "unknown"),
|
|
407
|
+
"automatic_allowed": (preconditions or {}).get(
|
|
408
|
+
"automatic_rollback", False)})
|
|
409
|
+
return {"ok": True, "stage": "execute", "dry_run": dry_run,
|
|
410
|
+
"results": results, "state": op.state,
|
|
411
|
+
"ledger_tip": ledger.tip,
|
|
412
|
+
"materials": {sid: m.to_dict() for sid, m
|
|
413
|
+
in materials.items()},
|
|
414
|
+
"material_hashes": {sid: m.hash() for sid, m
|
|
415
|
+
in materials.items()},
|
|
416
|
+
"rollback_plan": rb.to_dict()}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _capture_pre_state(action: str, params: dict[str, Any],
|
|
420
|
+
ctx_state: dict[str, Any] | None
|
|
421
|
+
) -> dict[str, Any]:
|
|
422
|
+
"""§8/§31 — assemble the pre-change snapshot for one step.
|
|
423
|
+
|
|
424
|
+
Sources, weakest→strongest:
|
|
425
|
+
1. params the action itself declares (`current_replicas`,
|
|
426
|
+
`resource_version`, `to_revision`…)
|
|
427
|
+
2. caller-supplied `pre_state` ctx (observation-sourced values
|
|
428
|
+
per step_id) — wins on conflict, provenance observed.
|
|
429
|
+
"""
|
|
430
|
+
pre: dict[str, Any] = {}
|
|
431
|
+
if action == "kubernetes.scale":
|
|
432
|
+
if params.get("current_replicas") is not None:
|
|
433
|
+
pre["replicas"] = params["current_replicas"]
|
|
434
|
+
if params.get("resource_version"):
|
|
435
|
+
pre["resource_version"] = params["resource_version"]
|
|
436
|
+
elif action == "kubernetes.annotate":
|
|
437
|
+
if params.get("previous_annotations") is not None:
|
|
438
|
+
pre["annotations"] = dict(params["previous_annotations"])
|
|
439
|
+
if params.get("resource_version"):
|
|
440
|
+
pre["resource_version"] = params["resource_version"]
|
|
441
|
+
elif action == "kubernetes.rollout_restart":
|
|
442
|
+
if params.get("current_revision") is not None:
|
|
443
|
+
pre["revision"] = params["current_revision"]
|
|
444
|
+
elif action == "argocd.sync":
|
|
445
|
+
for k in ("history_id", "previous_revision", "repo",
|
|
446
|
+
"git_revert_commit"):
|
|
447
|
+
if params.get(k) is not None:
|
|
448
|
+
pre[k] = params[k]
|
|
449
|
+
elif action in ("terraform.apply_saved_plan",
|
|
450
|
+
"tofu.apply_saved_plan"):
|
|
451
|
+
for k in ("source_ref", "workspace", "state_serial",
|
|
452
|
+
"resource_addresses", "plan_hash"):
|
|
453
|
+
if params.get(k) is not None:
|
|
454
|
+
pre[k] = params[k]
|
|
455
|
+
elif action in ("git.apply_patch", "git.commit", "git.open_pr"):
|
|
456
|
+
for k in ("pre_change_commit", "repo"):
|
|
457
|
+
if params.get(k) is not None:
|
|
458
|
+
pre[k] = params[k]
|
|
459
|
+
elif action == "git.delete_branch":
|
|
460
|
+
if params.get("branch_sha") is not None:
|
|
461
|
+
pre["branch_sha"] = params["branch_sha"]
|
|
462
|
+
if ctx_state:
|
|
463
|
+
for k, v in ctx_state.items(): # observed wins (§7)
|
|
464
|
+
if v is not None:
|
|
465
|
+
pre[k] = v
|
|
466
|
+
return pre
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _waves(env: ExecutionEnvelope) -> list[list[str]]:
|
|
470
|
+
"""Waves from step deps stored as params.__depends_on__ or envelope
|
|
471
|
+
order — simple: use declared order unless depends_on is present."""
|
|
472
|
+
deps = {a["step_id"]: set((a.get("params") or {}).get(
|
|
473
|
+
"__depends_on__", [])) for a in env.actions}
|
|
474
|
+
done: set[str] = set()
|
|
475
|
+
out: list[list[str]] = []
|
|
476
|
+
total = len(env.actions)
|
|
477
|
+
while len(done) < total:
|
|
478
|
+
w = [s for s, d in deps.items() if d <= done and s not in done]
|
|
479
|
+
if not w:
|
|
480
|
+
w = [s for s in deps if s not in done] # malformed → all
|
|
481
|
+
out.append(w)
|
|
482
|
+
done.update(w)
|
|
483
|
+
return out
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def finalize_verify(op: Operation, ledger: OperationLedger,
|
|
487
|
+
verification_result: dict[str, Any]) -> dict[str, Any]:
|
|
488
|
+
conv = verification_result.get("convergence", "unknown")
|
|
489
|
+
target = {"converged": "converged", "partially-converged":
|
|
490
|
+
"partially-converged", "not-converged": "failed",
|
|
491
|
+
"regressed": "rollback-planned",
|
|
492
|
+
"unknown": "unresolved"}.get(conv, "unresolved")
|
|
493
|
+
if op.state == "verifying":
|
|
494
|
+
r = op.transition(target, ledger, data={"convergence": conv})
|
|
495
|
+
if "refusal" in r:
|
|
496
|
+
return r
|
|
497
|
+
ledger.append("verified", op.operation_id, data=verification_result)
|
|
498
|
+
return {"operation_id": op.operation_id, "state": op.state,
|
|
499
|
+
"convergence": conv}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def execute_rollback(op: Operation, env: ExecutionEnvelope, *,
|
|
503
|
+
transports: dict[str, Any] | None = None,
|
|
504
|
+
ledger: OperationLedger | None = None,
|
|
505
|
+
locks: LockTable | None = None,
|
|
506
|
+
completed_results: list[dict[str, Any]] | None = None,
|
|
507
|
+
materials: dict[str, Any] | None = None,
|
|
508
|
+
rollback_plan: dict[str, Any] | None = None,
|
|
509
|
+
trigger: dict[str, Any] | None = None,
|
|
510
|
+
post_rollback_state: dict[str, Any] | None = None,
|
|
511
|
+
dry_run: bool = True,
|
|
512
|
+
at: str | None = None) -> dict[str, Any]:
|
|
513
|
+
"""§40–43/§119–127/§149–160 — execute a rollback built from
|
|
514
|
+
MATERIAL, under the same locks and idempotency as the forward pass.
|
|
515
|
+
|
|
516
|
+
- `trigger` (§98): typed cause {type, verification_id,
|
|
517
|
+
observed_delta, slo_evidence, at}. Never a bare string.
|
|
518
|
+
- `rollback_plan` (v2): built from captured materials. Only
|
|
519
|
+
status=executable runs typed actions; manual-only/impossible/
|
|
520
|
+
unresolved/requires-replan refuse honestly.
|
|
521
|
+
- Idempotent (§156): an already rolled-back op returns the prior
|
|
522
|
+
receipt instead of re-executing; same locks as forward (§158).
|
|
523
|
+
- A failed rollback step lands in `failed` with stage=rollback —
|
|
524
|
+
never `rolled-back` (§149–153); no automatic second rollback.
|
|
525
|
+
- `post_rollback_state` (§41–43): caller-supplied observation used
|
|
526
|
+
to verify restored|partially-restored|regressed|unknown.
|
|
527
|
+
"""
|
|
528
|
+
ledger = ledger or OperationLedger()
|
|
529
|
+
locks = locks or LockTable()
|
|
530
|
+
trigger = dict(trigger or {"type": "manual"})
|
|
531
|
+
rb = dict(rollback_plan or env.rollback or {})
|
|
532
|
+
strategy = rb.get("strategy", "unknown")
|
|
533
|
+
status = rb.get("status", "unresolved")
|
|
534
|
+
|
|
535
|
+
# §156 — duplicate rollback request → idempotent receipt
|
|
536
|
+
if op.state == "rolled-back":
|
|
537
|
+
return {"ok": True, "state": op.state,
|
|
538
|
+
"idempotent": "already-rolled-back"}
|
|
539
|
+
|
|
540
|
+
# §36 — only executable plans execute; everything else is an
|
|
541
|
+
# honest refusal, never an invented action
|
|
542
|
+
if status != "executable":
|
|
543
|
+
return {"ok": False, "state": op.state,
|
|
544
|
+
"refusal": {"refusal": "PF-OPS-ROLLBACK-NOT-EXECUTABLE",
|
|
545
|
+
"unlock": f"rollback status is "
|
|
546
|
+
f"'{status}' — manual remediation "
|
|
547
|
+
"or a new governed plan required"},
|
|
548
|
+
"strategy": strategy, "status": status,
|
|
549
|
+
"replans": rb.get("replans", []),
|
|
550
|
+
"limitations": rb.get("limitations", [])}
|
|
551
|
+
|
|
552
|
+
rb_actions = list(rb.get("actions", []))
|
|
553
|
+
mats = dict(materials or {})
|
|
554
|
+
if not rb_actions and completed_results:
|
|
555
|
+
comp = compensate_for([{"step_id": r.get("step_id"),
|
|
556
|
+
"action": r.get("action"),
|
|
557
|
+
"params": r.get("outputs", {}).get(
|
|
558
|
+
"params", {}),
|
|
559
|
+
"status": "completed" if r.get("ok")
|
|
560
|
+
else "failed"}
|
|
561
|
+
for r in completed_results],
|
|
562
|
+
materials=mats)
|
|
563
|
+
rb_actions = comp["compensating_actions"]
|
|
564
|
+
if comp["manual_steps"]:
|
|
565
|
+
rb.setdefault("limitations", []).append(
|
|
566
|
+
f"manual compensation needed for {comp['manual_steps']}")
|
|
567
|
+
if not rb_actions:
|
|
568
|
+
return {"ok": False, "state": op.state,
|
|
569
|
+
"refusal": {"refusal": "PF-OPS-ROLLBACK-EMPTY",
|
|
570
|
+
"unlock": "no rollback actions derivable — "
|
|
571
|
+
"manual remediation required"}}
|
|
572
|
+
|
|
573
|
+
# §158 — rollback holds the same resource locks as the forward op
|
|
574
|
+
lk = locks.acquire(op.resources or env.scope, op.operation_id,
|
|
575
|
+
ledger)
|
|
576
|
+
if "refusal" in lk:
|
|
577
|
+
return {"ok": False, "state": op.state, "stage": "locking",
|
|
578
|
+
"refusal": lk}
|
|
579
|
+
|
|
580
|
+
# §159 — rollback preconditions: the resource must still match the
|
|
581
|
+
# failed-forward state; external drift → human review, never blind
|
|
582
|
+
# revert of someone else's change.
|
|
583
|
+
precondition_refusals = _rollback_preconditions(
|
|
584
|
+
mats, rb.get("steps", []))
|
|
585
|
+
if precondition_refusals:
|
|
586
|
+
locks.release(op.operation_id, ledger)
|
|
587
|
+
return {"ok": False, "state": op.state,
|
|
588
|
+
"stage": "rollback-preconditions",
|
|
589
|
+
"refusal": {"refusal": "PF-OPS-ROLLBACK-PRECONDITION",
|
|
590
|
+
"unlock": "resource no longer matches "
|
|
591
|
+
"failed-forward state — human "
|
|
592
|
+
"review required (§159)",
|
|
593
|
+
"details": precondition_refusals}}
|
|
594
|
+
|
|
595
|
+
# legal walk into rolling-back
|
|
596
|
+
for st in ("rollback-planned", "rolling-back"):
|
|
597
|
+
if op.state == st:
|
|
598
|
+
continue
|
|
599
|
+
if st == "rollback-planned" and op.state in \
|
|
600
|
+
("executing", "rolling-back"):
|
|
601
|
+
continue # executing may jump straight to rolling-back
|
|
602
|
+
r = op.transition(st, ledger, data={"strategy": strategy})
|
|
603
|
+
if "refusal" in r:
|
|
604
|
+
locks.release(op.operation_id, ledger)
|
|
605
|
+
return {"ok": False, "state": op.state, "refusal": r}
|
|
606
|
+
|
|
607
|
+
ledger.append("rollback.started", op.operation_id,
|
|
608
|
+
data={"strategy": strategy, "status": status,
|
|
609
|
+
"trigger": trigger,
|
|
610
|
+
"material_hashes": rb.get("material_hashes", []),
|
|
611
|
+
"actions": len(rb_actions)})
|
|
612
|
+
results: list[dict[str, Any]] = []
|
|
613
|
+
transports = transports or {}
|
|
614
|
+
for i, a in enumerate(rb_actions):
|
|
615
|
+
sid = f"rb-{i}-{a.get('action', '?')}"
|
|
616
|
+
spec = spec_for(a.get("action", ""))
|
|
617
|
+
ex = executor_for(a.get("action", ""))
|
|
618
|
+
ledger.append("step.started", op.operation_id,
|
|
619
|
+
data={"step_id": sid, "action": a.get("action"),
|
|
620
|
+
"rollback": True, "dry_run": dry_run})
|
|
621
|
+
if spec is None or ex is None:
|
|
622
|
+
r = {"ok": False, "refusal": {
|
|
623
|
+
"refusal": "PF-OPS-UNKNOWN-ACTION",
|
|
624
|
+
"unlock": f"{a.get('action')} not in vocabulary"}}
|
|
625
|
+
else:
|
|
626
|
+
rec = ex.run_step(sid, a["action"], a.get("params", {}),
|
|
627
|
+
transport=transports.get(spec.executor),
|
|
628
|
+
dry_run=dry_run)
|
|
629
|
+
r = rec.to_dict()
|
|
630
|
+
results.append(r)
|
|
631
|
+
if r.get("ok"):
|
|
632
|
+
ledger.append("step.completed", op.operation_id,
|
|
633
|
+
data={"step_id": sid,
|
|
634
|
+
"receipt": r.get("receipt_hash"),
|
|
635
|
+
"rollback": True})
|
|
636
|
+
else:
|
|
637
|
+
ledger.append("step.failed", op.operation_id,
|
|
638
|
+
data={"step_id": sid,
|
|
639
|
+
"refusal": r.get("refusal"),
|
|
640
|
+
"rollback": True})
|
|
641
|
+
# §149–153 — failed rollback → failed + stage detail +
|
|
642
|
+
# human escalation; NO automatic rollback-of-rollback
|
|
643
|
+
op.transition("failed", ledger,
|
|
644
|
+
data={"stage": "rollback",
|
|
645
|
+
"escalation": "human-required"})
|
|
646
|
+
locks.release(op.operation_id, ledger)
|
|
647
|
+
return {"ok": False, "state": op.state,
|
|
648
|
+
"stage": "rollback",
|
|
649
|
+
"rollback_failed": True,
|
|
650
|
+
"escalation": "human-required",
|
|
651
|
+
"failed_step": sid, "results": results}
|
|
652
|
+
|
|
653
|
+
# §41–43 — rollback verification: command success ≠ restored.
|
|
654
|
+
ver = verify_rollback(mats, post_rollback_state or {})
|
|
655
|
+
ledger.append("rollback.completed", op.operation_id,
|
|
656
|
+
data={"strategy": strategy,
|
|
657
|
+
"actions": len(results),
|
|
658
|
+
"rollback_verification": ver["convergence"]})
|
|
659
|
+
op.transition("rolled-back", ledger,
|
|
660
|
+
data={"rollback_verification": ver["convergence"]})
|
|
661
|
+
locks.release(op.operation_id, ledger)
|
|
662
|
+
# §40 — rollback receipt: trigger + material + strategy + result +
|
|
663
|
+
# verification, hash-linked to the forward operation
|
|
664
|
+
receipt = {
|
|
665
|
+
"operation_id": op.operation_id,
|
|
666
|
+
"stage": "rollback",
|
|
667
|
+
"result": ver["convergence"],
|
|
668
|
+
"trigger": trigger,
|
|
669
|
+
"strategy": strategy,
|
|
670
|
+
"material_hashes": rb.get("material_hashes", []),
|
|
671
|
+
"rollback_plan_ref": env.hash(),
|
|
672
|
+
"actions": len(results),
|
|
673
|
+
"rollback_verification": ver,
|
|
674
|
+
"comparisons": ver.get("comparisons", []),
|
|
675
|
+
}
|
|
676
|
+
return {"ok": ver["convergence"] in ("restored", "not-applicable"),
|
|
677
|
+
"state": op.state, "results": results,
|
|
678
|
+
"strategy": strategy,
|
|
679
|
+
"rollback_verification": ver["convergence"],
|
|
680
|
+
"receipt": receipt}
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _rollback_preconditions(materials: dict[str, Any],
|
|
684
|
+
plan_steps: list[dict[str, Any]]
|
|
685
|
+
) -> list[dict[str, Any]]:
|
|
686
|
+
"""§159 — per-step precondition records. A material whose
|
|
687
|
+
`pre_state` carries `current_matches_forward=False` (caller flags
|
|
688
|
+
external drift between forward failure and rollback attempt)
|
|
689
|
+
requires human review."""
|
|
690
|
+
bad = []
|
|
691
|
+
step_map = {s.get("step_id"): s for s in plan_steps}
|
|
692
|
+
for sid, mat in materials.items():
|
|
693
|
+
m = mat.to_dict() if hasattr(mat, "to_dict") else dict(mat)
|
|
694
|
+
pre = m.get("pre_state", {})
|
|
695
|
+
if pre.get("current_matches_forward") is False:
|
|
696
|
+
bad.append({"step_id": sid,
|
|
697
|
+
"reason": "external-change-detected",
|
|
698
|
+
"action": step_map.get(sid, {}).get("action")})
|
|
699
|
+
return bad
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def verify_rollback(materials: dict[str, Any],
|
|
703
|
+
post_state: dict[str, Any] | None = None
|
|
704
|
+
) -> dict[str, Any]:
|
|
705
|
+
"""§41–43 — compare captured pre-state with the post-rollback
|
|
706
|
+
observation. Restored only when every captured dimension matches.
|
|
707
|
+
|
|
708
|
+
`post_state`: {step_id: {dim: observed_value}} from a fresh
|
|
709
|
+
observation after rollback. Missing observations → unknown, never
|
|
710
|
+
restored-by-assumption.
|
|
711
|
+
"""
|
|
712
|
+
post = post_state or {}
|
|
713
|
+
if not materials:
|
|
714
|
+
return {"convergence": "not-applicable",
|
|
715
|
+
"comparisons": [],
|
|
716
|
+
"note": "no materials captured — nothing to verify"}
|
|
717
|
+
if not post:
|
|
718
|
+
return {"convergence": "unknown",
|
|
719
|
+
"comparisons": [],
|
|
720
|
+
"note": "no post-rollback observation — cannot claim "
|
|
721
|
+
"restored (§43)"}
|
|
722
|
+
comps: list[dict[str, Any]] = []
|
|
723
|
+
restored = regressed = missing = 0
|
|
724
|
+
# concurrency/provenance tokens are captured for preconditions,
|
|
725
|
+
# not restorable values — a new write always bumps them
|
|
726
|
+
non_restorable = {"resource_version", "uid",
|
|
727
|
+
"current_matches_forward", "generation"}
|
|
728
|
+
for sid, mat in materials.items():
|
|
729
|
+
m = mat.to_dict() if hasattr(mat, "to_dict") else dict(mat)
|
|
730
|
+
want = {k: v for k, v in m.get("pre_state", {}).items()
|
|
731
|
+
if k not in non_restorable}
|
|
732
|
+
got = post.get(sid) or {}
|
|
733
|
+
for dim, expected in want.items():
|
|
734
|
+
observed = got.get(dim, "<unobserved>")
|
|
735
|
+
if observed == "<unobserved>":
|
|
736
|
+
outcome = "unobserved"
|
|
737
|
+
missing += 1
|
|
738
|
+
elif observed == expected:
|
|
739
|
+
outcome = "restored"
|
|
740
|
+
restored += 1
|
|
741
|
+
else:
|
|
742
|
+
outcome = "different"
|
|
743
|
+
regressed += 1
|
|
744
|
+
comps.append({"step_id": sid, "dimension": dim,
|
|
745
|
+
"expected": expected, "observed": observed,
|
|
746
|
+
"outcome": outcome})
|
|
747
|
+
total = restored + regressed + missing
|
|
748
|
+
if missing == total:
|
|
749
|
+
conv = "unknown"
|
|
750
|
+
elif regressed:
|
|
751
|
+
conv = "regressed" if restored == 0 else "partially-restored"
|
|
752
|
+
elif missing:
|
|
753
|
+
conv = "partially-restored"
|
|
754
|
+
else:
|
|
755
|
+
conv = "restored"
|
|
756
|
+
return {"convergence": conv, "comparisons": comps,
|
|
757
|
+
"restored": restored, "different": regressed,
|
|
758
|
+
"unobserved": missing}
|