@apexops/cli 0.10.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/capability-packs/azure-governance-discovery/source/scripts/discover.py +1189 -0
- package/assets/capability-packs/azure-governance-discovery/source/scripts/render_governance.py +676 -0
- package/assets/capability-packs/registry.v1.json +73 -0
- package/assets/client-projection-metadata/copilot-cli-agent-tools.json +92 -0
- package/assets/client-projections/github-copilot-cli/.github/agents/apex-architect.agent.md +78 -0
- package/assets/client-projections/github-copilot-cli/.github/agents/apex-operator.agent.md +71 -0
- package/assets/client-projections/github-copilot-cli/.github/agents/apex-planner.agent.md +79 -0
- package/assets/client-projections/github-copilot-cli/.github/agents/apex-requirements.agent.md +69 -0
- package/assets/client-projections/github-copilot-cli/.github/agents/apex.agent.md +52 -0
- package/assets/client-projections/github-copilot-cli/.github/copilot-instructions.md +13 -0
- package/assets/client-projections/github-copilot-cli/.github/mcp.json +54 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-architecture/SKILL.md +36 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/SKILL.md +65 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/references/presentation-conventions.md +52 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/references/reference-only-outlines.md +78 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/architecture-assessment.md +35 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/cost-estimate.md +47 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/deployment-summary.md +35 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/governance-constraints.md +35 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/implementation-plan.md +43 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/operations-runbook.md +35 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/requirements.md +42 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/resource-inventory.md +29 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/SKILL.md +52 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/references/decision-examples.md +38 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/references/decision-guardrails.md +36 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/references/decision-quality.md +21 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/references/decision-record-fields.md +57 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/SKILL.md +55 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/references/lambda-to-functions-assessment.md +29 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/references/migration-readiness.md +27 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/references/staged-validation-handoff.md +26 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/references/workload-mapping.md +27 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compliance/SKILL.md +58 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compliance/references/compliance-finding-criteria.md +63 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compliance/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compute/SKILL.md +36 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compute/references/compute-selection.md +26 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compute/references/pricing-evidence.md +11 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compute/references/recommendation-and-scale-rules.md +44 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cost-optimization/SKILL.md +61 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cost-optimization/references/cost-assessment-criteria.md +61 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cost-optimization/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/SKILL.md +59 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/avm-binding-guidance.md +29 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/baseline-fallbacks.md +53 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/cost-monitoring.md +47 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/decision-boundaries.md +19 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/governance-effects.md +28 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/naming.md +47 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/network-planning.md +47 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/security-baseline.md +40 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/service-selection.md +51 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/tag-precedence.md +31 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-deploy/SKILL.md +50 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-deploy/references/operational-checklist.md +26 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-deploy/references/preview-recovery-verification.md +48 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-diagnostics/SKILL.md +60 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-diagnostics/references/diagnostic-interpretation.md +42 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-diagnostics/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-governance/SKILL.md +52 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-governance/references/evidence-interpretation.md +58 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-governance/references/operational-checklist.md +25 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-kusto/SKILL.md +58 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-kusto/references/kql-evidence-interpretation.md +41 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-kusto/references/operational-checklist.md +25 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-prepare/SKILL.md +52 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-prepare/references/functions-recipe-pack.md +27 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-prepare/references/preparation-lineage.md +36 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-quotas/SKILL.md +53 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-quotas/references/capacity-decision-rules.md +48 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-quotas/references/operational-checklist.md +23 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-rbac/SKILL.md +45 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-rbac/references/assignment-intent.md +39 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-rbac/references/least-privilege-selection.md +50 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-resources/SKILL.md +54 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-resources/references/inventory-query-patterns.md +49 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-resources/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-storage/SKILL.md +35 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-storage/references/security-and-governance.md +12 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-storage/references/service-auth-and-sdk-boundary.md +33 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-storage/references/storage-selection.md +12 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-validate/SKILL.md +49 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-validate/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-validate/references/preflight-evidence.md +43 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/SKILL.md +41 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/avm-and-codegen-acceptance.md +11 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/codegen-acceptance-checklist.md +46 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/compiler-and-provider-gotchas.md +74 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/module-interfaces-and-parameters.md +56 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/network-and-observability.md +52 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-codegen/SKILL.md +32 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/SKILL.md +53 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/references/capability-receipts.md +21 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/references/design-and-diagnostics.md +36 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/references/oauth-registration-model.md +26 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/references/permissions-and-credentials.md +27 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-mermaid/SKILL.md +44 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-mermaid/references/styling.md +33 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-mermaid/references/syntax.md +99 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-microsoft-docs/SKILL.md +53 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-microsoft-docs/references/research-method.md +91 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-operations/SKILL.md +34 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-planning/SKILL.md +36 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-requirements/SKILL.md +43 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/SKILL.md +47 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/references/adoption-attestation.md +10 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/references/import-assessment.md +14 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/references/import-mapping.md +27 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/references/mapping-and-adoption-attestation.md +12 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/SKILL.md +49 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/module-composition-and-refactor.md +20 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/module-composition-and-state-boundaries.md +11 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/module-locks-and-codegen-acceptance.md +14 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/network-and-observability.md +12 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/plan-and-change-assessment.md +24 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/SKILL.md +45 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/references/evidence-acceptance.md +10 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/references/plan-mode-and-mock-design.md +12 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/references/plan-mode-test-design.md +23 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/references/test-design.md +13 -0
- package/assets/client-projections/github-copilot-cli/.github/skills/apex-workflow/SKILL.md +37 -0
- package/assets/client-projections/github-copilot-vscode/.github/agents/apex-architect.agent.md +87 -0
- package/assets/client-projections/github-copilot-vscode/.github/agents/apex-codegen.agent.md +54 -0
- package/assets/client-projections/github-copilot-vscode/.github/agents/apex-operator.agent.md +75 -0
- package/assets/client-projections/github-copilot-vscode/.github/agents/apex-planner.agent.md +89 -0
- package/assets/client-projections/github-copilot-vscode/.github/agents/apex-requirements.agent.md +78 -0
- package/assets/client-projections/github-copilot-vscode/.github/agents/apex-reviewer.agent.md +45 -0
- package/assets/client-projections/github-copilot-vscode/.github/agents/apex-validator.agent.md +50 -0
- package/assets/client-projections/github-copilot-vscode/.github/agents/apex.agent.md +71 -0
- package/assets/client-projections/github-copilot-vscode/.github/copilot-instructions.md +13 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-architecture/SKILL.md +36 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/SKILL.md +65 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/references/presentation-conventions.md +52 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/references/reference-only-outlines.md +78 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/architecture-assessment.md +35 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/cost-estimate.md +47 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/deployment-summary.md +35 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/governance-constraints.md +35 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/implementation-plan.md +43 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/operations-runbook.md +35 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/requirements.md +42 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/resource-inventory.md +29 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/SKILL.md +52 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/references/decision-examples.md +38 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/references/decision-guardrails.md +36 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/references/decision-quality.md +21 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/references/decision-record-fields.md +57 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/SKILL.md +55 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/references/lambda-to-functions-assessment.md +29 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/references/migration-readiness.md +27 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/references/staged-validation-handoff.md +26 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/references/workload-mapping.md +27 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compliance/SKILL.md +58 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compliance/references/compliance-finding-criteria.md +63 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compliance/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compute/SKILL.md +36 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compute/references/compute-selection.md +26 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compute/references/pricing-evidence.md +11 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compute/references/recommendation-and-scale-rules.md +44 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cost-optimization/SKILL.md +61 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cost-optimization/references/cost-assessment-criteria.md +61 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cost-optimization/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/SKILL.md +59 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/avm-binding-guidance.md +29 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/baseline-fallbacks.md +53 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/cost-monitoring.md +47 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/decision-boundaries.md +19 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/governance-effects.md +28 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/naming.md +47 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/network-planning.md +47 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/security-baseline.md +40 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/service-selection.md +51 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/tag-precedence.md +31 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-deploy/SKILL.md +50 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-deploy/references/operational-checklist.md +26 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-deploy/references/preview-recovery-verification.md +48 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-diagnostics/SKILL.md +60 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-diagnostics/references/diagnostic-interpretation.md +42 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-diagnostics/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-governance/SKILL.md +52 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-governance/references/evidence-interpretation.md +58 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-governance/references/operational-checklist.md +25 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-kusto/SKILL.md +58 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-kusto/references/kql-evidence-interpretation.md +41 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-kusto/references/operational-checklist.md +25 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-prepare/SKILL.md +52 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-prepare/references/functions-recipe-pack.md +27 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-prepare/references/preparation-lineage.md +36 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-quotas/SKILL.md +53 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-quotas/references/capacity-decision-rules.md +48 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-quotas/references/operational-checklist.md +23 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-rbac/SKILL.md +45 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-rbac/references/assignment-intent.md +39 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-rbac/references/least-privilege-selection.md +50 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-resources/SKILL.md +54 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-resources/references/inventory-query-patterns.md +49 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-resources/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-storage/SKILL.md +35 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-storage/references/security-and-governance.md +12 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-storage/references/service-auth-and-sdk-boundary.md +33 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-storage/references/storage-selection.md +12 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-validate/SKILL.md +49 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-validate/references/operational-checklist.md +24 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-validate/references/preflight-evidence.md +43 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/SKILL.md +41 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/avm-and-codegen-acceptance.md +11 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/codegen-acceptance-checklist.md +46 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/compiler-and-provider-gotchas.md +74 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/module-interfaces-and-parameters.md +56 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/network-and-observability.md +52 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-codegen/SKILL.md +32 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/SKILL.md +53 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/references/capability-receipts.md +21 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/references/design-and-diagnostics.md +36 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/references/oauth-registration-model.md +26 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/references/permissions-and-credentials.md +27 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-mermaid/SKILL.md +44 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-mermaid/references/styling.md +33 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-mermaid/references/syntax.md +99 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-microsoft-docs/SKILL.md +53 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-microsoft-docs/references/research-method.md +91 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-operations/SKILL.md +34 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-planning/SKILL.md +36 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-requirements/SKILL.md +43 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/SKILL.md +47 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/references/adoption-attestation.md +10 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/references/import-assessment.md +14 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/references/import-mapping.md +27 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/references/mapping-and-adoption-attestation.md +12 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/SKILL.md +49 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/module-composition-and-refactor.md +20 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/module-composition-and-state-boundaries.md +11 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/module-locks-and-codegen-acceptance.md +14 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/network-and-observability.md +12 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/plan-and-change-assessment.md +24 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/SKILL.md +45 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/references/evidence-acceptance.md +10 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/references/plan-mode-and-mock-design.md +12 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/references/plan-mode-test-design.md +23 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/references/test-design.md +13 -0
- package/assets/client-projections/github-copilot-vscode/.github/skills/apex-workflow/SKILL.md +37 -0
- package/assets/client-projections/github-copilot-vscode/.vscode/mcp.json +21 -0
- package/assets/config/capability-packs.v1.json +43 -0
- package/assets/config/defaults.v1.json +98 -0
- package/assets/config/improvement-policy.v1.json +37 -0
- package/assets/config/quality-scorecard.v1.json +105 -0
- package/assets/config/recipe-packs.v1.json +29 -0
- package/assets/config/runtime-bundle.v1.json +84 -0
- package/assets/config/toolchain.v1.json +134 -0
- package/assets/config/workflow.v1.json +876 -0
- package/assets/customizations/.github/agents/apex-architect.agent.md +80 -0
- package/assets/customizations/.github/agents/apex-codegen.agent.md +51 -0
- package/assets/customizations/.github/agents/apex-operator.agent.md +68 -0
- package/assets/customizations/.github/agents/apex-planner.agent.md +82 -0
- package/assets/customizations/.github/agents/apex-requirements.agent.md +71 -0
- package/assets/customizations/.github/agents/apex-reviewer.agent.md +42 -0
- package/assets/customizations/.github/agents/apex-validator.agent.md +47 -0
- package/assets/customizations/.github/agents/apex.agent.md +64 -0
- package/assets/customizations/.github/copilot-instructions.md +13 -0
- package/assets/customizations/.github/mcp.json +54 -0
- package/assets/customizations/.github/skills/apex-architecture/SKILL.md +36 -0
- package/assets/customizations/.github/skills/apex-artifacts/SKILL.md +65 -0
- package/assets/customizations/.github/skills/apex-artifacts/references/presentation-conventions.md +52 -0
- package/assets/customizations/.github/skills/apex-artifacts/references/reference-only-outlines.md +78 -0
- package/assets/customizations/.github/skills/apex-artifacts/templates/architecture-assessment.md +35 -0
- package/assets/customizations/.github/skills/apex-artifacts/templates/cost-estimate.md +47 -0
- package/assets/customizations/.github/skills/apex-artifacts/templates/deployment-summary.md +35 -0
- package/assets/customizations/.github/skills/apex-artifacts/templates/governance-constraints.md +35 -0
- package/assets/customizations/.github/skills/apex-artifacts/templates/implementation-plan.md +43 -0
- package/assets/customizations/.github/skills/apex-artifacts/templates/operations-runbook.md +35 -0
- package/assets/customizations/.github/skills/apex-artifacts/templates/requirements.md +42 -0
- package/assets/customizations/.github/skills/apex-artifacts/templates/resource-inventory.md +29 -0
- package/assets/customizations/.github/skills/apex-azure-adr/SKILL.md +52 -0
- package/assets/customizations/.github/skills/apex-azure-adr/references/decision-examples.md +38 -0
- package/assets/customizations/.github/skills/apex-azure-adr/references/decision-guardrails.md +36 -0
- package/assets/customizations/.github/skills/apex-azure-adr/references/decision-quality.md +21 -0
- package/assets/customizations/.github/skills/apex-azure-adr/references/decision-record-fields.md +57 -0
- package/assets/customizations/.github/skills/apex-azure-cloud-migrate/SKILL.md +55 -0
- package/assets/customizations/.github/skills/apex-azure-cloud-migrate/references/lambda-to-functions-assessment.md +29 -0
- package/assets/customizations/.github/skills/apex-azure-cloud-migrate/references/migration-readiness.md +27 -0
- package/assets/customizations/.github/skills/apex-azure-cloud-migrate/references/staged-validation-handoff.md +26 -0
- package/assets/customizations/.github/skills/apex-azure-cloud-migrate/references/workload-mapping.md +27 -0
- package/assets/customizations/.github/skills/apex-azure-compliance/SKILL.md +58 -0
- package/assets/customizations/.github/skills/apex-azure-compliance/references/compliance-finding-criteria.md +63 -0
- package/assets/customizations/.github/skills/apex-azure-compliance/references/operational-checklist.md +24 -0
- package/assets/customizations/.github/skills/apex-azure-compute/SKILL.md +36 -0
- package/assets/customizations/.github/skills/apex-azure-compute/references/compute-selection.md +26 -0
- package/assets/customizations/.github/skills/apex-azure-compute/references/pricing-evidence.md +11 -0
- package/assets/customizations/.github/skills/apex-azure-compute/references/recommendation-and-scale-rules.md +44 -0
- package/assets/customizations/.github/skills/apex-azure-cost-optimization/SKILL.md +61 -0
- package/assets/customizations/.github/skills/apex-azure-cost-optimization/references/cost-assessment-criteria.md +61 -0
- package/assets/customizations/.github/skills/apex-azure-cost-optimization/references/operational-checklist.md +24 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/SKILL.md +59 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/avm-binding-guidance.md +29 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/baseline-fallbacks.md +53 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/cost-monitoring.md +47 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/decision-boundaries.md +19 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/governance-effects.md +28 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/naming.md +47 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/network-planning.md +47 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/security-baseline.md +40 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/service-selection.md +51 -0
- package/assets/customizations/.github/skills/apex-azure-defaults/references/tag-precedence.md +31 -0
- package/assets/customizations/.github/skills/apex-azure-deploy/SKILL.md +50 -0
- package/assets/customizations/.github/skills/apex-azure-deploy/references/operational-checklist.md +26 -0
- package/assets/customizations/.github/skills/apex-azure-deploy/references/preview-recovery-verification.md +48 -0
- package/assets/customizations/.github/skills/apex-azure-diagnostics/SKILL.md +60 -0
- package/assets/customizations/.github/skills/apex-azure-diagnostics/references/diagnostic-interpretation.md +42 -0
- package/assets/customizations/.github/skills/apex-azure-diagnostics/references/operational-checklist.md +24 -0
- package/assets/customizations/.github/skills/apex-azure-governance/SKILL.md +52 -0
- package/assets/customizations/.github/skills/apex-azure-governance/references/evidence-interpretation.md +58 -0
- package/assets/customizations/.github/skills/apex-azure-governance/references/operational-checklist.md +25 -0
- package/assets/customizations/.github/skills/apex-azure-kusto/SKILL.md +58 -0
- package/assets/customizations/.github/skills/apex-azure-kusto/references/kql-evidence-interpretation.md +41 -0
- package/assets/customizations/.github/skills/apex-azure-kusto/references/operational-checklist.md +25 -0
- package/assets/customizations/.github/skills/apex-azure-prepare/SKILL.md +52 -0
- package/assets/customizations/.github/skills/apex-azure-prepare/references/functions-recipe-pack.md +27 -0
- package/assets/customizations/.github/skills/apex-azure-prepare/references/preparation-lineage.md +36 -0
- package/assets/customizations/.github/skills/apex-azure-quotas/SKILL.md +53 -0
- package/assets/customizations/.github/skills/apex-azure-quotas/references/capacity-decision-rules.md +48 -0
- package/assets/customizations/.github/skills/apex-azure-quotas/references/operational-checklist.md +23 -0
- package/assets/customizations/.github/skills/apex-azure-rbac/SKILL.md +45 -0
- package/assets/customizations/.github/skills/apex-azure-rbac/references/assignment-intent.md +39 -0
- package/assets/customizations/.github/skills/apex-azure-rbac/references/least-privilege-selection.md +50 -0
- package/assets/customizations/.github/skills/apex-azure-resources/SKILL.md +54 -0
- package/assets/customizations/.github/skills/apex-azure-resources/references/inventory-query-patterns.md +49 -0
- package/assets/customizations/.github/skills/apex-azure-resources/references/operational-checklist.md +24 -0
- package/assets/customizations/.github/skills/apex-azure-storage/SKILL.md +35 -0
- package/assets/customizations/.github/skills/apex-azure-storage/references/security-and-governance.md +12 -0
- package/assets/customizations/.github/skills/apex-azure-storage/references/service-auth-and-sdk-boundary.md +33 -0
- package/assets/customizations/.github/skills/apex-azure-storage/references/storage-selection.md +12 -0
- package/assets/customizations/.github/skills/apex-azure-validate/SKILL.md +49 -0
- package/assets/customizations/.github/skills/apex-azure-validate/references/operational-checklist.md +24 -0
- package/assets/customizations/.github/skills/apex-azure-validate/references/preflight-evidence.md +43 -0
- package/assets/customizations/.github/skills/apex-bicep-patterns/SKILL.md +41 -0
- package/assets/customizations/.github/skills/apex-bicep-patterns/references/avm-and-codegen-acceptance.md +11 -0
- package/assets/customizations/.github/skills/apex-bicep-patterns/references/codegen-acceptance-checklist.md +46 -0
- package/assets/customizations/.github/skills/apex-bicep-patterns/references/compiler-and-provider-gotchas.md +74 -0
- package/assets/customizations/.github/skills/apex-bicep-patterns/references/module-interfaces-and-parameters.md +56 -0
- package/assets/customizations/.github/skills/apex-bicep-patterns/references/network-and-observability.md +52 -0
- package/assets/customizations/.github/skills/apex-codegen/SKILL.md +32 -0
- package/assets/customizations/.github/skills/apex-entra-app-registration/SKILL.md +53 -0
- package/assets/customizations/.github/skills/apex-entra-app-registration/references/capability-receipts.md +21 -0
- package/assets/customizations/.github/skills/apex-entra-app-registration/references/design-and-diagnostics.md +36 -0
- package/assets/customizations/.github/skills/apex-entra-app-registration/references/oauth-registration-model.md +26 -0
- package/assets/customizations/.github/skills/apex-entra-app-registration/references/permissions-and-credentials.md +27 -0
- package/assets/customizations/.github/skills/apex-mermaid/SKILL.md +44 -0
- package/assets/customizations/.github/skills/apex-mermaid/references/styling.md +33 -0
- package/assets/customizations/.github/skills/apex-mermaid/references/syntax.md +99 -0
- package/assets/customizations/.github/skills/apex-microsoft-docs/SKILL.md +53 -0
- package/assets/customizations/.github/skills/apex-microsoft-docs/references/research-method.md +91 -0
- package/assets/customizations/.github/skills/apex-operations/SKILL.md +34 -0
- package/assets/customizations/.github/skills/apex-planning/SKILL.md +36 -0
- package/assets/customizations/.github/skills/apex-requirements/SKILL.md +43 -0
- package/assets/customizations/.github/skills/apex-terraform-import/SKILL.md +47 -0
- package/assets/customizations/.github/skills/apex-terraform-import/references/adoption-attestation.md +10 -0
- package/assets/customizations/.github/skills/apex-terraform-import/references/import-assessment.md +14 -0
- package/assets/customizations/.github/skills/apex-terraform-import/references/import-mapping.md +27 -0
- package/assets/customizations/.github/skills/apex-terraform-import/references/mapping-and-adoption-attestation.md +12 -0
- package/assets/customizations/.github/skills/apex-terraform-patterns/SKILL.md +49 -0
- package/assets/customizations/.github/skills/apex-terraform-patterns/references/module-composition-and-refactor.md +20 -0
- package/assets/customizations/.github/skills/apex-terraform-patterns/references/module-composition-and-state-boundaries.md +11 -0
- package/assets/customizations/.github/skills/apex-terraform-patterns/references/module-locks-and-codegen-acceptance.md +14 -0
- package/assets/customizations/.github/skills/apex-terraform-patterns/references/network-and-observability.md +12 -0
- package/assets/customizations/.github/skills/apex-terraform-patterns/references/plan-and-change-assessment.md +24 -0
- package/assets/customizations/.github/skills/apex-terraform-test/SKILL.md +45 -0
- package/assets/customizations/.github/skills/apex-terraform-test/references/evidence-acceptance.md +10 -0
- package/assets/customizations/.github/skills/apex-terraform-test/references/plan-mode-and-mock-design.md +12 -0
- package/assets/customizations/.github/skills/apex-terraform-test/references/plan-mode-test-design.md +23 -0
- package/assets/customizations/.github/skills/apex-terraform-test/references/test-design.md +13 -0
- package/assets/customizations/.github/skills/apex-workflow/SKILL.md +37 -0
- package/assets/customizations/.vscode/mcp.json +21 -0
- package/assets/customizations/README.md +22 -0
- package/assets/customizations/manifest.json +251 -0
- package/assets/manifest.json +5027 -0
- package/dist/assets.d.ts +69 -0
- package/dist/assets.js +336 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +643 -0
- package/dist/dependency-revision.d.ts +5 -0
- package/dist/dependency-revision.js +21 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.js +34 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/mcp.d.ts +5 -0
- package/dist/mcp.js +210 -0
- package/dist/provider-runtime.d.ts +27 -0
- package/dist/provider-runtime.js +229 -0
- package/dist/provider-transfer.d.ts +52 -0
- package/dist/provider-transfer.js +533 -0
- package/dist/service.d.ts +806 -0
- package/dist/service.js +3817 -0
- package/dist/state-transfer.d.ts +51 -0
- package/dist/state-transfer.js +559 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/workflow-validators.d.ts +80 -0
- package/dist/workflow-validators.js +843 -0
- package/package.json +57 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,3817 @@
|
|
|
1
|
+
import { ApprovalEvidenceV1Schema, ArchitectureAvailabilityV1Schema, ArchitectureV1Schema, CONTRACT_VERSION, CostEstimateV1Schema, DeploymentPreviewV1Schema, DiagnosisV1Schema, EnvironmentInputsV1Schema, EvidenceManifestV1Schema, ExecutionPlanAttestationV1Schema, GovernanceConstraintsV1Schema, IacBindingV1Schema, IacHandoffV1Schema, InputRequestV1Schema, InputValueV1Schema, InputSubmissionV1Schema, ImplementationIntentV1Schema, LogicalResourceManifestV1Schema, OperationRecordV1Schema, PolicyPropertyMapV1Schema, QualityMeasurementsV1Schema, QualityReportV1Schema, RequirementsV1Schema, ResourceInventoryV1Schema, ReviewFindingsV1Schema, RuntimeBundleLockV1Schema, WorkloadDecisionManifestV1Schema, hasOnlyTypedSecretReferences, hasValidInputRequestQuestions, hasValidCostArithmetic, hasValidLogicalResourceReferences, } from "@apexops/contracts";
|
|
2
|
+
import { Value } from "@sinclair/typebox/value";
|
|
3
|
+
import { CapabilityPackManager, FakeIaCProvider, ProcessRunner, generateBicepTree, generateTerraformTree, } from "@apexops/capabilities";
|
|
4
|
+
import { ContentCache, EvidencePolicy, EvidenceStore, EventJournal, ImprovementStore, ObjectStore, ProjectStore, RunRepository, ValidatorRegistry, WriterTransferStore, WorkflowEngine, assertTaskCurrent, atomicWriteBytes, atomicWriteJson, createTaskEnvelope, decideGate, inheritGate, openGate, sha256Bytes, sha256Json, validateInputAnswers, workflowValidatorOwnership, } from "@apexops/kernel";
|
|
5
|
+
import { DOCUMENT_REGISTRY, renderApprovalEvidence, renderDeploymentPreview, renderRequirementsDocument, renderResourceInventory, renderRunStatus, } from "@apexops/renderers";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { access, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
|
+
import { resolveBundledAssets } from "./assets.js";
|
|
11
|
+
import { dependencyRevision as calculateDependencyRevision } from "./dependency-revision.js";
|
|
12
|
+
import { ApexError, EXIT_CODES } from "./errors.js";
|
|
13
|
+
import { APEX_VERSION } from "./version.js";
|
|
14
|
+
import { registerWorkflowValidators, taskWorkflowValidatorInput, } from "./workflow-validators.js";
|
|
15
|
+
const ZERO_HASH = "0".repeat(64);
|
|
16
|
+
const TASK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const PREVIEW_TTL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
const APEX_GITIGNORE = "/cache/\n/local/\n/work/\n/runtime/capability-packs/\n";
|
|
19
|
+
const PROFILE_BOOTSTRAP_FILENAME = "apex-bootstrap.agent.md";
|
|
20
|
+
const PROFILE_BOOTSTRAP_RECEIPT = ".apex-bootstrap.lock.json";
|
|
21
|
+
function profileBootstrapAgent() {
|
|
22
|
+
return Buffer.from(`---
|
|
23
|
+
name: APEX Bootstrap
|
|
24
|
+
description: Create an APEX workspace through the supported bootstrap workflow.
|
|
25
|
+
target: vscode
|
|
26
|
+
user-invocable: true
|
|
27
|
+
disable-model-invocation: true
|
|
28
|
+
tools:
|
|
29
|
+
- vscode/askQuestions
|
|
30
|
+
- run_in_terminal
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Role
|
|
34
|
+
|
|
35
|
+
Guide a user through creating an APEX workspace. Ask for the project ID, display
|
|
36
|
+
name, environment, target scope, IaC track, and whether Git may be initialized.
|
|
37
|
+
|
|
38
|
+
## Workflow
|
|
39
|
+
|
|
40
|
+
1. Confirm the open folder is the intended workspace and is trusted.
|
|
41
|
+
2. Collect the onboarding values with \`vscode/askQuestions\`.
|
|
42
|
+
3. Run \`npx --yes @apexops/cli@${APEX_VERSION} bootstrap --project PROJECT_ID --name "DISPLAY_NAME" --environment ENVIRONMENT --target TARGET_SCOPE --iac IAC_TOOL --client github-copilot-vscode --yes\`.
|
|
43
|
+
Include \`--create-repo\` only after the user explicitly approves Git initialization.
|
|
44
|
+
4. Run \`apex setup --json\` and \`apex doctor --json\` from the workspace.
|
|
45
|
+
5. Ask the user to reload the VS Code window, then select the workspace APEX agent.
|
|
46
|
+
|
|
47
|
+
## Boundaries
|
|
48
|
+
|
|
49
|
+
Do not write workspace files, .apex state, MCP configuration, or managed agents.
|
|
50
|
+
Do not approve gates, deploy resources, or infer workflow state. The CLI owns
|
|
51
|
+
workspace initialization and the kernel owns all workflow authority.
|
|
52
|
+
`, "utf8");
|
|
53
|
+
}
|
|
54
|
+
const ARTIFACTS = {
|
|
55
|
+
requirements: ["requirements", RequirementsV1Schema],
|
|
56
|
+
"workload-decision-manifest": ["workload-decision-manifest", WorkloadDecisionManifestV1Schema],
|
|
57
|
+
architecture: ["architecture", ArchitectureV1Schema],
|
|
58
|
+
"cost-estimate": ["cost-estimate", CostEstimateV1Schema],
|
|
59
|
+
"review-findings": ["review-findings", ReviewFindingsV1Schema],
|
|
60
|
+
"governance-constraints": ["governance-constraints", GovernanceConstraintsV1Schema],
|
|
61
|
+
"policy-property-map": ["policy-property-map", PolicyPropertyMapV1Schema],
|
|
62
|
+
"implementation-intent": ["implementation-intent", ImplementationIntentV1Schema],
|
|
63
|
+
"iac-binding": ["iac-binding", IacBindingV1Schema],
|
|
64
|
+
"environment-inputs": ["environment-inputs", EnvironmentInputsV1Schema],
|
|
65
|
+
"logical-resource-manifest": ["logical-resource-manifest", LogicalResourceManifestV1Schema],
|
|
66
|
+
"iac-handoff": ["iac-handoff", IacHandoffV1Schema],
|
|
67
|
+
"validation-evidence": ["validation-evidence", EvidenceManifestV1Schema],
|
|
68
|
+
"deployment-preview": ["preview", DeploymentPreviewV1Schema],
|
|
69
|
+
"execution-plan-attestation": ["execution-plan-attestation", ExecutionPlanAttestationV1Schema],
|
|
70
|
+
"operation-record": ["operation", OperationRecordV1Schema],
|
|
71
|
+
"resource-inventory": ["inventory", ResourceInventoryV1Schema],
|
|
72
|
+
diagnosis: ["diagnosis", DiagnosisV1Schema],
|
|
73
|
+
"quality-report": ["quality-report", QualityReportV1Schema],
|
|
74
|
+
};
|
|
75
|
+
export const SUPPORTED_ARTIFACT_KINDS = Object.keys(ARTIFACTS);
|
|
76
|
+
const TASKS = [
|
|
77
|
+
{ id: "requirements", role: "requirements", outputs: ["requirements"] },
|
|
78
|
+
{ id: "requirements-review", role: "reviewer", outputs: ["review-findings"], reviewSubject: "requirements", gate: 1 },
|
|
79
|
+
{ id: "architecture", role: "architect", outputs: ["architecture", "cost-estimate", "workload-decision-manifest"] },
|
|
80
|
+
{ id: "architecture-review", role: "reviewer", outputs: ["review-findings"], reviewSubject: "architecture" },
|
|
81
|
+
{
|
|
82
|
+
id: "governance-discovery",
|
|
83
|
+
role: "governance-operator",
|
|
84
|
+
outputs: ["governance-constraints"],
|
|
85
|
+
capabilities: ["governance-discovery"],
|
|
86
|
+
},
|
|
87
|
+
{ id: "governance-reconciliation", role: "governance-operator", outputs: ["policy-property-map"] },
|
|
88
|
+
{
|
|
89
|
+
id: "governance-review",
|
|
90
|
+
role: "reviewer",
|
|
91
|
+
outputs: ["review-findings"],
|
|
92
|
+
reviewSubject: "governance-reconciliation",
|
|
93
|
+
gate: 2,
|
|
94
|
+
},
|
|
95
|
+
{ id: "plan", role: "planner", outputs: ["implementation-intent", "iac-binding", "environment-inputs"] },
|
|
96
|
+
{ id: "plan-review", role: "reviewer", outputs: ["review-findings"], reviewSubject: "plan", gate: 3 },
|
|
97
|
+
{ id: "codegen-bicep", role: "bicep-codegen", outputs: ["logical-resource-manifest", "iac-handoff"], track: "bicep" },
|
|
98
|
+
{
|
|
99
|
+
id: "codegen-terraform",
|
|
100
|
+
role: "terraform-codegen",
|
|
101
|
+
outputs: ["logical-resource-manifest", "iac-handoff"],
|
|
102
|
+
track: "terraform",
|
|
103
|
+
},
|
|
104
|
+
{ id: "validation-bicep", role: "validator", outputs: ["validation-evidence"], track: "bicep" },
|
|
105
|
+
{ id: "validation-terraform", role: "validator", outputs: ["validation-evidence"], track: "terraform" },
|
|
106
|
+
{ id: "diagnosis", role: "diagnostic-operator", outputs: ["diagnosis"] },
|
|
107
|
+
{ id: "quality", role: "quality-evaluator", outputs: ["quality-report"] },
|
|
108
|
+
];
|
|
109
|
+
const REQUIREMENTS_INTAKE = [
|
|
110
|
+
{
|
|
111
|
+
round: "business-discovery",
|
|
112
|
+
questions: [
|
|
113
|
+
{ id: "workload", prompt: "Briefly describe the workload and its users." },
|
|
114
|
+
{ id: "industry", prompt: "Which industry or domain does this workload serve?" },
|
|
115
|
+
{
|
|
116
|
+
id: "delivery-scenario",
|
|
117
|
+
prompt: "Is this a greenfield build, migration, modernization, or extension?",
|
|
118
|
+
options: ["greenfield", "migration", "modernization", "extension"],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "target-environments",
|
|
122
|
+
prompt: "Which environments are in scope?",
|
|
123
|
+
options: ["dev", "test", "staging", "prod"],
|
|
124
|
+
multiSelect: true,
|
|
125
|
+
valueType: "environment-set",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
round: "workload-pattern",
|
|
131
|
+
questions: [
|
|
132
|
+
{
|
|
133
|
+
id: "workload-pattern",
|
|
134
|
+
prompt: "Which workload pattern best fits the solution?",
|
|
135
|
+
options: ["web-api", "event-driven", "data-analytics", "iot", "batch"],
|
|
136
|
+
},
|
|
137
|
+
{ id: "scale", prompt: "Describe expected users, concurrency, throughput, or data volume." },
|
|
138
|
+
{ id: "budget", prompt: "State the monthly budget or explicitly defer it.", valueType: "budget" },
|
|
139
|
+
{
|
|
140
|
+
id: "data-sensitivity",
|
|
141
|
+
prompt: "Choose the data classification or explicitly defer it.",
|
|
142
|
+
valueType: "data-classification",
|
|
143
|
+
},
|
|
144
|
+
{ id: "iac-preference", prompt: "Choose the preferred infrastructure tool.", options: ["bicep", "terraform"] },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
round: "service-preferences",
|
|
149
|
+
questions: [
|
|
150
|
+
{ id: "retained-services", prompt: "List Azure services or integrations that must be retained, or 'none'." },
|
|
151
|
+
{ id: "prohibited-services", prompt: "List prohibited services or use 'none'." },
|
|
152
|
+
{
|
|
153
|
+
id: "service-preferences",
|
|
154
|
+
prompt: "State preferred compute, database, integration, and observability services.",
|
|
155
|
+
},
|
|
156
|
+
{ id: "sku-preferences", prompt: "State required SKUs, tier floors, reservations, or use 'no preference'." },
|
|
157
|
+
{ id: "environment-overrides", prompt: "State environment-specific service or sizing overrides, or 'none'." },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
round: "security-compliance",
|
|
162
|
+
questions: [
|
|
163
|
+
{
|
|
164
|
+
id: "compliance",
|
|
165
|
+
prompt: "List compliance and regulatory scopes or explicitly defer them.",
|
|
166
|
+
valueType: "compliance",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "security-controls",
|
|
170
|
+
prompt: "List required identity, network, encryption, and secret-management controls.",
|
|
171
|
+
},
|
|
172
|
+
{ id: "authentication", prompt: "Describe authentication and authorization requirements." },
|
|
173
|
+
{ id: "region", prompt: "State the required Azure region or use 'deferred: <owner>'." },
|
|
174
|
+
{ id: "availability-recovery", prompt: "State availability, RTO, RPO, backup, and recovery requirements." },
|
|
175
|
+
{
|
|
176
|
+
id: "recovery",
|
|
177
|
+
prompt: "Provide RTO and RPO in whole minutes or explicitly defer them.",
|
|
178
|
+
valueType: "recovery",
|
|
179
|
+
},
|
|
180
|
+
{ id: "operations", prompt: "State monitoring, alerting, support, and operational requirements." },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
const ARCHITECTURE_DECISIONS = [
|
|
185
|
+
{
|
|
186
|
+
id: "network-exposure",
|
|
187
|
+
questions: [
|
|
188
|
+
{
|
|
189
|
+
id: "network-exposure",
|
|
190
|
+
prompt: "Choose the architecture network exposure posture.",
|
|
191
|
+
options: ["private-only", "public-approved", "deferred"],
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
export class ApexService {
|
|
197
|
+
root;
|
|
198
|
+
clock;
|
|
199
|
+
idSource;
|
|
200
|
+
projects;
|
|
201
|
+
objects;
|
|
202
|
+
cache;
|
|
203
|
+
validators = new ValidatorRegistry();
|
|
204
|
+
providers;
|
|
205
|
+
architectureAvailabilityAdapter;
|
|
206
|
+
executableChecker;
|
|
207
|
+
azureAuthStatus;
|
|
208
|
+
customizationFailureInjector;
|
|
209
|
+
processRunner;
|
|
210
|
+
profileRoot;
|
|
211
|
+
improvementPolicy;
|
|
212
|
+
improvementRuntime;
|
|
213
|
+
requirementsDocumentTemplate;
|
|
214
|
+
constructor(root, options = {}) {
|
|
215
|
+
this.root = resolve(root);
|
|
216
|
+
this.clock = options.clock ?? (() => new Date());
|
|
217
|
+
this.idSource = options.idSource ?? (() => crypto.randomUUID());
|
|
218
|
+
this.projects = new ProjectStore(this.root, this.clock, this.idSource);
|
|
219
|
+
this.objects = new ObjectStore(this.root);
|
|
220
|
+
this.cache = new ContentCache(this.root);
|
|
221
|
+
for (const [, [validator, schema]] of Object.entries(ARTIFACTS))
|
|
222
|
+
this.validators.register(validator, schema);
|
|
223
|
+
this.validators.register("approval", ApprovalEvidenceV1Schema);
|
|
224
|
+
this.validators.register("runtime-lock", RuntimeBundleLockV1Schema);
|
|
225
|
+
this.validators.register("quality-measurements", QualityMeasurementsV1Schema);
|
|
226
|
+
this.validators.register("architecture-availability", ArchitectureAvailabilityV1Schema);
|
|
227
|
+
registerWorkflowValidators(this.validators);
|
|
228
|
+
this.providers = {
|
|
229
|
+
fake: new FakeIaCProvider({ track: "bicep", now: this.clock, nextId: this.idSource }),
|
|
230
|
+
...options.providers,
|
|
231
|
+
};
|
|
232
|
+
this.architectureAvailabilityAdapter = options.architectureAvailabilityAdapter;
|
|
233
|
+
this.executableChecker = options.executableChecker ?? (async (executable) => this.pathExecutableExists(executable));
|
|
234
|
+
this.azureAuthStatus =
|
|
235
|
+
options.azureAuthStatus ?? (async () => ({ authenticated: false, detail: "not-checked; run setup --live" }));
|
|
236
|
+
this.customizationFailureInjector = options.customizationFailureInjector;
|
|
237
|
+
this.processRunner = options.processRunner ?? new ProcessRunner();
|
|
238
|
+
this.profileRoot = resolve(options.profileRoot ?? join(homedir(), ".copilot", "agents"));
|
|
239
|
+
this.improvementPolicy = options.improvementPolicy;
|
|
240
|
+
}
|
|
241
|
+
async improvementObserve(input) {
|
|
242
|
+
const run = await this.currentRun();
|
|
243
|
+
return (await this.improvements(run)).observe({ projectId: run.projectId, runId: run.runId, ...input });
|
|
244
|
+
}
|
|
245
|
+
async improvementObservations() {
|
|
246
|
+
const run = await this.currentRun();
|
|
247
|
+
const observations = await (await this.improvements(run)).listObservations();
|
|
248
|
+
return observations.filter(({ projectId }) => projectId === run.projectId);
|
|
249
|
+
}
|
|
250
|
+
async improvementScan() {
|
|
251
|
+
const run = await this.currentRun();
|
|
252
|
+
return (await this.improvements(run)).scan(run.projectId);
|
|
253
|
+
}
|
|
254
|
+
async improvementProposals() {
|
|
255
|
+
const run = await this.currentRun();
|
|
256
|
+
return (await this.improvements(run)).listProposals(run.projectId);
|
|
257
|
+
}
|
|
258
|
+
async improvementDecide(input) {
|
|
259
|
+
const run = await this.currentRun();
|
|
260
|
+
return (await this.improvements(run)).decide({ projectId: run.projectId, ...input });
|
|
261
|
+
}
|
|
262
|
+
async improvementDeleteObservation(observationId) {
|
|
263
|
+
const run = await this.currentRun();
|
|
264
|
+
const store = await this.improvements(run);
|
|
265
|
+
const observation = (await store.listObservations()).find((item) => item.observationId === observationId);
|
|
266
|
+
if (observation === undefined || observation.projectId !== run.projectId) {
|
|
267
|
+
throw new ApexError("APEX_NOT_FOUND", "Improvement observation not found", EXIT_CODES.notFound);
|
|
268
|
+
}
|
|
269
|
+
await store.deleteObservation(observationId);
|
|
270
|
+
return { deleted: observationId };
|
|
271
|
+
}
|
|
272
|
+
async improvementPrune() {
|
|
273
|
+
const run = await this.currentRun();
|
|
274
|
+
return (await this.improvements(run)).prune();
|
|
275
|
+
}
|
|
276
|
+
async capabilityList(manifestPath) {
|
|
277
|
+
return this.capabilityPacks(manifestPath).list();
|
|
278
|
+
}
|
|
279
|
+
async capabilityStatus(id, manifestPath) {
|
|
280
|
+
return this.capabilityPacks(manifestPath).status(id);
|
|
281
|
+
}
|
|
282
|
+
async capabilityInstall(id, manifestPath, options = {}) {
|
|
283
|
+
return this.capabilityPacks(manifestPath).install(id, options);
|
|
284
|
+
}
|
|
285
|
+
async capabilityUpdate(id, manifestPath, options = {}) {
|
|
286
|
+
return this.capabilityPacks(manifestPath).update(id, options);
|
|
287
|
+
}
|
|
288
|
+
async capabilityRollback(id, manifestPath) {
|
|
289
|
+
return this.capabilityPacks(manifestPath).rollback(id);
|
|
290
|
+
}
|
|
291
|
+
async capabilityVerify(id, manifestPath) {
|
|
292
|
+
return this.capabilityPacks(manifestPath).verify(id);
|
|
293
|
+
}
|
|
294
|
+
async capabilityUninstall(id, manifestPath) {
|
|
295
|
+
return this.capabilityPacks(manifestPath).uninstall(id);
|
|
296
|
+
}
|
|
297
|
+
async init(input) {
|
|
298
|
+
await this.assertCleanInitialization();
|
|
299
|
+
await mkdir(join(this.root, ".apex"), { recursive: true });
|
|
300
|
+
try {
|
|
301
|
+
await this.ensureLocalGitBoundary();
|
|
302
|
+
const assets = await resolveBundledAssets();
|
|
303
|
+
const clientId = input.clientId ?? "github-copilot-vscode";
|
|
304
|
+
const selection = {
|
|
305
|
+
version: 1,
|
|
306
|
+
clientId,
|
|
307
|
+
sourceMode: input.customizationsSource === undefined ? "bundled-projection" : "custom-source",
|
|
308
|
+
...(input.customizationsSource === undefined ? {} : { customSource: resolve(input.customizationsSource) }),
|
|
309
|
+
};
|
|
310
|
+
await atomicWriteJson(join(this.root, ".apex", "customizations.selection.json"), selection);
|
|
311
|
+
await this.installCustomizations(input.customizationsSource ?? assets.clientProjections[clientId], false, assets.config, false, clientId);
|
|
312
|
+
await this.installCapabilityAssets(assets);
|
|
313
|
+
const runtimeLock = await this.createRuntimeLock(assets);
|
|
314
|
+
await atomicWriteJson(join(this.root, ".apex", "apex.lock.json"), runtimeLock);
|
|
315
|
+
return this.createProject(input);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (await this.pathExistsLstat(join(this.root, ".apex", "customizations.lock.json"))) {
|
|
319
|
+
await this.uninstallCustomizations();
|
|
320
|
+
}
|
|
321
|
+
await rm(join(this.root, ".apex"), { recursive: true, force: true });
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async bootstrap(input) {
|
|
326
|
+
await this.assertCleanInitialization();
|
|
327
|
+
await this.ensureWorkspaceGitRepository(input.createRepository === true);
|
|
328
|
+
const runtimeInstalled = await this.ensureWorkspaceRuntime();
|
|
329
|
+
const initialized = await this.init(input);
|
|
330
|
+
return { ...initialized, runtimeInstalled };
|
|
331
|
+
}
|
|
332
|
+
async profileStatus() {
|
|
333
|
+
const paths = this.profilePaths();
|
|
334
|
+
const receipt = await this.readProfileReceipt(paths.receipt);
|
|
335
|
+
const agent = await this.readProfileOptional(paths.agent);
|
|
336
|
+
if (receipt === undefined || agent === undefined)
|
|
337
|
+
return { installed: false, modified: receipt !== undefined || agent !== undefined };
|
|
338
|
+
return {
|
|
339
|
+
installed: sha256Bytes(agent) === receipt.contentHash,
|
|
340
|
+
modified: sha256Bytes(agent) !== receipt.contentHash,
|
|
341
|
+
version: receipt.packageVersion,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async profileInstall() {
|
|
345
|
+
await this.ensureProfileRoot();
|
|
346
|
+
const paths = this.profilePaths();
|
|
347
|
+
const content = profileBootstrapAgent();
|
|
348
|
+
const current = await this.readProfileOptional(paths.agent);
|
|
349
|
+
if (current !== undefined && !current.equals(content)) {
|
|
350
|
+
throw new ApexError("APEX_CONFLICT", "Profile APEX bootstrap agent was modified or is owned by another tool", EXIT_CODES.conflict);
|
|
351
|
+
}
|
|
352
|
+
await atomicWriteBytes(paths.agent, content);
|
|
353
|
+
await atomicWriteJson(paths.receipt, {
|
|
354
|
+
version: 1,
|
|
355
|
+
packageVersion: APEX_VERSION,
|
|
356
|
+
contentHash: sha256Bytes(content),
|
|
357
|
+
});
|
|
358
|
+
return { installed: current === undefined, version: APEX_VERSION };
|
|
359
|
+
}
|
|
360
|
+
async profileUpdate() {
|
|
361
|
+
await this.ensureProfileRoot();
|
|
362
|
+
const paths = this.profilePaths();
|
|
363
|
+
const receipt = await this.readProfileReceipt(paths.receipt);
|
|
364
|
+
const current = await this.readProfileOptional(paths.agent);
|
|
365
|
+
if (receipt === undefined || current === undefined) {
|
|
366
|
+
throw new ApexError("APEX_NOT_FOUND", "Profile APEX bootstrap agent is not installed", EXIT_CODES.notFound);
|
|
367
|
+
}
|
|
368
|
+
if (sha256Bytes(current) !== receipt.contentHash) {
|
|
369
|
+
throw new ApexError("APEX_CONFLICT", "Profile APEX bootstrap agent was modified", EXIT_CODES.conflict);
|
|
370
|
+
}
|
|
371
|
+
const content = profileBootstrapAgent();
|
|
372
|
+
await atomicWriteBytes(paths.agent, content);
|
|
373
|
+
await atomicWriteJson(paths.receipt, {
|
|
374
|
+
version: 1,
|
|
375
|
+
packageVersion: APEX_VERSION,
|
|
376
|
+
contentHash: sha256Bytes(content),
|
|
377
|
+
});
|
|
378
|
+
return { updated: !current.equals(content), version: APEX_VERSION };
|
|
379
|
+
}
|
|
380
|
+
async profileUninstall() {
|
|
381
|
+
await this.ensureProfileRoot();
|
|
382
|
+
const paths = this.profilePaths();
|
|
383
|
+
const receipt = await this.readProfileReceipt(paths.receipt);
|
|
384
|
+
const current = await this.readProfileOptional(paths.agent);
|
|
385
|
+
if (receipt !== undefined && current !== undefined && sha256Bytes(current) !== receipt.contentHash) {
|
|
386
|
+
throw new ApexError("APEX_CONFLICT", "Profile APEX bootstrap agent was modified", EXIT_CODES.conflict);
|
|
387
|
+
}
|
|
388
|
+
await rm(paths.agent, { force: true });
|
|
389
|
+
await rm(paths.receipt, { force: true });
|
|
390
|
+
return { removed: receipt !== undefined || current !== undefined };
|
|
391
|
+
}
|
|
392
|
+
async createProject(input) {
|
|
393
|
+
let runtimeLock;
|
|
394
|
+
try {
|
|
395
|
+
runtimeLock = JSON.parse(await readFile(join(this.root, ".apex", "apex.lock.json"), "utf8"));
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
if (error.code === "ENOENT") {
|
|
399
|
+
throw new ApexError("APEX_NOT_FOUND", "Initialize the workspace with apex init before creating a project", EXIT_CODES.notFound);
|
|
400
|
+
}
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
const runtimeLockHash = sha256Json(runtimeLock);
|
|
404
|
+
await this.projects.initializeProject({
|
|
405
|
+
projectId: input.projectId,
|
|
406
|
+
displayName: input.displayName ?? input.projectId,
|
|
407
|
+
defaultIacTool: input.iacTool ?? "bicep",
|
|
408
|
+
});
|
|
409
|
+
const run = await this.projects.createRun(input.projectId, {
|
|
410
|
+
environment: input.environment ?? "dev",
|
|
411
|
+
targetScope: input.targetScope ?? "local",
|
|
412
|
+
runtimeLockHash,
|
|
413
|
+
});
|
|
414
|
+
await this.writeSelection({ projectId: input.projectId, runId: run.runId });
|
|
415
|
+
await this.append(run, "project.created", { runtimeLockHash });
|
|
416
|
+
return { projectId: input.projectId, runId: run.runId };
|
|
417
|
+
}
|
|
418
|
+
async update(customizationsSource) {
|
|
419
|
+
const selection = await this.selection();
|
|
420
|
+
await this.ensureLocalGitBoundary();
|
|
421
|
+
const assets = await resolveBundledAssets();
|
|
422
|
+
const lock = JSON.parse(await readFile(join(this.root, ".apex", "customizations.lock.json"), "utf8"));
|
|
423
|
+
const persisted = await this.customizationSelection(lock.clientId);
|
|
424
|
+
const clientId = persisted.clientId;
|
|
425
|
+
if (persisted.sourceMode === "custom-source" && customizationsSource === undefined) {
|
|
426
|
+
throw new ApexError("APEX_USAGE", "Custom-source updates require --customizations-source", EXIT_CODES.usage);
|
|
427
|
+
}
|
|
428
|
+
const source = customizationsSource ?? assets.clientProjections[clientId];
|
|
429
|
+
const updated = await this.installCustomizations(source, true, assets.config, false, clientId);
|
|
430
|
+
await this.append(await this.run(selection), "customizations.updated", { source: resolve(source), updated });
|
|
431
|
+
return { updated };
|
|
432
|
+
}
|
|
433
|
+
async rollbackCustomizations() {
|
|
434
|
+
await this.recoverCustomizationTransaction();
|
|
435
|
+
const lockPath = join(this.root, ".apex", "customizations.lock.json");
|
|
436
|
+
const current = JSON.parse(await readFile(lockPath, "utf8"));
|
|
437
|
+
if (current.previousLockRef === undefined)
|
|
438
|
+
throw new ApexError("APEX_NOT_FOUND", "No prior customization bundle is available", EXIT_CODES.notFound);
|
|
439
|
+
if (!this.safeManagedRelativePath(current.previousLockRef)) {
|
|
440
|
+
throw new ApexError("APEX_VALIDATION", "Previous customization lock path is unsafe", EXIT_CODES.validation);
|
|
441
|
+
}
|
|
442
|
+
const previousPath = resolve(this.root, current.previousLockRef);
|
|
443
|
+
await this.assertSafeExistingPath(this.root, previousPath);
|
|
444
|
+
const previous = JSON.parse(await readFile(previousPath, "utf8"));
|
|
445
|
+
const restored = [];
|
|
446
|
+
const conflicts = [];
|
|
447
|
+
for (const [root, desired, installed] of [
|
|
448
|
+
[this.root, previous.files, current.files],
|
|
449
|
+
[join(this.root, ".apex", "runtime"), previous.runtime, current.runtime],
|
|
450
|
+
]) {
|
|
451
|
+
for (const file of desired) {
|
|
452
|
+
if (!this.safeManagedRelativePath(file.path)) {
|
|
453
|
+
throw new ApexError("APEX_VALIDATION", "Managed customization path is unsafe", EXIT_CODES.validation);
|
|
454
|
+
}
|
|
455
|
+
const destination = resolve(root, file.path);
|
|
456
|
+
await this.assertSafeDestination(root, destination);
|
|
457
|
+
const installedFile = installed.find(({ path }) => path === file.path);
|
|
458
|
+
const incoming = await this.readManagedBase(file.baseRef);
|
|
459
|
+
if (incoming === undefined) {
|
|
460
|
+
conflicts.push(file.path);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (await this.pathExistsLstat(destination)) {
|
|
464
|
+
const local = await readFile(destination);
|
|
465
|
+
const localHash = sha256Bytes(local);
|
|
466
|
+
if (installedFile !== undefined &&
|
|
467
|
+
localHash !== installedFile.currentHash &&
|
|
468
|
+
localHash !== installedFile.baseHash) {
|
|
469
|
+
const base = await this.readManagedBase(installedFile.baseRef);
|
|
470
|
+
const merged = base === undefined ? undefined : this.mergeText(base, local, incoming);
|
|
471
|
+
if (merged === undefined) {
|
|
472
|
+
conflicts.push(file.path);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
await atomicWriteBytes(destination, merged);
|
|
476
|
+
}
|
|
477
|
+
else
|
|
478
|
+
await atomicWriteBytes(destination, incoming);
|
|
479
|
+
}
|
|
480
|
+
else
|
|
481
|
+
await atomicWriteBytes(destination, incoming);
|
|
482
|
+
restored.push(file.path);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (conflicts.length === 0)
|
|
486
|
+
await atomicWriteJson(lockPath, previous);
|
|
487
|
+
return { restored, conflicts };
|
|
488
|
+
}
|
|
489
|
+
async uninstallCustomizations() {
|
|
490
|
+
await this.recoverCustomizationTransaction();
|
|
491
|
+
const lockPath = join(this.root, ".apex", "customizations.lock.json");
|
|
492
|
+
const lock = JSON.parse(await readFile(lockPath, "utf8"));
|
|
493
|
+
const removed = [];
|
|
494
|
+
const conflicts = [];
|
|
495
|
+
for (const file of lock.files) {
|
|
496
|
+
const destination = join(this.root, file.path);
|
|
497
|
+
if (!(await this.pathExistsLstat(destination)))
|
|
498
|
+
continue;
|
|
499
|
+
await this.assertSafeDestination(this.root, destination);
|
|
500
|
+
if (sha256Bytes(await readFile(destination)) !== file.currentHash) {
|
|
501
|
+
conflicts.push(file.path);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
await rm(destination);
|
|
505
|
+
removed.push(file.path);
|
|
506
|
+
}
|
|
507
|
+
await rm(lockPath, { force: true });
|
|
508
|
+
return { removed, conflicts };
|
|
509
|
+
}
|
|
510
|
+
async reinstallCustomizations(customizationsSource) {
|
|
511
|
+
await this.recoverCustomizationTransaction();
|
|
512
|
+
const lockPath = join(this.root, ".apex", "customizations.lock.json");
|
|
513
|
+
if (await this.exists(lockPath)) {
|
|
514
|
+
throw new ApexError("APEX_CONFLICT", "Managed customizations are already installed", EXIT_CODES.conflict);
|
|
515
|
+
}
|
|
516
|
+
const selection = await this.customizationSelection();
|
|
517
|
+
if (selection.sourceMode === "custom-source" && customizationsSource === undefined) {
|
|
518
|
+
throw new ApexError("APEX_USAGE", "Custom-source reinstall requires --customizations-source", EXIT_CODES.usage);
|
|
519
|
+
}
|
|
520
|
+
const assets = await resolveBundledAssets();
|
|
521
|
+
const source = customizationsSource ?? assets.clientProjections[selection.clientId];
|
|
522
|
+
const installed = await this.installCustomizations(source, false, assets.config, false, selection.clientId);
|
|
523
|
+
return { installed, clientId: selection.clientId };
|
|
524
|
+
}
|
|
525
|
+
async listProjects() {
|
|
526
|
+
const directory = join(this.root, ".apex", "projects");
|
|
527
|
+
let names;
|
|
528
|
+
try {
|
|
529
|
+
names = await readdir(directory);
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
if (error.code === "ENOENT")
|
|
533
|
+
return [];
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
const projects = await Promise.all(names.sort().map(async (projectId) => this.projects.getProject(projectId)));
|
|
537
|
+
return projects.map(({ projectId, displayName }) => ({ projectId, displayName }));
|
|
538
|
+
}
|
|
539
|
+
async use(projectId, runId) {
|
|
540
|
+
await this.projects.getProject(projectId);
|
|
541
|
+
const selectedRun = runId ?? (await this.latestRun(projectId));
|
|
542
|
+
await this.projects.getRun(projectId, selectedRun);
|
|
543
|
+
const selection = { projectId, runId: selectedRun };
|
|
544
|
+
await this.writeSelection(selection);
|
|
545
|
+
await this.append(await this.run(selection), "selection.changed", selection);
|
|
546
|
+
return selection;
|
|
547
|
+
}
|
|
548
|
+
async show(projectId) {
|
|
549
|
+
if (projectId !== undefined)
|
|
550
|
+
return this.projects.getProject(projectId);
|
|
551
|
+
const selection = await this.selection();
|
|
552
|
+
return { project: await this.projects.getProject(selection.projectId), run: await this.run(selection) };
|
|
553
|
+
}
|
|
554
|
+
async search(term, limit = 50) {
|
|
555
|
+
const needle = term.toLowerCase();
|
|
556
|
+
const results = [];
|
|
557
|
+
for (const project of await this.listProjects()) {
|
|
558
|
+
const matches = [];
|
|
559
|
+
const runNames = await readdir(join(this.projects.projectDirectory(project.projectId), "runs"));
|
|
560
|
+
for (const runName of runNames.sort()) {
|
|
561
|
+
const run = await this.projects.getRun(project.projectId, runName);
|
|
562
|
+
for (const event of await this.journal(run).replay()) {
|
|
563
|
+
if (`${event.type}\n${JSON.stringify(event.payload)}`.toLowerCase().includes(needle)) {
|
|
564
|
+
matches.push({ runId: run.runId, type: event.type, sequence: event.sequence });
|
|
565
|
+
if (matches.length >= limit)
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (matches.length >= limit)
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
if (project.projectId.toLowerCase().includes(needle) ||
|
|
573
|
+
project.displayName.toLowerCase().includes(needle) ||
|
|
574
|
+
matches.length > 0) {
|
|
575
|
+
results.push({ ...project, ...(matches.length === 0 ? {} : { matches }) });
|
|
576
|
+
}
|
|
577
|
+
if (results.length >= limit)
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
return results;
|
|
581
|
+
}
|
|
582
|
+
async history(limit = 100) {
|
|
583
|
+
const events = await this.journal(await this.currentRun()).replay();
|
|
584
|
+
return events
|
|
585
|
+
.slice(-Math.max(1, Math.min(limit, 500)))
|
|
586
|
+
.reverse()
|
|
587
|
+
.map((event) => ({
|
|
588
|
+
sequence: event.sequence,
|
|
589
|
+
type: event.type,
|
|
590
|
+
timestamp: event.timestamp,
|
|
591
|
+
artifactKinds: Object.keys(event.payload.artifactHashes ?? {}).sort(),
|
|
592
|
+
payload: event.payload,
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
async status() {
|
|
596
|
+
const selection = await this.selection();
|
|
597
|
+
const run = await this.run(selection);
|
|
598
|
+
let events = await this.journal(run).replay();
|
|
599
|
+
let route = await this.route(run, events);
|
|
600
|
+
if (route.task === undefined && route.blockers.length === 0) {
|
|
601
|
+
const completedEvents = await this.ensureTerminalCompletion(run, events);
|
|
602
|
+
if (completedEvents !== events) {
|
|
603
|
+
events = completedEvents;
|
|
604
|
+
route = await this.route(run, events);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
run,
|
|
609
|
+
head: events.at(-1)?.hash ?? null,
|
|
610
|
+
events: events.length,
|
|
611
|
+
task: route.task?.id ?? null,
|
|
612
|
+
blockers: route.blockers,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
async nextTask() {
|
|
616
|
+
const selection = await this.selection();
|
|
617
|
+
const run = await this.run(selection);
|
|
618
|
+
const events = await this.journal(run).replay();
|
|
619
|
+
const requirements = this.artifactHash(events, "requirements");
|
|
620
|
+
if (requirements === undefined) {
|
|
621
|
+
const pending = this.nextRequirementsIntakeRound(events);
|
|
622
|
+
if (pending !== undefined)
|
|
623
|
+
return { status: "needs_input", request: await this.issueRequirementsInput(run, pending) };
|
|
624
|
+
return { status: "task", task: await this.issueTask(run, TASKS[0], []) };
|
|
625
|
+
}
|
|
626
|
+
const route = await this.route(run, events);
|
|
627
|
+
if (route.blockers.length > 0)
|
|
628
|
+
throw new ApexError("APEX_AUTHORIZATION", route.blockers.join("; "), EXIT_CODES.authorization, route.blockers);
|
|
629
|
+
if (route.task === undefined)
|
|
630
|
+
throw new ApexError("APEX_NOT_FOUND", "No task is currently available", EXIT_CODES.notFound);
|
|
631
|
+
if (route.task.id === "architecture") {
|
|
632
|
+
const decision = this.architectureDecisionState(events, run.ownerEpoch);
|
|
633
|
+
if (decision.pending !== undefined)
|
|
634
|
+
return { status: "needs_input", request: decision.pending };
|
|
635
|
+
if (decision.staleTaskId !== undefined) {
|
|
636
|
+
return {
|
|
637
|
+
status: "needs_input",
|
|
638
|
+
request: await this.issueArchitectureDecision(run, await this.readTask(run, decision.staleTaskId)),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
if (decision.taskId !== undefined) {
|
|
642
|
+
const task = await this.readTask(run, decision.taskId);
|
|
643
|
+
if (task.expectedHead === events.at(-1)?.hash && task.ownerEpoch === run.ownerEpoch) {
|
|
644
|
+
return { status: "task", task };
|
|
645
|
+
}
|
|
646
|
+
return { status: "task", task: await this.issueTask(run, route.task, this.inputRefs(events, route.task)) };
|
|
647
|
+
}
|
|
648
|
+
const task = await this.issueTask(run, route.task, this.inputRefs(events, route.task));
|
|
649
|
+
return { status: "needs_input", request: await this.issueArchitectureDecision(run, task) };
|
|
650
|
+
}
|
|
651
|
+
return { status: "task", task: await this.issueTask(run, route.task, this.inputRefs(events, route.task)) };
|
|
652
|
+
}
|
|
653
|
+
async recordInput(input) {
|
|
654
|
+
let run;
|
|
655
|
+
try {
|
|
656
|
+
const selection = await this.selection();
|
|
657
|
+
run = await this.run(selection);
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
if (error instanceof Error && /mutation is already in progress/i.test(error.message)) {
|
|
661
|
+
throw new ApexError("APEX_STALE", "Input request is being recorded by another writer", EXIT_CODES.stale);
|
|
662
|
+
}
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
if (!Value.Check(InputSubmissionV1Schema, input)) {
|
|
666
|
+
throw new ApexError("APEX_VALIDATION", "Input submission is malformed", EXIT_CODES.validation);
|
|
667
|
+
}
|
|
668
|
+
const events = await this.journal(run).replay();
|
|
669
|
+
const requested = [...events]
|
|
670
|
+
.reverse()
|
|
671
|
+
.find((event) => event.type === "requirements.input-requested" || event.type === "architecture.decision-requested");
|
|
672
|
+
if (requested === undefined) {
|
|
673
|
+
throw new ApexError("APEX_CONFLICT", "No input request is pending", EXIT_CODES.conflict);
|
|
674
|
+
}
|
|
675
|
+
const request = requested.type === "requirements.input-requested"
|
|
676
|
+
? this.inputRequest(requested, run.ownerEpoch)
|
|
677
|
+
: this.architectureDecisionRequest(requested, run.ownerEpoch);
|
|
678
|
+
const recordedType = requested.type === "requirements.input-requested"
|
|
679
|
+
? "requirements.input-recorded"
|
|
680
|
+
: "architecture.decision-recorded";
|
|
681
|
+
if (events.some((event) => event.type === recordedType && event.payload.requestId === request.requestId)) {
|
|
682
|
+
throw new ApexError("APEX_CONFLICT", "Input request was already recorded", EXIT_CODES.conflict);
|
|
683
|
+
}
|
|
684
|
+
if (input.requestId !== request.requestId) {
|
|
685
|
+
throw new ApexError("APEX_CONFLICT", "Input request ID does not match the pending request", EXIT_CODES.conflict);
|
|
686
|
+
}
|
|
687
|
+
if (input.expectedHead !== request.expectedHead || events.at(-1)?.hash !== request.expectedHead) {
|
|
688
|
+
throw new ApexError("APEX_STALE", "Input request journal head is stale", EXIT_CODES.stale);
|
|
689
|
+
}
|
|
690
|
+
if (input.ownerEpoch !== request.ownerEpoch || run.ownerEpoch !== request.ownerEpoch) {
|
|
691
|
+
throw new ApexError("APEX_STALE", "Input request owner epoch is stale", EXIT_CODES.stale);
|
|
692
|
+
}
|
|
693
|
+
let normalizedAnswers;
|
|
694
|
+
try {
|
|
695
|
+
normalizedAnswers = validateInputAnswers(request.questions, input.answers);
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
throw new ApexError("APEX_VALIDATION", error instanceof Error ? error.message : "Input answers are invalid", EXIT_CODES.validation);
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
await this.append(run, recordedType, {
|
|
702
|
+
requestId: request.requestId,
|
|
703
|
+
...("intake" in request ? { intake: request.intake } : { decision: request.decision }),
|
|
704
|
+
answers: normalizedAnswers,
|
|
705
|
+
}, request.expectedHead);
|
|
706
|
+
if (request.decision !== undefined)
|
|
707
|
+
await this.refreshTaskHead(run, await this.readTask(run, request.decision.taskId));
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
if (error instanceof Error && /stale journal head|mutation is already in progress/i.test(error.message)) {
|
|
711
|
+
throw new ApexError("APEX_STALE", "Input request journal head is stale", EXIT_CODES.stale, undefined, error);
|
|
712
|
+
}
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
return { recorded: true, requestId: request.requestId };
|
|
716
|
+
}
|
|
717
|
+
async recordRequirementsInput(value) {
|
|
718
|
+
const pending = await this.nextTask();
|
|
719
|
+
if (pending.status !== "needs_input") {
|
|
720
|
+
throw new ApexError("APEX_CONFLICT", "No requirements input request is pending", EXIT_CODES.conflict);
|
|
721
|
+
}
|
|
722
|
+
const source = value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
723
|
+
const missing = pending.request.questions.filter(({ id }) => typeof source[id] !== "string" || source[id].length === 0);
|
|
724
|
+
if (missing.length > 0) {
|
|
725
|
+
throw new ApexError("APEX_VALIDATION", `Missing requirements input: ${missing.map(({ id }) => id).join(", ")}`, EXIT_CODES.validation);
|
|
726
|
+
}
|
|
727
|
+
await this.recordInput({
|
|
728
|
+
schemaVersion: CONTRACT_VERSION,
|
|
729
|
+
requestId: pending.request.requestId,
|
|
730
|
+
expectedHead: pending.request.expectedHead,
|
|
731
|
+
ownerEpoch: pending.request.ownerEpoch,
|
|
732
|
+
answers: pending.request.questions.map(({ id }) => ({
|
|
733
|
+
questionId: id,
|
|
734
|
+
value: source[id],
|
|
735
|
+
})),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
async taskContext(taskId) {
|
|
739
|
+
const run = await this.currentRun();
|
|
740
|
+
const task = await this.readTask(run, taskId);
|
|
741
|
+
const events = await this.journal(run).replay();
|
|
742
|
+
const route = await this.route(run, events);
|
|
743
|
+
const outputTemplates = {};
|
|
744
|
+
if (task.taskType === "requirements" || task.taskType === "plan") {
|
|
745
|
+
for (const kind of task.allowedOutputKinds) {
|
|
746
|
+
if (!SUPPORTED_ARTIFACT_KINDS.includes(kind)) {
|
|
747
|
+
throw new ApexError("APEX_INTERNAL", `Unsupported task output kind: ${kind}`, EXIT_CODES.internal);
|
|
748
|
+
}
|
|
749
|
+
outputTemplates[kind] = this.outputTemplate(kind, run, events);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const artifactHashes = this.acceptedArtifactHashes(events);
|
|
753
|
+
return {
|
|
754
|
+
task,
|
|
755
|
+
inputs: await Promise.all(task.inputRefs.map((hash) => this.objects.getJson(hash))),
|
|
756
|
+
artifactHashes,
|
|
757
|
+
recordedInput: task.taskType === "requirements" ? this.recordedRequirementsInput(events) : null,
|
|
758
|
+
decisions: task.taskType === "architecture" ? this.architectureDecisionValues(events, task.taskId) : {},
|
|
759
|
+
outputTemplates,
|
|
760
|
+
outputRoot: join(this.root, ".apex", "work", run.runId, taskId),
|
|
761
|
+
status: this.completedNodeIds(events).has(task.taskType) ? "completed" : "active",
|
|
762
|
+
blockers: route.blockers,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
async stageArtifact(taskId, output) {
|
|
766
|
+
const run = await this.currentRun();
|
|
767
|
+
const task = await this.readTask(run, taskId);
|
|
768
|
+
const head = await this.journal(run).head();
|
|
769
|
+
if (head === null)
|
|
770
|
+
throw new ApexError("APEX_STALE", "Task journal is empty", EXIT_CODES.stale);
|
|
771
|
+
assertTaskCurrent(task, head, run.ownerEpoch, this.clock);
|
|
772
|
+
if (!task.allowedOutputKinds.includes(output.kind)) {
|
|
773
|
+
throw new ApexError("APEX_VALIDATION", `Task does not allow ${output.kind}`, EXIT_CODES.validation);
|
|
774
|
+
}
|
|
775
|
+
this.validateOutput(run, output);
|
|
776
|
+
const bytes = Buffer.from(JSON.stringify(output.value), "utf8");
|
|
777
|
+
if (bytes.byteLength > task.maxOutputBytes) {
|
|
778
|
+
throw new ApexError("APEX_VALIDATION", "Task output exceeds its size limit", EXIT_CODES.validation);
|
|
779
|
+
}
|
|
780
|
+
const directory = join(this.root, ".apex", "work", run.runId, taskId);
|
|
781
|
+
const path = join(directory, `${output.kind}.json`);
|
|
782
|
+
await mkdir(directory, { recursive: true });
|
|
783
|
+
await atomicWriteJson(path, output.value);
|
|
784
|
+
const hash = sha256Bytes(await readFile(path));
|
|
785
|
+
await this.append(run, "artifact.staged", { taskId, kind: output.kind, hash, bytes: bytes.byteLength });
|
|
786
|
+
const expectedHead = await this.journal(run).head();
|
|
787
|
+
if (expectedHead === null)
|
|
788
|
+
throw new ApexError("APEX_STALE", "Staging event was not recorded", EXIT_CODES.stale);
|
|
789
|
+
await atomicWriteJson(join(this.projects.runDirectory(run.projectId, run.runId), "tasks", `${taskId}.json`), {
|
|
790
|
+
...task,
|
|
791
|
+
expectedHead,
|
|
792
|
+
});
|
|
793
|
+
return { taskId, kind: output.kind, path, bytes: bytes.byteLength, hash };
|
|
794
|
+
}
|
|
795
|
+
async stageFile(taskId, relativePath, content, expectedSha) {
|
|
796
|
+
const run = await this.currentRun();
|
|
797
|
+
const task = await this.readTask(run, taskId);
|
|
798
|
+
const head = await this.journal(run).head();
|
|
799
|
+
if (head === null)
|
|
800
|
+
throw new ApexError("APEX_STALE", "Task journal is empty", EXIT_CODES.stale);
|
|
801
|
+
assertTaskCurrent(task, head, run.ownerEpoch, this.clock);
|
|
802
|
+
if (!task.taskType.startsWith("codegen-"))
|
|
803
|
+
throw new ApexError("APEX_AUTHORIZATION", "Only code generation tasks may stage files", EXIT_CODES.authorization);
|
|
804
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
805
|
+
const suffixes = [".terraform.lock.hcl", ".tfvars.example", ".bicep", ".json", ".tf", ".md"];
|
|
806
|
+
if (isAbsolute(relativePath) ||
|
|
807
|
+
normalized.length === 0 ||
|
|
808
|
+
normalized.split("/").some((part) => part === "" || part === "." || part === "..") ||
|
|
809
|
+
!suffixes.some((suffix) => normalized.endsWith(suffix))) {
|
|
810
|
+
throw new ApexError("APEX_VALIDATION", `Unsafe or unsupported staged path: ${relativePath}`, EXIT_CODES.validation);
|
|
811
|
+
}
|
|
812
|
+
const codeRoot = resolve(this.root, ".apex", "work", run.runId, taskId, "code");
|
|
813
|
+
const path = resolve(codeRoot, normalized);
|
|
814
|
+
if (path !== codeRoot && !path.startsWith(`${codeRoot}${sep}`))
|
|
815
|
+
throw new ApexError("APEX_VALIDATION", "Staged path escapes the code root", EXIT_CODES.validation);
|
|
816
|
+
await this.assertNoSymlinkPath(codeRoot, path);
|
|
817
|
+
const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
|
|
818
|
+
const hash = sha256Bytes(bytes);
|
|
819
|
+
if (expectedSha !== undefined && expectedSha !== hash)
|
|
820
|
+
throw new ApexError("APEX_STALE", "Staged content hash does not match expected SHA", EXIT_CODES.stale);
|
|
821
|
+
let idempotent = false;
|
|
822
|
+
if (await this.exists(path)) {
|
|
823
|
+
const entry = await lstat(path);
|
|
824
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
825
|
+
throw new ApexError("APEX_VALIDATION", "Staged destination must be a regular file", EXIT_CODES.validation);
|
|
826
|
+
if (sha256Bytes(await readFile(path)) !== hash)
|
|
827
|
+
throw new ApexError("APEX_CONFLICT", `Refusing to overwrite staged file: ${normalized}`, EXIT_CODES.conflict);
|
|
828
|
+
idempotent = true;
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
const used = await this.directoryBytes(codeRoot);
|
|
832
|
+
if (used + bytes.byteLength > task.maxOutputBytes)
|
|
833
|
+
throw new ApexError("APEX_VALIDATION", "Task output exceeds its cumulative size limit", EXIT_CODES.validation);
|
|
834
|
+
await atomicWriteBytes(path, bytes, { refuseOverwrite: true });
|
|
835
|
+
await this.append(run, "file.staged", { taskId, path: normalized, hash, bytes: bytes.byteLength });
|
|
836
|
+
await this.refreshTaskHead(run, task);
|
|
837
|
+
}
|
|
838
|
+
return { taskId, path, bytes: bytes.byteLength, hash, idempotent };
|
|
839
|
+
}
|
|
840
|
+
async generateIac(taskId, options = {}) {
|
|
841
|
+
const run = await this.currentRun();
|
|
842
|
+
const task = await this.readTask(run, taskId);
|
|
843
|
+
if (task.taskType !== `codegen-${run.iacTool}`)
|
|
844
|
+
throw new ApexError("APEX_AUTHORIZATION", "Task is not the selected code generation task", EXIT_CODES.authorization);
|
|
845
|
+
const inputs = await Promise.all(task.inputRefs.map((hash) => this.objects.getJson(hash)));
|
|
846
|
+
const intent = inputs.find((value) => this.looksLikeIntent(value));
|
|
847
|
+
const binding = inputs.find((value) => this.looksLikeBinding(value, run.iacTool));
|
|
848
|
+
const environmentInputs = inputs.find((value) => this.looksLikeEnvironmentInputs(value));
|
|
849
|
+
if (intent === undefined || binding === undefined || environmentInputs === undefined) {
|
|
850
|
+
throw new ApexError("APEX_VALIDATION", "Code generation requires accepted intent, binding, and environment inputs", EXIT_CODES.validation);
|
|
851
|
+
}
|
|
852
|
+
const tree = run.iacTool === "bicep"
|
|
853
|
+
? generateBicepTree(intent, binding, {
|
|
854
|
+
...(options.existingResources === undefined ? {} : { existingResources: options.existingResources }),
|
|
855
|
+
})
|
|
856
|
+
: generateTerraformTree(intent, binding, {
|
|
857
|
+
...(options.existingResources === undefined ? {} : { existingResources: options.existingResources }),
|
|
858
|
+
...(options.azurermProviderConstraint === undefined
|
|
859
|
+
? {}
|
|
860
|
+
: { azurermProviderConstraint: options.azurermProviderConstraint }),
|
|
861
|
+
...(options.azapiProviderConstraint === undefined
|
|
862
|
+
? {}
|
|
863
|
+
: { azapiProviderConstraint: options.azapiProviderConstraint }),
|
|
864
|
+
...(options.lockFileContent === undefined ? {} : { lockFileContent: options.lockFileContent }),
|
|
865
|
+
});
|
|
866
|
+
const files = [];
|
|
867
|
+
for (const file of tree.files)
|
|
868
|
+
files.push(await this.stageFile(taskId, file.path, file.content));
|
|
869
|
+
const intentHash = sha256Json(intent);
|
|
870
|
+
const bindingHash = sha256Json(binding);
|
|
871
|
+
const environmentInputsHash = sha256Json(environmentInputs);
|
|
872
|
+
const handoff = {
|
|
873
|
+
schemaVersion: CONTRACT_VERSION,
|
|
874
|
+
projectId: run.projectId,
|
|
875
|
+
runId: run.runId,
|
|
876
|
+
track: run.iacTool,
|
|
877
|
+
rootPath: relative(this.root, resolve(this.root, ".apex", "work", run.runId, taskId, "code")),
|
|
878
|
+
treeHash: tree.treeHash,
|
|
879
|
+
intentHash,
|
|
880
|
+
bindingHash,
|
|
881
|
+
environmentInputsHash,
|
|
882
|
+
logicalResourceManifestHash: sha256Json(tree.logicalManifest),
|
|
883
|
+
requiredToolVersions: options.requiredToolVersions ?? { [run.iacTool]: "system" },
|
|
884
|
+
generatedAt: this.clock().toISOString(),
|
|
885
|
+
};
|
|
886
|
+
const completed = await this.completeTaskOutputs(taskId, [
|
|
887
|
+
{ kind: "logical-resource-manifest", value: tree.logicalManifest },
|
|
888
|
+
{ kind: "iac-handoff", value: handoff },
|
|
889
|
+
]);
|
|
890
|
+
return { files, outputHashes: completed.outputHashes, treeHash: tree.treeHash };
|
|
891
|
+
}
|
|
892
|
+
async validateTask(taskId, output) {
|
|
893
|
+
const run = await this.currentRun();
|
|
894
|
+
const task = await this.readTask(run, taskId);
|
|
895
|
+
const head = await this.journal(run).head();
|
|
896
|
+
if (head === null)
|
|
897
|
+
throw new ApexError("APEX_STALE", "Task journal is empty", EXIT_CODES.stale);
|
|
898
|
+
assertTaskCurrent(task, head, run.ownerEpoch, this.clock);
|
|
899
|
+
const staged = output === undefined
|
|
900
|
+
? undefined
|
|
901
|
+
: Array.isArray(output)
|
|
902
|
+
? await Promise.all(output.map((item) => this.stageArtifact(taskId, item)))
|
|
903
|
+
: await this.stageArtifact(taskId, output);
|
|
904
|
+
return { valid: true, taskId, ...(staged === undefined ? {} : { staged }) };
|
|
905
|
+
}
|
|
906
|
+
async completeTask(taskId, output) {
|
|
907
|
+
const completed = await this.acceptTaskOutputs(taskId, [output], true);
|
|
908
|
+
return { outputHash: completed.outputHashes[output.kind], summary: output.summary ?? `${output.kind} accepted` };
|
|
909
|
+
}
|
|
910
|
+
async completeTaskOutputs(taskId, outputs) {
|
|
911
|
+
return this.acceptTaskOutputs(taskId, outputs, false);
|
|
912
|
+
}
|
|
913
|
+
async acceptTaskOutputs(taskId, outputs, legacy) {
|
|
914
|
+
const run = await this.currentRun();
|
|
915
|
+
const task = await this.readTask(run, taskId);
|
|
916
|
+
const events = await this.journal(run).replay();
|
|
917
|
+
const head = events.at(-1)?.hash ?? null;
|
|
918
|
+
if (head === null)
|
|
919
|
+
throw new ApexError("APEX_STALE", "Task journal is empty", EXIT_CODES.stale);
|
|
920
|
+
assertTaskCurrent(task, head, run.ownerEpoch, this.clock);
|
|
921
|
+
if (outputs.length === 0)
|
|
922
|
+
throw new ApexError("APEX_VALIDATION", "Task bundle must contain outputs", EXIT_CODES.validation);
|
|
923
|
+
const kinds = outputs.map(({ kind }) => kind);
|
|
924
|
+
if (new Set(kinds).size !== kinds.length)
|
|
925
|
+
throw new ApexError("APEX_VALIDATION", "Task bundle contains duplicate kinds", EXIT_CODES.validation);
|
|
926
|
+
const descriptor = TASKS.find(({ id }) => id === task.taskType);
|
|
927
|
+
if (descriptor === undefined)
|
|
928
|
+
throw new ApexError("APEX_VALIDATION", `Unknown task type ${task.taskType}`, EXIT_CODES.validation);
|
|
929
|
+
const missing = descriptor.outputs.filter((kind) => !kinds.includes(kind));
|
|
930
|
+
if (missing.length > 0) {
|
|
931
|
+
if (legacy && descriptor.id === "plan" && kinds.length === 1 && kinds[0] === "implementation-intent") {
|
|
932
|
+
outputs = [...outputs, ...this.legacyPlanOutputs(run, outputs[0].value)];
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
throw new ApexError("APEX_VALIDATION", `Task bundle is missing: ${missing.join(", ")}`, EXIT_CODES.validation);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (const output of outputs) {
|
|
939
|
+
if (!task.allowedOutputKinds.includes(output.kind))
|
|
940
|
+
throw new ApexError("APEX_VALIDATION", `Task does not allow ${output.kind}`, EXIT_CODES.validation);
|
|
941
|
+
const bytes = Buffer.byteLength(JSON.stringify(output.value));
|
|
942
|
+
if (bytes > task.maxOutputBytes)
|
|
943
|
+
throw new ApexError("APEX_VALIDATION", "Task output exceeds its size limit", EXIT_CODES.validation);
|
|
944
|
+
this.validateOutput(run, output);
|
|
945
|
+
}
|
|
946
|
+
await this.validateBundle(run, descriptor, outputs, events);
|
|
947
|
+
const validation = await this.validateTaskValidators(run, task, descriptor, outputs, events);
|
|
948
|
+
const completionHead = await this.journal(run).head();
|
|
949
|
+
if (completionHead === null)
|
|
950
|
+
throw new ApexError("APEX_STALE", "Task journal is empty", EXIT_CODES.stale);
|
|
951
|
+
assertTaskCurrent(task, completionHead, run.ownerEpoch, this.clock);
|
|
952
|
+
const outputHashes = {};
|
|
953
|
+
for (const output of outputs)
|
|
954
|
+
outputHashes[output.kind] = await this.objects.putJson(output.value);
|
|
955
|
+
const requirementsOutput = outputs.find(({ kind }) => kind === "requirements");
|
|
956
|
+
const requirementsHash = outputHashes.requirements;
|
|
957
|
+
const renderedDocument = descriptor.id === "requirements"
|
|
958
|
+
? requirementsOutput === undefined || requirementsHash === undefined
|
|
959
|
+
? (() => {
|
|
960
|
+
throw new ApexError("APEX_INTERNAL", "Requirements completion is missing its accepted output", EXIT_CODES.internal);
|
|
961
|
+
})()
|
|
962
|
+
: await this.renderRequirementsDocument(requirementsOutput.value, requirementsHash)
|
|
963
|
+
: undefined;
|
|
964
|
+
const reviewBlockers = descriptor.reviewSubject === undefined ? [] : this.openReviewFindings(outputs[0].value);
|
|
965
|
+
const dependencyHash = sha256Json(outputHashes);
|
|
966
|
+
await this.append(run, "task.completed", {
|
|
967
|
+
taskId,
|
|
968
|
+
nodeId: descriptor.id,
|
|
969
|
+
artifactHashes: outputHashes,
|
|
970
|
+
...(renderedDocument === undefined ? {} : { renderedDocuments: [renderedDocument] }),
|
|
971
|
+
validatorIds: validation.validatorIds,
|
|
972
|
+
...(Object.keys(validation.evidenceRefs).length === 0 ? {} : { validatorEvidenceRefs: validation.evidenceRefs }),
|
|
973
|
+
...(Object.keys(validation.evidenceModes).length === 0
|
|
974
|
+
? {}
|
|
975
|
+
: { validatorEvidenceModes: validation.evidenceModes }),
|
|
976
|
+
reviewBlockers,
|
|
977
|
+
...(descriptor.reviewSubject === undefined
|
|
978
|
+
? {}
|
|
979
|
+
: {
|
|
980
|
+
reviewHash: outputHashes["review-findings"],
|
|
981
|
+
subjectHash: outputs[0].value.subjectHash,
|
|
982
|
+
dependencyHash,
|
|
983
|
+
}),
|
|
984
|
+
legacy,
|
|
985
|
+
});
|
|
986
|
+
if (descriptor.reviewSubject !== undefined) {
|
|
987
|
+
if (reviewBlockers.length === 0 && descriptor.gate !== undefined) {
|
|
988
|
+
await this.openRunGate(await this.currentRun(), descriptor.gate, dependencyHash);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
else if (legacy && descriptor.id === "requirements") {
|
|
992
|
+
await this.openRunGate(await this.currentRun(), 1, sha256Json(outputHashes));
|
|
993
|
+
}
|
|
994
|
+
return { outputHashes, summary: outputs.map(({ kind }) => kind).join(", ") + " accepted" };
|
|
995
|
+
}
|
|
996
|
+
async renderRequirementsDocument(value, artifactHash) {
|
|
997
|
+
const { content, templateHash } = await this.requirementsDocumentMarkdown(value, artifactHash);
|
|
998
|
+
return {
|
|
999
|
+
documentId: "requirements",
|
|
1000
|
+
templateHash,
|
|
1001
|
+
// State transfer exports immutable objects as canonical JSON.
|
|
1002
|
+
outputHash: await this.objects.putJson({ contentType: "text/markdown", content }),
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
async requirementsDocumentMarkdown(value, artifactHash) {
|
|
1006
|
+
if (!Value.Check(RequirementsV1Schema, value)) {
|
|
1007
|
+
throw new ApexError("APEX_INTERNAL", "Requirements document source is invalid", EXIT_CODES.internal);
|
|
1008
|
+
}
|
|
1009
|
+
const { content: template, hash: templateHash } = await this.requirementsDocumentTemplateContent();
|
|
1010
|
+
let document;
|
|
1011
|
+
try {
|
|
1012
|
+
document = renderRequirementsDocument(value, template, artifactHash, templateHash);
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
throw new ApexError("APEX_INTERNAL", error instanceof Error ? error.message : "Requirements document renderer failed", EXIT_CODES.internal);
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
content: document,
|
|
1019
|
+
templateHash,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
async requirementsDocumentTemplateContent() {
|
|
1023
|
+
this.requirementsDocumentTemplate ??= (async () => {
|
|
1024
|
+
const assets = await resolveBundledAssets();
|
|
1025
|
+
const templateDefinition = DOCUMENT_REGISTRY.requirements?.template;
|
|
1026
|
+
if (templateDefinition === undefined) {
|
|
1027
|
+
throw new ApexError("APEX_INTERNAL", "Requirements template is unregistered", EXIT_CODES.internal);
|
|
1028
|
+
}
|
|
1029
|
+
const content = await readFile(join(assets.customizations, templateDefinition.assetPath), "utf8");
|
|
1030
|
+
return { content, hash: sha256Bytes(Buffer.from(content, "utf8")) };
|
|
1031
|
+
})();
|
|
1032
|
+
return this.requirementsDocumentTemplate;
|
|
1033
|
+
}
|
|
1034
|
+
async cancelTask(taskId) {
|
|
1035
|
+
const run = await this.currentRun();
|
|
1036
|
+
await this.readTask(run, taskId);
|
|
1037
|
+
await this.append(run, "task.cancelled", { taskId });
|
|
1038
|
+
}
|
|
1039
|
+
async resolveReview(resolution) {
|
|
1040
|
+
const run = await this.currentRun();
|
|
1041
|
+
const events = await this.journal(run).replay();
|
|
1042
|
+
const reviewEvent = [...events]
|
|
1043
|
+
.reverse()
|
|
1044
|
+
.find((event) => event.type === "task.completed" &&
|
|
1045
|
+
event.payload.reviewHash === resolution.reviewHash);
|
|
1046
|
+
if (reviewEvent === undefined)
|
|
1047
|
+
throw new ApexError("APEX_STALE", "Resolution reviewHash is not the current accepted review", EXIT_CODES.stale);
|
|
1048
|
+
const reviewPayload = reviewEvent.payload;
|
|
1049
|
+
if (reviewPayload.subjectHash !== resolution.subjectHash ||
|
|
1050
|
+
reviewPayload.dependencyHash !== resolution.dependencyHash) {
|
|
1051
|
+
throw new ApexError("APEX_STALE", "Resolution is not bound to the accepted review dependency", EXIT_CODES.stale);
|
|
1052
|
+
}
|
|
1053
|
+
this.assertReviewResolution(resolution);
|
|
1054
|
+
const review = await this.objects.getJson(resolution.reviewHash);
|
|
1055
|
+
const finding = review.findings.find(({ id }) => id === resolution.findingId);
|
|
1056
|
+
if (finding === undefined || finding.disposition !== "open")
|
|
1057
|
+
throw new ApexError("APEX_VALIDATION", "Resolution finding does not exist and remain open", EXIT_CODES.validation);
|
|
1058
|
+
if (resolution.disposition === "accepted-risk" && ["critical", "high"].includes(finding.severity))
|
|
1059
|
+
throw new ApexError("APEX_AUTHORIZATION", `${finding.severity} findings cannot be accepted as risk by default policy`, EXIT_CODES.authorization);
|
|
1060
|
+
const priorResolutions = events.flatMap((event) => {
|
|
1061
|
+
if (event.type !== "review.resolved")
|
|
1062
|
+
return [];
|
|
1063
|
+
const prior = event.payload.resolution;
|
|
1064
|
+
return prior?.reviewHash === resolution.reviewHash && prior.findingId === resolution.findingId ? [prior] : [];
|
|
1065
|
+
});
|
|
1066
|
+
if (priorResolutions.some((prior) => sha256Json(prior) === sha256Json(resolution)))
|
|
1067
|
+
return;
|
|
1068
|
+
const hasCurrentResolution = priorResolutions.some((prior) => prior.disposition === "fixed" ||
|
|
1069
|
+
(prior.expiresAt !== undefined && Date.parse(prior.expiresAt) > this.clock().getTime()));
|
|
1070
|
+
if (hasCurrentResolution)
|
|
1071
|
+
throw new ApexError("APEX_CONFLICT", "Finding already has a conflicting resolution", EXIT_CODES.conflict);
|
|
1072
|
+
await this.append(run, "review.resolved", { resolution: resolution });
|
|
1073
|
+
const updatedEvents = await this.journal(run).replay();
|
|
1074
|
+
const nodeId = reviewPayload.nodeId;
|
|
1075
|
+
const descriptor = typeof nodeId === "string" ? TASKS.find(({ id }) => id === nodeId) : undefined;
|
|
1076
|
+
if (descriptor?.gate !== undefined && this.reviewBlockers(updatedEvents, descriptor.id).length === 0) {
|
|
1077
|
+
const current = await this.currentRun();
|
|
1078
|
+
const gateState = current.gates.find(({ gate }) => gate === descriptor.gate)?.state;
|
|
1079
|
+
if (gateState === "closed" || gateState === "invalidated") {
|
|
1080
|
+
await this.openRunGate(current, descriptor.gate, resolution.dependencyHash);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
assertReviewResolution(value) {
|
|
1085
|
+
const hashes = [value.reviewHash, value.subjectHash, value.dependencyHash, ...value.evidenceRefs];
|
|
1086
|
+
if (!value.findingId ||
|
|
1087
|
+
!value.actor ||
|
|
1088
|
+
!value.rationale ||
|
|
1089
|
+
!["fixed", "accepted-risk"].includes(value.disposition) ||
|
|
1090
|
+
!hashes.every((hash) => /^[0-9a-f]{64}$/.test(hash))) {
|
|
1091
|
+
throw new ApexError("APEX_VALIDATION", "Invalid review resolution document", EXIT_CODES.validation);
|
|
1092
|
+
}
|
|
1093
|
+
if (value.disposition === "fixed" && value.evidenceRefs.length === 0)
|
|
1094
|
+
throw new ApexError("APEX_VALIDATION", "Fixed findings require evidenceRefs", EXIT_CODES.validation);
|
|
1095
|
+
if (value.disposition === "accepted-risk" &&
|
|
1096
|
+
(value.expiresAt === undefined || Date.parse(value.expiresAt) <= this.clock().getTime())) {
|
|
1097
|
+
throw new ApexError("APEX_VALIDATION", "Accepted risk requires a future expiry", EXIT_CODES.validation);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async decideGateNumber(gateNumber, decision, actor, options = {}) {
|
|
1101
|
+
const run = await this.currentRun();
|
|
1102
|
+
const gate = run.gates.find(({ gate }) => gate === gateNumber);
|
|
1103
|
+
if (gate === undefined)
|
|
1104
|
+
throw new ApexError("APEX_USAGE", `Unknown gate ${gateNumber}`, EXIT_CODES.usage);
|
|
1105
|
+
const events = await this.journal(run).replay();
|
|
1106
|
+
const previewHash = gateNumber === 4 ? this.latestPayloadHash(events, "preview.created", "previewHash") : undefined;
|
|
1107
|
+
if (gateNumber === 4 && previewHash === undefined) {
|
|
1108
|
+
throw new ApexError("APEX_VALIDATION", "Gate 4 requires a deployment preview", EXIT_CODES.validation);
|
|
1109
|
+
}
|
|
1110
|
+
if (options.recipientIdentity !== undefined && gateNumber !== 4) {
|
|
1111
|
+
throw new ApexError("APEX_VALIDATION", "An explicit approval recipient is limited to TTY Gate 4 decisions", EXIT_CODES.validation);
|
|
1112
|
+
}
|
|
1113
|
+
if (options.recipientIdentity !== undefined && options.recipientIdentity.trim().length === 0) {
|
|
1114
|
+
throw new ApexError("APEX_VALIDATION", "Approval recipient must be nonempty", EXIT_CODES.validation);
|
|
1115
|
+
}
|
|
1116
|
+
const previewObjectHash = gateNumber === 4 ? this.latestPayloadHash(events, "preview.created", "previewObjectHash") : undefined;
|
|
1117
|
+
if (gateNumber === 4 && previewObjectHash === undefined) {
|
|
1118
|
+
throw new ApexError("APEX_VALIDATION", "Gate 4 preview evidence is incomplete", EXIT_CODES.validation);
|
|
1119
|
+
}
|
|
1120
|
+
const preview = previewObjectHash === undefined ? undefined : await this.objects.getJson(previewObjectHash);
|
|
1121
|
+
if (preview !== undefined && Date.parse(preview.expiresAt) <= this.clock().getTime()) {
|
|
1122
|
+
throw new ApexError("APEX_STALE", "Deployment preview has expired", EXIT_CODES.stale);
|
|
1123
|
+
}
|
|
1124
|
+
const transferStore = new WriterTransferStore(this.projects.runDirectory(run.projectId, run.runId), this.clock);
|
|
1125
|
+
if (gateNumber === 4)
|
|
1126
|
+
await this.assertCurrentWriterAuthority(run, transferStore);
|
|
1127
|
+
const writerLease = gateNumber === 4 ? await transferStore.leaseStore().current() : null;
|
|
1128
|
+
const writerTransferClaimHash = preview === undefined || preview.ownerEpoch === run.ownerEpoch
|
|
1129
|
+
? undefined
|
|
1130
|
+
: await transferStore.proveOneHopPostPreviewLineage(preview.previewHash, preview.ownerEpoch);
|
|
1131
|
+
if (preview !== undefined && preview.ownerEpoch !== run.ownerEpoch && writerTransferClaimHash === null) {
|
|
1132
|
+
throw new ApexError("APEX_STALE", "Preview writer transfer lineage is invalid", EXIT_CODES.stale);
|
|
1133
|
+
}
|
|
1134
|
+
const currentRecipientIdentity = await this.currentRecipientIdentity(run);
|
|
1135
|
+
const approvalRecipientIdentity = options.recipientIdentity ?? currentRecipientIdentity;
|
|
1136
|
+
const decidedAt = this.clock().toISOString();
|
|
1137
|
+
const approvalExpiresAt = preview === undefined
|
|
1138
|
+
? undefined
|
|
1139
|
+
: new Date(Math.min(Date.parse(preview.expiresAt), writerLease === null ? Number.POSITIVE_INFINITY : Date.parse(writerLease.expiresAt))).toISOString();
|
|
1140
|
+
const commonApproval = {
|
|
1141
|
+
schemaVersion: CONTRACT_VERSION,
|
|
1142
|
+
projectId: run.projectId,
|
|
1143
|
+
runId: run.runId,
|
|
1144
|
+
gate: gateNumber,
|
|
1145
|
+
decision,
|
|
1146
|
+
actor,
|
|
1147
|
+
dependencyHash: gate.dependencyHash,
|
|
1148
|
+
...(previewHash === undefined ? {} : { previewHash }),
|
|
1149
|
+
...(writerTransferClaimHash === undefined || writerTransferClaimHash === null ? {} : { writerTransferClaimHash }),
|
|
1150
|
+
writerEpoch: run.ownerEpoch,
|
|
1151
|
+
decidedAt,
|
|
1152
|
+
...(approvalExpiresAt === undefined ? {} : { expiresAt: approvalExpiresAt }),
|
|
1153
|
+
};
|
|
1154
|
+
const approval = {
|
|
1155
|
+
...commonApproval,
|
|
1156
|
+
mechanism: "tty",
|
|
1157
|
+
recipientIdentity: approvalRecipientIdentity,
|
|
1158
|
+
};
|
|
1159
|
+
this.assertValid("approval", approval);
|
|
1160
|
+
const validatorIds = decision === "approved"
|
|
1161
|
+
? await this.validateGateValidators(run, gate, events, approval, approvalRecipientIdentity)
|
|
1162
|
+
: [];
|
|
1163
|
+
if (gateNumber === 4) {
|
|
1164
|
+
await this.assertCurrentWriterAuthority(run, transferStore);
|
|
1165
|
+
if (approval.expiresAt === undefined || Date.parse(approval.expiresAt) <= this.clock().getTime()) {
|
|
1166
|
+
throw new ApexError("APEX_STALE", "Approval authority expired before it could be recorded", EXIT_CODES.stale);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const approvalHash = await this.objects.putJson(approval);
|
|
1170
|
+
const updated = {
|
|
1171
|
+
...run,
|
|
1172
|
+
gates: run.gates.map((item) => (item.gate === gateNumber ? decideGate(item, decision, decidedAt) : item)),
|
|
1173
|
+
};
|
|
1174
|
+
await this.mutateRun(run, updated, "gate.decided", {
|
|
1175
|
+
gate: gateNumber,
|
|
1176
|
+
approvalHash,
|
|
1177
|
+
...(previewHash === undefined ? {} : { previewHash }),
|
|
1178
|
+
...(validatorIds.length === 0 ? {} : { validatorIds }),
|
|
1179
|
+
}, gateNumber === 4 ? (events.at(-1)?.hash ?? null) : undefined);
|
|
1180
|
+
return approval;
|
|
1181
|
+
}
|
|
1182
|
+
async preview(options) {
|
|
1183
|
+
const run = await this.currentRun();
|
|
1184
|
+
await this.assertCurrentWriterAuthority(run, new WriterTransferStore(this.projects.runDirectory(run.projectId, run.runId), this.clock));
|
|
1185
|
+
const events = await this.journal(run).replay();
|
|
1186
|
+
await this.assertPreviewReady(run, events);
|
|
1187
|
+
const dependencyRevision = this.dependencyRevision(run, events);
|
|
1188
|
+
const intendedExecutionRecipientIdentity = options.recipientIdentity ?? (await this.currentRecipientIdentity(run));
|
|
1189
|
+
const intentHash = this.artifactHash(events, "implementation-intent");
|
|
1190
|
+
if (intentHash === undefined)
|
|
1191
|
+
throw new ApexError("APEX_VALIDATION", "Implementation intent is required", EXIT_CODES.validation);
|
|
1192
|
+
const intent = await this.objects.getJson(intentHash);
|
|
1193
|
+
if (options.provider !== "fake") {
|
|
1194
|
+
if (options.provider !== run.iacTool) {
|
|
1195
|
+
throw new ApexError("APEX_VALIDATION", `Provider '${options.provider}' does not match selected track '${run.iacTool}'`, EXIT_CODES.validation);
|
|
1196
|
+
}
|
|
1197
|
+
const provider = this.providers[options.provider];
|
|
1198
|
+
if (provider === undefined) {
|
|
1199
|
+
throw new ApexError("APEX_VALIDATION", `${options.provider} provider is not configured; inject it through ApexService providers or configure the CLI provider registry`, EXIT_CODES.validation);
|
|
1200
|
+
}
|
|
1201
|
+
const request = {
|
|
1202
|
+
projectId: run.projectId,
|
|
1203
|
+
runId: run.runId,
|
|
1204
|
+
environment: run.environment,
|
|
1205
|
+
target: run.targetScope,
|
|
1206
|
+
commit: dependencyRevision,
|
|
1207
|
+
dependencyRevision,
|
|
1208
|
+
ownerEpoch: run.ownerEpoch,
|
|
1209
|
+
executionRecipientIdentity: intendedExecutionRecipientIdentity,
|
|
1210
|
+
inputHash: intentHash,
|
|
1211
|
+
iacHash: this.artifactHash(events, "iac-handoff") ?? sha256Json(intent.resources),
|
|
1212
|
+
policyHash: this.artifactHash(events, "policy-property-map") ?? run.runtimeLockHash,
|
|
1213
|
+
resources: intent.resources.map((resource) => ({
|
|
1214
|
+
logicalId: resource.id,
|
|
1215
|
+
resourceId: `${options.provider}://${run.environment}/${resource.id}`,
|
|
1216
|
+
type: resource.type,
|
|
1217
|
+
location: run.environment,
|
|
1218
|
+
properties: { purpose: resource.purpose },
|
|
1219
|
+
})),
|
|
1220
|
+
blockers: [],
|
|
1221
|
+
ttlMs: options.expiresInMs ?? PREVIEW_TTL_MS,
|
|
1222
|
+
};
|
|
1223
|
+
const preview = options.operation === "apply" ? await provider.previewApply(request) : await provider.previewDestroy(request);
|
|
1224
|
+
this.assertValid("preview", preview);
|
|
1225
|
+
let attestation;
|
|
1226
|
+
if (options.provider === "terraform" && "attestation" in provider && typeof provider.attestation === "function") {
|
|
1227
|
+
attestation = provider.attestation(preview.previewHash);
|
|
1228
|
+
if (attestation !== undefined)
|
|
1229
|
+
this.assertValid("execution-plan-attestation", attestation);
|
|
1230
|
+
}
|
|
1231
|
+
const validation = await this.validatePreviewValidators(run, events, preview, options.provider, options.operation, intent, attestation, intendedExecutionRecipientIdentity);
|
|
1232
|
+
const previewObjectHash = await this.objects.putJson(preview);
|
|
1233
|
+
const attestationHash = attestation === undefined ? undefined : await this.objects.putJson(attestation);
|
|
1234
|
+
await this.append(run, "preview.requested", {
|
|
1235
|
+
provider: options.provider,
|
|
1236
|
+
operation: options.operation,
|
|
1237
|
+
track: run.iacTool,
|
|
1238
|
+
targetScope: run.targetScope,
|
|
1239
|
+
});
|
|
1240
|
+
await this.append(await this.currentRun(), "preview.created", {
|
|
1241
|
+
previewHash: preview.previewHash,
|
|
1242
|
+
previewObjectHash,
|
|
1243
|
+
provider: options.provider,
|
|
1244
|
+
providerRequestHash: sha256Json(request),
|
|
1245
|
+
commit: preview.commit,
|
|
1246
|
+
validatorIds: validation.validatorIds,
|
|
1247
|
+
evidenceMode: validation.evidenceMode,
|
|
1248
|
+
...(validation.omittedValidatorIds.length === 0 ? {} : { omittedValidatorIds: validation.omittedValidatorIds }),
|
|
1249
|
+
...(attestationHash === undefined ? {} : { attestationHash }),
|
|
1250
|
+
});
|
|
1251
|
+
await this.openRunGate(await this.currentRun(), 4, preview.previewHash);
|
|
1252
|
+
return preview;
|
|
1253
|
+
}
|
|
1254
|
+
const createdAt = this.clock().toISOString();
|
|
1255
|
+
const base = {
|
|
1256
|
+
schemaVersion: CONTRACT_VERSION,
|
|
1257
|
+
projectId: run.projectId,
|
|
1258
|
+
runId: run.runId,
|
|
1259
|
+
environment: run.environment,
|
|
1260
|
+
track: run.iacTool,
|
|
1261
|
+
operation: options.operation,
|
|
1262
|
+
target: run.targetScope,
|
|
1263
|
+
commit: dependencyRevision,
|
|
1264
|
+
dependencyRevision,
|
|
1265
|
+
ownerEpoch: run.ownerEpoch,
|
|
1266
|
+
inputHash: intentHash,
|
|
1267
|
+
iacHash: sha256Json(intent.resources),
|
|
1268
|
+
policyHash: this.artifactHash(events, "policy-property-map") ?? run.runtimeLockHash,
|
|
1269
|
+
changes: intent.resources.map((resource) => ({
|
|
1270
|
+
resourceId: `fake://${run.environment}/${resource.id}`,
|
|
1271
|
+
action: options.operation === "destroy" ? "delete" : "create",
|
|
1272
|
+
material: true,
|
|
1273
|
+
details: resource.type,
|
|
1274
|
+
})),
|
|
1275
|
+
blockers: [],
|
|
1276
|
+
createdAt,
|
|
1277
|
+
expiresAt: new Date(this.clock().getTime() + (options.expiresInMs ?? PREVIEW_TTL_MS)).toISOString(),
|
|
1278
|
+
};
|
|
1279
|
+
const preview = { ...base, previewHash: sha256Json(base) };
|
|
1280
|
+
this.assertValid("preview", preview);
|
|
1281
|
+
const validation = await this.validatePreviewValidators(run, events, preview, "fake", options.operation, intent, undefined, intendedExecutionRecipientIdentity);
|
|
1282
|
+
const previewObjectHash = await this.objects.putJson(preview);
|
|
1283
|
+
await this.append(run, "preview.requested", {
|
|
1284
|
+
provider: options.provider,
|
|
1285
|
+
operation: options.operation,
|
|
1286
|
+
track: run.iacTool,
|
|
1287
|
+
targetScope: run.targetScope,
|
|
1288
|
+
});
|
|
1289
|
+
await this.append(run, "preview.created", {
|
|
1290
|
+
previewHash: preview.previewHash,
|
|
1291
|
+
previewObjectHash,
|
|
1292
|
+
provider: options.provider,
|
|
1293
|
+
commit: preview.commit,
|
|
1294
|
+
validatorIds: validation.validatorIds,
|
|
1295
|
+
evidenceMode: validation.evidenceMode,
|
|
1296
|
+
...(validation.omittedValidatorIds.length === 0 ? {} : { omittedValidatorIds: validation.omittedValidatorIds }),
|
|
1297
|
+
});
|
|
1298
|
+
await this.openRunGate(await this.currentRun(), 4, preview.previewHash);
|
|
1299
|
+
return preview;
|
|
1300
|
+
}
|
|
1301
|
+
async currentPreview() {
|
|
1302
|
+
const run = await this.currentRun();
|
|
1303
|
+
const events = await this.journal(run).replay();
|
|
1304
|
+
await this.assertPreviewReady(run, events);
|
|
1305
|
+
return this.render("preview");
|
|
1306
|
+
}
|
|
1307
|
+
async currentApproval() {
|
|
1308
|
+
const run = await this.currentRun();
|
|
1309
|
+
const approvalHash = this.latestPayloadHash(await this.journal(run).replay(), "gate.decided", "approvalHash", (payload) => payload.gate === 4);
|
|
1310
|
+
if (approvalHash === undefined) {
|
|
1311
|
+
throw new ApexError("APEX_NOT_FOUND", "No Gate 4 approval exists for this run", EXIT_CODES.notFound);
|
|
1312
|
+
}
|
|
1313
|
+
return this.objects.getJson(approvalHash);
|
|
1314
|
+
}
|
|
1315
|
+
async deploy(expectedPreviewHash) {
|
|
1316
|
+
const run = await this.currentRun();
|
|
1317
|
+
const transferStore = new WriterTransferStore(this.projects.runDirectory(run.projectId, run.runId), this.clock);
|
|
1318
|
+
await this.assertCurrentWriterAuthority(run, transferStore);
|
|
1319
|
+
const events = await this.journal(run).replay();
|
|
1320
|
+
await this.assertPreviewReady(run, events);
|
|
1321
|
+
const previewHash = this.latestPayloadHash(events, "preview.created", "previewHash");
|
|
1322
|
+
const previewObjectHash = this.latestPayloadHash(events, "preview.created", "previewObjectHash");
|
|
1323
|
+
const approvalHash = this.latestPayloadHash(events, "gate.decided", "approvalHash", (payload) => payload.gate === 4);
|
|
1324
|
+
if (previewHash === undefined || previewObjectHash === undefined || approvalHash === undefined) {
|
|
1325
|
+
throw new ApexError("APEX_AUTHORIZATION", "Preview and Gate 4 approval are required", EXIT_CODES.authorization);
|
|
1326
|
+
}
|
|
1327
|
+
const completed = [...events]
|
|
1328
|
+
.reverse()
|
|
1329
|
+
.find((event) => event.type === "deployment.completed" &&
|
|
1330
|
+
event.payload.previewHash === previewHash);
|
|
1331
|
+
if (completed !== undefined) {
|
|
1332
|
+
const payload = completed.payload;
|
|
1333
|
+
if (typeof payload.operationHash === "string" && typeof payload.inventoryHash === "string") {
|
|
1334
|
+
return {
|
|
1335
|
+
operation: await this.objects.getJson(payload.operationHash),
|
|
1336
|
+
inventory: await this.objects.getJson(payload.inventoryHash),
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const incompleteExecution = [...events]
|
|
1341
|
+
.reverse()
|
|
1342
|
+
.find((event) => (event.type === "deployment.executed" || event.type === "deployment.indeterminate") &&
|
|
1343
|
+
event.payload.previewHash === previewHash);
|
|
1344
|
+
if (incompleteExecution !== undefined) {
|
|
1345
|
+
throw new ApexError("APEX_CONFLICT", "Deployment outcome is incomplete or indeterminate; run reconcile before retrying", EXIT_CODES.conflict);
|
|
1346
|
+
}
|
|
1347
|
+
if (expectedPreviewHash !== undefined && expectedPreviewHash !== previewHash) {
|
|
1348
|
+
throw new ApexError("APEX_STALE", "Requested preview hash is not current", EXIT_CODES.stale);
|
|
1349
|
+
}
|
|
1350
|
+
const preview = await this.objects.getJson(previewObjectHash);
|
|
1351
|
+
const approval = await this.objects.getJson(approvalHash);
|
|
1352
|
+
if (Date.parse(preview.expiresAt) <= this.clock().getTime())
|
|
1353
|
+
throw new ApexError("APEX_STALE", "Deployment preview has expired", EXIT_CODES.stale);
|
|
1354
|
+
const writerTransferClaimHash = preview.ownerEpoch === run.ownerEpoch
|
|
1355
|
+
? undefined
|
|
1356
|
+
: approval.writerEpoch === preview.ownerEpoch
|
|
1357
|
+
? await transferStore.proveOneHopPostApprovalLineage(preview.previewHash, preview.ownerEpoch, approvalHash)
|
|
1358
|
+
: await transferStore.proveOneHopPostPreviewLineage(preview.previewHash, preview.ownerEpoch);
|
|
1359
|
+
const approvalAfterTransfer = approval.writerEpoch === run.ownerEpoch &&
|
|
1360
|
+
approval.writerTransferClaimHash === (writerTransferClaimHash ?? undefined);
|
|
1361
|
+
const approvalBeforeTransfer = preview.ownerEpoch + 1 === run.ownerEpoch &&
|
|
1362
|
+
approval.writerEpoch === preview.ownerEpoch &&
|
|
1363
|
+
approval.writerTransferClaimHash === undefined &&
|
|
1364
|
+
writerTransferClaimHash !== null &&
|
|
1365
|
+
writerTransferClaimHash !== undefined &&
|
|
1366
|
+
approval.recipientIdentity === (await this.currentRecipientIdentity(run));
|
|
1367
|
+
if ((preview.ownerEpoch !== run.ownerEpoch && writerTransferClaimHash === null) ||
|
|
1368
|
+
(!approvalAfterTransfer && !approvalBeforeTransfer))
|
|
1369
|
+
throw new ApexError("APEX_STALE", "Owner epoch changed", EXIT_CODES.stale);
|
|
1370
|
+
const dependencyRevision = this.dependencyRevision(run, events);
|
|
1371
|
+
if (preview.dependencyRevision !== dependencyRevision || preview.commit !== dependencyRevision)
|
|
1372
|
+
throw new ApexError("APEX_STALE", "Deployment dependencies changed after preview", EXIT_CODES.stale);
|
|
1373
|
+
if (approval.decision !== "approved" ||
|
|
1374
|
+
approval.previewHash !== preview.previewHash ||
|
|
1375
|
+
approval.dependencyHash !== preview.previewHash) {
|
|
1376
|
+
throw new ApexError("APEX_AUTHORIZATION", "Approval does not authorize the exact preview", EXIT_CODES.authorization);
|
|
1377
|
+
}
|
|
1378
|
+
const providerName = [...events].reverse().find((event) => event.type === "preview.created")?.payload;
|
|
1379
|
+
const provider = providerName?.provider === "bicep" || providerName?.provider === "terraform" ? providerName.provider : "fake";
|
|
1380
|
+
const commonValidatorIds = await this.validateDeployValidators(run, provider, preview, approval, previewHash, dependencyRevision, "pre", undefined, undefined, undefined, writerTransferClaimHash ?? undefined);
|
|
1381
|
+
const declaredValidatorIds = await this.deployValidatorIds(run);
|
|
1382
|
+
if (providerName?.provider === "bicep" || providerName?.provider === "terraform") {
|
|
1383
|
+
const provider = this.providers[providerName.provider];
|
|
1384
|
+
if (provider === undefined)
|
|
1385
|
+
throw new ApexError("APEX_VALIDATION", `${providerName.provider} provider is no longer configured`, EXIT_CODES.validation);
|
|
1386
|
+
let operation;
|
|
1387
|
+
try {
|
|
1388
|
+
operation =
|
|
1389
|
+
preview.operation === "apply"
|
|
1390
|
+
? await provider.apply(preview, approval, {
|
|
1391
|
+
head: preview.commit,
|
|
1392
|
+
dependencyRevision,
|
|
1393
|
+
ownerEpoch: run.ownerEpoch,
|
|
1394
|
+
...(writerTransferClaimHash === undefined || writerTransferClaimHash === null
|
|
1395
|
+
? {}
|
|
1396
|
+
: { previousOwnerEpoch: preview.ownerEpoch, writerTransferClaimHash }),
|
|
1397
|
+
recipientIdentity: approval.recipientIdentity ?? "local",
|
|
1398
|
+
})
|
|
1399
|
+
: await provider.destroy(preview, approval, {
|
|
1400
|
+
head: preview.commit,
|
|
1401
|
+
dependencyRevision,
|
|
1402
|
+
ownerEpoch: run.ownerEpoch,
|
|
1403
|
+
...(writerTransferClaimHash === undefined || writerTransferClaimHash === null
|
|
1404
|
+
? {}
|
|
1405
|
+
: { previousOwnerEpoch: preview.ownerEpoch, writerTransferClaimHash }),
|
|
1406
|
+
recipientIdentity: approval.recipientIdentity ?? "local",
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
catch (error) {
|
|
1410
|
+
await this.append(run, "deployment.indeterminate", {
|
|
1411
|
+
provider: providerName.provider,
|
|
1412
|
+
previewHash,
|
|
1413
|
+
approvalHash,
|
|
1414
|
+
preValidatorIds: commonValidatorIds,
|
|
1415
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1416
|
+
});
|
|
1417
|
+
throw error;
|
|
1418
|
+
}
|
|
1419
|
+
const executionEvidence = provider.executionEvidence?.(operation.operationId);
|
|
1420
|
+
await this.append(run, "deployment.executed", {
|
|
1421
|
+
provider: providerName.provider,
|
|
1422
|
+
previewHash,
|
|
1423
|
+
approvalHash,
|
|
1424
|
+
operation: operation,
|
|
1425
|
+
preValidatorIds: commonValidatorIds,
|
|
1426
|
+
...(executionEvidence === undefined ? {} : { executionEvidence: executionEvidence }),
|
|
1427
|
+
});
|
|
1428
|
+
return this.completeNativeDeployment(run, events, providerName.provider, provider, preview, approval, previewHash, approvalHash, dependencyRevision, commonValidatorIds, operation, executionEvidence, writerTransferClaimHash ?? undefined);
|
|
1429
|
+
}
|
|
1430
|
+
const now = this.clock().toISOString();
|
|
1431
|
+
const operation = {
|
|
1432
|
+
schemaVersion: CONTRACT_VERSION,
|
|
1433
|
+
operationId: this.idSource(),
|
|
1434
|
+
projectId: run.projectId,
|
|
1435
|
+
runId: run.runId,
|
|
1436
|
+
providerOperationId: `fake-${preview.previewHash.slice(0, 16)}`,
|
|
1437
|
+
operation: preview.operation,
|
|
1438
|
+
state: "succeeded",
|
|
1439
|
+
previewHash,
|
|
1440
|
+
approvalHash: sha256Json(approval),
|
|
1441
|
+
ownerEpoch: run.ownerEpoch,
|
|
1442
|
+
updatedAt: now,
|
|
1443
|
+
};
|
|
1444
|
+
this.assertValid("operation", operation);
|
|
1445
|
+
const operationHash = await this.objects.putJson(operation);
|
|
1446
|
+
const inventory = {
|
|
1447
|
+
schemaVersion: CONTRACT_VERSION,
|
|
1448
|
+
projectId: run.projectId,
|
|
1449
|
+
runId: run.runId,
|
|
1450
|
+
deploymentHash: operationHash,
|
|
1451
|
+
collectedAt: now,
|
|
1452
|
+
resources: preview.operation === "destroy"
|
|
1453
|
+
? []
|
|
1454
|
+
: preview.changes.map((change) => ({
|
|
1455
|
+
logicalId: basename(change.resourceId),
|
|
1456
|
+
resourceId: change.resourceId,
|
|
1457
|
+
type: change.details ?? "fake/resource",
|
|
1458
|
+
location: run.environment,
|
|
1459
|
+
properties: { provider: "fake", previewHash },
|
|
1460
|
+
})),
|
|
1461
|
+
};
|
|
1462
|
+
this.assertValid("inventory", inventory);
|
|
1463
|
+
const inventoryValidatorIds = await this.validateInventoryValidators(run, preview, operation, inventory);
|
|
1464
|
+
const inventoryHash = await this.objects.putJson(inventory);
|
|
1465
|
+
const omittedValidatorIds = declaredValidatorIds.filter((id) => !commonValidatorIds.includes(id));
|
|
1466
|
+
await this.append(run, "deployment.completed", {
|
|
1467
|
+
operationHash,
|
|
1468
|
+
inventoryHash,
|
|
1469
|
+
previewHash,
|
|
1470
|
+
approvalHash,
|
|
1471
|
+
provider: "fake",
|
|
1472
|
+
validatorIds: commonValidatorIds,
|
|
1473
|
+
preValidatorIds: commonValidatorIds,
|
|
1474
|
+
postValidatorIds: [],
|
|
1475
|
+
inventoryValidatorIds,
|
|
1476
|
+
...(omittedValidatorIds.length === 0 ? {} : { omittedValidatorIds }),
|
|
1477
|
+
evidenceMode: "simulated",
|
|
1478
|
+
});
|
|
1479
|
+
return { operation, inventory };
|
|
1480
|
+
}
|
|
1481
|
+
async completeNativeDeployment(run, events, providerName, provider, preview, approval, previewHash, approvalHash, dependencyRevision, commonValidatorIds, operation, executionEvidence, provedPreviewTransferClaimHash) {
|
|
1482
|
+
this.assertValid("operation", operation);
|
|
1483
|
+
let attestation;
|
|
1484
|
+
if (providerName === "terraform") {
|
|
1485
|
+
const attestationHash = this.latestPayloadHash(events, "preview.created", "attestationHash");
|
|
1486
|
+
if (attestationHash !== undefined) {
|
|
1487
|
+
attestation = await this.objects.getJson(attestationHash);
|
|
1488
|
+
this.assertValid("execution-plan-attestation", attestation);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const providerValidatorIds = await this.validateDeployValidators(run, providerName, preview, approval, previewHash, dependencyRevision, "post", operation, executionEvidence, attestation, provedPreviewTransferClaimHash);
|
|
1492
|
+
const receiptValidatorIds = [...(executionEvidence?.validatorIds ?? [])];
|
|
1493
|
+
if (executionEvidence === undefined ||
|
|
1494
|
+
new Set(receiptValidatorIds).size !== receiptValidatorIds.length ||
|
|
1495
|
+
JSON.stringify(receiptValidatorIds.sort()) !== JSON.stringify([...providerValidatorIds].sort())) {
|
|
1496
|
+
throw new ApexError("APEX_VALIDATION", "Provider execution receipt validator IDs do not match the workflow manifest", EXIT_CODES.validation);
|
|
1497
|
+
}
|
|
1498
|
+
const operationHash = await this.objects.putJson(operation);
|
|
1499
|
+
const inventory = await provider.inventory(run.projectId, run.runId);
|
|
1500
|
+
this.assertValid("inventory", inventory);
|
|
1501
|
+
const inventoryValidatorIds = await this.validateInventoryValidators(run, preview, operation, inventory);
|
|
1502
|
+
const inventoryHash = await this.objects.putJson(inventory);
|
|
1503
|
+
const declaredValidatorIds = await this.deployValidatorIds(run);
|
|
1504
|
+
await this.append(run, "deployment.completed", {
|
|
1505
|
+
operationHash,
|
|
1506
|
+
inventoryHash,
|
|
1507
|
+
previewHash,
|
|
1508
|
+
approvalHash,
|
|
1509
|
+
provider: providerName,
|
|
1510
|
+
validatorIds: declaredValidatorIds.filter((id) => [...commonValidatorIds, ...providerValidatorIds].includes(id)),
|
|
1511
|
+
preValidatorIds: commonValidatorIds,
|
|
1512
|
+
postValidatorIds: providerValidatorIds,
|
|
1513
|
+
inventoryValidatorIds,
|
|
1514
|
+
evidenceMode: "native",
|
|
1515
|
+
});
|
|
1516
|
+
return { operation, inventory };
|
|
1517
|
+
}
|
|
1518
|
+
async inventory() {
|
|
1519
|
+
const run = await this.currentRun();
|
|
1520
|
+
const hash = this.latestPayloadHash(await this.journal(run).replay(), "deployment.completed", "inventoryHash");
|
|
1521
|
+
if (hash === undefined)
|
|
1522
|
+
throw new ApexError("APEX_NOT_FOUND", "No inventory exists for this run", EXIT_CODES.notFound);
|
|
1523
|
+
return this.objects.getJson(hash);
|
|
1524
|
+
}
|
|
1525
|
+
async promote(environment, targetScope) {
|
|
1526
|
+
const source = await this.currentRun();
|
|
1527
|
+
const sourceEvents = await this.journal(source).replay();
|
|
1528
|
+
if (![1, 2, 3].every((gate) => this.gateApproved(source, gate)))
|
|
1529
|
+
throw new ApexError("APEX_AUTHORIZATION", "Promotion requires approved Gates 1-3", EXIT_CODES.authorization);
|
|
1530
|
+
const promoted = await this.projects.createRun(source.projectId, {
|
|
1531
|
+
environment,
|
|
1532
|
+
targetScope,
|
|
1533
|
+
runtimeLockHash: source.runtimeLockHash,
|
|
1534
|
+
iacTool: source.iacTool,
|
|
1535
|
+
parentRunId: source.runId,
|
|
1536
|
+
});
|
|
1537
|
+
const sameScope = source.targetScope === targetScope;
|
|
1538
|
+
const inherited = promoted.gates.map((gate) => {
|
|
1539
|
+
const sourceGate = source.gates.find(({ gate: number }) => number === gate.gate);
|
|
1540
|
+
const inheritable = gate.gate === 1 || (sameScope && gate.gate <= 3);
|
|
1541
|
+
return inheritable && (sourceGate.state === "approved" || sourceGate.state === "inherited")
|
|
1542
|
+
? inheritGate(sourceGate, source.runId, sourceGate.dependencyHash, this.clock().toISOString())
|
|
1543
|
+
: gate;
|
|
1544
|
+
});
|
|
1545
|
+
const updated = { ...promoted, gates: inherited };
|
|
1546
|
+
await atomicWriteJson(this.runPath(updated), updated);
|
|
1547
|
+
const neutralNodes = new Set([
|
|
1548
|
+
"requirements",
|
|
1549
|
+
"requirements-review",
|
|
1550
|
+
"architecture",
|
|
1551
|
+
"architecture-review",
|
|
1552
|
+
...(sameScope ? ["plan", "plan-review"] : []),
|
|
1553
|
+
...(sameScope ? ["governance-discovery", "governance-reconciliation", "governance-review"] : []),
|
|
1554
|
+
]);
|
|
1555
|
+
const inheritedReviewHashes = new Set();
|
|
1556
|
+
for (const event of sourceEvents) {
|
|
1557
|
+
if (event.type !== "task.completed")
|
|
1558
|
+
continue;
|
|
1559
|
+
const payload = event.payload;
|
|
1560
|
+
if (typeof payload.nodeId !== "string" || !neutralNodes.has(payload.nodeId))
|
|
1561
|
+
continue;
|
|
1562
|
+
if (typeof payload.reviewHash === "string")
|
|
1563
|
+
inheritedReviewHashes.add(payload.reviewHash);
|
|
1564
|
+
await this.append(updated, "task.completed", {
|
|
1565
|
+
...event.payload,
|
|
1566
|
+
inherited: true,
|
|
1567
|
+
inheritedFromRunId: source.runId,
|
|
1568
|
+
inheritedEventHash: event.hash,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
if (!sameScope) {
|
|
1572
|
+
for (const event of sourceEvents) {
|
|
1573
|
+
if (event.type !== "task.completed")
|
|
1574
|
+
continue;
|
|
1575
|
+
const payload = event.payload;
|
|
1576
|
+
if (payload.nodeId !== "plan")
|
|
1577
|
+
continue;
|
|
1578
|
+
await this.append(updated, "artifacts.inherited", {
|
|
1579
|
+
nodeId: "plan",
|
|
1580
|
+
artifactHashes: (payload.artifactHashes ?? {}),
|
|
1581
|
+
inheritedFromRunId: source.runId,
|
|
1582
|
+
inheritedEventHash: event.hash,
|
|
1583
|
+
requiresRevalidation: true,
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
for (const event of sourceEvents) {
|
|
1588
|
+
if (event.type !== "review.resolved")
|
|
1589
|
+
continue;
|
|
1590
|
+
const resolution = event.payload.resolution;
|
|
1591
|
+
if (resolution === undefined || !inheritedReviewHashes.has(resolution.reviewHash))
|
|
1592
|
+
continue;
|
|
1593
|
+
await this.append(updated, "review.resolved", {
|
|
1594
|
+
resolution: resolution,
|
|
1595
|
+
inherited: true,
|
|
1596
|
+
inheritedFromRunId: source.runId,
|
|
1597
|
+
inheritedEventHash: event.hash,
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
await this.append(updated, "run.promoted", {
|
|
1601
|
+
parentRunId: source.runId,
|
|
1602
|
+
sourceDependencyRevision: this.dependencyRevision(source, sourceEvents),
|
|
1603
|
+
targetScopeChanged: !sameScope,
|
|
1604
|
+
invalidated: [
|
|
1605
|
+
...(!sameScope ? ["governance", "gate-2", "gate-3"] : []),
|
|
1606
|
+
"codegen",
|
|
1607
|
+
"validation",
|
|
1608
|
+
"preview",
|
|
1609
|
+
"gate-4",
|
|
1610
|
+
"deploy",
|
|
1611
|
+
"inventory",
|
|
1612
|
+
],
|
|
1613
|
+
});
|
|
1614
|
+
await this.writeSelection({ projectId: updated.projectId, runId: updated.runId });
|
|
1615
|
+
return updated;
|
|
1616
|
+
}
|
|
1617
|
+
async doctor(fix = false, yes = false, live = false) {
|
|
1618
|
+
const apexExists = await this.exists(join(this.root, ".apex"));
|
|
1619
|
+
if (fix && yes && apexExists) {
|
|
1620
|
+
await this.ensureLocalGitBoundary(true);
|
|
1621
|
+
const assets = await resolveBundledAssets();
|
|
1622
|
+
const lock = JSON.parse(await readFile(join(this.root, ".apex", "customizations.lock.json"), "utf8"));
|
|
1623
|
+
const selection = await this.customizationSelection(lock.clientId);
|
|
1624
|
+
if (selection.sourceMode === "custom-source") {
|
|
1625
|
+
throw new ApexError("APEX_USAGE", "Custom-source repair requires an explicit customization source update", EXIT_CODES.usage);
|
|
1626
|
+
}
|
|
1627
|
+
const clientId = selection.clientId;
|
|
1628
|
+
await this.installCustomizations(assets.clientProjections[clientId], true, assets.config, true, clientId);
|
|
1629
|
+
await this.installCapabilityAssets(assets);
|
|
1630
|
+
await atomicWriteJson(join(this.root, ".apex", "apex.lock.json"), await this.createRuntimeLock(assets));
|
|
1631
|
+
}
|
|
1632
|
+
const checks = [
|
|
1633
|
+
{
|
|
1634
|
+
id: "node",
|
|
1635
|
+
ok: Number(process.versions.node.split(".")[0]) >= 24,
|
|
1636
|
+
value: process.versions.node,
|
|
1637
|
+
remedy: "Install Node.js 24 or newer",
|
|
1638
|
+
},
|
|
1639
|
+
{ id: "workspace", ok: await this.exists(this.root), value: this.root, remedy: "Restore the workspace root" },
|
|
1640
|
+
{ id: "apex", ok: apexExists, value: join(this.root, ".apex"), remedy: "Run apex init" },
|
|
1641
|
+
];
|
|
1642
|
+
if (apexExists) {
|
|
1643
|
+
const run = await this.currentRun();
|
|
1644
|
+
for (const executable of run.iacTool === "bicep" ? ["az", "bicep"] : ["az", "terraform"]) {
|
|
1645
|
+
checks.push({
|
|
1646
|
+
id: `executable:${executable}`,
|
|
1647
|
+
ok: await this.executableChecker(executable),
|
|
1648
|
+
value: executable,
|
|
1649
|
+
remedy: `Install ${executable} and add it to PATH`,
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
const auth = await this.azureAuthStatus(live);
|
|
1653
|
+
checks.push({
|
|
1654
|
+
id: "azure-auth",
|
|
1655
|
+
ok: auth.authenticated,
|
|
1656
|
+
value: auth.detail,
|
|
1657
|
+
remedy: "Authenticate with Azure outside doctor, then run setup --live",
|
|
1658
|
+
});
|
|
1659
|
+
checks.push(await this.localGitBoundaryCheck());
|
|
1660
|
+
checks.push(...(await this.managedFileChecks()));
|
|
1661
|
+
checks.push(...(await this.runtimeLockChecks(run)));
|
|
1662
|
+
const route = await this.route(run, await this.journal(run).replay());
|
|
1663
|
+
if (route.task !== undefined) {
|
|
1664
|
+
const packStatuses = await this.capabilityPacks().requiredForWorkflows([route.task.id]);
|
|
1665
|
+
checks.push(...packStatuses.map((status) => ({
|
|
1666
|
+
id: `capability-pack:${status.id}`,
|
|
1667
|
+
ok: status.state === "installed",
|
|
1668
|
+
value: status.reason ?? status.state,
|
|
1669
|
+
remedy: status.action,
|
|
1670
|
+
})));
|
|
1671
|
+
}
|
|
1672
|
+
const providerConfigured = await this.exists(join(this.root, ".apex", "provider-config.json"));
|
|
1673
|
+
checks.push({
|
|
1674
|
+
id: "provider-config",
|
|
1675
|
+
ok: providerConfigured,
|
|
1676
|
+
value: providerConfigured ? "configured" : "missing",
|
|
1677
|
+
remedy: "Provide --provider-config with non-secret backend settings",
|
|
1678
|
+
});
|
|
1679
|
+
checks.push({
|
|
1680
|
+
id: "backend-readiness",
|
|
1681
|
+
ok: providerConfigured,
|
|
1682
|
+
value: providerConfigured ? "configured; connectivity not executed" : "not configured",
|
|
1683
|
+
remedy: "Configure the selected provider backend",
|
|
1684
|
+
});
|
|
1685
|
+
checks.push({
|
|
1686
|
+
id: "ci-plan-transport",
|
|
1687
|
+
ok: false,
|
|
1688
|
+
value: "production CI transport unavailable; local encrypted exact-plan supported",
|
|
1689
|
+
remedy: "Use local exact-plan deployment or provide an approved CI transport implementation",
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
const remedies = checks
|
|
1693
|
+
.filter((check) => !check.ok && check.remedy !== undefined)
|
|
1694
|
+
.map((check) => `${fix && !yes ? "Preview: " : ""}${check.remedy}`);
|
|
1695
|
+
const actionable = checks.find((check) => !check.ok && check.id !== "ci-plan-transport");
|
|
1696
|
+
return {
|
|
1697
|
+
healthy: checks.every((check) => check.ok || check.id === "ci-plan-transport"),
|
|
1698
|
+
checks,
|
|
1699
|
+
remedies,
|
|
1700
|
+
nextAction: actionable?.remedy ??
|
|
1701
|
+
(checks.some((check) => check.id === "ci-plan-transport")
|
|
1702
|
+
? "Use local exact-plan deployment"
|
|
1703
|
+
: "No action required"),
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
async validate() {
|
|
1707
|
+
const run = await this.currentRun();
|
|
1708
|
+
const head = await this.journal(run).head();
|
|
1709
|
+
const input = {
|
|
1710
|
+
dependencies: { journalHead: head ?? ZERO_HASH },
|
|
1711
|
+
config: { runtimeLockHash: run.runtimeLockHash },
|
|
1712
|
+
toolchain: { cliVersion: APEX_VERSION },
|
|
1713
|
+
};
|
|
1714
|
+
const cached = await this.cache.get(input);
|
|
1715
|
+
if (cached !== null)
|
|
1716
|
+
return cached;
|
|
1717
|
+
const result = { valid: true, events: (await this.journal(run).replay()).length };
|
|
1718
|
+
await this.cache.set(input, result);
|
|
1719
|
+
return result;
|
|
1720
|
+
}
|
|
1721
|
+
async cacheStatus() {
|
|
1722
|
+
const directory = join(this.root, ".apex", "cache", "content");
|
|
1723
|
+
if (!(await this.exists(directory)))
|
|
1724
|
+
return { entries: 0 };
|
|
1725
|
+
return { entries: (await readdir(directory)).filter((name) => /^[0-9a-f]{64}\.json$/.test(name)).length };
|
|
1726
|
+
}
|
|
1727
|
+
async clearCache() {
|
|
1728
|
+
return { cleared: await this.cache.invalidate() };
|
|
1729
|
+
}
|
|
1730
|
+
async createWriterTransfer(input) {
|
|
1731
|
+
const run = await this.currentRun();
|
|
1732
|
+
if (input.commit !== input.currentHead)
|
|
1733
|
+
throw new ApexError("APEX_STALE", "Transfer commit does not match current Git head", EXIT_CODES.stale);
|
|
1734
|
+
const transfers = new WriterTransferStore(this.projects.runDirectory(run.projectId, run.runId), this.clock);
|
|
1735
|
+
const ownership = await transfers.currentOwnership();
|
|
1736
|
+
const expectedSender = ownership === null && run.ownerEpoch === 1 ? "local" : ownership?.ownerId;
|
|
1737
|
+
if (expectedSender === undefined ||
|
|
1738
|
+
(ownership?.ownerEpoch !== undefined && ownership.ownerEpoch !== run.ownerEpoch)) {
|
|
1739
|
+
throw new ApexError("APEX_STALE", "Current writer ownership does not match the run epoch", EXIT_CODES.stale);
|
|
1740
|
+
}
|
|
1741
|
+
if (input.sender !== expectedSender) {
|
|
1742
|
+
throw new ApexError("APEX_AUTHORIZATION", "Transfer sender is not the current writer", EXIT_CODES.authorization);
|
|
1743
|
+
}
|
|
1744
|
+
if (ownership === null) {
|
|
1745
|
+
const lease = await transfers.leaseStore().current();
|
|
1746
|
+
if (lease === null)
|
|
1747
|
+
await transfers.leaseStore().acquireAtEpoch("local", run.ownerEpoch, input.ttlMs);
|
|
1748
|
+
else if (lease.ownerId !== "local" || lease.ownerEpoch !== run.ownerEpoch) {
|
|
1749
|
+
throw new ApexError("APEX_STALE", "Local writer lease does not match the run epoch", EXIT_CODES.stale);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return transfers.create({
|
|
1753
|
+
projectId: run.projectId,
|
|
1754
|
+
runId: run.runId,
|
|
1755
|
+
repository: input.repository,
|
|
1756
|
+
branch: input.branch,
|
|
1757
|
+
commit: input.commit,
|
|
1758
|
+
workflowId: input.workflowId,
|
|
1759
|
+
sender: input.sender,
|
|
1760
|
+
recipient: input.recipient,
|
|
1761
|
+
...(input.approvalEnvironment === undefined ? {} : { approvalEnvironment: input.approvalEnvironment }),
|
|
1762
|
+
currentEpoch: run.ownerEpoch,
|
|
1763
|
+
currentGitHead: input.currentHead,
|
|
1764
|
+
ttlMs: input.ttlMs,
|
|
1765
|
+
eventId: this.idSource(),
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
async acceptWriterTransfer(claimHash, recipient, currentHead) {
|
|
1769
|
+
const run = await this.currentRun();
|
|
1770
|
+
return new WriterTransferStore(this.projects.runDirectory(run.projectId, run.runId), this.clock).accept({
|
|
1771
|
+
claimHash,
|
|
1772
|
+
recipient,
|
|
1773
|
+
currentGitHead: currentHead,
|
|
1774
|
+
eventId: this.idSource(),
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
async currentWriter() {
|
|
1778
|
+
const run = await this.currentRun();
|
|
1779
|
+
return new WriterTransferStore(this.projects.runDirectory(run.projectId, run.runId), this.clock).currentOwnership();
|
|
1780
|
+
}
|
|
1781
|
+
async currentRecipientIdentity(run) {
|
|
1782
|
+
const ownership = await new WriterTransferStore(this.projects.runDirectory(run.projectId, run.runId), this.clock).currentActiveOwnership();
|
|
1783
|
+
return ownership?.ownerEpoch === run.ownerEpoch ? ownership.ownerId : "local";
|
|
1784
|
+
}
|
|
1785
|
+
async assertCurrentWriterAuthority(run, transfers) {
|
|
1786
|
+
const ownership = await transfers.currentOwnership();
|
|
1787
|
+
if (ownership === null && run.ownerEpoch === 1 && !(await transfers.hasPendingTransfer()))
|
|
1788
|
+
return;
|
|
1789
|
+
const active = await transfers.currentActiveOwnership();
|
|
1790
|
+
if (active?.ownerEpoch !== run.ownerEpoch) {
|
|
1791
|
+
throw new ApexError("APEX_STALE", "Current writer authority is missing or expired", EXIT_CODES.stale);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
async acceptEvidence(input) {
|
|
1795
|
+
if ((input.value === undefined) === (input.file === undefined))
|
|
1796
|
+
throw new ApexError("APEX_USAGE", "Evidence requires exactly one value or file", EXIT_CODES.usage);
|
|
1797
|
+
const store = await this.evidenceStore(input.kind, input.contentType);
|
|
1798
|
+
let value = input.file === undefined ? input.value : await readFile(resolve(input.file));
|
|
1799
|
+
if (input.kind === "architecture-availability-v1") {
|
|
1800
|
+
if (input.contentType !== "application/json")
|
|
1801
|
+
throw new ApexError("APEX_VALIDATION", "Architecture availability evidence must use application/json", EXIT_CODES.validation);
|
|
1802
|
+
let parsed;
|
|
1803
|
+
try {
|
|
1804
|
+
parsed = value instanceof Uint8Array ? JSON.parse(Buffer.from(value).toString("utf8")) : value;
|
|
1805
|
+
}
|
|
1806
|
+
catch (error) {
|
|
1807
|
+
throw new ApexError("APEX_VALIDATION", "Architecture availability evidence is not valid JSON", EXIT_CODES.validation, undefined, { cause: error });
|
|
1808
|
+
}
|
|
1809
|
+
this.assertValid("architecture-availability", parsed);
|
|
1810
|
+
const availability = parsed;
|
|
1811
|
+
if (availability.mode === "native") {
|
|
1812
|
+
if (this.architectureAvailabilityAdapter === undefined) {
|
|
1813
|
+
throw new ApexError("APEX_AUTHORIZATION", "Native architecture availability evidence requires an authorized capability adapter", EXIT_CODES.authorization);
|
|
1814
|
+
}
|
|
1815
|
+
await this.architectureAvailabilityAdapter(availability);
|
|
1816
|
+
}
|
|
1817
|
+
for (const check of Object.values(availability.checks)) {
|
|
1818
|
+
try {
|
|
1819
|
+
await this.objects.getBytes(check.evidenceRef);
|
|
1820
|
+
}
|
|
1821
|
+
catch (error) {
|
|
1822
|
+
throw new ApexError("APEX_VALIDATION", `Architecture availability source evidence is unavailable: ${check.evidenceRef}`, EXIT_CODES.validation, undefined, { cause: error });
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
value = availability;
|
|
1826
|
+
}
|
|
1827
|
+
const accepted = await store.accept({
|
|
1828
|
+
kind: input.kind,
|
|
1829
|
+
contentType: input.contentType,
|
|
1830
|
+
value,
|
|
1831
|
+
required: input.required,
|
|
1832
|
+
});
|
|
1833
|
+
await this.append(await this.currentRun(), "evidence.accepted", {
|
|
1834
|
+
kind: input.kind,
|
|
1835
|
+
contentType: input.contentType,
|
|
1836
|
+
status: accepted.status,
|
|
1837
|
+
bytes: accepted.bytes,
|
|
1838
|
+
...(accepted.hash === undefined ? {} : { hash: accepted.hash }),
|
|
1839
|
+
});
|
|
1840
|
+
return accepted;
|
|
1841
|
+
}
|
|
1842
|
+
async setTelemetryConsent(consent) {
|
|
1843
|
+
await (await this.evidenceStore()).setTelemetryConsent(consent);
|
|
1844
|
+
return { consent };
|
|
1845
|
+
}
|
|
1846
|
+
async exportTelemetry() {
|
|
1847
|
+
return (await this.evidenceStore()).exportTelemetry();
|
|
1848
|
+
}
|
|
1849
|
+
async deleteTelemetry() {
|
|
1850
|
+
await (await this.evidenceStore()).deleteTelemetry();
|
|
1851
|
+
return { deleted: true };
|
|
1852
|
+
}
|
|
1853
|
+
async render(kind) {
|
|
1854
|
+
const run = await this.currentRun();
|
|
1855
|
+
if (kind === "status")
|
|
1856
|
+
return renderRunStatus(run);
|
|
1857
|
+
const events = await this.journal(run).replay();
|
|
1858
|
+
if (kind === "requirements") {
|
|
1859
|
+
const hash = this.artifactHash(events, "requirements");
|
|
1860
|
+
if (hash === undefined)
|
|
1861
|
+
throw new ApexError("APEX_NOT_FOUND", "No requirements artifact exists", EXIT_CODES.notFound);
|
|
1862
|
+
return (await this.requirementsDocumentMarkdown(await this.objects.getJson(hash), hash)).content;
|
|
1863
|
+
}
|
|
1864
|
+
const map = {
|
|
1865
|
+
preview: ["preview.created", "previewObjectHash", renderDeploymentPreview],
|
|
1866
|
+
approval: ["gate.decided", "approvalHash", renderApprovalEvidence],
|
|
1867
|
+
inventory: ["deployment.completed", "inventoryHash", renderResourceInventory],
|
|
1868
|
+
};
|
|
1869
|
+
const [eventType, field, renderer] = map[kind];
|
|
1870
|
+
const hash = this.latestPayloadHash(events, eventType, field);
|
|
1871
|
+
if (hash === undefined)
|
|
1872
|
+
throw new ApexError("APEX_NOT_FOUND", `No ${kind} artifact exists`, EXIT_CODES.notFound);
|
|
1873
|
+
return renderer((await this.objects.getJson(hash)));
|
|
1874
|
+
}
|
|
1875
|
+
async diagnose() {
|
|
1876
|
+
return { status: await this.status(), doctor: await this.doctor() };
|
|
1877
|
+
}
|
|
1878
|
+
async reconcile() {
|
|
1879
|
+
const run = await this.currentRun();
|
|
1880
|
+
const events = await this.journal(run).replay();
|
|
1881
|
+
const completedInventoryHash = this.latestPayloadHash(events, "deployment.completed", "inventoryHash");
|
|
1882
|
+
if (completedInventoryHash !== undefined) {
|
|
1883
|
+
const inventory = await this.objects.getJson(completedInventoryHash);
|
|
1884
|
+
await this.append(run, "deployment.reconciled", { deploymentHash: inventory.deploymentHash });
|
|
1885
|
+
return inventory;
|
|
1886
|
+
}
|
|
1887
|
+
const executed = [...events].reverse().find((event) => event.type === "deployment.executed");
|
|
1888
|
+
if (executed === undefined)
|
|
1889
|
+
throw new ApexError("APEX_NOT_FOUND", "No deployment execution exists for this run", EXIT_CODES.notFound);
|
|
1890
|
+
const payload = executed.payload;
|
|
1891
|
+
if ((payload.provider !== "bicep" && payload.provider !== "terraform") ||
|
|
1892
|
+
typeof payload.previewHash !== "string" ||
|
|
1893
|
+
typeof payload.approvalHash !== "string" ||
|
|
1894
|
+
payload.operation === null ||
|
|
1895
|
+
typeof payload.operation !== "object" ||
|
|
1896
|
+
!Array.isArray(payload.preValidatorIds)) {
|
|
1897
|
+
throw new ApexError("APEX_VALIDATION", "Deployment execution receipt is invalid", EXIT_CODES.validation);
|
|
1898
|
+
}
|
|
1899
|
+
const provider = this.providers[payload.provider];
|
|
1900
|
+
if (provider === undefined)
|
|
1901
|
+
throw new ApexError("APEX_VALIDATION", `${payload.provider} provider is required to reconcile deployment inventory`, EXIT_CODES.validation);
|
|
1902
|
+
const previewObjectHash = this.latestPayloadHash(events, "preview.created", "previewObjectHash", (candidate) => candidate.previewHash === payload.previewHash);
|
|
1903
|
+
if (previewObjectHash === undefined)
|
|
1904
|
+
throw new ApexError("APEX_VALIDATION", "Executed deployment preview object is missing", EXIT_CODES.validation);
|
|
1905
|
+
const preview = await this.objects.getJson(previewObjectHash);
|
|
1906
|
+
const approval = await this.objects.getJson(payload.approvalHash);
|
|
1907
|
+
const completed = await this.completeNativeDeployment(run, events, payload.provider, provider, preview, approval, payload.previewHash, payload.approvalHash, preview.dependencyRevision, payload.preValidatorIds.filter((id) => typeof id === "string"), payload.operation, payload.executionEvidence);
|
|
1908
|
+
await this.append(run, "deployment.reconciled", { deploymentHash: completed.inventory.deploymentHash });
|
|
1909
|
+
return completed.inventory;
|
|
1910
|
+
}
|
|
1911
|
+
async setup(live = false) {
|
|
1912
|
+
return this.doctor(false, false, live);
|
|
1913
|
+
}
|
|
1914
|
+
async issueTask(run, descriptor, inputRefs = []) {
|
|
1915
|
+
const head = await this.journal(run).head();
|
|
1916
|
+
if (head === null)
|
|
1917
|
+
throw new ApexError("APEX_STALE", "Cannot issue a task before run initialization", EXIT_CODES.stale);
|
|
1918
|
+
const task = createTaskEnvelope({
|
|
1919
|
+
projectId: run.projectId,
|
|
1920
|
+
runId: run.runId,
|
|
1921
|
+
role: descriptor.role,
|
|
1922
|
+
taskType: descriptor.id,
|
|
1923
|
+
expectedHead: head,
|
|
1924
|
+
ownerEpoch: run.ownerEpoch,
|
|
1925
|
+
inputRefs,
|
|
1926
|
+
allowedOutputKinds: descriptor.outputs,
|
|
1927
|
+
capabilityGrants: (descriptor.capabilities ?? []).map((capability) => ({
|
|
1928
|
+
capability,
|
|
1929
|
+
sideEffect: "remote",
|
|
1930
|
+
expiresAt: new Date(this.clock().getTime() + TASK_TTL_MS).toISOString(),
|
|
1931
|
+
})),
|
|
1932
|
+
maxOutputBytes: 4 * 1024 * 1024,
|
|
1933
|
+
ttlMs: TASK_TTL_MS,
|
|
1934
|
+
}, this.clock, this.idSource);
|
|
1935
|
+
await atomicWriteJson(join(this.projects.runDirectory(run.projectId, run.runId), "tasks", `${task.taskId}.json`), task, { refuseOverwrite: true });
|
|
1936
|
+
await this.append(run, "task.issued", { taskId: task.taskId, taskType: descriptor.id });
|
|
1937
|
+
const currentHead = await this.journal(run).head();
|
|
1938
|
+
const current = { ...task, expectedHead: currentHead };
|
|
1939
|
+
await atomicWriteJson(join(this.projects.runDirectory(run.projectId, run.runId), "tasks", `${task.taskId}.json`), current);
|
|
1940
|
+
return current;
|
|
1941
|
+
}
|
|
1942
|
+
async openRunGate(run, gateNumber, dependencyHash) {
|
|
1943
|
+
const gate = run.gates.find(({ gate }) => gate === gateNumber);
|
|
1944
|
+
const reopened = gateNumber === 4 && ["open", "approved", "rejected"].includes(gate.state);
|
|
1945
|
+
const nextGate = reopened
|
|
1946
|
+
? { gate: 4, state: "open", dependencyHash }
|
|
1947
|
+
: openGate(gate, dependencyHash);
|
|
1948
|
+
const updated = {
|
|
1949
|
+
...run,
|
|
1950
|
+
gates: run.gates.map((item) => (item.gate === gateNumber ? nextGate : item)),
|
|
1951
|
+
};
|
|
1952
|
+
await this.mutateRun(run, updated, reopened ? "gate.reopened" : "gate.opened", {
|
|
1953
|
+
gate: gateNumber,
|
|
1954
|
+
dependencyHash,
|
|
1955
|
+
...(reopened ? { previousState: gate.state } : {}),
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
async mutateRun(before, after, eventType, payload, expectedJournalHead) {
|
|
1959
|
+
const repository = this.runRepository(before);
|
|
1960
|
+
try {
|
|
1961
|
+
await repository.mutate({
|
|
1962
|
+
expectedRunHash: sha256Json(before),
|
|
1963
|
+
...(expectedJournalHead === undefined ? {} : { expectedJournalHead }),
|
|
1964
|
+
event: {
|
|
1965
|
+
eventId: this.idSource(),
|
|
1966
|
+
projectId: before.projectId,
|
|
1967
|
+
runId: before.runId,
|
|
1968
|
+
type: eventType,
|
|
1969
|
+
timestamp: this.clock().toISOString(),
|
|
1970
|
+
ownerEpoch: after.ownerEpoch,
|
|
1971
|
+
payload,
|
|
1972
|
+
},
|
|
1973
|
+
update: () => after,
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
catch (error) {
|
|
1977
|
+
if (error instanceof Error &&
|
|
1978
|
+
(error.message.startsWith("Stale run hash") || error.message.startsWith("Stale journal head")))
|
|
1979
|
+
throw new ApexError("APEX_STALE", "Run or journal state changed", EXIT_CODES.stale, undefined, error);
|
|
1980
|
+
throw error;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
inputRequest(event, ownerEpoch) {
|
|
1984
|
+
const payload = event.payload;
|
|
1985
|
+
const request = {
|
|
1986
|
+
schemaVersion: CONTRACT_VERSION,
|
|
1987
|
+
requestId: payload.requestId,
|
|
1988
|
+
expectedHead: event.hash,
|
|
1989
|
+
ownerEpoch,
|
|
1990
|
+
intake: payload.intake,
|
|
1991
|
+
questions: payload.questions,
|
|
1992
|
+
};
|
|
1993
|
+
if (!Value.Check(InputRequestV1Schema, request) || !hasValidInputRequestQuestions(request.questions)) {
|
|
1994
|
+
throw new ApexError("APEX_INTERNAL", "Persisted input request is invalid", EXIT_CODES.internal);
|
|
1995
|
+
}
|
|
1996
|
+
return request;
|
|
1997
|
+
}
|
|
1998
|
+
architectureDecisionRequest(event, ownerEpoch) {
|
|
1999
|
+
const payload = event.payload;
|
|
2000
|
+
const request = {
|
|
2001
|
+
schemaVersion: CONTRACT_VERSION,
|
|
2002
|
+
requestId: payload.requestId,
|
|
2003
|
+
expectedHead: event.hash,
|
|
2004
|
+
ownerEpoch,
|
|
2005
|
+
decision: payload.decision,
|
|
2006
|
+
questions: payload.questions,
|
|
2007
|
+
};
|
|
2008
|
+
if (!Value.Check(InputRequestV1Schema, request) ||
|
|
2009
|
+
request.decision === undefined ||
|
|
2010
|
+
!hasValidInputRequestQuestions(request.questions)) {
|
|
2011
|
+
throw new ApexError("APEX_INTERNAL", "Persisted architecture decision request is invalid", EXIT_CODES.internal);
|
|
2012
|
+
}
|
|
2013
|
+
return request;
|
|
2014
|
+
}
|
|
2015
|
+
architectureDecisionState(events, ownerEpoch) {
|
|
2016
|
+
const requested = [...events].reverse().find((event) => event.type === "architecture.decision-requested");
|
|
2017
|
+
if (requested === undefined)
|
|
2018
|
+
return {};
|
|
2019
|
+
const request = this.architectureDecisionRequest(requested, requested.ownerEpoch);
|
|
2020
|
+
const recorded = events.some((event) => event.type === "architecture.decision-recorded" &&
|
|
2021
|
+
event.payload.requestId === request.requestId);
|
|
2022
|
+
if (recorded)
|
|
2023
|
+
return { taskId: request.decision.taskId };
|
|
2024
|
+
if (request.expectedHead !== events.at(-1)?.hash || request.ownerEpoch !== ownerEpoch) {
|
|
2025
|
+
return { staleTaskId: request.decision.taskId };
|
|
2026
|
+
}
|
|
2027
|
+
return { pending: request };
|
|
2028
|
+
}
|
|
2029
|
+
architectureDecisionValues(events, taskId) {
|
|
2030
|
+
const result = {};
|
|
2031
|
+
for (const event of events) {
|
|
2032
|
+
if (event.type !== "architecture.decision-recorded")
|
|
2033
|
+
continue;
|
|
2034
|
+
const payload = event.payload;
|
|
2035
|
+
if (payload.decision?.taskId !== taskId ||
|
|
2036
|
+
typeof payload.decision.id !== "string" ||
|
|
2037
|
+
!Array.isArray(payload.answers)) {
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
for (const answer of payload.answers) {
|
|
2041
|
+
const value = answer;
|
|
2042
|
+
if (typeof value.questionId === "string" && Value.Check(InputValueV1Schema, value.value)) {
|
|
2043
|
+
result[value.questionId] = value.value;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
return result;
|
|
2048
|
+
}
|
|
2049
|
+
async issueArchitectureDecision(run, task) {
|
|
2050
|
+
const decision = ARCHITECTURE_DECISIONS[0];
|
|
2051
|
+
if (!hasValidInputRequestQuestions(decision.questions)) {
|
|
2052
|
+
throw new ApexError("APEX_INTERNAL", "Kernel architecture decision questions are invalid", EXIT_CODES.internal);
|
|
2053
|
+
}
|
|
2054
|
+
const event = await this.append(run, "architecture.decision-requested", {
|
|
2055
|
+
requestId: this.idSource(),
|
|
2056
|
+
decision: { taskId: task.taskId, id: decision.id },
|
|
2057
|
+
questions: decision.questions,
|
|
2058
|
+
});
|
|
2059
|
+
await this.refreshTaskHead(run, task);
|
|
2060
|
+
return this.architectureDecisionRequest(event, run.ownerEpoch);
|
|
2061
|
+
}
|
|
2062
|
+
nextRequirementsIntakeRound(events) {
|
|
2063
|
+
const requests = events.filter((event) => event.type === "requirements.input-requested");
|
|
2064
|
+
if (requests.some((event) => {
|
|
2065
|
+
const intake = event.payload.intake;
|
|
2066
|
+
return intake === null || typeof intake !== "object";
|
|
2067
|
+
})) {
|
|
2068
|
+
throw new ApexError("APEX_CONFLICT", "Requirements intake is incompatible; start a new run", EXIT_CODES.conflict);
|
|
2069
|
+
}
|
|
2070
|
+
const recordedRequestIds = new Set(events.flatMap((event) => event.type === "requirements.input-recorded" &&
|
|
2071
|
+
typeof event.payload.requestId === "string"
|
|
2072
|
+
? [event.payload.requestId]
|
|
2073
|
+
: []));
|
|
2074
|
+
const latest = requests.at(-1);
|
|
2075
|
+
if (latest !== undefined && !recordedRequestIds.has(latest.payload.requestId)) {
|
|
2076
|
+
const intake = latest.payload.intake;
|
|
2077
|
+
const ordinal = intake.ordinal;
|
|
2078
|
+
const round = typeof ordinal === "number" && Number.isInteger(ordinal) ? REQUIREMENTS_INTAKE[ordinal - 1] : undefined;
|
|
2079
|
+
if (round === undefined || intake.round !== round.round || intake.total !== REQUIREMENTS_INTAKE.length) {
|
|
2080
|
+
throw new ApexError("APEX_CONFLICT", "Requirements intake is incompatible; start a new run", EXIT_CODES.conflict);
|
|
2081
|
+
}
|
|
2082
|
+
return round;
|
|
2083
|
+
}
|
|
2084
|
+
return REQUIREMENTS_INTAKE[recordedRequestIds.size];
|
|
2085
|
+
}
|
|
2086
|
+
async issueRequirementsInput(run, round) {
|
|
2087
|
+
const events = await this.journal(run).replay();
|
|
2088
|
+
const questions = this.requirementsIntakeQuestions(round, events);
|
|
2089
|
+
if (!hasValidInputRequestQuestions(questions)) {
|
|
2090
|
+
throw new ApexError("APEX_INTERNAL", "Kernel input questions are invalid", EXIT_CODES.internal);
|
|
2091
|
+
}
|
|
2092
|
+
const latest = [...events].reverse().find((event) => event.type === "requirements.input-requested");
|
|
2093
|
+
if (latest !== undefined) {
|
|
2094
|
+
const recorded = events.some((event) => event.type === "requirements.input-recorded" &&
|
|
2095
|
+
event.payload.requestId === latest.payload.requestId);
|
|
2096
|
+
if (!recorded && latest.hash === events.at(-1)?.hash && latest.ownerEpoch === run.ownerEpoch) {
|
|
2097
|
+
return this.inputRequest(latest, run.ownerEpoch);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
const ordinal = REQUIREMENTS_INTAKE.findIndex(({ round: id }) => id === round.round) + 1;
|
|
2101
|
+
const event = await this.append(run, "requirements.input-requested", {
|
|
2102
|
+
requestId: this.idSource(),
|
|
2103
|
+
intake: { round: round.round, ordinal, total: 4 },
|
|
2104
|
+
questions,
|
|
2105
|
+
});
|
|
2106
|
+
return this.inputRequest(event, run.ownerEpoch);
|
|
2107
|
+
}
|
|
2108
|
+
requirementsIntakeQuestions(round, events) {
|
|
2109
|
+
if (round.round !== "workload-pattern")
|
|
2110
|
+
return round.questions;
|
|
2111
|
+
const scenario = this.recordedRequirementsInput(events)?.["delivery-scenario"];
|
|
2112
|
+
if (scenario !== "migration" && scenario !== "modernization")
|
|
2113
|
+
return round.questions;
|
|
2114
|
+
return [
|
|
2115
|
+
...round.questions,
|
|
2116
|
+
{ id: "current-platform", prompt: "Describe the current platform and hosting model." },
|
|
2117
|
+
{ id: "migration-pain-points", prompt: "Describe the problems the migration or modernization must address." },
|
|
2118
|
+
{ id: "preserve-components", prompt: "List components, integrations, or data that must be preserved." },
|
|
2119
|
+
];
|
|
2120
|
+
}
|
|
2121
|
+
async append(run, type, payload, expectedHead) {
|
|
2122
|
+
const journal = this.journal(run);
|
|
2123
|
+
return await journal.append({
|
|
2124
|
+
eventId: this.idSource(),
|
|
2125
|
+
projectId: run.projectId,
|
|
2126
|
+
runId: run.runId,
|
|
2127
|
+
type,
|
|
2128
|
+
timestamp: this.clock().toISOString(),
|
|
2129
|
+
ownerEpoch: run.ownerEpoch,
|
|
2130
|
+
expectedHead: expectedHead === undefined ? await journal.head() : expectedHead,
|
|
2131
|
+
payload,
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
journal(run) {
|
|
2135
|
+
return new EventJournal(join(this.projects.runDirectory(run.projectId, run.runId), "journal"));
|
|
2136
|
+
}
|
|
2137
|
+
runPath(run) {
|
|
2138
|
+
return join(this.projects.runDirectory(run.projectId, run.runId), "run.json");
|
|
2139
|
+
}
|
|
2140
|
+
async currentRun() {
|
|
2141
|
+
await this.recoverCustomizationTransaction();
|
|
2142
|
+
return this.run(await this.selection());
|
|
2143
|
+
}
|
|
2144
|
+
async run(selection) {
|
|
2145
|
+
const run = await this.runRepository(selection).read();
|
|
2146
|
+
const events = await this.journal(run).replay();
|
|
2147
|
+
this.assertRequirementsIntakeAdmitted(events, run.ownerEpoch);
|
|
2148
|
+
const retired = events.some((event) => {
|
|
2149
|
+
if (event.type !== "task.completed")
|
|
2150
|
+
return false;
|
|
2151
|
+
const hashes = event.payload.artifactHashes;
|
|
2152
|
+
return typeof hashes?.["sku-manifest"] === "string";
|
|
2153
|
+
});
|
|
2154
|
+
if (retired) {
|
|
2155
|
+
throw new ApexError("APEX_CONFLICT", "Run uses retired sku-manifest-v1; start a new run and regenerate artifacts", EXIT_CODES.conflict, {
|
|
2156
|
+
retiredArtifact: "sku-manifest-v1",
|
|
2157
|
+
replacementArtifact: "workload-decision-manifest-v1",
|
|
2158
|
+
recovery: "start-new-run",
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
return run;
|
|
2162
|
+
}
|
|
2163
|
+
assertRequirementsIntakeAdmitted(events, ownerEpoch) {
|
|
2164
|
+
for (const event of events) {
|
|
2165
|
+
if (event.type !== "requirements.input-requested")
|
|
2166
|
+
continue;
|
|
2167
|
+
let request;
|
|
2168
|
+
try {
|
|
2169
|
+
request = this.inputRequest(event, ownerEpoch);
|
|
2170
|
+
}
|
|
2171
|
+
catch {
|
|
2172
|
+
throw new ApexError("APEX_CONFLICT", "Requirements intake is incompatible; start a new run", EXIT_CODES.conflict);
|
|
2173
|
+
}
|
|
2174
|
+
const intake = request.intake;
|
|
2175
|
+
const round = intake === undefined ? undefined : REQUIREMENTS_INTAKE[intake.ordinal - 1];
|
|
2176
|
+
if (round === undefined ||
|
|
2177
|
+
intake === undefined ||
|
|
2178
|
+
round.round !== intake.round ||
|
|
2179
|
+
intake.total !== REQUIREMENTS_INTAKE.length) {
|
|
2180
|
+
throw new ApexError("APEX_CONFLICT", "Requirements intake is incompatible; start a new run", EXIT_CODES.conflict);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
runRepository(selection) {
|
|
2185
|
+
return new RunRepository(this.projects.runDirectory(selection.projectId, selection.runId), {
|
|
2186
|
+
clock: this.clock,
|
|
2187
|
+
idSource: this.idSource,
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
async selection() {
|
|
2191
|
+
return JSON.parse(await readFile(join(this.root, ".apex", "config.json"), "utf8"));
|
|
2192
|
+
}
|
|
2193
|
+
async writeSelection(selection) {
|
|
2194
|
+
await atomicWriteJson(join(this.root, ".apex", "config.json"), selection);
|
|
2195
|
+
}
|
|
2196
|
+
latestPayloadHash(events, type, field, predicate) {
|
|
2197
|
+
for (const event of [...events].reverse()) {
|
|
2198
|
+
const payload = event.payload;
|
|
2199
|
+
if (event.type === type && (predicate?.(payload) ?? true) && typeof payload[field] === "string")
|
|
2200
|
+
return payload[field];
|
|
2201
|
+
}
|
|
2202
|
+
return undefined;
|
|
2203
|
+
}
|
|
2204
|
+
artifactHash(events, kind) {
|
|
2205
|
+
for (const event of [...events].reverse()) {
|
|
2206
|
+
if (event.type !== "task.completed")
|
|
2207
|
+
continue;
|
|
2208
|
+
const hashes = event.payload.artifactHashes;
|
|
2209
|
+
if (typeof hashes?.[kind] === "string")
|
|
2210
|
+
return hashes[kind];
|
|
2211
|
+
}
|
|
2212
|
+
const legacyFields = {
|
|
2213
|
+
requirements: "requirementsHash",
|
|
2214
|
+
"implementation-intent": "intentHash",
|
|
2215
|
+
};
|
|
2216
|
+
const field = legacyFields[kind];
|
|
2217
|
+
return field === undefined ? undefined : this.latestPayloadHash(events, "task.completed", field);
|
|
2218
|
+
}
|
|
2219
|
+
acceptedArtifactHashes(events) {
|
|
2220
|
+
const hashes = {};
|
|
2221
|
+
const legacyFields = {
|
|
2222
|
+
requirements: "requirementsHash",
|
|
2223
|
+
"implementation-intent": "intentHash",
|
|
2224
|
+
};
|
|
2225
|
+
for (const event of events) {
|
|
2226
|
+
if (event.type !== "task.completed")
|
|
2227
|
+
continue;
|
|
2228
|
+
const payload = event.payload;
|
|
2229
|
+
for (const [kind, hash] of Object.entries(payload.artifactHashes ?? {})) {
|
|
2230
|
+
if (typeof hash === "string")
|
|
2231
|
+
hashes[kind] = hash;
|
|
2232
|
+
}
|
|
2233
|
+
for (const [kind, field] of Object.entries(legacyFields)) {
|
|
2234
|
+
const hash = payload[field];
|
|
2235
|
+
if (typeof hash === "string" && hashes[kind] === undefined)
|
|
2236
|
+
hashes[kind] = hash;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return hashes;
|
|
2240
|
+
}
|
|
2241
|
+
inputRefs(events, descriptor) {
|
|
2242
|
+
const availabilityKinds = new Set([
|
|
2243
|
+
"architecture-availability-v1",
|
|
2244
|
+
"pricing-evidence",
|
|
2245
|
+
"quota-evidence",
|
|
2246
|
+
"regionalAvailability-evidence",
|
|
2247
|
+
"regional-availability-evidence",
|
|
2248
|
+
]);
|
|
2249
|
+
const hashes = events.flatMap((event) => {
|
|
2250
|
+
if (event.type === "task.completed") {
|
|
2251
|
+
const artifactHashes = event.payload.artifactHashes ?? {};
|
|
2252
|
+
return Object.values(artifactHashes).filter((hash) => typeof hash === "string");
|
|
2253
|
+
}
|
|
2254
|
+
if (event.type === "evidence.accepted" && descriptor.id === "architecture") {
|
|
2255
|
+
const payload = event.payload;
|
|
2256
|
+
return typeof payload.hash === "string" &&
|
|
2257
|
+
typeof payload.kind === "string" &&
|
|
2258
|
+
payload.status === "accepted" &&
|
|
2259
|
+
availabilityKinds.has(payload.kind)
|
|
2260
|
+
? [payload.hash]
|
|
2261
|
+
: [];
|
|
2262
|
+
}
|
|
2263
|
+
if (event.type === "deployment.completed" && descriptor.id === "diagnosis") {
|
|
2264
|
+
const payload = event.payload;
|
|
2265
|
+
return [payload.operationHash, payload.inventoryHash].filter((hash) => typeof hash === "string");
|
|
2266
|
+
}
|
|
2267
|
+
return [];
|
|
2268
|
+
});
|
|
2269
|
+
return [...new Set(hashes)];
|
|
2270
|
+
}
|
|
2271
|
+
recordedRequirementsInput(events) {
|
|
2272
|
+
const result = {};
|
|
2273
|
+
let recorded = false;
|
|
2274
|
+
for (const event of events) {
|
|
2275
|
+
if (event.type !== "requirements.input-recorded")
|
|
2276
|
+
continue;
|
|
2277
|
+
const answers = event.payload.answers;
|
|
2278
|
+
if (!Array.isArray(answers))
|
|
2279
|
+
continue;
|
|
2280
|
+
recorded = true;
|
|
2281
|
+
for (const answer of answers) {
|
|
2282
|
+
const value = answer;
|
|
2283
|
+
if (typeof value.questionId !== "string" || !Value.Check(InputValueV1Schema, value.value)) {
|
|
2284
|
+
throw new ApexError("APEX_INTERNAL", "Recorded requirements input is invalid", EXIT_CODES.internal);
|
|
2285
|
+
}
|
|
2286
|
+
result[value.questionId] = value.value;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return recorded ? result : null;
|
|
2290
|
+
}
|
|
2291
|
+
outputTemplate(kind, run, events) {
|
|
2292
|
+
if (kind === "requirements") {
|
|
2293
|
+
const input = this.recordedRequirementsInput(events);
|
|
2294
|
+
return {
|
|
2295
|
+
schemaVersion: CONTRACT_VERSION,
|
|
2296
|
+
projectId: run.projectId,
|
|
2297
|
+
workload: typeof input?.workload === "string" ? input.workload : "",
|
|
2298
|
+
environment: run.environment,
|
|
2299
|
+
...this.requirementsTemplateFromIntake(input),
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
if (kind === "implementation-intent") {
|
|
2303
|
+
const sourceHashes = Object.fromEntries(["requirements", "architecture", "governance-constraints", "policy-property-map"].flatMap((artifact) => {
|
|
2304
|
+
const hash = this.artifactHash(events, artifact);
|
|
2305
|
+
return hash === undefined ? [] : [[artifact, hash]];
|
|
2306
|
+
}));
|
|
2307
|
+
return {
|
|
2308
|
+
schemaVersion: CONTRACT_VERSION,
|
|
2309
|
+
projectId: run.projectId,
|
|
2310
|
+
runId: run.runId,
|
|
2311
|
+
sourceHashes,
|
|
2312
|
+
resources: [
|
|
2313
|
+
{
|
|
2314
|
+
id: "RESOURCE_ID",
|
|
2315
|
+
type: "SERVICE_TYPE",
|
|
2316
|
+
purpose: "Describe the resource purpose",
|
|
2317
|
+
dependsOn: [],
|
|
2318
|
+
controls: [],
|
|
2319
|
+
},
|
|
2320
|
+
],
|
|
2321
|
+
outputs: ["OUTPUT_NAME"],
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
if (kind === "iac-binding") {
|
|
2325
|
+
return {
|
|
2326
|
+
schemaVersion: CONTRACT_VERSION,
|
|
2327
|
+
projectId: run.projectId,
|
|
2328
|
+
runId: run.runId,
|
|
2329
|
+
track: run.iacTool,
|
|
2330
|
+
intentHash: "0".repeat(64),
|
|
2331
|
+
resourceBindings: {
|
|
2332
|
+
RESOURCE_ID: {
|
|
2333
|
+
implementation: "IMPLEMENTATION_ADDRESS",
|
|
2334
|
+
version: "IMPLEMENTATION_VERSION",
|
|
2335
|
+
parameters: {},
|
|
2336
|
+
},
|
|
2337
|
+
},
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
if (kind === "environment-inputs") {
|
|
2341
|
+
return {
|
|
2342
|
+
schemaVersion: CONTRACT_VERSION,
|
|
2343
|
+
projectId: run.projectId,
|
|
2344
|
+
runId: run.runId,
|
|
2345
|
+
environment: run.environment,
|
|
2346
|
+
inputs: {
|
|
2347
|
+
location: { kind: "value", value: "REGION" },
|
|
2348
|
+
credential: {
|
|
2349
|
+
kind: "secret-reference",
|
|
2350
|
+
provider: "azure-key-vault",
|
|
2351
|
+
reference: "KEY_VAULT_SECRET_REFERENCE",
|
|
2352
|
+
},
|
|
2353
|
+
},
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
return { schemaId: ARTIFACTS[kind][1].$id };
|
|
2357
|
+
}
|
|
2358
|
+
requirementsTemplateFromIntake(input) {
|
|
2359
|
+
const fields = [
|
|
2360
|
+
{ id: "availability-recovery", label: "Availability and recovery", priority: "must" },
|
|
2361
|
+
{ id: "security-controls", label: "Security controls", priority: "must" },
|
|
2362
|
+
{ id: "compliance", label: "Compliance", priority: "should" },
|
|
2363
|
+
{ id: "authentication", label: "Authentication", priority: "should" },
|
|
2364
|
+
{ id: "operations", label: "Operations", priority: "should" },
|
|
2365
|
+
{ id: "budget", label: "Budget", priority: "should" },
|
|
2366
|
+
{ id: "iac-preference", label: "IaC preference", priority: "should" },
|
|
2367
|
+
{ id: "service-preferences", label: "Service preferences", priority: "should" },
|
|
2368
|
+
{ id: "sku-preferences", label: "SKU and sizing preferences", priority: "should" },
|
|
2369
|
+
{ id: "environment-overrides", label: "Environment overrides", priority: "should" },
|
|
2370
|
+
{ id: "retained-services", label: "Retained services", priority: "should" },
|
|
2371
|
+
{ id: "prohibited-services", label: "Prohibited services", priority: "should" },
|
|
2372
|
+
{ id: "data-sensitivity", label: "Data sensitivity", priority: "should" },
|
|
2373
|
+
{ id: "scale", label: "Scale", priority: "should" },
|
|
2374
|
+
{ id: "workload-pattern", label: "Workload pattern", priority: "should" },
|
|
2375
|
+
{ id: "delivery-scenario", label: "Delivery scenario", priority: "should" },
|
|
2376
|
+
{ id: "region", label: "Region", priority: "should" },
|
|
2377
|
+
];
|
|
2378
|
+
const requirements = fields.flatMap(({ id, label, priority }, index) => {
|
|
2379
|
+
const value = input?.[id];
|
|
2380
|
+
const text = this.renderInputValue(value);
|
|
2381
|
+
if (typeof text !== "string" || text.length === 0)
|
|
2382
|
+
return [];
|
|
2383
|
+
const status = /^deferred:/iu.test(text.trim())
|
|
2384
|
+
? "deferred"
|
|
2385
|
+
: /^unknown$/iu.test(text.trim())
|
|
2386
|
+
? "unknown"
|
|
2387
|
+
: "confirmed";
|
|
2388
|
+
return [
|
|
2389
|
+
{
|
|
2390
|
+
id: `REQ-${String(index + 1).padStart(3, "0")}`,
|
|
2391
|
+
statement: `${label}: ${text}`,
|
|
2392
|
+
priority,
|
|
2393
|
+
status,
|
|
2394
|
+
source: `intake:${id}`,
|
|
2395
|
+
},
|
|
2396
|
+
];
|
|
2397
|
+
});
|
|
2398
|
+
const assumptions = ["industry", "target-environments"].flatMap((id) => {
|
|
2399
|
+
const value = input?.[id];
|
|
2400
|
+
const text = this.renderInputValue(value);
|
|
2401
|
+
return typeof text === "string" && text.length > 0 ? [`${id}: ${text}`] : [];
|
|
2402
|
+
});
|
|
2403
|
+
const unknowns = requirements.filter(({ status }) => status !== "confirmed").map(({ statement }) => statement);
|
|
2404
|
+
return {
|
|
2405
|
+
requirements: requirements.length > 0
|
|
2406
|
+
? requirements
|
|
2407
|
+
: [
|
|
2408
|
+
{
|
|
2409
|
+
id: "REQ-001",
|
|
2410
|
+
statement: "deferred: requirements owner",
|
|
2411
|
+
priority: "must",
|
|
2412
|
+
status: "deferred",
|
|
2413
|
+
source: "intake:missing",
|
|
2414
|
+
},
|
|
2415
|
+
],
|
|
2416
|
+
assumptions,
|
|
2417
|
+
unknowns,
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
renderInputValue(value) {
|
|
2421
|
+
if (typeof value === "string")
|
|
2422
|
+
return value;
|
|
2423
|
+
if (Array.isArray(value))
|
|
2424
|
+
return value.join(", ");
|
|
2425
|
+
if (value === undefined)
|
|
2426
|
+
return undefined;
|
|
2427
|
+
switch (value.kind) {
|
|
2428
|
+
case "deferred":
|
|
2429
|
+
return `deferred: ${value.owner}`;
|
|
2430
|
+
case "unknown":
|
|
2431
|
+
return "unknown";
|
|
2432
|
+
case "budget":
|
|
2433
|
+
return `${value.currency} ${value.amount} ${value.cadence}`;
|
|
2434
|
+
case "recovery":
|
|
2435
|
+
return `RTO ${value.rtoMinutes} minutes; RPO ${value.rpoMinutes} minutes`;
|
|
2436
|
+
case "data-classification":
|
|
2437
|
+
return value.classification;
|
|
2438
|
+
case "compliance":
|
|
2439
|
+
return value.scopes.join(", ");
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
completedNodeIds(events) {
|
|
2443
|
+
return new Set(events.flatMap((event) => {
|
|
2444
|
+
if (event.type !== "task.completed")
|
|
2445
|
+
return [];
|
|
2446
|
+
const nodeId = event.payload.nodeId;
|
|
2447
|
+
return typeof nodeId === "string" ? [nodeId] : [];
|
|
2448
|
+
}));
|
|
2449
|
+
}
|
|
2450
|
+
async lockedWorkflowEngine(run) {
|
|
2451
|
+
const runtimeRoot = join(this.root, ".apex", "runtime");
|
|
2452
|
+
const workflowBytes = await readFile(join(runtimeRoot, "workflow.v1.json"));
|
|
2453
|
+
const lock = JSON.parse(await readFile(join(this.root, ".apex", "apex.lock.json"), "utf8"));
|
|
2454
|
+
this.assertValid("runtime-lock", lock);
|
|
2455
|
+
if (sha256Json(lock) !== run.runtimeLockHash || sha256Bytes(workflowBytes) !== lock.workflowHash) {
|
|
2456
|
+
throw new ApexError("APEX_STALE", "Runtime workflow lock is not current", EXIT_CODES.stale);
|
|
2457
|
+
}
|
|
2458
|
+
return new WorkflowEngine(JSON.parse(workflowBytes.toString("utf8")));
|
|
2459
|
+
}
|
|
2460
|
+
async route(run, events) {
|
|
2461
|
+
const completed = this.completedNodeIds(events);
|
|
2462
|
+
const legacy = events.some((event) => event.type === "task.completed" && event.payload.legacy === true);
|
|
2463
|
+
if (legacy) {
|
|
2464
|
+
if (!completed.has("requirements"))
|
|
2465
|
+
return { task: TASKS[0], blockers: [] };
|
|
2466
|
+
if (!this.gateApproved(run, 1))
|
|
2467
|
+
return { blockers: ["Gate 1 approval is required"] };
|
|
2468
|
+
if (!completed.has("plan"))
|
|
2469
|
+
return { task: TASKS.find(({ id }) => id === "plan"), blockers: [] };
|
|
2470
|
+
return { blockers: [] };
|
|
2471
|
+
}
|
|
2472
|
+
const manifest = (await this.lockedWorkflowEngine(run)).manifest;
|
|
2473
|
+
const ordered = manifest.nodes
|
|
2474
|
+
.flatMap((node) => {
|
|
2475
|
+
const descriptor = TASKS.find(({ id }) => id === node.id);
|
|
2476
|
+
const reviews = TASKS.filter(({ reviewSubject }) => reviewSubject === node.id);
|
|
2477
|
+
return [...(descriptor === undefined ? [] : [descriptor]), ...reviews];
|
|
2478
|
+
})
|
|
2479
|
+
.filter(({ track }) => track === undefined || track === run.iacTool);
|
|
2480
|
+
for (const descriptor of ordered) {
|
|
2481
|
+
if (completed.has(descriptor.id)) {
|
|
2482
|
+
const reviewBlockers = descriptor.reviewSubject === undefined ? [] : this.reviewBlockers(events, descriptor.id);
|
|
2483
|
+
if (reviewBlockers.length > 0)
|
|
2484
|
+
return {
|
|
2485
|
+
blockers: reviewBlockers,
|
|
2486
|
+
...(descriptor.gate === undefined ? {} : { reviewGate: descriptor.gate }),
|
|
2487
|
+
};
|
|
2488
|
+
if (descriptor.gate !== undefined && !this.gateApproved(run, descriptor.gate)) {
|
|
2489
|
+
return { blockers: [`Gate ${descriptor.gate} approval is required`] };
|
|
2490
|
+
}
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
if (descriptor.id === "diagnosis" && !events.some(({ type }) => type === "deployment.completed")) {
|
|
2494
|
+
if (events.some(({ type }) => type === "deployment.executed" || type === "deployment.indeterminate")) {
|
|
2495
|
+
return { blockers: ["Deployment executed but requires reconciliation"] };
|
|
2496
|
+
}
|
|
2497
|
+
const preview = this.latestPayloadHash(events, "preview.created", "previewHash");
|
|
2498
|
+
if (preview === undefined)
|
|
2499
|
+
return { blockers: ["Deployment preview is required"] };
|
|
2500
|
+
if (!this.gateApproved(run, 4))
|
|
2501
|
+
return { blockers: ["Gate 4 approval is required"] };
|
|
2502
|
+
return { blockers: ["Deployment and inventory are required"] };
|
|
2503
|
+
}
|
|
2504
|
+
return { task: descriptor, blockers: [] };
|
|
2505
|
+
}
|
|
2506
|
+
return { blockers: [] };
|
|
2507
|
+
}
|
|
2508
|
+
gateApproved(run, gateNumber) {
|
|
2509
|
+
const state = run.gates.find(({ gate }) => gate === gateNumber)?.state;
|
|
2510
|
+
return state === "approved" || state === "inherited";
|
|
2511
|
+
}
|
|
2512
|
+
reviewBlockers(events, nodeId) {
|
|
2513
|
+
const event = [...events]
|
|
2514
|
+
.reverse()
|
|
2515
|
+
.find((candidate) => candidate.type === "task.completed" && candidate.payload.nodeId === nodeId);
|
|
2516
|
+
const reviewHash = event?.payload?.reviewHash;
|
|
2517
|
+
const subjectHash = event?.payload?.subjectHash;
|
|
2518
|
+
const dependencyHash = event?.payload?.dependencyHash;
|
|
2519
|
+
const resolutions = new Set(events.flatMap((candidate) => {
|
|
2520
|
+
if (candidate.type !== "review.resolved")
|
|
2521
|
+
return [];
|
|
2522
|
+
const resolution = candidate.payload.resolution;
|
|
2523
|
+
if (resolution === undefined)
|
|
2524
|
+
return [];
|
|
2525
|
+
return resolution.reviewHash === reviewHash &&
|
|
2526
|
+
resolution.subjectHash === subjectHash &&
|
|
2527
|
+
resolution.dependencyHash === dependencyHash &&
|
|
2528
|
+
(resolution.disposition === "fixed" ||
|
|
2529
|
+
(resolution.expiresAt !== undefined && Date.parse(resolution.expiresAt) > this.clock().getTime()))
|
|
2530
|
+
? [resolution.findingId]
|
|
2531
|
+
: [];
|
|
2532
|
+
}));
|
|
2533
|
+
const blockers = event?.payload?.reviewBlockers;
|
|
2534
|
+
return Array.isArray(blockers)
|
|
2535
|
+
? blockers
|
|
2536
|
+
.filter((id) => typeof id === "string" && !resolutions.has(id))
|
|
2537
|
+
.map((id) => `Review finding ${id} must be resolved`)
|
|
2538
|
+
: [];
|
|
2539
|
+
}
|
|
2540
|
+
dependencyRevision(run, events) {
|
|
2541
|
+
return calculateDependencyRevision(run, events);
|
|
2542
|
+
}
|
|
2543
|
+
async assertPreviewReady(run, events) {
|
|
2544
|
+
if (!this.gateApproved(run, 3))
|
|
2545
|
+
throw new ApexError("APEX_AUTHORIZATION", "Gate 3 approval is required before preview", EXIT_CODES.authorization);
|
|
2546
|
+
const engine = await this.lockedWorkflowEngine(run);
|
|
2547
|
+
const aliases = {
|
|
2548
|
+
requirements: "requirements-v1",
|
|
2549
|
+
"workload-decision-manifest": "workload-decision-manifest-v1",
|
|
2550
|
+
architecture: "architecture-v1",
|
|
2551
|
+
"cost-estimate": "cost-estimate-v1",
|
|
2552
|
+
"governance-constraints": "governance-constraints-v1",
|
|
2553
|
+
"policy-property-map": "policy-property-map-v1",
|
|
2554
|
+
"implementation-intent": "implementation-intent-v1",
|
|
2555
|
+
"iac-binding": "iac-binding-v1",
|
|
2556
|
+
"environment-inputs": "environment-inputs-v1",
|
|
2557
|
+
"logical-resource-manifest": "logical-resource-manifest-v1",
|
|
2558
|
+
"iac-handoff": "iac-handoff-v1",
|
|
2559
|
+
"validation-evidence": "validation-evidence-v1",
|
|
2560
|
+
};
|
|
2561
|
+
const artifacts = {
|
|
2562
|
+
"project-config": true,
|
|
2563
|
+
defaults: true,
|
|
2564
|
+
toolchain: true,
|
|
2565
|
+
"runtime-bundle": true,
|
|
2566
|
+
"governance-capability-lock": true,
|
|
2567
|
+
"quota-evidence": true,
|
|
2568
|
+
"pricing-evidence": true,
|
|
2569
|
+
"regional-availability-evidence": true,
|
|
2570
|
+
"quota-evidence-current": true,
|
|
2571
|
+
"accepted-risk-scopes": true,
|
|
2572
|
+
...(run.iacTool === "bicep"
|
|
2573
|
+
? { "bicep-tree": true }
|
|
2574
|
+
: { "terraform-tree": true, "terraform-lockfile": true, "terraform-state-lineage-and-serial": true }),
|
|
2575
|
+
};
|
|
2576
|
+
for (const event of events) {
|
|
2577
|
+
if (event.type !== "task.completed")
|
|
2578
|
+
continue;
|
|
2579
|
+
const payload = event.payload;
|
|
2580
|
+
for (const [kind, hash] of Object.entries(payload.artifactHashes ?? {})) {
|
|
2581
|
+
const alias = aliases[kind];
|
|
2582
|
+
if (alias !== undefined && typeof hash === "string")
|
|
2583
|
+
artifacts[alias] = hash;
|
|
2584
|
+
}
|
|
2585
|
+
if (typeof payload.nodeId === "string" && payload.nodeId.endsWith("-review")) {
|
|
2586
|
+
artifacts[payload.nodeId] = true;
|
|
2587
|
+
if (payload.nodeId === "governance-review")
|
|
2588
|
+
artifacts["governance-reconciliation-review"] = true;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
const completedNodes = [
|
|
2592
|
+
...this.completedNodeIds(events),
|
|
2593
|
+
...run.gates.flatMap((gate) => gate.state === "approved" || gate.state === "inherited" ? [`gate-${gate.gate}`] : []),
|
|
2594
|
+
];
|
|
2595
|
+
const route = engine.route({
|
|
2596
|
+
run: { iacTool: run.iacTool, targetScope: run.targetScope },
|
|
2597
|
+
artifacts,
|
|
2598
|
+
completedNodes,
|
|
2599
|
+
gateStates: Object.fromEntries(run.gates.map((gate) => [`gate-${gate.gate}`, gate.state])),
|
|
2600
|
+
});
|
|
2601
|
+
const expectedNode = `preview-${run.iacTool}`;
|
|
2602
|
+
if (route.nextTask !== expectedNode) {
|
|
2603
|
+
const blockers = route.blockers.length > 0 ? route.blockers : [`workflow-route:${route.currentNode ?? "none"}`];
|
|
2604
|
+
throw new ApexError("APEX_AUTHORIZATION", `Workflow is not ready for ${expectedNode}: ${blockers.join(", ")}`, EXIT_CODES.authorization, blockers);
|
|
2605
|
+
}
|
|
2606
|
+
for (const reviewNode of ["requirements-review", "architecture-review", "governance-review", "plan-review"]) {
|
|
2607
|
+
const blockers = this.reviewBlockers(events, reviewNode);
|
|
2608
|
+
if (blockers.length > 0)
|
|
2609
|
+
throw new ApexError("APEX_AUTHORIZATION", blockers.join("; "), EXIT_CODES.authorization, blockers);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
openReviewFindings(value) {
|
|
2613
|
+
const review = value;
|
|
2614
|
+
return (review.findings ?? [])
|
|
2615
|
+
.filter((finding) => finding.disposition === "open" && finding.severity !== "info")
|
|
2616
|
+
.map(({ id }) => id);
|
|
2617
|
+
}
|
|
2618
|
+
validateOutput(run, output) {
|
|
2619
|
+
const descriptor = ARTIFACTS[output.kind];
|
|
2620
|
+
this.assertValid(descriptor[0], output.value);
|
|
2621
|
+
if (output.value === null || typeof output.value !== "object")
|
|
2622
|
+
return;
|
|
2623
|
+
const identity = output.value;
|
|
2624
|
+
if (identity.projectId !== undefined && identity.projectId !== run.projectId) {
|
|
2625
|
+
throw new ApexError("APEX_VALIDATION", "Task output project identity does not match the selected run", EXIT_CODES.validation);
|
|
2626
|
+
}
|
|
2627
|
+
if (identity.runId !== undefined && identity.runId !== run.runId) {
|
|
2628
|
+
throw new ApexError("APEX_VALIDATION", "Task output run identity does not match the selected run", EXIT_CODES.validation);
|
|
2629
|
+
}
|
|
2630
|
+
if (identity.track !== undefined && identity.track !== run.iacTool) {
|
|
2631
|
+
throw new ApexError("APEX_VALIDATION", `Artifact track '${String(identity.track)}' does not match selected track '${run.iacTool}'`, EXIT_CODES.validation);
|
|
2632
|
+
}
|
|
2633
|
+
if (output.kind === "environment-inputs") {
|
|
2634
|
+
const inputs = output.value;
|
|
2635
|
+
if (inputs.environment !== run.environment || !hasOnlyTypedSecretReferences(inputs)) {
|
|
2636
|
+
throw new ApexError("APEX_VALIDATION", "Environment inputs do not match the run or contain an invalid secret reference", EXIT_CODES.validation);
|
|
2637
|
+
}
|
|
2638
|
+
for (const [name, input] of Object.entries(inputs.inputs)) {
|
|
2639
|
+
if (input.kind === "value" && /secret|password|token|credential|api[-_]?key/i.test(name)) {
|
|
2640
|
+
throw new ApexError("APEX_VALIDATION", `Environment input '${name}' must use a secret-reference`, EXIT_CODES.validation);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
if (output.kind === "cost-estimate" && !hasValidCostArithmetic(output.value)) {
|
|
2645
|
+
throw new ApexError("APEX_VALIDATION", "Cost estimate arithmetic does not reconcile", EXIT_CODES.validation);
|
|
2646
|
+
}
|
|
2647
|
+
if (output.kind === "logical-resource-manifest" &&
|
|
2648
|
+
!hasValidLogicalResourceReferences(output.value)) {
|
|
2649
|
+
throw new ApexError("APEX_VALIDATION", "Logical resource dependencies are invalid", EXIT_CODES.validation);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
async validateBundle(run, descriptor, outputs, events) {
|
|
2653
|
+
const byKind = Object.fromEntries(outputs.map((output) => [output.kind, output.value]));
|
|
2654
|
+
if (descriptor.id === "architecture") {
|
|
2655
|
+
const manifest = byKind["workload-decision-manifest"];
|
|
2656
|
+
const requirementsHash = this.artifactHash(events, "requirements");
|
|
2657
|
+
if (requirementsHash === undefined ||
|
|
2658
|
+
manifest.projectId !== run.projectId ||
|
|
2659
|
+
manifest.runId !== run.runId ||
|
|
2660
|
+
manifest.environment !== run.environment ||
|
|
2661
|
+
!manifest.environments.includes(run.environment) ||
|
|
2662
|
+
manifest.sourceRequirementsHash !== requirementsHash ||
|
|
2663
|
+
manifest.architectureHash !== sha256Json(byKind.architecture) ||
|
|
2664
|
+
manifest.costEstimateHash !== sha256Json(byKind["cost-estimate"])) {
|
|
2665
|
+
throw new ApexError("APEX_VALIDATION", "Workload decision manifest does not bind accepted artifacts", EXIT_CODES.validation);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
if (descriptor.id === "plan") {
|
|
2669
|
+
const intent = byKind["implementation-intent"];
|
|
2670
|
+
const binding = byKind["iac-binding"];
|
|
2671
|
+
const resourceIds = new Set(intent.resources.map(({ id }) => id));
|
|
2672
|
+
const bindingIds = Object.keys(binding.resourceBindings);
|
|
2673
|
+
if (binding.track !== run.iacTool ||
|
|
2674
|
+
binding.intentHash !== sha256Json(intent) ||
|
|
2675
|
+
bindingIds.length !== resourceIds.size ||
|
|
2676
|
+
bindingIds.some((id) => !resourceIds.has(id))) {
|
|
2677
|
+
throw new ApexError("APEX_VALIDATION", "IaC binding track, intent hash, or resource coverage is invalid", EXIT_CODES.validation);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
if (descriptor.id.startsWith("codegen-")) {
|
|
2681
|
+
const manifest = byKind["logical-resource-manifest"];
|
|
2682
|
+
const handoff = byKind["iac-handoff"];
|
|
2683
|
+
if (handoff.logicalResourceManifestHash !== sha256Json(manifest) ||
|
|
2684
|
+
handoff.intentHash !== this.artifactHash(events, "implementation-intent") ||
|
|
2685
|
+
handoff.bindingHash !== this.artifactHash(events, "iac-binding") ||
|
|
2686
|
+
handoff.environmentInputsHash !== this.artifactHash(events, "environment-inputs") ||
|
|
2687
|
+
!/^[0-9a-f]{64}$/.test(handoff.treeHash)) {
|
|
2688
|
+
throw new ApexError("APEX_VALIDATION", "IaC handoff does not bind the accepted logical manifest and code tree", EXIT_CODES.validation);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
if (descriptor.id === "governance-reconciliation") {
|
|
2692
|
+
const policy = byKind["policy-property-map"];
|
|
2693
|
+
if (policy.governanceHash !== this.artifactHash(events, "governance-constraints")) {
|
|
2694
|
+
throw new ApexError("APEX_VALIDATION", "Policy property map does not bind accepted governance constraints", EXIT_CODES.validation);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
if (descriptor.reviewSubject !== undefined) {
|
|
2698
|
+
const review = byKind["review-findings"];
|
|
2699
|
+
const subjectKind = descriptor.reviewSubject === "governance-reconciliation" ? "policy-property-map" : descriptor.reviewSubject;
|
|
2700
|
+
if (review.subjectKind !== subjectKind)
|
|
2701
|
+
throw new ApexError("APEX_VALIDATION", `Review subject must be ${subjectKind}`, EXIT_CODES.validation);
|
|
2702
|
+
const expectedHash = subjectKind === "plan"
|
|
2703
|
+
? this.artifactHash(events, "implementation-intent")
|
|
2704
|
+
: this.artifactHash(events, subjectKind);
|
|
2705
|
+
if (expectedHash === undefined || review.subjectHash !== expectedHash) {
|
|
2706
|
+
throw new ApexError("APEX_VALIDATION", `Review does not bind the accepted ${subjectKind} artifact`, EXIT_CODES.validation);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
async validateTaskValidators(run, task, descriptor, outputs, events) {
|
|
2711
|
+
const workflow = await this.lockedWorkflowEngine(run);
|
|
2712
|
+
const nodeId = descriptor.reviewSubject ?? descriptor.id;
|
|
2713
|
+
const node = workflow.manifest.nodes.find(({ id }) => id === nodeId);
|
|
2714
|
+
if (node === undefined)
|
|
2715
|
+
throw new Error(`Workflow task ${nodeId} has no manifest node`);
|
|
2716
|
+
const boundary = descriptor.reviewSubject !== undefined
|
|
2717
|
+
? "review"
|
|
2718
|
+
: descriptor.id.startsWith("validation-")
|
|
2719
|
+
? "validation"
|
|
2720
|
+
: descriptor.id === "quality"
|
|
2721
|
+
? "quality"
|
|
2722
|
+
: descriptor.id === "diagnosis"
|
|
2723
|
+
? "diagnosis"
|
|
2724
|
+
: "task-output";
|
|
2725
|
+
const boundaries = new Set([boundary, ...(descriptor.id === "architecture" ? ["external-evidence"] : [])]);
|
|
2726
|
+
const validatorIds = node.validators.filter((id) => {
|
|
2727
|
+
const validatorBoundary = workflowValidatorOwnership(id)?.boundary;
|
|
2728
|
+
return validatorBoundary !== undefined && boundaries.has(validatorBoundary);
|
|
2729
|
+
});
|
|
2730
|
+
const artifactHashes = this.acceptedArtifactHashes(events);
|
|
2731
|
+
const artifacts = Object.fromEntries(await Promise.all(Object.entries(artifactHashes).map(async ([kind, hash]) => [kind, await this.objects.getJson(hash)])));
|
|
2732
|
+
let availabilityEvidence;
|
|
2733
|
+
let availabilityHash;
|
|
2734
|
+
let deploymentOperation;
|
|
2735
|
+
let deploymentInventory;
|
|
2736
|
+
if (descriptor.id === "architecture") {
|
|
2737
|
+
const pinnedRefs = new Set(task.inputRefs);
|
|
2738
|
+
availabilityHash = this.latestPayloadHash(events, "evidence.accepted", "hash", (payload) => payload.kind === "architecture-availability-v1" &&
|
|
2739
|
+
payload.status === "accepted" &&
|
|
2740
|
+
typeof payload.hash === "string" &&
|
|
2741
|
+
pinnedRefs.has(payload.hash));
|
|
2742
|
+
if (availabilityHash !== undefined) {
|
|
2743
|
+
availabilityEvidence = await this.objects.getJson(availabilityHash);
|
|
2744
|
+
this.assertValid("architecture-availability", availabilityEvidence);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
if (descriptor.id === "diagnosis") {
|
|
2748
|
+
const completed = [...events].reverse().find((event) => event.type === "deployment.completed");
|
|
2749
|
+
const payload = completed?.payload;
|
|
2750
|
+
if (typeof payload?.operationHash === "string" &&
|
|
2751
|
+
typeof payload.inventoryHash === "string" &&
|
|
2752
|
+
task.inputRefs.includes(payload.operationHash) &&
|
|
2753
|
+
task.inputRefs.includes(payload.inventoryHash)) {
|
|
2754
|
+
deploymentOperation = await this.objects.getJson(payload.operationHash);
|
|
2755
|
+
deploymentInventory = await this.objects.getJson(payload.inventoryHash);
|
|
2756
|
+
this.assertValid("operation", deploymentOperation);
|
|
2757
|
+
this.assertValid("inventory", deploymentInventory);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
const context = {
|
|
2761
|
+
nodeId,
|
|
2762
|
+
now: this.clock().toISOString(),
|
|
2763
|
+
track: run.iacTool,
|
|
2764
|
+
targetScope: run.targetScope,
|
|
2765
|
+
inputRefs: task.inputRefs,
|
|
2766
|
+
outputs: Object.fromEntries(outputs.map(({ kind, value }) => [kind, value])),
|
|
2767
|
+
artifacts,
|
|
2768
|
+
artifactHashes,
|
|
2769
|
+
...(availabilityEvidence === undefined ? {} : { availabilityEvidence }),
|
|
2770
|
+
...(deploymentOperation === undefined ? {} : { deploymentOperation }),
|
|
2771
|
+
...(deploymentInventory === undefined ? {} : { deploymentInventory }),
|
|
2772
|
+
...(nodeId === "quality" ? await this.qualityValidatorContext() : {}),
|
|
2773
|
+
};
|
|
2774
|
+
for (const id of validatorIds) {
|
|
2775
|
+
this.assertValid(id, taskWorkflowValidatorInput(id, context));
|
|
2776
|
+
}
|
|
2777
|
+
return {
|
|
2778
|
+
validatorIds,
|
|
2779
|
+
evidenceRefs: availabilityHash === undefined ? {} : { "business:availability-current": availabilityHash },
|
|
2780
|
+
evidenceModes: availabilityEvidence === undefined ? {} : { "business:availability-current": availabilityEvidence.mode },
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
async qualityValidatorContext() {
|
|
2784
|
+
let qualityMeasurements;
|
|
2785
|
+
try {
|
|
2786
|
+
qualityMeasurements = JSON.parse(await readFile(join(this.root, ".apex", "quality", "measurements.json"), "utf8"));
|
|
2787
|
+
}
|
|
2788
|
+
catch (error) {
|
|
2789
|
+
if (error.code === "ENOENT") {
|
|
2790
|
+
throw new ApexError("APEX_VALIDATION", "Quality measurements are unavailable; run quality evaluate first", EXIT_CODES.validation);
|
|
2791
|
+
}
|
|
2792
|
+
throw error;
|
|
2793
|
+
}
|
|
2794
|
+
this.assertValid("quality-measurements", qualityMeasurements);
|
|
2795
|
+
return {
|
|
2796
|
+
scorecard: JSON.parse(await readFile(join(this.root, ".apex", "runtime", "quality-scorecard.v1.json"), "utf8")),
|
|
2797
|
+
qualityMeasurements,
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
async validateGateValidators(run, gate, events, approval, expectedApprovalRecipientIdentity) {
|
|
2801
|
+
const workflow = await this.lockedWorkflowEngine(run);
|
|
2802
|
+
const nodeId = `gate-${gate.gate}`;
|
|
2803
|
+
const node = workflow.manifest.nodes.find(({ id }) => id === nodeId);
|
|
2804
|
+
if (node === undefined)
|
|
2805
|
+
throw new Error(`Workflow gate ${nodeId} has no manifest node`);
|
|
2806
|
+
const validatorIds = node.validators.filter((id) => workflowValidatorOwnership(id)?.boundary === "gate");
|
|
2807
|
+
const artifactHashes = {};
|
|
2808
|
+
for (const event of events) {
|
|
2809
|
+
if (event.type !== "task.completed")
|
|
2810
|
+
continue;
|
|
2811
|
+
const hashes = event.payload.artifactHashes ?? {};
|
|
2812
|
+
for (const [kind, hash] of Object.entries(hashes))
|
|
2813
|
+
if (typeof hash === "string")
|
|
2814
|
+
artifactHashes[kind] = hash;
|
|
2815
|
+
}
|
|
2816
|
+
const completedNodes = [...this.completedNodeIds(events)].sort();
|
|
2817
|
+
const reviewNodesByGate = {
|
|
2818
|
+
1: ["requirements-review"],
|
|
2819
|
+
2: ["architecture-review", "governance-review"],
|
|
2820
|
+
3: ["plan-review"],
|
|
2821
|
+
4: ["requirements-review", "architecture-review", "governance-review", "plan-review"],
|
|
2822
|
+
};
|
|
2823
|
+
let reviewNodes = reviewNodesByGate[gate.gate] ?? [];
|
|
2824
|
+
let legacyRequirements = false;
|
|
2825
|
+
let expectedDependencyHash;
|
|
2826
|
+
const dependencyReview = { 1: "requirements-review", 2: "governance-review", 3: "plan-review" }[gate.gate];
|
|
2827
|
+
if (dependencyReview !== undefined) {
|
|
2828
|
+
expectedDependencyHash = this.latestPayloadHash(events, "task.completed", "dependencyHash", (payload) => payload.nodeId === dependencyReview);
|
|
2829
|
+
}
|
|
2830
|
+
if (gate.gate === 1 && expectedDependencyHash === undefined) {
|
|
2831
|
+
const legacyEvent = [...events]
|
|
2832
|
+
.reverse()
|
|
2833
|
+
.find((event) => event.type === "task.completed" &&
|
|
2834
|
+
event.payload.nodeId === "requirements" &&
|
|
2835
|
+
event.payload.legacy === true);
|
|
2836
|
+
const hashes = legacyEvent?.payload?.artifactHashes;
|
|
2837
|
+
if (hashes !== undefined) {
|
|
2838
|
+
legacyRequirements = true;
|
|
2839
|
+
reviewNodes = [];
|
|
2840
|
+
expectedDependencyHash = sha256Json(hashes);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
let preview;
|
|
2844
|
+
if (gate.gate === 4) {
|
|
2845
|
+
const previewObjectHash = this.latestPayloadHash(events, "preview.created", "previewObjectHash");
|
|
2846
|
+
if (previewObjectHash !== undefined)
|
|
2847
|
+
preview = await this.objects.getJson(previewObjectHash);
|
|
2848
|
+
expectedDependencyHash = preview?.previewHash;
|
|
2849
|
+
}
|
|
2850
|
+
const provedPreviewTransferClaimHash = preview === undefined || preview.ownerEpoch === run.ownerEpoch
|
|
2851
|
+
? undefined
|
|
2852
|
+
: ((await new WriterTransferStore(this.projects.runDirectory(run.projectId, run.runId), this.clock).proveOneHopPostPreviewLineage(preview.previewHash, preview.ownerEpoch)) ?? undefined);
|
|
2853
|
+
const context = {
|
|
2854
|
+
gateNumber: gate.gate,
|
|
2855
|
+
now: this.clock().toISOString(),
|
|
2856
|
+
run,
|
|
2857
|
+
gate,
|
|
2858
|
+
approval,
|
|
2859
|
+
artifactHashes,
|
|
2860
|
+
completedNodes,
|
|
2861
|
+
reviewBlockers: reviewNodes.flatMap((reviewNode) => this.reviewBlockers(events, reviewNode)),
|
|
2862
|
+
currentDependencyRevision: this.dependencyRevision(run, events),
|
|
2863
|
+
legacyRequirements,
|
|
2864
|
+
expectedApprovalRecipientIdentity,
|
|
2865
|
+
...(provedPreviewTransferClaimHash === undefined ? {} : { provedPreviewTransferClaimHash }),
|
|
2866
|
+
...(expectedDependencyHash === undefined ? {} : { expectedDependencyHash }),
|
|
2867
|
+
...(preview === undefined ? {} : { preview }),
|
|
2868
|
+
};
|
|
2869
|
+
for (const id of validatorIds)
|
|
2870
|
+
this.assertValid(id, context);
|
|
2871
|
+
return validatorIds;
|
|
2872
|
+
}
|
|
2873
|
+
async validatePreviewValidators(run, events, preview, provider, expectedOperation, intent, attestation, intendedExecutionRecipientIdentity = "local") {
|
|
2874
|
+
const workflow = await this.lockedWorkflowEngine(run);
|
|
2875
|
+
const nodeId = `preview-${run.iacTool}`;
|
|
2876
|
+
const node = workflow.manifest.nodes.find(({ id }) => id === nodeId);
|
|
2877
|
+
if (node === undefined)
|
|
2878
|
+
throw new Error(`Workflow preview ${nodeId} has no manifest node`);
|
|
2879
|
+
const declaredIds = node.validators.filter((id) => workflowValidatorOwnership(id)?.boundary === "preview");
|
|
2880
|
+
const omittedValidatorIds = provider === "fake" ? declaredIds.filter((id) => id === "terraform:saved-plan-binding") : [];
|
|
2881
|
+
const validatorIds = declaredIds.filter((id) => !omittedValidatorIds.includes(id));
|
|
2882
|
+
const context = {
|
|
2883
|
+
now: this.clock().toISOString(),
|
|
2884
|
+
run,
|
|
2885
|
+
provider,
|
|
2886
|
+
expectedOperation,
|
|
2887
|
+
preview,
|
|
2888
|
+
intent,
|
|
2889
|
+
expectedInputHash: this.artifactHash(events, "implementation-intent") ?? sha256Json(intent),
|
|
2890
|
+
expectedIacHash: provider === "fake"
|
|
2891
|
+
? sha256Json(intent.resources)
|
|
2892
|
+
: (this.artifactHash(events, "iac-handoff") ?? sha256Json(intent.resources)),
|
|
2893
|
+
expectedPolicyHash: this.artifactHash(events, "policy-property-map") ?? run.runtimeLockHash,
|
|
2894
|
+
currentDependencyRevision: this.dependencyRevision(run, events),
|
|
2895
|
+
expectedResourceIds: intent.resources.map(({ id }) => `${provider}://${run.environment}/${id}`),
|
|
2896
|
+
intendedExecutionRecipientIdentity,
|
|
2897
|
+
...(attestation === undefined ? {} : { attestation }),
|
|
2898
|
+
};
|
|
2899
|
+
for (const id of validatorIds)
|
|
2900
|
+
this.assertValid(id, context);
|
|
2901
|
+
return { validatorIds, omittedValidatorIds, evidenceMode: provider === "fake" ? "simulated" : "native" };
|
|
2902
|
+
}
|
|
2903
|
+
async validateDeployValidators(run, provider, preview, approval, expectedPreviewHash, currentDependencyRevision, phase, operation, executionEvidence, attestation, provedPreviewTransferClaimHash) {
|
|
2904
|
+
const workflow = await this.lockedWorkflowEngine(run);
|
|
2905
|
+
const nodeId = `deploy-${run.iacTool}`;
|
|
2906
|
+
const node = workflow.manifest.nodes.find(({ id }) => id === nodeId);
|
|
2907
|
+
if (node === undefined)
|
|
2908
|
+
throw new Error(`Workflow deployment ${nodeId} has no manifest node`);
|
|
2909
|
+
const owner = phase === "pre" ? "@apexops/cli" : "@apexops/capabilities";
|
|
2910
|
+
const validatorIds = node.validators.filter((id) => {
|
|
2911
|
+
const ownership = workflowValidatorOwnership(id);
|
|
2912
|
+
return ownership?.boundary === "deploy" && ownership.owner === owner;
|
|
2913
|
+
});
|
|
2914
|
+
const context = {
|
|
2915
|
+
now: this.clock().toISOString(),
|
|
2916
|
+
run,
|
|
2917
|
+
provider,
|
|
2918
|
+
preview,
|
|
2919
|
+
approval,
|
|
2920
|
+
expectedPreviewHash,
|
|
2921
|
+
currentDependencyRevision,
|
|
2922
|
+
...(provedPreviewTransferClaimHash === undefined ? {} : { provedPreviewTransferClaimHash }),
|
|
2923
|
+
...(operation === undefined ? {} : { operation }),
|
|
2924
|
+
...(executionEvidence === undefined ? {} : { executionEvidence }),
|
|
2925
|
+
...(attestation === undefined ? {} : { attestation }),
|
|
2926
|
+
};
|
|
2927
|
+
for (const id of validatorIds)
|
|
2928
|
+
this.assertValid(id, context);
|
|
2929
|
+
return validatorIds;
|
|
2930
|
+
}
|
|
2931
|
+
async deployValidatorIds(run) {
|
|
2932
|
+
const workflow = await this.lockedWorkflowEngine(run);
|
|
2933
|
+
return (workflow.manifest.nodes
|
|
2934
|
+
.find(({ id }) => id === `deploy-${run.iacTool}`)
|
|
2935
|
+
?.validators.filter((id) => workflowValidatorOwnership(id)?.boundary === "deploy") ?? []);
|
|
2936
|
+
}
|
|
2937
|
+
async validateInventoryValidators(run, preview, operation, inventory) {
|
|
2938
|
+
const workflow = await this.lockedWorkflowEngine(run);
|
|
2939
|
+
const node = workflow.manifest.nodes.find(({ id }) => id === "inventory");
|
|
2940
|
+
if (node === undefined)
|
|
2941
|
+
throw new Error("Workflow inventory node is missing");
|
|
2942
|
+
const validatorIds = node.validators.filter((id) => workflowValidatorOwnership(id)?.boundary === "inventory");
|
|
2943
|
+
const context = { run, preview, operation, inventory };
|
|
2944
|
+
for (const id of validatorIds)
|
|
2945
|
+
this.assertValid(id, context);
|
|
2946
|
+
return validatorIds;
|
|
2947
|
+
}
|
|
2948
|
+
async ensureTerminalCompletion(run, events) {
|
|
2949
|
+
if (events.some(({ type }) => type === "workflow.completed"))
|
|
2950
|
+
return events;
|
|
2951
|
+
const workflow = await this.lockedWorkflowEngine(run);
|
|
2952
|
+
const legacy = events.some((event) => event.type === "task.completed" && event.payload.legacy === true);
|
|
2953
|
+
const artifactAliases = {
|
|
2954
|
+
requirements: "requirements-v1",
|
|
2955
|
+
"workload-decision-manifest": "workload-decision-manifest-v1",
|
|
2956
|
+
architecture: "architecture-v1",
|
|
2957
|
+
"cost-estimate": "cost-estimate-v1",
|
|
2958
|
+
"governance-constraints": "governance-constraints-v1",
|
|
2959
|
+
"policy-property-map": "policy-property-map-v1",
|
|
2960
|
+
"implementation-intent": "implementation-intent-v1",
|
|
2961
|
+
"iac-binding": "iac-binding-v1",
|
|
2962
|
+
"environment-inputs": "environment-inputs-v1",
|
|
2963
|
+
"logical-resource-manifest": "logical-resource-manifest-v1",
|
|
2964
|
+
"iac-handoff": "iac-handoff-v1",
|
|
2965
|
+
"validation-evidence": "validation-evidence-v1",
|
|
2966
|
+
diagnosis: "diagnosis-report-v1",
|
|
2967
|
+
"quality-report": "quality-report-v1",
|
|
2968
|
+
};
|
|
2969
|
+
const artifacts = {};
|
|
2970
|
+
for (const event of events) {
|
|
2971
|
+
const payload = event.payload;
|
|
2972
|
+
if (event.type === "task.completed") {
|
|
2973
|
+
const hashes = payload.artifactHashes ?? {};
|
|
2974
|
+
for (const [kind, hash] of Object.entries(hashes)) {
|
|
2975
|
+
if (typeof hash === "string")
|
|
2976
|
+
artifacts[artifactAliases[kind] ?? kind] = hash;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
if (event.type === "preview.created" && typeof payload.previewHash === "string") {
|
|
2980
|
+
artifacts["deployment-preview-v1"] = payload.previewHash;
|
|
2981
|
+
}
|
|
2982
|
+
if (event.type === "gate.decided" && payload.gate === 4 && typeof payload.approvalHash === "string") {
|
|
2983
|
+
artifacts["approval-evidence-v1"] = payload.approvalHash;
|
|
2984
|
+
}
|
|
2985
|
+
if (event.type === "deployment.completed") {
|
|
2986
|
+
if (typeof payload.operationHash === "string")
|
|
2987
|
+
artifacts["operation-record-v1"] = payload.operationHash;
|
|
2988
|
+
if (typeof payload.inventoryHash === "string")
|
|
2989
|
+
artifacts["resource-inventory-v1"] = payload.inventoryHash;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
const activeValidatorIds = legacy
|
|
2993
|
+
? events.flatMap((event) => {
|
|
2994
|
+
if (event.type !== "task.completed")
|
|
2995
|
+
return [];
|
|
2996
|
+
const ids = event.payload.validatorIds;
|
|
2997
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
2998
|
+
})
|
|
2999
|
+
: workflow.activeValidatorIds({
|
|
3000
|
+
run: { iacTool: run.iacTool, targetScope: run.targetScope },
|
|
3001
|
+
artifacts,
|
|
3002
|
+
});
|
|
3003
|
+
const executedValidatorIds = new Set();
|
|
3004
|
+
const simulatedOmittedValidatorIds = new Set();
|
|
3005
|
+
for (const event of events) {
|
|
3006
|
+
const payload = event.payload;
|
|
3007
|
+
for (const ids of [payload.validatorIds, payload.inventoryValidatorIds]) {
|
|
3008
|
+
if (Array.isArray(ids)) {
|
|
3009
|
+
for (const id of ids)
|
|
3010
|
+
if (typeof id === "string")
|
|
3011
|
+
executedValidatorIds.add(id);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
const omissionNodeId = payload.evidenceMode === "simulated" && payload.provider === "fake"
|
|
3015
|
+
? event.type === "preview.created"
|
|
3016
|
+
? `preview-${run.iacTool}`
|
|
3017
|
+
: event.type === "deployment.completed"
|
|
3018
|
+
? `deploy-${run.iacTool}`
|
|
3019
|
+
: undefined
|
|
3020
|
+
: undefined;
|
|
3021
|
+
const allowedOmissions = new Set(workflow.manifest.nodes
|
|
3022
|
+
.find(({ id }) => id === omissionNodeId)
|
|
3023
|
+
?.validators.filter((id) => workflowValidatorOwnership(id)?.owner === "@apexops/capabilities") ?? []);
|
|
3024
|
+
if (Array.isArray(payload.omittedValidatorIds)) {
|
|
3025
|
+
for (const id of payload.omittedValidatorIds) {
|
|
3026
|
+
if (typeof id === "string" && allowedOmissions.has(id))
|
|
3027
|
+
simulatedOmittedValidatorIds.add(id);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
for (const gate of run.gates) {
|
|
3032
|
+
if (gate.state !== "inherited")
|
|
3033
|
+
continue;
|
|
3034
|
+
const node = workflow.manifest.nodes.find(({ id }) => id === `gate-${gate.gate}`);
|
|
3035
|
+
for (const id of node?.validators ?? []) {
|
|
3036
|
+
if (workflowValidatorOwnership(id)?.boundary === "gate")
|
|
3037
|
+
executedValidatorIds.add(id);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
const terminalIds = workflow.manifest.nodes
|
|
3041
|
+
.find(({ id }) => id === "completed")
|
|
3042
|
+
?.validators.filter((id) => workflowValidatorOwnership(id)?.boundary === "terminal") ?? [];
|
|
3043
|
+
const deduplicatedActiveValidatorIds = [...new Set(activeValidatorIds)].sort();
|
|
3044
|
+
const context = {
|
|
3045
|
+
activeValidatorIds: deduplicatedActiveValidatorIds,
|
|
3046
|
+
executedValidatorIds: [...executedValidatorIds].sort(),
|
|
3047
|
+
simulatedOmittedValidatorIds: [...simulatedOmittedValidatorIds].sort(),
|
|
3048
|
+
};
|
|
3049
|
+
for (const id of terminalIds)
|
|
3050
|
+
this.assertValid(id, context);
|
|
3051
|
+
await this.append(run, "workflow.completed", {
|
|
3052
|
+
validatorIds: terminalIds,
|
|
3053
|
+
activeValidatorIds: deduplicatedActiveValidatorIds,
|
|
3054
|
+
executedValidatorIds: [...context.executedValidatorIds],
|
|
3055
|
+
simulatedOmittedValidatorIds: [...context.simulatedOmittedValidatorIds],
|
|
3056
|
+
...(legacy ? { legacy: true } : {}),
|
|
3057
|
+
});
|
|
3058
|
+
return this.journal(run).replay();
|
|
3059
|
+
}
|
|
3060
|
+
legacyPlanOutputs(run, intent) {
|
|
3061
|
+
const intentHash = sha256Json(intent);
|
|
3062
|
+
const resources = intent.resources;
|
|
3063
|
+
return [
|
|
3064
|
+
{
|
|
3065
|
+
kind: "iac-binding",
|
|
3066
|
+
value: {
|
|
3067
|
+
schemaVersion: CONTRACT_VERSION,
|
|
3068
|
+
projectId: run.projectId,
|
|
3069
|
+
runId: run.runId,
|
|
3070
|
+
track: run.iacTool,
|
|
3071
|
+
intentHash,
|
|
3072
|
+
resourceBindings: Object.fromEntries(resources.map(({ id, type }) => [id, { implementation: type, version: "legacy", parameters: {} }])),
|
|
3073
|
+
},
|
|
3074
|
+
},
|
|
3075
|
+
{
|
|
3076
|
+
kind: "environment-inputs",
|
|
3077
|
+
value: {
|
|
3078
|
+
schemaVersion: CONTRACT_VERSION,
|
|
3079
|
+
projectId: run.projectId,
|
|
3080
|
+
runId: run.runId,
|
|
3081
|
+
environment: run.environment,
|
|
3082
|
+
inputs: {},
|
|
3083
|
+
},
|
|
3084
|
+
},
|
|
3085
|
+
];
|
|
3086
|
+
}
|
|
3087
|
+
async readTask(run, taskId) {
|
|
3088
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u.test(taskId)) {
|
|
3089
|
+
throw new ApexError("APEX_VALIDATION", "Task ID is invalid", EXIT_CODES.validation);
|
|
3090
|
+
}
|
|
3091
|
+
try {
|
|
3092
|
+
return JSON.parse(await readFile(join(this.projects.runDirectory(run.projectId, run.runId), "tasks", `${taskId}.json`), "utf8"));
|
|
3093
|
+
}
|
|
3094
|
+
catch (error) {
|
|
3095
|
+
if (error.code === "ENOENT") {
|
|
3096
|
+
throw new ApexError("APEX_NOT_FOUND", "Task was not found", EXIT_CODES.notFound, undefined, { cause: error });
|
|
3097
|
+
}
|
|
3098
|
+
throw error;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
async refreshTaskHead(run, task) {
|
|
3102
|
+
const expectedHead = await this.journal(run).head();
|
|
3103
|
+
if (expectedHead === null)
|
|
3104
|
+
throw new ApexError("APEX_STALE", "Task journal is empty", EXIT_CODES.stale);
|
|
3105
|
+
await atomicWriteJson(join(this.projects.runDirectory(run.projectId, run.runId), "tasks", `${task.taskId}.json`), {
|
|
3106
|
+
...task,
|
|
3107
|
+
expectedHead,
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
async assertNoSymlinkPath(root, destination) {
|
|
3111
|
+
await mkdir(root, { recursive: true });
|
|
3112
|
+
const rootStat = await lstat(root);
|
|
3113
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
|
|
3114
|
+
throw new ApexError("APEX_VALIDATION", "Code staging root must be a real directory", EXIT_CODES.validation);
|
|
3115
|
+
let current = root;
|
|
3116
|
+
for (const segment of relative(root, destination).split(sep).slice(0, -1)) {
|
|
3117
|
+
current = join(current, segment);
|
|
3118
|
+
if (!(await this.exists(current)))
|
|
3119
|
+
continue;
|
|
3120
|
+
const entry = await lstat(current);
|
|
3121
|
+
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
3122
|
+
throw new ApexError("APEX_VALIDATION", "Code staging path contains a non-directory or symlink", EXIT_CODES.validation);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
async directoryBytes(directory) {
|
|
3126
|
+
if (!(await this.exists(directory)))
|
|
3127
|
+
return 0;
|
|
3128
|
+
let total = 0;
|
|
3129
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
3130
|
+
const path = join(directory, entry.name);
|
|
3131
|
+
if (entry.isSymbolicLink())
|
|
3132
|
+
throw new ApexError("APEX_VALIDATION", "Code staging tree contains a symlink", EXIT_CODES.validation);
|
|
3133
|
+
total += entry.isDirectory() ? await this.directoryBytes(path) : (await stat(path)).size;
|
|
3134
|
+
}
|
|
3135
|
+
return total;
|
|
3136
|
+
}
|
|
3137
|
+
looksLikeIntent(value) {
|
|
3138
|
+
return (value !== null &&
|
|
3139
|
+
typeof value === "object" &&
|
|
3140
|
+
Array.isArray(value.resources) &&
|
|
3141
|
+
Array.isArray(value.outputs) &&
|
|
3142
|
+
typeof value.sourceHashes === "object");
|
|
3143
|
+
}
|
|
3144
|
+
looksLikeBinding(value, track) {
|
|
3145
|
+
return (value !== null &&
|
|
3146
|
+
typeof value === "object" &&
|
|
3147
|
+
value.track === track &&
|
|
3148
|
+
typeof value.intentHash === "string" &&
|
|
3149
|
+
typeof value.resourceBindings === "object");
|
|
3150
|
+
}
|
|
3151
|
+
looksLikeEnvironmentInputs(value) {
|
|
3152
|
+
return (value !== null &&
|
|
3153
|
+
typeof value === "object" &&
|
|
3154
|
+
typeof value.environment === "string" &&
|
|
3155
|
+
typeof value.inputs === "object");
|
|
3156
|
+
}
|
|
3157
|
+
assertValid(name, value) {
|
|
3158
|
+
const result = this.validators.validate(name, value);
|
|
3159
|
+
const issues = result.issues.filter((issue) => !issue.message.startsWith("Unknown format 'date"));
|
|
3160
|
+
const invalidDateTimes = name.startsWith("schema:") || !name.includes(":") ? this.invalidDateTimes(value) : [];
|
|
3161
|
+
if (issues.length > 0 || invalidDateTimes.length > 0) {
|
|
3162
|
+
throw new ApexError("APEX_VALIDATION", `${name} validation failed`, EXIT_CODES.validation, [
|
|
3163
|
+
...issues,
|
|
3164
|
+
...invalidDateTimes.map((path) => ({ path, message: "Invalid ISO date-time" })),
|
|
3165
|
+
]);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
invalidDateTimes(value, path = "") {
|
|
3169
|
+
if (Array.isArray(value)) {
|
|
3170
|
+
return value.flatMap((item, index) => this.invalidDateTimes(item, `${path}/${index}`));
|
|
3171
|
+
}
|
|
3172
|
+
if (value === null || typeof value !== "object")
|
|
3173
|
+
return [];
|
|
3174
|
+
return Object.entries(value).flatMap(([key, item]) => {
|
|
3175
|
+
const itemPath = `${path}/${key}`;
|
|
3176
|
+
if ((key.endsWith("At") || key === "expiresAt") && typeof item === "string") {
|
|
3177
|
+
return Number.isNaN(Date.parse(item)) || new Date(item).toISOString() !== item ? [itemPath] : [];
|
|
3178
|
+
}
|
|
3179
|
+
return this.invalidDateTimes(item, itemPath);
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
async improvements(run) {
|
|
3183
|
+
if (this.improvementPolicy !== undefined) {
|
|
3184
|
+
this.improvementRuntime ??= new ImprovementStore(this.root, this.improvementPolicy, this.clock);
|
|
3185
|
+
return this.improvementRuntime;
|
|
3186
|
+
}
|
|
3187
|
+
const [policyBytes, lockBytes] = await Promise.all([
|
|
3188
|
+
readFile(join(this.root, ".apex", "runtime", "improvement-policy.v1.json")),
|
|
3189
|
+
readFile(join(this.root, ".apex", "apex.lock.json")),
|
|
3190
|
+
]);
|
|
3191
|
+
const lock = JSON.parse(lockBytes.toString("utf8"));
|
|
3192
|
+
this.assertValid("runtime-lock", lock);
|
|
3193
|
+
if (sha256Json(lock) !== run.runtimeLockHash || sha256Bytes(policyBytes) !== lock.improvementPolicyHash) {
|
|
3194
|
+
throw new ApexError("APEX_STALE", "Improvement policy lock is not current", EXIT_CODES.stale);
|
|
3195
|
+
}
|
|
3196
|
+
this.improvementRuntime ??= new ImprovementStore(this.root, JSON.parse(policyBytes.toString("utf8")), this.clock);
|
|
3197
|
+
return this.improvementRuntime;
|
|
3198
|
+
}
|
|
3199
|
+
async latestRun(projectId) {
|
|
3200
|
+
const names = await readdir(join(this.projects.projectDirectory(projectId), "runs"));
|
|
3201
|
+
const runs = await Promise.all(names.map((name) => this.projects.getRun(projectId, name)));
|
|
3202
|
+
const latest = runs.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
3203
|
+
if (latest === undefined)
|
|
3204
|
+
throw new ApexError("APEX_NOT_FOUND", "Project has no runs", EXIT_CODES.notFound);
|
|
3205
|
+
return latest.runId;
|
|
3206
|
+
}
|
|
3207
|
+
async customizationSelection(legacyClientId = "github-copilot-vscode") {
|
|
3208
|
+
try {
|
|
3209
|
+
const value = JSON.parse(await readFile(join(this.root, ".apex", "customizations.selection.json"), "utf8"));
|
|
3210
|
+
if (value.version !== 1 ||
|
|
3211
|
+
!["github-copilot-cli", "github-copilot-vscode"].includes(value.clientId) ||
|
|
3212
|
+
!["bundled-projection", "custom-source"].includes(value.sourceMode) ||
|
|
3213
|
+
(value.sourceMode === "custom-source" && typeof value.customSource !== "string")) {
|
|
3214
|
+
throw new ApexError("APEX_VALIDATION", "Customization selection is invalid", EXIT_CODES.validation);
|
|
3215
|
+
}
|
|
3216
|
+
return value;
|
|
3217
|
+
}
|
|
3218
|
+
catch (error) {
|
|
3219
|
+
if (error.code !== "ENOENT")
|
|
3220
|
+
throw error;
|
|
3221
|
+
return { version: 1, clientId: legacyClientId, sourceMode: "bundled-projection" };
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
async assertCleanInitialization() {
|
|
3225
|
+
if (await this.exists(join(this.root, ".apex")))
|
|
3226
|
+
throw new ApexError("APEX_CONFLICT", "Workspace is already initialized", EXIT_CODES.conflict);
|
|
3227
|
+
}
|
|
3228
|
+
async createRuntimeLock(assets) {
|
|
3229
|
+
const runtimeRoot = join(this.root, ".apex", "runtime");
|
|
3230
|
+
const packs = JSON.parse(await readFile(join(runtimeRoot, "capability-packs.registry.json"), "utf8"));
|
|
3231
|
+
const requiredCapabilityPacks = (packs.packs ?? []).map((pack) => {
|
|
3232
|
+
const id = typeof pack.id === "string" ? pack.id : pack.name;
|
|
3233
|
+
if (typeof id !== "string" || id.length === 0)
|
|
3234
|
+
throw new ApexError("APEX_VALIDATION", "Capability pack has no identifier", EXIT_CODES.validation);
|
|
3235
|
+
return id;
|
|
3236
|
+
});
|
|
3237
|
+
const validatorPath = (await this.exists(join(runtimeRoot, "runtime-bundle.v1.json")))
|
|
3238
|
+
? join(runtimeRoot, "runtime-bundle.v1.json")
|
|
3239
|
+
: join(runtimeRoot, "toolchain.v1.json");
|
|
3240
|
+
const lock = {
|
|
3241
|
+
schemaVersion: CONTRACT_VERSION,
|
|
3242
|
+
cliVersion: APEX_VERSION,
|
|
3243
|
+
customizationVersion: assets.manifest.sources.customizations,
|
|
3244
|
+
workflowHash: sha256Bytes(await readFile(join(runtimeRoot, "workflow.v1.json"))),
|
|
3245
|
+
defaultsHash: sha256Bytes(await readFile(join(runtimeRoot, "defaults.v1.json"))),
|
|
3246
|
+
validatorHash: sha256Bytes(await readFile(validatorPath)),
|
|
3247
|
+
qualityScorecardHash: sha256Bytes(await readFile(join(runtimeRoot, "quality-scorecard.v1.json"))),
|
|
3248
|
+
improvementPolicyHash: sha256Bytes(await readFile(join(runtimeRoot, "improvement-policy.v1.json"))),
|
|
3249
|
+
requiredCapabilityPacks: [...new Set(requiredCapabilityPacks)].sort(),
|
|
3250
|
+
};
|
|
3251
|
+
this.assertValid("runtime-lock", lock);
|
|
3252
|
+
return lock;
|
|
3253
|
+
}
|
|
3254
|
+
capabilityPacks(manifestPath) {
|
|
3255
|
+
return new CapabilityPackManager({
|
|
3256
|
+
root: this.root,
|
|
3257
|
+
manifestPath: manifestPath ?? join(this.root, ".apex", "runtime", "capability-packs.registry.json"),
|
|
3258
|
+
processRunner: this.processRunner,
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
async runtimeLockChecks(run) {
|
|
3262
|
+
const path = join(this.root, ".apex", "apex.lock.json");
|
|
3263
|
+
try {
|
|
3264
|
+
const lock = JSON.parse(await readFile(path, "utf8"));
|
|
3265
|
+
this.assertValid("runtime-lock", lock);
|
|
3266
|
+
const runtimeRoot = join(this.root, ".apex", "runtime");
|
|
3267
|
+
const validatorPath = (await this.exists(join(runtimeRoot, "runtime-bundle.v1.json")))
|
|
3268
|
+
? join(runtimeRoot, "runtime-bundle.v1.json")
|
|
3269
|
+
: join(runtimeRoot, "toolchain.v1.json");
|
|
3270
|
+
const hashes = [
|
|
3271
|
+
{ id: "runtime-lock:workflow", expected: lock.workflowHash, path: join(runtimeRoot, "workflow.v1.json") },
|
|
3272
|
+
{ id: "runtime-lock:defaults", expected: lock.defaultsHash, path: join(runtimeRoot, "defaults.v1.json") },
|
|
3273
|
+
{ id: "runtime-lock:validators", expected: lock.validatorHash, path: validatorPath },
|
|
3274
|
+
{
|
|
3275
|
+
id: "runtime-lock:quality-scorecard",
|
|
3276
|
+
expected: lock.qualityScorecardHash,
|
|
3277
|
+
path: join(runtimeRoot, "quality-scorecard.v1.json"),
|
|
3278
|
+
},
|
|
3279
|
+
{
|
|
3280
|
+
id: "runtime-lock:improvement-policy",
|
|
3281
|
+
expected: lock.improvementPolicyHash,
|
|
3282
|
+
path: join(runtimeRoot, "improvement-policy.v1.json"),
|
|
3283
|
+
},
|
|
3284
|
+
];
|
|
3285
|
+
const checks = await Promise.all(hashes.map(async (item) => {
|
|
3286
|
+
const actual = (await this.exists(item.path)) ? sha256Bytes(await readFile(item.path)) : "missing";
|
|
3287
|
+
return {
|
|
3288
|
+
id: item.id,
|
|
3289
|
+
ok: actual === item.expected,
|
|
3290
|
+
value: actual,
|
|
3291
|
+
remedy: "Run doctor --fix --yes to reinstall bundled runtime files",
|
|
3292
|
+
};
|
|
3293
|
+
}));
|
|
3294
|
+
checks.unshift({
|
|
3295
|
+
id: "runtime-lock:run-binding",
|
|
3296
|
+
ok: sha256Json(lock) === run.runtimeLockHash,
|
|
3297
|
+
value: sha256Json(lock),
|
|
3298
|
+
remedy: "Reinitialize the run against the installed runtime lock",
|
|
3299
|
+
});
|
|
3300
|
+
const packs = JSON.parse(await readFile(join(runtimeRoot, "capability-packs.registry.json"), "utf8"));
|
|
3301
|
+
const available = new Set((packs.packs ?? []).flatMap(({ id }) => (id === undefined ? [] : [id])));
|
|
3302
|
+
checks.push({
|
|
3303
|
+
id: "capability-packs",
|
|
3304
|
+
ok: lock.requiredCapabilityPacks.every((id) => available.has(id)),
|
|
3305
|
+
value: [...available].sort().join(","),
|
|
3306
|
+
remedy: "Run doctor --fix --yes to reinstall the capability pack manifest",
|
|
3307
|
+
});
|
|
3308
|
+
return checks;
|
|
3309
|
+
}
|
|
3310
|
+
catch (error) {
|
|
3311
|
+
return [
|
|
3312
|
+
{
|
|
3313
|
+
id: "runtime-lock",
|
|
3314
|
+
ok: false,
|
|
3315
|
+
value: error instanceof Error ? error.message : "invalid",
|
|
3316
|
+
remedy: "Run doctor --fix --yes to reinstall the runtime lock",
|
|
3317
|
+
},
|
|
3318
|
+
];
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
async ensureLocalGitBoundary(repair = false) {
|
|
3322
|
+
const apexRoot = join(this.root, ".apex");
|
|
3323
|
+
const path = join(apexRoot, ".gitignore");
|
|
3324
|
+
await this.assertSafeDestination(apexRoot, path);
|
|
3325
|
+
const current = await this.readOptional(path);
|
|
3326
|
+
if (current?.toString("utf8") === APEX_GITIGNORE)
|
|
3327
|
+
return;
|
|
3328
|
+
if (current !== undefined && !repair)
|
|
3329
|
+
throw new ApexError("APEX_CONFLICT", "APEX local Git boundary was modified", EXIT_CODES.conflict);
|
|
3330
|
+
await atomicWriteBytes(path, Buffer.from(APEX_GITIGNORE));
|
|
3331
|
+
}
|
|
3332
|
+
async ensureWorkspaceRuntime() {
|
|
3333
|
+
const packagePath = join(this.root, "node_modules", "@apexops", "cli", "package.json");
|
|
3334
|
+
try {
|
|
3335
|
+
const installed = JSON.parse(await readFile(packagePath, "utf8"));
|
|
3336
|
+
if (installed.version === APEX_VERSION)
|
|
3337
|
+
return false;
|
|
3338
|
+
if (typeof installed.version === "string") {
|
|
3339
|
+
throw new ApexError("APEX_CONFLICT", `Workspace has @apexops/cli@${installed.version}; expected ${APEX_VERSION}`, EXIT_CODES.conflict);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
catch (error) {
|
|
3343
|
+
if (error instanceof ApexError)
|
|
3344
|
+
throw error;
|
|
3345
|
+
if (error.code !== "ENOENT")
|
|
3346
|
+
throw error;
|
|
3347
|
+
}
|
|
3348
|
+
try {
|
|
3349
|
+
await this.processRunner.run({
|
|
3350
|
+
executable: "npm",
|
|
3351
|
+
args: [
|
|
3352
|
+
"install",
|
|
3353
|
+
"--save-dev",
|
|
3354
|
+
"--save-exact",
|
|
3355
|
+
"--ignore-scripts",
|
|
3356
|
+
"--no-audit",
|
|
3357
|
+
"--no-fund",
|
|
3358
|
+
`@apexops/cli@${APEX_VERSION}`,
|
|
3359
|
+
],
|
|
3360
|
+
cwd: this.root,
|
|
3361
|
+
timeoutMs: 120_000,
|
|
3362
|
+
maxOutputBytes: 1_048_576,
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
catch (error) {
|
|
3366
|
+
throw new ApexError("APEX_INTERNAL", `Unable to install @apexops/cli@${APEX_VERSION} in the workspace`, EXIT_CODES.internal, undefined, { cause: error });
|
|
3367
|
+
}
|
|
3368
|
+
return true;
|
|
3369
|
+
}
|
|
3370
|
+
async ensureWorkspaceGitRepository(createRepository) {
|
|
3371
|
+
const gitPath = join(this.root, ".git");
|
|
3372
|
+
if (await this.pathExistsLstat(gitPath))
|
|
3373
|
+
return;
|
|
3374
|
+
if (!createRepository) {
|
|
3375
|
+
throw new ApexError("APEX_USAGE", "Bootstrap requires a Git repository; rerun with createRepository: true and --yes to initialize one", EXIT_CODES.usage);
|
|
3376
|
+
}
|
|
3377
|
+
try {
|
|
3378
|
+
await this.processRunner.run({
|
|
3379
|
+
executable: "git",
|
|
3380
|
+
args: ["init"],
|
|
3381
|
+
cwd: this.root,
|
|
3382
|
+
timeoutMs: 30_000,
|
|
3383
|
+
maxOutputBytes: 64 * 1024,
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
catch (error) {
|
|
3387
|
+
throw new ApexError("APEX_INTERNAL", "Unable to initialize the workspace Git repository", EXIT_CODES.internal, undefined, { cause: error });
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
profilePaths() {
|
|
3391
|
+
return {
|
|
3392
|
+
agent: join(this.profileRoot, PROFILE_BOOTSTRAP_FILENAME),
|
|
3393
|
+
receipt: join(this.profileRoot, PROFILE_BOOTSTRAP_RECEIPT),
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
async ensureProfileRoot() {
|
|
3397
|
+
await mkdir(this.profileRoot, { recursive: true });
|
|
3398
|
+
const metadata = await lstat(this.profileRoot);
|
|
3399
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
3400
|
+
throw new ApexError("APEX_VALIDATION", "Profile agent directory must be a real directory", EXIT_CODES.validation);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
async readProfileReceipt(path) {
|
|
3404
|
+
const bytes = await this.readProfileOptional(path);
|
|
3405
|
+
if (bytes === undefined)
|
|
3406
|
+
return undefined;
|
|
3407
|
+
const receipt = JSON.parse(bytes.toString("utf8"));
|
|
3408
|
+
if (receipt.version !== 1 ||
|
|
3409
|
+
typeof receipt.packageVersion !== "string" ||
|
|
3410
|
+
!/^[a-f0-9]{64}$/.test(receipt.contentHash)) {
|
|
3411
|
+
throw new ApexError("APEX_VALIDATION", "Profile APEX bootstrap receipt is invalid", EXIT_CODES.validation);
|
|
3412
|
+
}
|
|
3413
|
+
return receipt;
|
|
3414
|
+
}
|
|
3415
|
+
async readProfileOptional(path) {
|
|
3416
|
+
try {
|
|
3417
|
+
const metadata = await lstat(path);
|
|
3418
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
3419
|
+
throw new ApexError("APEX_VALIDATION", "Profile APEX bootstrap path must be a regular file", EXIT_CODES.validation);
|
|
3420
|
+
}
|
|
3421
|
+
return await readFile(path);
|
|
3422
|
+
}
|
|
3423
|
+
catch (error) {
|
|
3424
|
+
if (error.code === "ENOENT")
|
|
3425
|
+
return undefined;
|
|
3426
|
+
throw error;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
async localGitBoundaryCheck() {
|
|
3430
|
+
const path = join(this.root, ".apex", ".gitignore");
|
|
3431
|
+
try {
|
|
3432
|
+
if (!(await this.pathExistsLstat(path))) {
|
|
3433
|
+
return {
|
|
3434
|
+
id: "local-git-boundary",
|
|
3435
|
+
ok: false,
|
|
3436
|
+
value: "missing",
|
|
3437
|
+
remedy: "Run doctor --fix --yes to restore the APEX local Git boundary",
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
await this.assertSafeExistingPath(join(this.root, ".apex"), path);
|
|
3441
|
+
const actual = await readFile(path);
|
|
3442
|
+
const actualHash = sha256Bytes(actual);
|
|
3443
|
+
return {
|
|
3444
|
+
id: "local-git-boundary",
|
|
3445
|
+
ok: actual.toString("utf8") === APEX_GITIGNORE,
|
|
3446
|
+
value: actualHash,
|
|
3447
|
+
remedy: "Run doctor --fix --yes to restore the APEX local Git boundary",
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
catch (error) {
|
|
3451
|
+
return {
|
|
3452
|
+
id: "local-git-boundary",
|
|
3453
|
+
ok: false,
|
|
3454
|
+
value: error instanceof Error ? error.message : "invalid",
|
|
3455
|
+
remedy: "Run doctor --fix --yes to restore the APEX local Git boundary",
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
async managedFileChecks() {
|
|
3460
|
+
const path = join(this.root, ".apex", "customizations.lock.json");
|
|
3461
|
+
try {
|
|
3462
|
+
const lock = JSON.parse(await readFile(path, "utf8"));
|
|
3463
|
+
const workspaceFiles = lock.files.map((file) => ({ ...file, root: this.root }));
|
|
3464
|
+
const runtimeRoot = join(this.root, ".apex", "runtime");
|
|
3465
|
+
const runtimeFiles = lock.runtime.map((file) => ({ ...file, root: runtimeRoot }));
|
|
3466
|
+
return await Promise.all([...workspaceFiles, ...runtimeFiles].map(async (file) => {
|
|
3467
|
+
if (!this.safeManagedRelativePath(file.path)) {
|
|
3468
|
+
throw new ApexError("APEX_VALIDATION", "Managed customization path is unsafe", EXIT_CODES.validation);
|
|
3469
|
+
}
|
|
3470
|
+
const destination = resolve(file.root, file.path);
|
|
3471
|
+
await this.assertSafeDestination(file.root, destination);
|
|
3472
|
+
const actual = (await this.exists(destination)) ? sha256Bytes(await readFile(destination)) : "missing";
|
|
3473
|
+
const hashesValid = [file.sourceHash, file.baseHash, file.currentHash].every((hash) => /^[0-9a-f]{64}$/.test(hash));
|
|
3474
|
+
return {
|
|
3475
|
+
id: `managed:${relative(this.root, destination)}`,
|
|
3476
|
+
ok: hashesValid && actual === file.currentHash,
|
|
3477
|
+
value: actual,
|
|
3478
|
+
remedy: "Run doctor --fix --yes to reinstall bundled managed files",
|
|
3479
|
+
};
|
|
3480
|
+
}));
|
|
3481
|
+
}
|
|
3482
|
+
catch (error) {
|
|
3483
|
+
return [
|
|
3484
|
+
{
|
|
3485
|
+
id: "managed-files",
|
|
3486
|
+
ok: false,
|
|
3487
|
+
value: error instanceof Error ? error.message : "invalid",
|
|
3488
|
+
remedy: "Run doctor --fix --yes to reinstall bundled managed files",
|
|
3489
|
+
},
|
|
3490
|
+
];
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
async evidenceStore(kind, contentType) {
|
|
3494
|
+
const defaults = JSON.parse(await readFile(join(this.root, ".apex", "runtime", "defaults.v1.json"), "utf8"));
|
|
3495
|
+
const maxBytes = defaults.evidence?.budgets?.perEntryBytes;
|
|
3496
|
+
if (!Number.isInteger(maxBytes) || maxBytes === undefined || maxBytes < 1)
|
|
3497
|
+
throw new ApexError("APEX_VALIDATION", "Runtime evidence byte budget is invalid", EXIT_CODES.validation);
|
|
3498
|
+
const kinds = kind === undefined
|
|
3499
|
+
? {}
|
|
3500
|
+
: {
|
|
3501
|
+
[kind]: {
|
|
3502
|
+
contentTypes: [contentType],
|
|
3503
|
+
maxBytes,
|
|
3504
|
+
retention: defaults.evidence?.immutableKinds?.includes(kind)
|
|
3505
|
+
? "immutable"
|
|
3506
|
+
: "project",
|
|
3507
|
+
},
|
|
3508
|
+
};
|
|
3509
|
+
return new EvidenceStore(this.root, new EvidencePolicy({ kinds }));
|
|
3510
|
+
}
|
|
3511
|
+
async pathExecutableExists(executable) {
|
|
3512
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter).filter((item) => item.length > 0)) {
|
|
3513
|
+
try {
|
|
3514
|
+
await access(join(directory, executable), constants.X_OK);
|
|
3515
|
+
return true;
|
|
3516
|
+
}
|
|
3517
|
+
catch (error) {
|
|
3518
|
+
if (error.code !== "ENOENT" && error.code !== "EACCES")
|
|
3519
|
+
throw error;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
return false;
|
|
3523
|
+
}
|
|
3524
|
+
async installCustomizations(sourcePath, update, runtimeSource, repair = false, clientId = "github-copilot-vscode") {
|
|
3525
|
+
await this.recoverCustomizationTransaction();
|
|
3526
|
+
const source = resolve(sourcePath);
|
|
3527
|
+
const sourceStat = await lstat(source);
|
|
3528
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink())
|
|
3529
|
+
throw new ApexError("APEX_VALIDATION", "Customizations source must be a real directory", EXIT_CODES.validation);
|
|
3530
|
+
await this.assertSafeExistingPath(source, source);
|
|
3531
|
+
const lockPath = join(this.root, ".apex", "customizations.lock.json");
|
|
3532
|
+
let previous;
|
|
3533
|
+
if (update)
|
|
3534
|
+
previous = JSON.parse(await readFile(lockPath, "utf8"));
|
|
3535
|
+
const transactionRoot = join(this.root, ".apex", "local", `update-${this.idSource()}`);
|
|
3536
|
+
const entries = [];
|
|
3537
|
+
const prepare = async (sourceRoot, destinationRoot, oldFiles, label) => {
|
|
3538
|
+
const sourceFiles = await this.walkFiles(sourceRoot);
|
|
3539
|
+
const managed = [];
|
|
3540
|
+
const incomingPaths = new Set();
|
|
3541
|
+
for (const absoluteSource of sourceFiles) {
|
|
3542
|
+
const path = relative(sourceRoot, absoluteSource);
|
|
3543
|
+
incomingPaths.add(path);
|
|
3544
|
+
const destination = resolve(destinationRoot, path);
|
|
3545
|
+
if (destination !== destinationRoot && !destination.startsWith(`${destinationRoot}${sep}`))
|
|
3546
|
+
throw new ApexError("APEX_VALIDATION", `${label} destination escapes its root`, EXIT_CODES.validation);
|
|
3547
|
+
await this.assertSafeDestination(destinationRoot, destination);
|
|
3548
|
+
const incoming = await readFile(absoluteSource);
|
|
3549
|
+
const sourceHash = sha256Bytes(incoming);
|
|
3550
|
+
const old = oldFiles.find((file) => file.path === path);
|
|
3551
|
+
let output = incoming;
|
|
3552
|
+
const existed = await this.pathExistsLstat(destination);
|
|
3553
|
+
if (existed) {
|
|
3554
|
+
const current = await readFile(destination);
|
|
3555
|
+
const currentHash = sha256Bytes(current);
|
|
3556
|
+
if (old === undefined && currentHash !== sourceHash)
|
|
3557
|
+
throw new ApexError("APEX_CONFLICT", `Existing file conflicts with managed ${label}: ${path}`, EXIT_CODES.conflict);
|
|
3558
|
+
if (!repair && old !== undefined && currentHash !== old.currentHash && currentHash !== old.baseHash) {
|
|
3559
|
+
const base = await this.readManagedBase(old.baseRef);
|
|
3560
|
+
const merged = base === undefined ? undefined : this.mergeText(base, current, incoming);
|
|
3561
|
+
if (merged === undefined)
|
|
3562
|
+
throw new ApexError("APEX_CONFLICT", `Managed ${label} conflict: ${path}`, EXIT_CODES.conflict);
|
|
3563
|
+
output = Buffer.from(merged);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
const staged = join(transactionRoot, "incoming", label, path);
|
|
3567
|
+
await atomicWriteBytes(staged, output);
|
|
3568
|
+
const backup = existed ? join(transactionRoot, "backup", label, path) : undefined;
|
|
3569
|
+
if (backup !== undefined)
|
|
3570
|
+
await atomicWriteBytes(backup, await readFile(destination));
|
|
3571
|
+
entries.push({ destination, staged, ...(backup === undefined ? {} : { backup }), existed });
|
|
3572
|
+
const baseRef = join(".apex", "customization-bases", sourceHash, label, path);
|
|
3573
|
+
const baseDestination = join(this.root, baseRef);
|
|
3574
|
+
await this.assertSafeDestination(this.root, baseDestination);
|
|
3575
|
+
entries.push({
|
|
3576
|
+
destination: baseDestination,
|
|
3577
|
+
staged: join(transactionRoot, "bases", sourceHash, label, path),
|
|
3578
|
+
existed: await this.pathExistsLstat(baseDestination),
|
|
3579
|
+
});
|
|
3580
|
+
await atomicWriteBytes(entries.at(-1).staged, incoming);
|
|
3581
|
+
managed.push({ path, sourceHash, baseHash: sourceHash, currentHash: sha256Bytes(output), baseRef });
|
|
3582
|
+
}
|
|
3583
|
+
for (const old of oldFiles.filter((file) => !incomingPaths.has(file.path))) {
|
|
3584
|
+
const destination = resolve(destinationRoot, old.path);
|
|
3585
|
+
await this.assertSafeDestination(destinationRoot, destination);
|
|
3586
|
+
if (!(await this.pathExistsLstat(destination)))
|
|
3587
|
+
continue;
|
|
3588
|
+
const currentHash = sha256Bytes(await readFile(destination));
|
|
3589
|
+
if (currentHash !== old.currentHash && currentHash !== old.baseHash)
|
|
3590
|
+
throw new ApexError("APEX_CONFLICT", `Removed managed ${label} was locally modified: ${old.path}`, EXIT_CODES.conflict);
|
|
3591
|
+
const backup = join(transactionRoot, "backup", label, old.path);
|
|
3592
|
+
await atomicWriteBytes(backup, await readFile(destination));
|
|
3593
|
+
entries.push({ destination, backup, existed: true, remove: true });
|
|
3594
|
+
}
|
|
3595
|
+
return managed;
|
|
3596
|
+
};
|
|
3597
|
+
const managed = await prepare(source, this.root, previous?.files ?? [], "customization");
|
|
3598
|
+
const runtime = runtimeSource === undefined
|
|
3599
|
+
? (previous?.runtime ?? [])
|
|
3600
|
+
: await prepare(resolve(runtimeSource), join(this.root, ".apex", "runtime"), previous?.runtime ?? [], "runtime");
|
|
3601
|
+
let previousLockRef;
|
|
3602
|
+
if (previous !== undefined) {
|
|
3603
|
+
const previousHash = sha256Json(previous);
|
|
3604
|
+
previousLockRef = join(".apex", "customization-bases", "locks", `${previousHash}.json`);
|
|
3605
|
+
const previousLockDestination = join(this.root, previousLockRef);
|
|
3606
|
+
if (!(await this.pathExistsLstat(previousLockDestination))) {
|
|
3607
|
+
const stagedPreviousLock = join(transactionRoot, "previous-lock.json");
|
|
3608
|
+
await atomicWriteJson(stagedPreviousLock, previous);
|
|
3609
|
+
entries.push({ destination: previousLockDestination, staged: stagedPreviousLock, existed: false });
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
const nextLock = {
|
|
3613
|
+
version: 1,
|
|
3614
|
+
source,
|
|
3615
|
+
clientId,
|
|
3616
|
+
runtime,
|
|
3617
|
+
files: managed,
|
|
3618
|
+
...(previousLockRef === undefined ? {} : { previousLockRef }),
|
|
3619
|
+
};
|
|
3620
|
+
const stagedLock = join(transactionRoot, "customizations.lock.json");
|
|
3621
|
+
await atomicWriteJson(stagedLock, nextLock);
|
|
3622
|
+
const lockExisted = await this.pathExistsLstat(lockPath);
|
|
3623
|
+
const lockBackup = lockExisted ? join(transactionRoot, "backup", "customizations.lock.json") : undefined;
|
|
3624
|
+
if (lockBackup !== undefined)
|
|
3625
|
+
await atomicWriteBytes(lockBackup, await readFile(lockPath));
|
|
3626
|
+
entries.push({
|
|
3627
|
+
destination: lockPath,
|
|
3628
|
+
staged: stagedLock,
|
|
3629
|
+
...(lockBackup === undefined ? {} : { backup: lockBackup }),
|
|
3630
|
+
existed: lockExisted,
|
|
3631
|
+
});
|
|
3632
|
+
const transactionPath = join(transactionRoot, "transaction.json");
|
|
3633
|
+
await atomicWriteJson(transactionPath, {
|
|
3634
|
+
version: 1,
|
|
3635
|
+
status: "applying",
|
|
3636
|
+
entries,
|
|
3637
|
+
});
|
|
3638
|
+
await atomicWriteJson(join(this.root, ".apex", "local", "customization-transaction.json"), { transactionPath });
|
|
3639
|
+
try {
|
|
3640
|
+
for (const [index, entry] of entries.entries()) {
|
|
3641
|
+
await this.customizationFailureInjector?.(index, entry.destination);
|
|
3642
|
+
if (entry.remove)
|
|
3643
|
+
await rm(entry.destination, { force: true });
|
|
3644
|
+
else {
|
|
3645
|
+
await mkdir(resolve(entry.destination, ".."), { recursive: true });
|
|
3646
|
+
await rename(entry.staged, entry.destination);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
await rm(join(this.root, ".apex", "local", "customization-transaction.json"), { force: true });
|
|
3650
|
+
await rm(transactionRoot, { recursive: true, force: true });
|
|
3651
|
+
return managed.map(({ path }) => path);
|
|
3652
|
+
}
|
|
3653
|
+
catch (error) {
|
|
3654
|
+
await this.rollbackCustomizationTransaction({ version: 1, status: "applying", entries });
|
|
3655
|
+
await rm(join(this.root, ".apex", "local", "customization-transaction.json"), { force: true });
|
|
3656
|
+
await rm(transactionRoot, { recursive: true, force: true });
|
|
3657
|
+
throw error;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
async installCapabilityAssets(assets) {
|
|
3661
|
+
const runtimeRoot = join(this.root, ".apex", "runtime");
|
|
3662
|
+
const destination = join(runtimeRoot, "capability-packs");
|
|
3663
|
+
await this.assertSafeExistingPath(assets.capabilityPacks, assets.capabilityPacks);
|
|
3664
|
+
await rm(destination, { recursive: true, force: true });
|
|
3665
|
+
await cp(assets.capabilityPacks, destination, {
|
|
3666
|
+
recursive: true,
|
|
3667
|
+
dereference: false,
|
|
3668
|
+
errorOnExist: true,
|
|
3669
|
+
force: false,
|
|
3670
|
+
});
|
|
3671
|
+
await rename(join(destination, "registry.v1.json"), join(runtimeRoot, "capability-packs.registry.json"));
|
|
3672
|
+
}
|
|
3673
|
+
async recoverCustomizationTransaction() {
|
|
3674
|
+
const pointer = join(this.root, ".apex", "local", "customization-transaction.json");
|
|
3675
|
+
try {
|
|
3676
|
+
const { transactionPath } = JSON.parse(await readFile(pointer, "utf8"));
|
|
3677
|
+
const localRoot = join(this.root, ".apex", "local");
|
|
3678
|
+
const resolvedTransaction = resolve(transactionPath);
|
|
3679
|
+
await this.assertSafeExistingPath(localRoot, resolvedTransaction);
|
|
3680
|
+
const transaction = JSON.parse(await readFile(resolvedTransaction, "utf8"));
|
|
3681
|
+
await this.rollbackCustomizationTransaction(transaction);
|
|
3682
|
+
await rm(resolve(resolvedTransaction, ".."), { recursive: true, force: true });
|
|
3683
|
+
await rm(pointer, { force: true });
|
|
3684
|
+
}
|
|
3685
|
+
catch (error) {
|
|
3686
|
+
if (error.code !== "ENOENT")
|
|
3687
|
+
throw error;
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
async rollbackCustomizationTransaction(transaction) {
|
|
3691
|
+
for (const entry of [...transaction.entries].reverse()) {
|
|
3692
|
+
await this.assertSafeDestination(this.root, resolve(entry.destination));
|
|
3693
|
+
if (entry.backup !== undefined && (await this.pathExistsLstat(entry.backup))) {
|
|
3694
|
+
await this.assertSafeExistingPath(join(this.root, ".apex", "local"), resolve(entry.backup));
|
|
3695
|
+
}
|
|
3696
|
+
if (entry.existed && entry.backup !== undefined && (await this.pathExistsLstat(entry.backup)))
|
|
3697
|
+
await atomicWriteBytes(entry.destination, await readFile(entry.backup));
|
|
3698
|
+
else if (!entry.existed)
|
|
3699
|
+
await rm(entry.destination, { force: true });
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
safeManagedRelativePath(path) {
|
|
3703
|
+
return (path.length > 0 &&
|
|
3704
|
+
!path.includes("\\") &&
|
|
3705
|
+
!path.includes("\0") &&
|
|
3706
|
+
!isAbsolute(path) &&
|
|
3707
|
+
path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== ".."));
|
|
3708
|
+
}
|
|
3709
|
+
async readManagedBase(baseRef) {
|
|
3710
|
+
if (baseRef === undefined)
|
|
3711
|
+
return undefined;
|
|
3712
|
+
if (!this.safeManagedRelativePath(baseRef)) {
|
|
3713
|
+
throw new ApexError("APEX_VALIDATION", "Managed customization base path is unsafe", EXIT_CODES.validation);
|
|
3714
|
+
}
|
|
3715
|
+
const path = resolve(this.root, baseRef);
|
|
3716
|
+
if (!(await this.pathExistsLstat(path)))
|
|
3717
|
+
return undefined;
|
|
3718
|
+
await this.assertSafeExistingPath(this.root, path);
|
|
3719
|
+
return await readFile(path);
|
|
3720
|
+
}
|
|
3721
|
+
mergeText(base, local, incoming) {
|
|
3722
|
+
if ([base, local, incoming].some((value) => value.includes(0)))
|
|
3723
|
+
return undefined;
|
|
3724
|
+
const baseText = base.toString("utf8");
|
|
3725
|
+
const localText = local.toString("utf8");
|
|
3726
|
+
const incomingText = incoming.toString("utf8");
|
|
3727
|
+
if (localText === baseText)
|
|
3728
|
+
return incoming;
|
|
3729
|
+
if (incomingText === baseText)
|
|
3730
|
+
return local;
|
|
3731
|
+
const baseLines = baseText.split("\n");
|
|
3732
|
+
const localLines = localText.split("\n");
|
|
3733
|
+
const incomingLines = incomingText.split("\n");
|
|
3734
|
+
if (baseLines.length !== localLines.length || baseLines.length !== incomingLines.length)
|
|
3735
|
+
return undefined;
|
|
3736
|
+
const merged = baseLines.map((line, index) => {
|
|
3737
|
+
const localLine = localLines[index];
|
|
3738
|
+
const incomingLine = incomingLines[index];
|
|
3739
|
+
if (localLine !== line && incomingLine !== line && localLine !== incomingLine)
|
|
3740
|
+
return undefined;
|
|
3741
|
+
return localLine !== line ? localLine : incomingLine;
|
|
3742
|
+
});
|
|
3743
|
+
return merged.includes(undefined) ? undefined : Buffer.from(merged.join("\n"));
|
|
3744
|
+
}
|
|
3745
|
+
async assertSafeDestination(root, destination) {
|
|
3746
|
+
const resolvedRoot = resolve(root);
|
|
3747
|
+
const resolvedDestination = resolve(destination);
|
|
3748
|
+
if (resolvedDestination !== resolvedRoot && !resolvedDestination.startsWith(`${resolvedRoot}${sep}`))
|
|
3749
|
+
throw new ApexError("APEX_VALIDATION", "Managed destination escapes its root", EXIT_CODES.validation);
|
|
3750
|
+
let current = resolvedRoot;
|
|
3751
|
+
if (await this.pathExistsLstat(current))
|
|
3752
|
+
await this.assertSafeExistingPath(resolvedRoot, current);
|
|
3753
|
+
for (const segment of relative(resolvedRoot, resolvedDestination).split(sep)) {
|
|
3754
|
+
if (!segment)
|
|
3755
|
+
continue;
|
|
3756
|
+
current = join(current, segment);
|
|
3757
|
+
if (await this.pathExistsLstat(current))
|
|
3758
|
+
await this.assertSafeExistingPath(resolvedRoot, current);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
async assertSafeExistingPath(root, path) {
|
|
3762
|
+
const entry = await lstat(path);
|
|
3763
|
+
if (entry.isSymbolicLink())
|
|
3764
|
+
throw new ApexError("APEX_VALIDATION", `Managed path contains a symlink: ${path}`, EXIT_CODES.validation);
|
|
3765
|
+
const actual = await realpath(path);
|
|
3766
|
+
const actualRoot = await realpath(root);
|
|
3767
|
+
if (actual !== actualRoot && !actual.startsWith(`${actualRoot}${sep}`))
|
|
3768
|
+
throw new ApexError("APEX_VALIDATION", `Managed path escapes its root: ${path}`, EXIT_CODES.validation);
|
|
3769
|
+
}
|
|
3770
|
+
async pathExistsLstat(path) {
|
|
3771
|
+
try {
|
|
3772
|
+
await lstat(path);
|
|
3773
|
+
return true;
|
|
3774
|
+
}
|
|
3775
|
+
catch (error) {
|
|
3776
|
+
if (error.code === "ENOENT")
|
|
3777
|
+
return false;
|
|
3778
|
+
throw error;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
async readOptional(path) {
|
|
3782
|
+
try {
|
|
3783
|
+
return await readFile(path);
|
|
3784
|
+
}
|
|
3785
|
+
catch (error) {
|
|
3786
|
+
if (error.code === "ENOENT")
|
|
3787
|
+
return undefined;
|
|
3788
|
+
throw error;
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
async walkFiles(directory) {
|
|
3792
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
3793
|
+
const files = [];
|
|
3794
|
+
for (const entry of entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))) {
|
|
3795
|
+
const path = join(directory, entry.name);
|
|
3796
|
+
if (entry.isSymbolicLink())
|
|
3797
|
+
throw new ApexError("APEX_VALIDATION", `Customization source contains a symlink: ${path}`, EXIT_CODES.validation);
|
|
3798
|
+
if (entry.isDirectory())
|
|
3799
|
+
files.push(...(await this.walkFiles(path)));
|
|
3800
|
+
else if (entry.isFile())
|
|
3801
|
+
files.push(path);
|
|
3802
|
+
}
|
|
3803
|
+
return files;
|
|
3804
|
+
}
|
|
3805
|
+
async exists(path) {
|
|
3806
|
+
try {
|
|
3807
|
+
await stat(path);
|
|
3808
|
+
return true;
|
|
3809
|
+
}
|
|
3810
|
+
catch (error) {
|
|
3811
|
+
if (error.code === "ENOENT")
|
|
3812
|
+
return false;
|
|
3813
|
+
throw error;
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
//# sourceMappingURL=service.js.map
|