@hunter-harness/workflow-harness 0.2.43 → 0.2.44
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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/general/claude-code/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/claude-code/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/claude-code/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/claude-code/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/claude-code/contracts/service-session.schema.json +147 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +12 -0
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +267 -22
- package/harness/bundles/general/claude-code/scripts/harness_change.py +137 -2
- package/harness/bundles/general/claude-code/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/general/claude-code/scripts/harness_environment.py +55 -25
- package/harness/bundles/general/claude-code/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +289 -52
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +5 -0
- package/harness/bundles/general/claude-code/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/claude-code/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +244 -4
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +543 -132
- package/harness/bundles/general/claude-code/scripts/harness_service.py +1469 -194
- package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/general/claude-code/scripts/harness_verification.py +79 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/general/codebuddy/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/codebuddy/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/codebuddy/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/codebuddy/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/codebuddy/contracts/service-session.schema.json +147 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +12 -0
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +267 -22
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +137 -2
- package/harness/bundles/general/codebuddy/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/general/codebuddy/scripts/harness_environment.py +55 -25
- package/harness/bundles/general/codebuddy/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +289 -52
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +5 -0
- package/harness/bundles/general/codebuddy/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/codebuddy/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +244 -4
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +543 -132
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +1469 -194
- package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/general/codebuddy/scripts/harness_verification.py +79 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/general/codex/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/codex/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/codex/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/codex/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/codex/contracts/service-session.schema.json +147 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +12 -0
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +267 -22
- package/harness/bundles/general/codex/scripts/harness_change.py +137 -2
- package/harness/bundles/general/codex/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/general/codex/scripts/harness_environment.py +55 -25
- package/harness/bundles/general/codex/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/codex/scripts/harness_integration.py +289 -52
- package/harness/bundles/general/codex/scripts/harness_ledger.py +5 -0
- package/harness/bundles/general/codex/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/codex/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/codex/scripts/harness_profile.py +244 -4
- package/harness/bundles/general/codex/scripts/harness_runtime.py +543 -132
- package/harness/bundles/general/codex/scripts/harness_service.py +1469 -194
- package/harness/bundles/general/codex/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/general/codex/scripts/harness_verification.py +79 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/general/cursor/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/cursor/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/cursor/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/cursor/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/cursor/contracts/service-session.schema.json +147 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +12 -0
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +267 -22
- package/harness/bundles/general/cursor/scripts/harness_change.py +137 -2
- package/harness/bundles/general/cursor/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/general/cursor/scripts/harness_environment.py +55 -25
- package/harness/bundles/general/cursor/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/cursor/scripts/harness_integration.py +289 -52
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +5 -0
- package/harness/bundles/general/cursor/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/cursor/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/cursor/scripts/harness_profile.py +244 -4
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +543 -132
- package/harness/bundles/general/cursor/scripts/harness_service.py +1469 -194
- package/harness/bundles/general/cursor/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/general/cursor/scripts/harness_verification.py +79 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/java/claude-code/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/claude-code/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/claude-code/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/claude-code/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/claude-code/contracts/service-session.schema.json +147 -0
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +267 -22
- package/harness/bundles/java/claude-code/scripts/harness_change.py +137 -2
- package/harness/bundles/java/claude-code/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/java/claude-code/scripts/harness_environment.py +55 -25
- package/harness/bundles/java/claude-code/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +289 -52
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +5 -0
- package/harness/bundles/java/claude-code/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/claude-code/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +244 -4
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +543 -132
- package/harness/bundles/java/claude-code/scripts/harness_service.py +1469 -194
- package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/java/claude-code/scripts/harness_verification.py +79 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/java/codebuddy/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/codebuddy/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/codebuddy/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/codebuddy/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/codebuddy/contracts/service-session.schema.json +147 -0
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +267 -22
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +137 -2
- package/harness/bundles/java/codebuddy/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/java/codebuddy/scripts/harness_environment.py +55 -25
- package/harness/bundles/java/codebuddy/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +289 -52
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +5 -0
- package/harness/bundles/java/codebuddy/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/codebuddy/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +244 -4
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +543 -132
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +1469 -194
- package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/java/codebuddy/scripts/harness_verification.py +79 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/java/codex/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/codex/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/codex/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/codex/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/codex/contracts/service-session.schema.json +147 -0
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +267 -22
- package/harness/bundles/java/codex/scripts/harness_change.py +137 -2
- package/harness/bundles/java/codex/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/java/codex/scripts/harness_environment.py +55 -25
- package/harness/bundles/java/codex/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/codex/scripts/harness_integration.py +289 -52
- package/harness/bundles/java/codex/scripts/harness_ledger.py +5 -0
- package/harness/bundles/java/codex/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/codex/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/codex/scripts/harness_profile.py +244 -4
- package/harness/bundles/java/codex/scripts/harness_runtime.py +543 -132
- package/harness/bundles/java/codex/scripts/harness_service.py +1469 -194
- package/harness/bundles/java/codex/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/java/codex/scripts/harness_verification.py +79 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/build-profile-v3.schema.json +100 -0
- package/harness/bundles/java/cursor/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/cursor/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/cursor/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/cursor/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/cursor/contracts/service-session.schema.json +147 -0
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +12 -0
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +267 -22
- package/harness/bundles/java/cursor/scripts/harness_change.py +137 -2
- package/harness/bundles/java/cursor/scripts/harness_efficiency.py +153 -0
- package/harness/bundles/java/cursor/scripts/harness_environment.py +55 -25
- package/harness/bundles/java/cursor/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/cursor/scripts/harness_integration.py +289 -52
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +5 -0
- package/harness/bundles/java/cursor/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/cursor/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/cursor/scripts/harness_profile.py +244 -4
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +543 -132
- package/harness/bundles/java/cursor/scripts/harness_service.py +1469 -194
- package/harness/bundles/java/cursor/scripts/harness_test_runner.py +107 -117
- package/harness/bundles/java/cursor/scripts/harness_verification.py +79 -0
- package/harness/manifests/general/claude-code.json +58 -26
- package/harness/manifests/general/codebuddy.json +58 -26
- package/harness/manifests/general/codex.json +58 -26
- package/harness/manifests/general/cursor.json +58 -26
- package/harness/manifests/java/claude-code.json +60 -28
- package/harness/manifests/java/codebuddy.json +60 -28
- package/harness/manifests/java/codex.json +60 -28
- package/harness/manifests/java/cursor.json +60 -28
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Canonical managed execution contract reader.
|
|
3
|
+
|
|
4
|
+
The JSON Schema files under ``harness/contracts`` are the publication source
|
|
5
|
+
of truth. This stdlib-only reader applies the same security-sensitive
|
|
6
|
+
invariants at runtime without accepting caller-provided identity assertions as
|
|
7
|
+
observed OS facts.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SHA256_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
18
|
+
PROVENANCE = {"OBSERVED", "ATTESTED", "UNAVAILABLE"}
|
|
19
|
+
RUN_STATUSES = {
|
|
20
|
+
"PREPARING",
|
|
21
|
+
"STARTING",
|
|
22
|
+
"RUNNING",
|
|
23
|
+
"FINALIZING",
|
|
24
|
+
"OK",
|
|
25
|
+
"FAIL",
|
|
26
|
+
"CANCELLED",
|
|
27
|
+
"INCOMPLETE",
|
|
28
|
+
}
|
|
29
|
+
SERVICE_STATUSES = {
|
|
30
|
+
"STARTING",
|
|
31
|
+
"READY",
|
|
32
|
+
"STOPPING",
|
|
33
|
+
"STOPPED",
|
|
34
|
+
"FAILED",
|
|
35
|
+
"STALE_IDENTITY_MISMATCH",
|
|
36
|
+
"RETIRED",
|
|
37
|
+
}
|
|
38
|
+
MANAGED_EXECUTION_EXIT_CODE_BY_REASON = {
|
|
39
|
+
"CHILD_EXIT_ZERO": 0,
|
|
40
|
+
"USER_CANCELLED": 0,
|
|
41
|
+
"SERVICE_START_REQUESTED": 0,
|
|
42
|
+
"SERVICE_READY": 0,
|
|
43
|
+
"SERVICE_REUSED": 0,
|
|
44
|
+
"SERVICE_STOP_REQUESTED": 0,
|
|
45
|
+
"SERVICE_STOPPED": 0,
|
|
46
|
+
"SERVICE_RETIRED": 0,
|
|
47
|
+
"SERVICE_SUPERSEDER_ALREADY_LINKED": 0,
|
|
48
|
+
"ARGUMENT_INVALID": 2,
|
|
49
|
+
"CURSOR_INVALID": 2,
|
|
50
|
+
"PROFILE_INVALID": 2,
|
|
51
|
+
"PROFILE_MIGRATION_UNSAFE": 2,
|
|
52
|
+
"SESSION_SCHEMA_INVALID": 2,
|
|
53
|
+
"PYTHON_RUNTIME_NOT_FOUND": 3,
|
|
54
|
+
"WORKER_LAUNCH_FAILED": 3,
|
|
55
|
+
"LAUNCHER_FAILED": 3,
|
|
56
|
+
"CHILD_EXIT_NONZERO": 3,
|
|
57
|
+
"TIMEOUT": 3,
|
|
58
|
+
"SERVICE_START_FAILED": 3,
|
|
59
|
+
"SERVICE_STOP_FAILED": 3,
|
|
60
|
+
"SERVICE_STOP_TIMEOUT": 3,
|
|
61
|
+
"RESOURCE_LOCK_BUSY": 4,
|
|
62
|
+
"SERVICE_MUTATION_CONFLICT": 4,
|
|
63
|
+
"SERVICE_TRANSITION_CONFLICT": 4,
|
|
64
|
+
"SERVICE_SUPERSEDER_CONFLICT": 4,
|
|
65
|
+
"PROCESS_CREATE_TIME_MISMATCH": 5,
|
|
66
|
+
"PROCESS_EXECUTABLE_MISMATCH": 5,
|
|
67
|
+
"PROCESS_ARGV_MISMATCH": 5,
|
|
68
|
+
"PROCESS_CWD_MISMATCH": 5,
|
|
69
|
+
"PROCESS_PARENT_MISMATCH": 5,
|
|
70
|
+
"PROCESS_OWNER_MISMATCH": 5,
|
|
71
|
+
"PROCESS_IDENTITY_MISMATCH": 5,
|
|
72
|
+
"WORKER_IDENTITY_MISMATCH": 5,
|
|
73
|
+
"SERVICE_IDENTITY_STALE": 5,
|
|
74
|
+
"SERVICE_STOP_IDENTITY_LOST": 5,
|
|
75
|
+
"LEASE_CAS_MISMATCH": 5,
|
|
76
|
+
"LISTENER_IDENTITY_UNVERIFIABLE": 5,
|
|
77
|
+
"SENSITIVE_EVIDENCE_QUARANTINE_FAILED": 5,
|
|
78
|
+
"IDENTITY_UNVERIFIABLE": 6,
|
|
79
|
+
"HEARTBEAT_LOST": 6,
|
|
80
|
+
"SERVICE_HEARTBEAT_STALE": 6,
|
|
81
|
+
"SERVICE_CLEANUP_INCOMPLETE": 6,
|
|
82
|
+
"LOG_DECODE_DEGRADED": 6,
|
|
83
|
+
}
|
|
84
|
+
MANAGED_EXECUTION_REASON_CODES = frozenset(
|
|
85
|
+
MANAGED_EXECUTION_EXIT_CODE_BY_REASON
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _record(
|
|
90
|
+
value: Any,
|
|
91
|
+
required: set[str],
|
|
92
|
+
*,
|
|
93
|
+
optional: set[str] | None = None,
|
|
94
|
+
) -> tuple[dict[str, Any] | None, list[str]]:
|
|
95
|
+
if not isinstance(value, dict):
|
|
96
|
+
return None, ["value must be an object"]
|
|
97
|
+
keys = set(value)
|
|
98
|
+
missing = sorted(required - keys)
|
|
99
|
+
unexpected = sorted(keys - required - (optional or set()))
|
|
100
|
+
errors = [f"missing field: {item}" for item in missing]
|
|
101
|
+
errors.extend(f"unexpected field: {item}" for item in unexpected)
|
|
102
|
+
return value, errors
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _nonempty(value: Any) -> bool:
|
|
106
|
+
return isinstance(value, str) and bool(value)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _nullable_nonempty(value: Any) -> bool:
|
|
110
|
+
return value is None or _nonempty(value)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _digest(value: Any) -> bool:
|
|
114
|
+
return isinstance(value, str) and SHA256_RE.fullmatch(value) is not None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _nullable_digest(value: Any) -> bool:
|
|
118
|
+
return value is None or _digest(value)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _positive_int(value: Any) -> bool:
|
|
122
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _nonnegative_int(value: Any) -> bool:
|
|
126
|
+
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _validate_process_identity(value: Any) -> list[str]:
|
|
130
|
+
required = {
|
|
131
|
+
"schemaVersion",
|
|
132
|
+
"pid",
|
|
133
|
+
"alive",
|
|
134
|
+
"createdAt",
|
|
135
|
+
"executable",
|
|
136
|
+
"argvHash",
|
|
137
|
+
"workingDirectory",
|
|
138
|
+
"parentIdentity",
|
|
139
|
+
"ownerTokenHash",
|
|
140
|
+
"treeIdentity",
|
|
141
|
+
"fieldProvenance",
|
|
142
|
+
"capabilities",
|
|
143
|
+
}
|
|
144
|
+
record, errors = _record(
|
|
145
|
+
value,
|
|
146
|
+
required,
|
|
147
|
+
optional={"commandHash", "parentChain", "startedAt"},
|
|
148
|
+
)
|
|
149
|
+
if record is None:
|
|
150
|
+
return errors
|
|
151
|
+
if record["schemaVersion"] != 1:
|
|
152
|
+
errors.append("schemaVersion must equal 1")
|
|
153
|
+
if not _positive_int(record["pid"]):
|
|
154
|
+
errors.append("pid must be a positive integer")
|
|
155
|
+
if not isinstance(record["alive"], bool):
|
|
156
|
+
errors.append("alive must be boolean")
|
|
157
|
+
for field in ("createdAt", "executable", "workingDirectory"):
|
|
158
|
+
if not _nullable_nonempty(record[field]):
|
|
159
|
+
errors.append(f"{field} must be null or a non-empty string")
|
|
160
|
+
for field in ("argvHash", "ownerTokenHash"):
|
|
161
|
+
if not _nullable_digest(record[field]):
|
|
162
|
+
errors.append(f"{field} must be null or a sha256 digest")
|
|
163
|
+
if "commandHash" in record and not _digest(record["commandHash"]):
|
|
164
|
+
errors.append("commandHash must be a sha256 digest")
|
|
165
|
+
if "parentChain" in record and not isinstance(record["parentChain"], list):
|
|
166
|
+
errors.append("parentChain must be an array")
|
|
167
|
+
if "startedAt" in record and not _nonempty(record["startedAt"]):
|
|
168
|
+
errors.append("startedAt must be a non-empty string")
|
|
169
|
+
|
|
170
|
+
parent = record["parentIdentity"]
|
|
171
|
+
if parent is not None:
|
|
172
|
+
parent_record, parent_errors = _record(
|
|
173
|
+
parent, {"pid", "createdAt", "executable"}
|
|
174
|
+
)
|
|
175
|
+
errors.extend(f"parentIdentity.{item}" for item in parent_errors)
|
|
176
|
+
if parent_record is not None:
|
|
177
|
+
if not _positive_int(parent_record["pid"]):
|
|
178
|
+
errors.append("parentIdentity.pid must be positive")
|
|
179
|
+
for field in ("createdAt", "executable"):
|
|
180
|
+
if not _nullable_nonempty(parent_record[field]):
|
|
181
|
+
errors.append(f"parentIdentity.{field} is invalid")
|
|
182
|
+
|
|
183
|
+
tree = record["treeIdentity"]
|
|
184
|
+
if tree is not None:
|
|
185
|
+
tree_record, tree_errors = _record(
|
|
186
|
+
tree,
|
|
187
|
+
{"platform", "proofKind", "memberPids", "complete"},
|
|
188
|
+
optional={"groupId", "sessionId"},
|
|
189
|
+
)
|
|
190
|
+
errors.extend(f"treeIdentity.{item}" for item in tree_errors)
|
|
191
|
+
if tree_record is not None:
|
|
192
|
+
if tree_record["platform"] not in {"WINDOWS", "LINUX", "POSIX"}:
|
|
193
|
+
errors.append("treeIdentity.platform is invalid")
|
|
194
|
+
if not _nonempty(tree_record["proofKind"]):
|
|
195
|
+
errors.append("treeIdentity.proofKind is invalid")
|
|
196
|
+
members = tree_record["memberPids"]
|
|
197
|
+
if (
|
|
198
|
+
not isinstance(members, list)
|
|
199
|
+
or any(not _positive_int(item) for item in members)
|
|
200
|
+
or len(set(members)) != len(members)
|
|
201
|
+
):
|
|
202
|
+
errors.append("treeIdentity.memberPids is invalid")
|
|
203
|
+
if not isinstance(tree_record["complete"], bool):
|
|
204
|
+
errors.append("treeIdentity.complete must be boolean")
|
|
205
|
+
for field in ("groupId", "sessionId"):
|
|
206
|
+
if field in tree_record and not _positive_int(tree_record[field]):
|
|
207
|
+
errors.append(f"treeIdentity.{field} is invalid")
|
|
208
|
+
|
|
209
|
+
provenance_fields = {
|
|
210
|
+
"pid",
|
|
211
|
+
"createdAt",
|
|
212
|
+
"executable",
|
|
213
|
+
"argvHash",
|
|
214
|
+
"workingDirectory",
|
|
215
|
+
"parentIdentity",
|
|
216
|
+
"ownerTokenHash",
|
|
217
|
+
"treeIdentity",
|
|
218
|
+
}
|
|
219
|
+
provenance, provenance_errors = _record(
|
|
220
|
+
record["fieldProvenance"], provenance_fields
|
|
221
|
+
)
|
|
222
|
+
errors.extend(f"fieldProvenance.{item}" for item in provenance_errors)
|
|
223
|
+
if provenance is not None:
|
|
224
|
+
if provenance["pid"] != "OBSERVED":
|
|
225
|
+
errors.append("fieldProvenance.pid must be OBSERVED")
|
|
226
|
+
for field in provenance_fields - {"pid"}:
|
|
227
|
+
if provenance[field] not in PROVENANCE:
|
|
228
|
+
errors.append(f"fieldProvenance.{field} is invalid")
|
|
229
|
+
|
|
230
|
+
capability_fields = {
|
|
231
|
+
"canObserveCreateTime",
|
|
232
|
+
"canObserveExecutable",
|
|
233
|
+
"canObserveArgv",
|
|
234
|
+
"canObserveWorkingDirectory",
|
|
235
|
+
"canObserveParent",
|
|
236
|
+
"canEnumerateTree",
|
|
237
|
+
"canVerifyOwnership",
|
|
238
|
+
}
|
|
239
|
+
capabilities, capability_errors = _record(
|
|
240
|
+
record["capabilities"], capability_fields
|
|
241
|
+
)
|
|
242
|
+
errors.extend(f"capabilities.{item}" for item in capability_errors)
|
|
243
|
+
if capabilities is not None:
|
|
244
|
+
for field in capability_fields:
|
|
245
|
+
if not isinstance(capabilities[field], bool):
|
|
246
|
+
errors.append(f"capabilities.{field} must be boolean")
|
|
247
|
+
return errors
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _validate_heartbeat(
|
|
251
|
+
value: Any,
|
|
252
|
+
*,
|
|
253
|
+
service: bool,
|
|
254
|
+
) -> list[str]:
|
|
255
|
+
if value is None:
|
|
256
|
+
return []
|
|
257
|
+
required = {
|
|
258
|
+
"kind",
|
|
259
|
+
"writerIdentity",
|
|
260
|
+
"lastSeenAt",
|
|
261
|
+
"ttlSeconds",
|
|
262
|
+
"staleReason",
|
|
263
|
+
}
|
|
264
|
+
if service:
|
|
265
|
+
required.add("generation")
|
|
266
|
+
record, errors = _record(value, required)
|
|
267
|
+
if record is None:
|
|
268
|
+
return errors
|
|
269
|
+
kinds = {"SUPERVISOR", "OBSERVER"} if service else {
|
|
270
|
+
"WORKER",
|
|
271
|
+
"SUPERVISOR",
|
|
272
|
+
"OBSERVER",
|
|
273
|
+
}
|
|
274
|
+
if record["kind"] not in kinds:
|
|
275
|
+
errors.append("kind is invalid")
|
|
276
|
+
if not _digest(record["writerIdentity"]):
|
|
277
|
+
errors.append("writerIdentity must be a sha256 digest")
|
|
278
|
+
if not _nonempty(record["lastSeenAt"]):
|
|
279
|
+
errors.append("lastSeenAt is invalid")
|
|
280
|
+
if not _positive_int(record["ttlSeconds"]):
|
|
281
|
+
errors.append("ttlSeconds must be positive")
|
|
282
|
+
if not _nullable_nonempty(record["staleReason"]):
|
|
283
|
+
errors.append("staleReason is invalid")
|
|
284
|
+
if service and not _positive_int(record["generation"]):
|
|
285
|
+
errors.append("generation must be positive")
|
|
286
|
+
return errors
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _validate_run_session(value: Any) -> list[str]:
|
|
290
|
+
required = {
|
|
291
|
+
"schemaVersion",
|
|
292
|
+
"sessionId",
|
|
293
|
+
"status",
|
|
294
|
+
"reasonCode",
|
|
295
|
+
"exitCode",
|
|
296
|
+
"processIdentity",
|
|
297
|
+
"heartbeat",
|
|
298
|
+
"logs",
|
|
299
|
+
"cleanup",
|
|
300
|
+
"resultDigest",
|
|
301
|
+
}
|
|
302
|
+
record, errors = _record(value, required)
|
|
303
|
+
if record is None:
|
|
304
|
+
return errors
|
|
305
|
+
if record["schemaVersion"] != 1:
|
|
306
|
+
errors.append("schemaVersion must equal 1")
|
|
307
|
+
if not _nonempty(record["sessionId"]):
|
|
308
|
+
errors.append("sessionId is invalid")
|
|
309
|
+
if record["status"] not in RUN_STATUSES:
|
|
310
|
+
errors.append("status is invalid")
|
|
311
|
+
if record["reasonCode"] not in MANAGED_EXECUTION_REASON_CODES:
|
|
312
|
+
errors.append("reasonCode is invalid")
|
|
313
|
+
if record["processIdentity"] is not None:
|
|
314
|
+
errors.extend(
|
|
315
|
+
f"processIdentity.{item}"
|
|
316
|
+
for item in _validate_process_identity(record["processIdentity"])
|
|
317
|
+
)
|
|
318
|
+
errors.extend(
|
|
319
|
+
f"heartbeat.{item}"
|
|
320
|
+
for item in _validate_heartbeat(record["heartbeat"], service=False)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
logs, log_errors = _record(record["logs"], {"stdout", "stderr"})
|
|
324
|
+
errors.extend(f"logs.{item}" for item in log_errors)
|
|
325
|
+
if logs is not None:
|
|
326
|
+
for stream in ("stdout", "stderr"):
|
|
327
|
+
stream_record, stream_errors = _record(
|
|
328
|
+
logs[stream], {"cursor", "rawDigest", "decodeStatus"}
|
|
329
|
+
)
|
|
330
|
+
errors.extend(f"logs.{stream}.{item}" for item in stream_errors)
|
|
331
|
+
if stream_record is None:
|
|
332
|
+
continue
|
|
333
|
+
if not _nonnegative_int(stream_record["cursor"]):
|
|
334
|
+
errors.append(f"logs.{stream}.cursor is invalid")
|
|
335
|
+
if not _digest(stream_record["rawDigest"]):
|
|
336
|
+
errors.append(f"logs.{stream}.rawDigest is invalid")
|
|
337
|
+
if stream_record["decodeStatus"] not in {
|
|
338
|
+
"OK",
|
|
339
|
+
"LOG_DECODE_DEGRADED",
|
|
340
|
+
}:
|
|
341
|
+
errors.append(f"logs.{stream}.decodeStatus is invalid")
|
|
342
|
+
|
|
343
|
+
cleanup, cleanup_errors = _record(
|
|
344
|
+
record["cleanup"], {"complete", "reasonCode"}
|
|
345
|
+
)
|
|
346
|
+
errors.extend(f"cleanup.{item}" for item in cleanup_errors)
|
|
347
|
+
if cleanup is not None:
|
|
348
|
+
if not isinstance(cleanup["complete"], bool):
|
|
349
|
+
errors.append("cleanup.complete must be boolean")
|
|
350
|
+
reason = cleanup["reasonCode"]
|
|
351
|
+
if reason is not None and reason not in MANAGED_EXECUTION_REASON_CODES:
|
|
352
|
+
errors.append("cleanup.reasonCode is invalid")
|
|
353
|
+
|
|
354
|
+
status = record["status"]
|
|
355
|
+
exit_code = record["exitCode"]
|
|
356
|
+
if status in {"OK", "FAIL"}:
|
|
357
|
+
if not isinstance(exit_code, int) or isinstance(exit_code, bool):
|
|
358
|
+
errors.append("OK/FAIL requires a final exitCode")
|
|
359
|
+
if not _digest(record["resultDigest"]):
|
|
360
|
+
errors.append("terminal run requires resultDigest")
|
|
361
|
+
elif status == "INCOMPLETE":
|
|
362
|
+
if exit_code is not None:
|
|
363
|
+
errors.append("INCOMPLETE must not contain an exitCode")
|
|
364
|
+
if not _digest(record["resultDigest"]):
|
|
365
|
+
errors.append("INCOMPLETE requires an evidence digest")
|
|
366
|
+
elif exit_code is not None and (
|
|
367
|
+
not isinstance(exit_code, int) or isinstance(exit_code, bool)
|
|
368
|
+
):
|
|
369
|
+
errors.append("exitCode must be integer or null")
|
|
370
|
+
if not _nullable_digest(record["resultDigest"]):
|
|
371
|
+
errors.append("resultDigest must be null or a sha256 digest")
|
|
372
|
+
return errors
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _validate_service_session(value: Any) -> list[str]:
|
|
376
|
+
required = {
|
|
377
|
+
"schemaVersion",
|
|
378
|
+
"serviceId",
|
|
379
|
+
"sessionId",
|
|
380
|
+
"status",
|
|
381
|
+
"reasonCode",
|
|
382
|
+
"serviceGeneration",
|
|
383
|
+
"stateRevision",
|
|
384
|
+
"operationId",
|
|
385
|
+
"fingerprint",
|
|
386
|
+
"processIdentity",
|
|
387
|
+
"heartbeat",
|
|
388
|
+
"leaseIdentity",
|
|
389
|
+
"transitionHistory",
|
|
390
|
+
"cleanupComplete",
|
|
391
|
+
"supersedesSessionId",
|
|
392
|
+
}
|
|
393
|
+
record, errors = _record(
|
|
394
|
+
value,
|
|
395
|
+
required,
|
|
396
|
+
optional={
|
|
397
|
+
"pid",
|
|
398
|
+
"startedBy",
|
|
399
|
+
"moduleInputsHash",
|
|
400
|
+
"moduleInputsFiles",
|
|
401
|
+
"profile",
|
|
402
|
+
"startCommandHash",
|
|
403
|
+
"overlayPath",
|
|
404
|
+
"startedAt",
|
|
405
|
+
"command",
|
|
406
|
+
"argv",
|
|
407
|
+
"ownedPorts",
|
|
408
|
+
"processAttestation",
|
|
409
|
+
"ownershipProof",
|
|
410
|
+
"servicePid",
|
|
411
|
+
"jobId",
|
|
412
|
+
"leasedPort",
|
|
413
|
+
"leaseOwner",
|
|
414
|
+
"worktreeRoot",
|
|
415
|
+
"executionRoot",
|
|
416
|
+
"changeId",
|
|
417
|
+
"attemptId",
|
|
418
|
+
"instanceTokenHash",
|
|
419
|
+
"identityCompleteness",
|
|
420
|
+
},
|
|
421
|
+
)
|
|
422
|
+
if record is None:
|
|
423
|
+
return errors
|
|
424
|
+
if record["schemaVersion"] != 1:
|
|
425
|
+
errors.append("schemaVersion must equal 1")
|
|
426
|
+
for field in ("serviceId", "sessionId", "operationId"):
|
|
427
|
+
if not _nonempty(record[field]):
|
|
428
|
+
errors.append(f"{field} is invalid")
|
|
429
|
+
if record["status"] not in SERVICE_STATUSES:
|
|
430
|
+
errors.append("status is invalid")
|
|
431
|
+
if record["reasonCode"] not in MANAGED_EXECUTION_REASON_CODES:
|
|
432
|
+
errors.append("reasonCode is invalid")
|
|
433
|
+
for field in ("serviceGeneration", "stateRevision"):
|
|
434
|
+
if not _positive_int(record[field]):
|
|
435
|
+
errors.append(f"{field} must be positive")
|
|
436
|
+
if not _digest(record["fingerprint"]):
|
|
437
|
+
errors.append("fingerprint must be a sha256 digest")
|
|
438
|
+
if record["processIdentity"] is not None:
|
|
439
|
+
errors.extend(
|
|
440
|
+
f"processIdentity.{item}"
|
|
441
|
+
for item in _validate_process_identity(record["processIdentity"])
|
|
442
|
+
)
|
|
443
|
+
errors.extend(
|
|
444
|
+
f"heartbeat.{item}"
|
|
445
|
+
for item in _validate_heartbeat(record["heartbeat"], service=True)
|
|
446
|
+
)
|
|
447
|
+
lease = record["leaseIdentity"]
|
|
448
|
+
if lease is not None:
|
|
449
|
+
lease_record, lease_errors = _record(
|
|
450
|
+
lease,
|
|
451
|
+
{"leaseId", "changeId", "runId", "expiresAt", "generation"},
|
|
452
|
+
optional={"listenerIdentity"},
|
|
453
|
+
)
|
|
454
|
+
errors.extend(f"leaseIdentity.{item}" for item in lease_errors)
|
|
455
|
+
if lease_record is not None:
|
|
456
|
+
for field in ("leaseId", "changeId", "runId", "expiresAt"):
|
|
457
|
+
if not _nonempty(lease_record[field]):
|
|
458
|
+
errors.append(f"leaseIdentity.{field} is invalid")
|
|
459
|
+
if not _positive_int(lease_record["generation"]):
|
|
460
|
+
errors.append("leaseIdentity.generation must be positive")
|
|
461
|
+
history = record["transitionHistory"]
|
|
462
|
+
if not isinstance(history, list):
|
|
463
|
+
errors.append("transitionHistory must be an array")
|
|
464
|
+
else:
|
|
465
|
+
for index, item in enumerate(history):
|
|
466
|
+
transition, transition_errors = _record(
|
|
467
|
+
item,
|
|
468
|
+
{"from", "to", "reasonCode", "revision"},
|
|
469
|
+
optional={"operationId", "at"},
|
|
470
|
+
)
|
|
471
|
+
errors.extend(
|
|
472
|
+
f"transitionHistory[{index}].{error}"
|
|
473
|
+
for error in transition_errors
|
|
474
|
+
)
|
|
475
|
+
if transition is None:
|
|
476
|
+
continue
|
|
477
|
+
if not _nullable_nonempty(transition["from"]):
|
|
478
|
+
errors.append(f"transitionHistory[{index}].from is invalid")
|
|
479
|
+
if not _nonempty(transition["to"]):
|
|
480
|
+
errors.append(f"transitionHistory[{index}].to is invalid")
|
|
481
|
+
if transition["reasonCode"] not in MANAGED_EXECUTION_REASON_CODES:
|
|
482
|
+
errors.append(
|
|
483
|
+
f"transitionHistory[{index}].reasonCode is invalid"
|
|
484
|
+
)
|
|
485
|
+
if not _positive_int(transition["revision"]):
|
|
486
|
+
errors.append(f"transitionHistory[{index}].revision is invalid")
|
|
487
|
+
if not isinstance(record["cleanupComplete"], bool):
|
|
488
|
+
errors.append("cleanupComplete must be boolean")
|
|
489
|
+
if not _nullable_nonempty(record["supersedesSessionId"]):
|
|
490
|
+
errors.append("supersedesSessionId is invalid")
|
|
491
|
+
return errors
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _validate_retirement_receipt(value: Any) -> list[str]:
|
|
495
|
+
required = {
|
|
496
|
+
"schemaVersion",
|
|
497
|
+
"receiptId",
|
|
498
|
+
"operationId",
|
|
499
|
+
"serviceId",
|
|
500
|
+
"oldSessionId",
|
|
501
|
+
"oldGeneration",
|
|
502
|
+
"state",
|
|
503
|
+
"retirementStateCommit",
|
|
504
|
+
"leaseCleanup",
|
|
505
|
+
"cleanupComplete",
|
|
506
|
+
"awaitingSuperseder",
|
|
507
|
+
"supersededBySessionId",
|
|
508
|
+
"receiptDigest",
|
|
509
|
+
}
|
|
510
|
+
record, errors = _record(value, required)
|
|
511
|
+
if record is None:
|
|
512
|
+
return errors
|
|
513
|
+
if record["schemaVersion"] != 1:
|
|
514
|
+
errors.append("schemaVersion must equal 1")
|
|
515
|
+
for field in ("receiptId", "operationId", "serviceId", "oldSessionId"):
|
|
516
|
+
if not _nonempty(record[field]):
|
|
517
|
+
errors.append(f"{field} is invalid")
|
|
518
|
+
if not _positive_int(record["oldGeneration"]):
|
|
519
|
+
errors.append("oldGeneration must be positive")
|
|
520
|
+
if record["state"] not in {"PENDING", "FINALIZED"}:
|
|
521
|
+
errors.append("state is invalid")
|
|
522
|
+
state_commit, commit_errors = _record(
|
|
523
|
+
record["retirementStateCommit"], {"status", "reasonCode"}
|
|
524
|
+
)
|
|
525
|
+
errors.extend(f"retirementStateCommit.{item}" for item in commit_errors)
|
|
526
|
+
if state_commit is not None:
|
|
527
|
+
if state_commit["status"] not in {"PENDING", "COMMITTED", "CONFLICT"}:
|
|
528
|
+
errors.append("retirementStateCommit.status is invalid")
|
|
529
|
+
if state_commit["reasonCode"] not in MANAGED_EXECUTION_REASON_CODES:
|
|
530
|
+
errors.append("retirementStateCommit.reasonCode is invalid")
|
|
531
|
+
lease, lease_errors = _record(
|
|
532
|
+
record["leaseCleanup"], {"status", "reasonCode", "leaseId"}
|
|
533
|
+
)
|
|
534
|
+
errors.extend(f"leaseCleanup.{item}" for item in lease_errors)
|
|
535
|
+
if lease is not None:
|
|
536
|
+
if lease["status"] not in {
|
|
537
|
+
"PENDING",
|
|
538
|
+
"RELEASED",
|
|
539
|
+
"RETAINED",
|
|
540
|
+
"UNVERIFIED",
|
|
541
|
+
}:
|
|
542
|
+
errors.append("leaseCleanup.status is invalid")
|
|
543
|
+
if lease["reasonCode"] not in MANAGED_EXECUTION_REASON_CODES:
|
|
544
|
+
errors.append("leaseCleanup.reasonCode is invalid")
|
|
545
|
+
if not _nullable_nonempty(lease["leaseId"]):
|
|
546
|
+
errors.append("leaseCleanup.leaseId is invalid")
|
|
547
|
+
expected_cleanup = lease["status"] == "RELEASED"
|
|
548
|
+
if record["cleanupComplete"] is not expected_cleanup:
|
|
549
|
+
errors.append("cleanupComplete contradicts leaseCleanup.status")
|
|
550
|
+
if not isinstance(record["cleanupComplete"], bool):
|
|
551
|
+
errors.append("cleanupComplete must be boolean")
|
|
552
|
+
if not isinstance(record["awaitingSuperseder"], bool):
|
|
553
|
+
errors.append("awaitingSuperseder must be boolean")
|
|
554
|
+
if not _nullable_nonempty(record["supersededBySessionId"]):
|
|
555
|
+
errors.append("supersededBySessionId is invalid")
|
|
556
|
+
if (
|
|
557
|
+
record["awaitingSuperseder"] is True
|
|
558
|
+
and record["supersededBySessionId"] is not None
|
|
559
|
+
):
|
|
560
|
+
errors.append("awaiting receipt cannot already name a superseder")
|
|
561
|
+
if not _digest(record["receiptDigest"]):
|
|
562
|
+
errors.append("receiptDigest must be a sha256 digest")
|
|
563
|
+
return errors
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
_VALIDATORS: dict[str, Callable[[Any], list[str]]] = {
|
|
567
|
+
"process-identity": _validate_process_identity,
|
|
568
|
+
"run-session": _validate_run_session,
|
|
569
|
+
"service-session": _validate_service_session,
|
|
570
|
+
"service-retirement-receipt": _validate_retirement_receipt,
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def parse_execution_contract(contract: str, value: Any) -> dict[str, Any]:
|
|
575
|
+
"""Parse one managed execution contract without mutating caller data."""
|
|
576
|
+
|
|
577
|
+
validator = _VALIDATORS.get(contract)
|
|
578
|
+
if validator is None:
|
|
579
|
+
return {
|
|
580
|
+
"ok": False,
|
|
581
|
+
"code": "SESSION_SCHEMA_INVALID",
|
|
582
|
+
"errors": [f"unknown execution contract: {contract}"],
|
|
583
|
+
}
|
|
584
|
+
errors = validator(value)
|
|
585
|
+
if errors:
|
|
586
|
+
return {
|
|
587
|
+
"ok": False,
|
|
588
|
+
"code": "SESSION_SCHEMA_INVALID",
|
|
589
|
+
"errors": errors,
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
"ok": True,
|
|
593
|
+
"code": "CONTRACT_VALID",
|
|
594
|
+
"value": deepcopy(value),
|
|
595
|
+
"errors": [],
|
|
596
|
+
}
|