@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
|
@@ -33,6 +33,12 @@ SCHEMA_VERSION = 1
|
|
|
33
33
|
RUN_SESSION_SCHEMA_VERSION = 1
|
|
34
34
|
RUN_TERMINAL_STATUSES = {"OK", "FAIL", "INCOMPLETE", "CANCELLED"}
|
|
35
35
|
_CHANGE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
36
|
+
SECRET_SCAN_RULES_VERSION = "secret-scan-v1"
|
|
37
|
+
SECRET_SCAN_RECEIPT_REL = Path("meta") / "secret-scan-receipt.json"
|
|
38
|
+
_SENSITIVE_ASSIGNMENT = re.compile(
|
|
39
|
+
rb"(?i)[\"']?(?:password|passwd|token|secret|cookie|authorization|database[_-]?url)"
|
|
40
|
+
rb"[\"']?\s*[:=]\s*[\"']?[^\s\"']{4,}"
|
|
41
|
+
)
|
|
36
42
|
_ADAPTERS = {
|
|
37
43
|
"claude-code": (".worktrees", "harness/"),
|
|
38
44
|
"codex": (".worktrees", "harness/"),
|
|
@@ -116,6 +122,248 @@ def _sha256_json(value: Any) -> str:
|
|
|
116
122
|
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
117
123
|
|
|
118
124
|
|
|
125
|
+
def _publishable_tree_digest(root: Path) -> str:
|
|
126
|
+
"""Hash publishable bytes while excluding the self-referential scan receipt."""
|
|
127
|
+
root = root.expanduser().resolve()
|
|
128
|
+
digest = hashlib.sha256()
|
|
129
|
+
receipt = (root / SECRET_SCAN_RECEIPT_REL).resolve()
|
|
130
|
+
if not root.is_dir():
|
|
131
|
+
raise OSError(f"publishable evidence root not found: {root}")
|
|
132
|
+
for path in sorted(root.rglob("*")):
|
|
133
|
+
if not path.is_file() or path.resolve() == receipt:
|
|
134
|
+
continue
|
|
135
|
+
relative = path.relative_to(root).as_posix()
|
|
136
|
+
data = path.read_bytes()
|
|
137
|
+
digest.update(relative.encode("utf-8"))
|
|
138
|
+
digest.update(b"\0")
|
|
139
|
+
digest.update(str(len(data)).encode("ascii"))
|
|
140
|
+
digest.update(b"\0")
|
|
141
|
+
digest.update(hashlib.sha256(data).digest())
|
|
142
|
+
digest.update(b"\0")
|
|
143
|
+
return "sha256:" + digest.hexdigest()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _sensitive_evidence_receipt_path(change_root: Path) -> Path:
|
|
147
|
+
return change_root.expanduser().resolve() / SECRET_SCAN_RECEIPT_REL
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _private_evidence_root(explicit: Path | None = None) -> Path:
|
|
151
|
+
configured = os.environ.get("HARNESS_PRIVATE_EVIDENCE_ROOT")
|
|
152
|
+
return (explicit if explicit is not None else Path(configured) if configured else
|
|
153
|
+
Path.home() / ".harness" / "private-evidence").expanduser().resolve()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _secure_private_path(path: Path, *, directory: bool) -> str:
|
|
157
|
+
"""Best-effort owner-only permissions, with an explicit ACL result on Windows."""
|
|
158
|
+
mode = 0o700 if directory else 0o600
|
|
159
|
+
try:
|
|
160
|
+
path.chmod(mode)
|
|
161
|
+
except OSError:
|
|
162
|
+
pass
|
|
163
|
+
if os.name != "nt":
|
|
164
|
+
return "POSIX_OWNER_ONLY"
|
|
165
|
+
try:
|
|
166
|
+
account = os.environ.get("USERNAME") or os.environ.get("USER")
|
|
167
|
+
if not account:
|
|
168
|
+
account = subprocess.check_output(
|
|
169
|
+
["whoami"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8"
|
|
170
|
+
).strip()
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
["icacls", str(path), "/inheritance:r", "/grant:r", f"{account}:F"],
|
|
173
|
+
capture_output=True,
|
|
174
|
+
text=True,
|
|
175
|
+
encoding="utf-8",
|
|
176
|
+
errors="replace",
|
|
177
|
+
check=False,
|
|
178
|
+
timeout=10,
|
|
179
|
+
)
|
|
180
|
+
return "WINDOWS_CURRENT_USER_ONLY" if result.returncode == 0 else "WINDOWS_ACL_UNVERIFIED"
|
|
181
|
+
except (OSError, subprocess.SubprocessError):
|
|
182
|
+
return "WINDOWS_ACL_UNVERIFIED"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _sensitive_candidates(root: Path) -> list[dict[str, Any]]:
|
|
186
|
+
"""Find high-confidence plaintext assignments without treating prose as a secret."""
|
|
187
|
+
receipt = _sensitive_evidence_receipt_path(root)
|
|
188
|
+
candidates: list[dict[str, Any]] = []
|
|
189
|
+
for path in sorted(root.rglob("*")):
|
|
190
|
+
if not path.is_file() or path.resolve() == receipt.resolve():
|
|
191
|
+
continue
|
|
192
|
+
try:
|
|
193
|
+
raw = path.read_bytes()
|
|
194
|
+
except OSError:
|
|
195
|
+
continue
|
|
196
|
+
match = _SENSITIVE_ASSIGNMENT.search(raw)
|
|
197
|
+
if match is None:
|
|
198
|
+
continue
|
|
199
|
+
candidates.append({
|
|
200
|
+
"path": path.relative_to(root).as_posix(),
|
|
201
|
+
"reasonCode": "PLAINTEXT_SENSITIVE_ASSIGNMENT",
|
|
202
|
+
"digest": "sha256:" + hashlib.sha256(raw).hexdigest(),
|
|
203
|
+
})
|
|
204
|
+
return candidates
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def publishable_tree_digest(root: Path) -> str:
|
|
208
|
+
"""Public digest helper shared by the archive publication gate."""
|
|
209
|
+
return _publishable_tree_digest(root)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def sensitive_evidence_receipt_path(change_root: Path) -> Path:
|
|
213
|
+
return _sensitive_evidence_receipt_path(change_root)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def sensitive_evidence_candidates(root: Path) -> list[dict[str, Any]]:
|
|
217
|
+
return _sensitive_candidates(root)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _write_sensitive_scan_receipt(
|
|
221
|
+
change_root: Path,
|
|
222
|
+
*,
|
|
223
|
+
entries: list[dict[str, Any]],
|
|
224
|
+
unresolved: list[dict[str, Any]],
|
|
225
|
+
status: str,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
change_root = change_root.expanduser().resolve()
|
|
228
|
+
receipt = {
|
|
229
|
+
"schemaVersion": 1,
|
|
230
|
+
"rulesVersion": SECRET_SCAN_RULES_VERSION,
|
|
231
|
+
"changeId": change_root.name,
|
|
232
|
+
"status": status,
|
|
233
|
+
"unresolvedFailures": unresolved,
|
|
234
|
+
"entries": entries,
|
|
235
|
+
"publishableTreeDigest": _publishable_tree_digest(change_root),
|
|
236
|
+
"publicationExcluded": True,
|
|
237
|
+
"recordedAt": now_iso(),
|
|
238
|
+
}
|
|
239
|
+
atomic_write_json(_sensitive_evidence_receipt_path(change_root), receipt)
|
|
240
|
+
return receipt
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def quarantine_sensitive_evidence(
|
|
244
|
+
source: Path,
|
|
245
|
+
*,
|
|
246
|
+
change_root: Path,
|
|
247
|
+
reason: str = "sensitive evidence",
|
|
248
|
+
private_root: Path | None = None,
|
|
249
|
+
) -> dict[str, Any]:
|
|
250
|
+
"""Atomically move legacy plaintext evidence outside any publishable tree."""
|
|
251
|
+
source = source.expanduser().resolve()
|
|
252
|
+
change_root = change_root.expanduser().resolve()
|
|
253
|
+
private = _private_evidence_root(private_root)
|
|
254
|
+
if not source.is_file():
|
|
255
|
+
return {"ok": False, "reasonCode": "SENSITIVE_EVIDENCE_SOURCE_MISSING", "sourcePath": str(source)}
|
|
256
|
+
if private == change_root or private.is_relative_to(change_root):
|
|
257
|
+
return {
|
|
258
|
+
"ok": False,
|
|
259
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
260
|
+
"error": "private quarantine root must be outside the publishable change root",
|
|
261
|
+
}
|
|
262
|
+
try:
|
|
263
|
+
raw_digest = "sha256:" + hashlib.sha256(source.read_bytes()).hexdigest()
|
|
264
|
+
quarantine_dir = private / uuid.uuid4().hex
|
|
265
|
+
quarantine_dir.mkdir(parents=True, exist_ok=False)
|
|
266
|
+
acl_dir = _secure_private_path(quarantine_dir, directory=True)
|
|
267
|
+
target = quarantine_dir / "payload.bin"
|
|
268
|
+
os.replace(source, target)
|
|
269
|
+
acl_file = _secure_private_path(target, directory=False)
|
|
270
|
+
if os.name == "nt" and (
|
|
271
|
+
acl_dir != "WINDOWS_CURRENT_USER_ONLY"
|
|
272
|
+
or acl_file != "WINDOWS_CURRENT_USER_ONLY"
|
|
273
|
+
):
|
|
274
|
+
os.replace(target, source)
|
|
275
|
+
shutil.rmtree(quarantine_dir, ignore_errors=True)
|
|
276
|
+
return {
|
|
277
|
+
"ok": False,
|
|
278
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
279
|
+
"error": "private quarantine ACL could not be restricted to the current user",
|
|
280
|
+
}
|
|
281
|
+
moved_digest = "sha256:" + hashlib.sha256(target.read_bytes()).hexdigest()
|
|
282
|
+
if moved_digest != raw_digest:
|
|
283
|
+
os.replace(target, source)
|
|
284
|
+
shutil.rmtree(quarantine_dir, ignore_errors=True)
|
|
285
|
+
return {
|
|
286
|
+
"ok": False,
|
|
287
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
288
|
+
"error": "quarantine digest verification failed",
|
|
289
|
+
}
|
|
290
|
+
try:
|
|
291
|
+
prior = json.loads(
|
|
292
|
+
_sensitive_evidence_receipt_path(change_root).read_text(
|
|
293
|
+
encoding="utf-8-sig"
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
except (OSError, json.JSONDecodeError):
|
|
297
|
+
prior = {}
|
|
298
|
+
entries = list(prior.get("entries") or []) if isinstance(prior, dict) else []
|
|
299
|
+
entries.append({
|
|
300
|
+
"sourcePath": (
|
|
301
|
+
source.relative_to(change_root).as_posix()
|
|
302
|
+
if source.is_relative_to(change_root)
|
|
303
|
+
else source.name
|
|
304
|
+
),
|
|
305
|
+
"sourceDigest": raw_digest,
|
|
306
|
+
"privatePath": str(target),
|
|
307
|
+
"reason": reason[:160],
|
|
308
|
+
"status": "QUARANTINED",
|
|
309
|
+
"acl": {"directory": acl_dir, "file": acl_file},
|
|
310
|
+
"movedAt": now_iso(),
|
|
311
|
+
})
|
|
312
|
+
try:
|
|
313
|
+
receipt = _write_sensitive_scan_receipt(
|
|
314
|
+
change_root,
|
|
315
|
+
entries=entries,
|
|
316
|
+
unresolved=[],
|
|
317
|
+
status="QUARANTINED",
|
|
318
|
+
)
|
|
319
|
+
except BaseException as exc:
|
|
320
|
+
try:
|
|
321
|
+
os.replace(target, source)
|
|
322
|
+
except OSError:
|
|
323
|
+
pass
|
|
324
|
+
shutil.rmtree(quarantine_dir, ignore_errors=True)
|
|
325
|
+
return {
|
|
326
|
+
"ok": False,
|
|
327
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
328
|
+
"error": str(exc),
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
"ok": True,
|
|
332
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINED",
|
|
333
|
+
"sourceDigest": raw_digest,
|
|
334
|
+
"privatePath": str(target),
|
|
335
|
+
"receiptPath": str(_sensitive_evidence_receipt_path(change_root)),
|
|
336
|
+
"receipt": receipt,
|
|
337
|
+
}
|
|
338
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
339
|
+
# No copy is retained on failure. If os.replace crossed filesystems it
|
|
340
|
+
# fails before removing the source, which is the required fail-closed path.
|
|
341
|
+
return {
|
|
342
|
+
"ok": False,
|
|
343
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
344
|
+
"error": str(exc),
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def ensure_sensitive_evidence_scan_receipt(change_root: Path) -> dict[str, Any]:
|
|
349
|
+
"""Create a digest-bound scan receipt for a tree with no plaintext findings."""
|
|
350
|
+
change_root = change_root.expanduser().resolve()
|
|
351
|
+
unresolved = _sensitive_candidates(change_root)
|
|
352
|
+
if unresolved:
|
|
353
|
+
return _write_sensitive_scan_receipt(
|
|
354
|
+
change_root,
|
|
355
|
+
entries=[],
|
|
356
|
+
unresolved=unresolved,
|
|
357
|
+
status="FAIL",
|
|
358
|
+
)
|
|
359
|
+
return _write_sensitive_scan_receipt(
|
|
360
|
+
change_root,
|
|
361
|
+
entries=[],
|
|
362
|
+
unresolved=[],
|
|
363
|
+
status="OK",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
119
367
|
def _run_sessions_root(state_root: Path) -> Path:
|
|
120
368
|
return state_root.expanduser().resolve() / "runtime" / "run-sessions"
|
|
121
369
|
|
|
@@ -130,17 +378,114 @@ def _run_receipt_path(state_root: Path, session_id: str) -> Path:
|
|
|
130
378
|
return _run_session_root(state_root, session_id) / "session.json"
|
|
131
379
|
|
|
132
380
|
|
|
381
|
+
def _resource_lock_secrets_path(state_root: Path, session_id: str) -> Path:
|
|
382
|
+
return _run_session_root(state_root, session_id) / "resource-lock-secrets.json"
|
|
383
|
+
|
|
384
|
+
|
|
133
385
|
def _load_run_receipt(state_root: Path, session_id: str) -> dict[str, Any]:
|
|
134
386
|
path = _run_receipt_path(state_root, session_id)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
387
|
+
last_error: OSError | None = None
|
|
388
|
+
for attempt in range(20):
|
|
389
|
+
try:
|
|
390
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
391
|
+
break
|
|
392
|
+
except FileNotFoundError as exc:
|
|
393
|
+
raise ValueError(f"RUN_SESSION_NOT_FOUND: {session_id}") from exc
|
|
394
|
+
except PermissionError as exc:
|
|
395
|
+
last_error = exc
|
|
396
|
+
if attempt == 19:
|
|
397
|
+
raise ValueError(
|
|
398
|
+
f"RUN_SESSION_RECEIPT_BUSY: {session_id}"
|
|
399
|
+
) from exc
|
|
400
|
+
time.sleep(0.01 * (attempt + 1))
|
|
401
|
+
else:
|
|
402
|
+
raise ValueError(f"RUN_SESSION_RECEIPT_BUSY: {session_id}") from last_error
|
|
139
403
|
if not isinstance(value, dict):
|
|
140
404
|
raise ValueError(f"RUN_SESSION_CORRUPT: {session_id}")
|
|
141
405
|
return value
|
|
142
406
|
|
|
143
407
|
|
|
408
|
+
def _load_resource_lock_tokens(
|
|
409
|
+
state_root: Path,
|
|
410
|
+
session_id: str,
|
|
411
|
+
receipt: dict[str, Any],
|
|
412
|
+
) -> dict[str, str]:
|
|
413
|
+
"""Load lock tokens from a private sidecar; legacy receipts may inline them."""
|
|
414
|
+
path = _resource_lock_secrets_path(state_root, session_id)
|
|
415
|
+
try:
|
|
416
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
417
|
+
if isinstance(value, dict) and isinstance(value.get("tokens"), dict):
|
|
418
|
+
return {
|
|
419
|
+
str(name): str(token)
|
|
420
|
+
for name, token in value["tokens"].items()
|
|
421
|
+
if isinstance(name, str) and isinstance(token, str)
|
|
422
|
+
}
|
|
423
|
+
except (OSError, json.JSONDecodeError, TypeError):
|
|
424
|
+
pass
|
|
425
|
+
legacy = receipt.get("resourceLockTokens")
|
|
426
|
+
if isinstance(legacy, dict):
|
|
427
|
+
return {
|
|
428
|
+
str(name): str(token)
|
|
429
|
+
for name, token in legacy.items()
|
|
430
|
+
if isinstance(name, str) and isinstance(token, str)
|
|
431
|
+
}
|
|
432
|
+
return {}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _receipt_identity_state(
|
|
436
|
+
receipt: dict[str, Any],
|
|
437
|
+
*,
|
|
438
|
+
field: str = "workerIdentity",
|
|
439
|
+
) -> bool | None:
|
|
440
|
+
"""Verify a canonical provider attestation, with legacy facade fallback."""
|
|
441
|
+
identity = receipt.get(field)
|
|
442
|
+
pid = identity.get("pid") if isinstance(identity, dict) else receipt.get("workerPid")
|
|
443
|
+
if isinstance(identity, dict) and {
|
|
444
|
+
"schemaVersion",
|
|
445
|
+
"createdAt",
|
|
446
|
+
"fieldProvenance",
|
|
447
|
+
"capabilities",
|
|
448
|
+
} <= set(identity):
|
|
449
|
+
from harness_process import observe_process_identity, verify_process_identity
|
|
450
|
+
|
|
451
|
+
observed = observe_process_identity(pid)
|
|
452
|
+
decision = verify_process_identity(identity, observed)
|
|
453
|
+
if decision.get("ok") is True:
|
|
454
|
+
return True
|
|
455
|
+
if decision.get("reasonCode") == "IDENTITY_UNVERIFIABLE":
|
|
456
|
+
return None
|
|
457
|
+
return False
|
|
458
|
+
from harness_service import verify_process_identity
|
|
459
|
+
|
|
460
|
+
legacy = {
|
|
461
|
+
"pid": pid,
|
|
462
|
+
"startedAt": (
|
|
463
|
+
identity.get("startedAt")
|
|
464
|
+
if isinstance(identity, dict)
|
|
465
|
+
else None
|
|
466
|
+
),
|
|
467
|
+
"processIdentity": {
|
|
468
|
+
"executable": (
|
|
469
|
+
identity.get("executable")
|
|
470
|
+
if isinstance(identity, dict)
|
|
471
|
+
else None
|
|
472
|
+
)
|
|
473
|
+
},
|
|
474
|
+
}
|
|
475
|
+
return verify_process_identity(legacy)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _capture_provider_members(spawned: Any) -> None:
|
|
479
|
+
"""Persist current provider-owned members before a later cleanup."""
|
|
480
|
+
proof = getattr(spawned, "ownershipProof", None)
|
|
481
|
+
leader = getattr(spawned, "attestation", None)
|
|
482
|
+
if not isinstance(proof, dict) or not isinstance(leader, dict):
|
|
483
|
+
return
|
|
484
|
+
from harness_process import capture_owned_members
|
|
485
|
+
|
|
486
|
+
capture_owned_members(spawned)
|
|
487
|
+
|
|
488
|
+
|
|
144
489
|
def _resource_lock_root() -> Path:
|
|
145
490
|
return Path(
|
|
146
491
|
os.environ.get(
|
|
@@ -366,25 +711,86 @@ def _acquire_resource_locks(
|
|
|
366
711
|
|
|
367
712
|
|
|
368
713
|
def _write_run_receipt(state_root: Path, receipt: dict[str, Any]) -> None:
|
|
714
|
+
_update_receipt_contract(receipt)
|
|
715
|
+
publishable = json.loads(json.dumps(receipt, ensure_ascii=False))
|
|
716
|
+
raw_tokens = publishable.pop("resourceLockTokens", {})
|
|
717
|
+
if isinstance(raw_tokens, dict):
|
|
718
|
+
publishable["resourceLockTokenHashes"] = {
|
|
719
|
+
str(name): _sha256_json(str(token))
|
|
720
|
+
for name, token in raw_tokens.items()
|
|
721
|
+
if isinstance(name, str) and isinstance(token, str)
|
|
722
|
+
}
|
|
369
723
|
atomic_write_json(
|
|
370
724
|
_run_receipt_path(state_root, str(receipt["sessionId"])),
|
|
371
|
-
|
|
725
|
+
publishable,
|
|
372
726
|
)
|
|
373
727
|
|
|
374
728
|
|
|
375
729
|
def _process_identity(pid: int, executable_hint: str) -> dict[str, Any]:
|
|
376
|
-
from
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
730
|
+
from harness_process import observe_process_identity
|
|
731
|
+
|
|
732
|
+
observed = observe_process_identity(pid)
|
|
733
|
+
if not observed.get("executable"):
|
|
734
|
+
observed["executable"] = _absolute_executable(executable_hint)
|
|
735
|
+
observed["fieldProvenance"]["executable"] = "ATTESTED"
|
|
736
|
+
if not observed.get("createdAt"):
|
|
737
|
+
observed["createdAt"] = now_iso()
|
|
738
|
+
observed["fieldProvenance"]["createdAt"] = "ATTESTED"
|
|
739
|
+
# ``startedAt`` is a compatibility alias used by legacy session readers;
|
|
740
|
+
# canonical identity comparisons use ``createdAt`` and provenance.
|
|
741
|
+
observed["startedAt"] = observed.get("createdAt")
|
|
742
|
+
return observed
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _log_contract(path: Path) -> dict[str, Any]:
|
|
746
|
+
raw = b""
|
|
747
|
+
if path.is_file():
|
|
748
|
+
try:
|
|
749
|
+
raw = path.read_bytes()
|
|
750
|
+
except OSError:
|
|
751
|
+
raw = b""
|
|
752
|
+
try:
|
|
753
|
+
raw.decode("utf-8")
|
|
754
|
+
decode_status = "OK"
|
|
755
|
+
except UnicodeDecodeError:
|
|
756
|
+
decode_status = "LOG_DECODE_DEGRADED"
|
|
380
757
|
return {
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
758
|
+
"cursor": len(raw),
|
|
759
|
+
"rawDigest": "sha256:" + hashlib.sha256(raw).hexdigest(),
|
|
760
|
+
"decodeStatus": decode_status,
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _update_receipt_contract(receipt: dict[str, Any]) -> None:
|
|
765
|
+
worker = receipt.get("workerIdentity")
|
|
766
|
+
writer_source = worker if isinstance(worker, dict) else {
|
|
767
|
+
"pid": receipt.get("workerPid") or receipt.get("launcherPid")
|
|
768
|
+
}
|
|
769
|
+
heartbeat_kind = "WORKER" if isinstance(worker, dict) else "SUPERVISOR"
|
|
770
|
+
receipt["heartbeat"] = {
|
|
771
|
+
"kind": heartbeat_kind,
|
|
772
|
+
"writerIdentity": _sha256_json(writer_source),
|
|
773
|
+
"lastSeenAt": str(receipt.get("lastHeartbeatAt") or now_iso()),
|
|
774
|
+
"ttlSeconds": max(1, int(float(receipt.get("heartbeatSeconds") or 30))),
|
|
775
|
+
"staleReason": (
|
|
776
|
+
"SERVICE_HEARTBEAT_STALE"
|
|
777
|
+
if receipt.get("reasonCode") == "HEARTBEAT_LOST"
|
|
778
|
+
else None
|
|
386
779
|
),
|
|
387
|
-
|
|
780
|
+
}
|
|
781
|
+
receipt["logs"] = {
|
|
782
|
+
"stdout": _log_contract(Path(str(receipt.get("stdoutPath") or ""))),
|
|
783
|
+
"stderr": _log_contract(Path(str(receipt.get("stderrPath") or ""))),
|
|
784
|
+
}
|
|
785
|
+
cleanup_status = str(receipt.get("cleanupStatus") or "")
|
|
786
|
+
complete = cleanup_status in {
|
|
787
|
+
"NO_CHILD",
|
|
788
|
+
"PROCESS_EXITED",
|
|
789
|
+
"PROCESS_TREE_TERMINATED",
|
|
790
|
+
}
|
|
791
|
+
receipt["cleanup"] = {
|
|
792
|
+
"complete": complete,
|
|
793
|
+
"reasonCode": None if complete else receipt.get("reasonCode"),
|
|
388
794
|
}
|
|
389
795
|
|
|
390
796
|
|
|
@@ -537,6 +943,14 @@ def start_run_session(
|
|
|
537
943
|
_write_run_receipt(state_root, receipt)
|
|
538
944
|
return receipt
|
|
539
945
|
receipt["resourceLockTokens"] = lock_tokens
|
|
946
|
+
atomic_write_json(
|
|
947
|
+
_resource_lock_secrets_path(state_root, session_id),
|
|
948
|
+
{"schemaVersion": 1, "tokens": lock_tokens},
|
|
949
|
+
)
|
|
950
|
+
try:
|
|
951
|
+
_resource_lock_secrets_path(state_root, session_id).chmod(0o600)
|
|
952
|
+
except OSError:
|
|
953
|
+
pass
|
|
540
954
|
_write_run_receipt(state_root, receipt)
|
|
541
955
|
atomic_write_json(
|
|
542
956
|
spec_path,
|
|
@@ -575,6 +989,7 @@ def start_run_session(
|
|
|
575
989
|
)
|
|
576
990
|
except OSError as exc:
|
|
577
991
|
spec_path.unlink(missing_ok=True)
|
|
992
|
+
_resource_lock_secrets_path(state_root, session_id).unlink(missing_ok=True)
|
|
578
993
|
locks_released = _release_resource_locks(
|
|
579
994
|
state_root,
|
|
580
995
|
session_id,
|
|
@@ -613,22 +1028,32 @@ def start_run_session(
|
|
|
613
1028
|
def _terminate_managed_child(
|
|
614
1029
|
process: subprocess.Popen[bytes],
|
|
615
1030
|
job: Any,
|
|
1031
|
+
*,
|
|
1032
|
+
attestation: dict[str, Any] | None = None,
|
|
1033
|
+
ownership_proof: dict[str, Any] | None = None,
|
|
616
1034
|
) -> bool:
|
|
617
|
-
from
|
|
1035
|
+
from harness_process import terminate_owned_tree
|
|
618
1036
|
|
|
619
|
-
if
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1037
|
+
if isinstance(attestation, dict) and isinstance(ownership_proof, dict):
|
|
1038
|
+
result = terminate_owned_tree(
|
|
1039
|
+
attestation,
|
|
1040
|
+
ownership_proof,
|
|
1041
|
+
{"graceSeconds": 3.0},
|
|
1042
|
+
)
|
|
1043
|
+
if not result.get("cleanupComplete"):
|
|
624
1044
|
return False
|
|
625
|
-
|
|
626
|
-
|
|
1045
|
+
elif job is not None and getattr(job, "handle", None) is not None:
|
|
1046
|
+
# Compatibility for callers still passing the old Windows job object;
|
|
1047
|
+
# this branch is only used for an already-owned handle, never a PID.
|
|
1048
|
+
job.terminate_and_wait()
|
|
1049
|
+
else:
|
|
1050
|
+
# PID-only cleanup is intentionally forbidden by the process provider.
|
|
1051
|
+
return False
|
|
627
1052
|
try:
|
|
628
1053
|
process.wait(timeout=5)
|
|
629
1054
|
except subprocess.TimeoutExpired:
|
|
630
1055
|
return False
|
|
631
|
-
return
|
|
1056
|
+
return process.poll() is not None
|
|
632
1057
|
|
|
633
1058
|
|
|
634
1059
|
def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
@@ -637,6 +1062,11 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
637
1062
|
session_root = _run_session_root(state_root, session_id)
|
|
638
1063
|
spec_path = session_root / "launch-spec.json"
|
|
639
1064
|
receipt = _load_run_receipt(state_root, session_id)
|
|
1065
|
+
receipt["resourceLockTokens"] = _load_resource_lock_tokens(
|
|
1066
|
+
state_root,
|
|
1067
|
+
session_id,
|
|
1068
|
+
receipt,
|
|
1069
|
+
)
|
|
640
1070
|
try:
|
|
641
1071
|
spec = json.loads(spec_path.read_text(encoding="utf-8-sig"))
|
|
642
1072
|
except (OSError, json.JSONDecodeError) as exc:
|
|
@@ -690,27 +1120,32 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
690
1120
|
stderr_path = Path(str(receipt["stderrPath"]))
|
|
691
1121
|
stdout_path.parent.mkdir(parents=True, exist_ok=True)
|
|
692
1122
|
job = None
|
|
1123
|
+
spawned = None
|
|
1124
|
+
ownership_proof: dict[str, Any] | None = None
|
|
1125
|
+
identity: dict[str, Any] | None = None
|
|
693
1126
|
process: subprocess.Popen[bytes] | None = None
|
|
694
1127
|
try:
|
|
695
1128
|
with stdout_path.open("wb", buffering=0) as stdout, stderr_path.open(
|
|
696
1129
|
"wb", buffering=0
|
|
697
1130
|
) as stderr:
|
|
698
|
-
popen_kwargs: dict[str, Any] = {
|
|
699
|
-
"cwd": str(receipt["workingDirectory"]),
|
|
700
|
-
"env": os.environ.copy(),
|
|
701
|
-
"stdin": subprocess.DEVNULL,
|
|
702
|
-
"stdout": stdout,
|
|
703
|
-
"stderr": stderr,
|
|
704
|
-
"shell": False,
|
|
705
|
-
"close_fds": True,
|
|
706
|
-
}
|
|
707
|
-
if os.name == "nt":
|
|
708
|
-
popen_kwargs["creationflags"] = 0x00000200
|
|
709
|
-
else:
|
|
710
|
-
popen_kwargs["start_new_session"] = True
|
|
711
1131
|
try:
|
|
712
|
-
|
|
713
|
-
|
|
1132
|
+
from harness_process import spawn_structured_argv
|
|
1133
|
+
|
|
1134
|
+
spawned = spawn_structured_argv(
|
|
1135
|
+
argv,
|
|
1136
|
+
cwd=Path(str(receipt["workingDirectory"])),
|
|
1137
|
+
environment={
|
|
1138
|
+
"PYTHONUTF8": "1",
|
|
1139
|
+
"PYTHONIOENCODING": "utf-8",
|
|
1140
|
+
},
|
|
1141
|
+
owner_token=f"run:{session_id}",
|
|
1142
|
+
stdout=stdout,
|
|
1143
|
+
stderr=stderr,
|
|
1144
|
+
)
|
|
1145
|
+
process = spawned.process
|
|
1146
|
+
ownership_proof = spawned.ownershipProof
|
|
1147
|
+
_capture_provider_members(spawned)
|
|
1148
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
714
1149
|
receipt.update(
|
|
715
1150
|
{
|
|
716
1151
|
"status": "INCOMPLETE",
|
|
@@ -723,56 +1158,19 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
723
1158
|
}
|
|
724
1159
|
)
|
|
725
1160
|
return 1
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
candidate = _WindowsKillOnCloseJob()
|
|
731
|
-
if candidate.assign(process):
|
|
732
|
-
job = candidate
|
|
733
|
-
else:
|
|
734
|
-
identity_session = {
|
|
735
|
-
"pid": process.pid,
|
|
736
|
-
"startedAt": identity["startedAt"],
|
|
737
|
-
"processIdentity": {
|
|
738
|
-
"executable": identity["executable"],
|
|
739
|
-
},
|
|
740
|
-
}
|
|
741
|
-
verified = verify_process_identity(identity_session) is True
|
|
742
|
-
terminated = (
|
|
743
|
-
_terminate_managed_child(process, None)
|
|
744
|
-
if verified
|
|
745
|
-
else False
|
|
746
|
-
)
|
|
747
|
-
receipt.update(
|
|
748
|
-
{
|
|
749
|
-
"servicePid": process.pid,
|
|
750
|
-
"processIdentity": identity,
|
|
751
|
-
"startedAt": identity["startedAt"],
|
|
752
|
-
"status": "INCOMPLETE",
|
|
753
|
-
"reasonCode": "PROCESS_TREE_ISOLATION_UNAVAILABLE",
|
|
754
|
-
"stage": "spawn",
|
|
755
|
-
"testProcessStarted": True,
|
|
756
|
-
"processTreeIsolated": False,
|
|
757
|
-
"exitCode": process.poll(),
|
|
758
|
-
"cleanupStatus": (
|
|
759
|
-
"PROCESS_TREE_TERMINATED"
|
|
760
|
-
if terminated
|
|
761
|
-
else "PROCESS_TERMINATION_UNCONFIRMED"
|
|
762
|
-
),
|
|
763
|
-
}
|
|
764
|
-
)
|
|
765
|
-
return 1
|
|
1161
|
+
assert spawned is not None and process is not None
|
|
1162
|
+
identity = spawned.attestation
|
|
1163
|
+
receipt["processOwnershipProof"] = ownership_proof
|
|
766
1164
|
_stage_transition(receipt, "execute")
|
|
767
1165
|
receipt.update(
|
|
768
1166
|
{
|
|
769
1167
|
"servicePid": process.pid,
|
|
770
1168
|
"processIdentity": identity,
|
|
771
|
-
"startedAt": identity
|
|
1169
|
+
"startedAt": identity.get("createdAt"),
|
|
772
1170
|
"status": "RUNNING",
|
|
773
1171
|
"reasonCode": "CHILD_RUNNING",
|
|
774
1172
|
"testProcessStarted": True,
|
|
775
|
-
"processTreeIsolated":
|
|
1173
|
+
"processTreeIsolated": ownership_proof is not None,
|
|
776
1174
|
}
|
|
777
1175
|
)
|
|
778
1176
|
_write_run_receipt(state_root, receipt)
|
|
@@ -791,15 +1189,18 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
791
1189
|
termination_reason = "CANCEL_REQUESTED"
|
|
792
1190
|
if termination_reason is not None:
|
|
793
1191
|
diagnostic_path = session_root / "diagnostic.json"
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1192
|
+
from harness_process import (
|
|
1193
|
+
observe_process_identity,
|
|
1194
|
+
verify_process_identity as verify_attestation,
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
verified = verify_attestation(
|
|
1198
|
+
identity,
|
|
1199
|
+
observe_process_identity(process.pid),
|
|
1200
|
+
).get("ok") is True
|
|
1201
|
+
job_owned = ownership_proof is not None
|
|
1202
|
+
if spawned is not None:
|
|
1203
|
+
_capture_provider_members(spawned)
|
|
803
1204
|
diagnostic = {
|
|
804
1205
|
"schemaVersion": 1,
|
|
805
1206
|
"sessionId": session_id,
|
|
@@ -825,8 +1226,13 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
825
1226
|
}
|
|
826
1227
|
atomic_write_json(diagnostic_path, diagnostic)
|
|
827
1228
|
receipt["diagnosticPath"] = str(diagnostic_path)
|
|
828
|
-
if job_owned
|
|
829
|
-
terminated = _terminate_managed_child(
|
|
1229
|
+
if job_owned and verified:
|
|
1230
|
+
terminated = _terminate_managed_child(
|
|
1231
|
+
process,
|
|
1232
|
+
job,
|
|
1233
|
+
attestation=identity,
|
|
1234
|
+
ownership_proof=ownership_proof,
|
|
1235
|
+
)
|
|
830
1236
|
job = None
|
|
831
1237
|
if not terminated:
|
|
832
1238
|
receipt.update(
|
|
@@ -864,7 +1270,14 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
864
1270
|
"cleanupStatus": "PROCESS_TREE_TERMINATION_REQUIRED",
|
|
865
1271
|
}
|
|
866
1272
|
)
|
|
867
|
-
|
|
1273
|
+
if spawned is not None:
|
|
1274
|
+
_capture_provider_members(spawned)
|
|
1275
|
+
_terminate_managed_child(
|
|
1276
|
+
process,
|
|
1277
|
+
job,
|
|
1278
|
+
attestation=identity,
|
|
1279
|
+
ownership_proof=ownership_proof,
|
|
1280
|
+
)
|
|
868
1281
|
job = None
|
|
869
1282
|
break
|
|
870
1283
|
_write_run_receipt(state_root, receipt)
|
|
@@ -914,8 +1327,15 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
914
1327
|
}
|
|
915
1328
|
)
|
|
916
1329
|
finally:
|
|
917
|
-
if
|
|
918
|
-
|
|
1330
|
+
if process is not None and process.poll() is None and ownership_proof is not None:
|
|
1331
|
+
if spawned is not None:
|
|
1332
|
+
_capture_provider_members(spawned)
|
|
1333
|
+
tree_drained = _terminate_managed_child(
|
|
1334
|
+
process,
|
|
1335
|
+
job,
|
|
1336
|
+
attestation=identity if isinstance(identity, dict) else None,
|
|
1337
|
+
ownership_proof=ownership_proof,
|
|
1338
|
+
)
|
|
919
1339
|
if not tree_drained:
|
|
920
1340
|
receipt.update(
|
|
921
1341
|
{
|
|
@@ -944,6 +1364,10 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
944
1364
|
session_id,
|
|
945
1365
|
dict(receipt.get("resourceLockTokens") or {}),
|
|
946
1366
|
)
|
|
1367
|
+
try:
|
|
1368
|
+
_resource_lock_secrets_path(state_root, session_id).unlink(missing_ok=True)
|
|
1369
|
+
except OSError:
|
|
1370
|
+
locks_released = False
|
|
947
1371
|
if not locks_released:
|
|
948
1372
|
receipt.update(
|
|
949
1373
|
{
|
|
@@ -977,32 +1401,17 @@ def _run_session_worker_with_log(state_root: Path, session_id: str) -> int:
|
|
|
977
1401
|
|
|
978
1402
|
|
|
979
1403
|
def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
|
|
980
|
-
from harness_service import is_pid_alive
|
|
1404
|
+
from harness_service import is_pid_alive
|
|
981
1405
|
|
|
982
1406
|
receipt = _load_run_receipt(state_root, session_id)
|
|
983
1407
|
worker_pid = receipt.get("workerPid")
|
|
984
1408
|
worker_identity = receipt.get("workerIdentity")
|
|
985
|
-
worker_session = {
|
|
986
|
-
"pid": worker_pid,
|
|
987
|
-
"startedAt": (
|
|
988
|
-
worker_identity.get("startedAt")
|
|
989
|
-
if isinstance(worker_identity, dict)
|
|
990
|
-
else None
|
|
991
|
-
),
|
|
992
|
-
"processIdentity": {
|
|
993
|
-
"executable": (
|
|
994
|
-
worker_identity.get("executable")
|
|
995
|
-
if isinstance(worker_identity, dict)
|
|
996
|
-
else None
|
|
997
|
-
)
|
|
998
|
-
},
|
|
999
|
-
}
|
|
1000
1409
|
if receipt.get("status") in RUN_TERMINAL_STATUSES:
|
|
1001
1410
|
if (
|
|
1002
1411
|
isinstance(worker_pid, int)
|
|
1003
1412
|
and worker_pid > 0
|
|
1004
1413
|
and is_pid_alive(worker_pid)
|
|
1005
|
-
and
|
|
1414
|
+
and _receipt_identity_state(receipt) is True
|
|
1006
1415
|
):
|
|
1007
1416
|
finalizing = dict(receipt)
|
|
1008
1417
|
finalizing.update(
|
|
@@ -1036,7 +1445,7 @@ def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
|
|
|
1036
1445
|
and heartbeat_age < startup_grace
|
|
1037
1446
|
):
|
|
1038
1447
|
return receipt
|
|
1039
|
-
identity_state =
|
|
1448
|
+
identity_state = _receipt_identity_state(receipt)
|
|
1040
1449
|
worker_gone = (
|
|
1041
1450
|
isinstance(worker_pid, int)
|
|
1042
1451
|
and worker_pid > 0
|
|
@@ -1079,6 +1488,7 @@ def read_run_session_log(
|
|
|
1079
1488
|
receipt = _load_run_receipt(state_root, session_id)
|
|
1080
1489
|
path = Path(str(receipt[f"{stream}Path"]))
|
|
1081
1490
|
raw = b""
|
|
1491
|
+
decode_status = "OK"
|
|
1082
1492
|
size = path.stat().st_size if path.is_file() else 0
|
|
1083
1493
|
if path.is_file() and cursor < size:
|
|
1084
1494
|
with path.open("rb") as handle:
|
|
@@ -1090,8 +1500,25 @@ def read_run_session_log(
|
|
|
1090
1500
|
break
|
|
1091
1501
|
except UnicodeDecodeError as exc:
|
|
1092
1502
|
if exc.end != len(raw):
|
|
1503
|
+
# An invalid sequence in the middle is durable evidence,
|
|
1504
|
+
# so consume it and expose replacement text instead of
|
|
1505
|
+
# silently dropping bytes from the cursor stream.
|
|
1093
1506
|
text = raw.decode("utf-8", errors="replace")
|
|
1507
|
+
decode_status = "LOG_DECODE_DEGRADED"
|
|
1094
1508
|
break
|
|
1509
|
+
if (
|
|
1510
|
+
receipt.get("status") in RUN_TERMINAL_STATUSES
|
|
1511
|
+
and cursor + len(raw) >= size
|
|
1512
|
+
):
|
|
1513
|
+
# Once the producer is terminal, an incomplete trailing
|
|
1514
|
+
# sequence cannot be completed by a later write. Preserve
|
|
1515
|
+
# and consume it with an explicit degraded status.
|
|
1516
|
+
text = raw.decode("utf-8", errors="replace")
|
|
1517
|
+
decode_status = "LOG_DECODE_DEGRADED"
|
|
1518
|
+
break
|
|
1519
|
+
# A live writer may have split a multibyte character across
|
|
1520
|
+
# this read. Keep the incomplete suffix for the next cursor
|
|
1521
|
+
# request so callers never see duplicate replacement glyphs.
|
|
1095
1522
|
raw = raw[: exc.start]
|
|
1096
1523
|
else:
|
|
1097
1524
|
text = ""
|
|
@@ -1108,6 +1535,8 @@ def read_run_session_log(
|
|
|
1108
1535
|
"text": text,
|
|
1109
1536
|
"eof": terminal and next_cursor >= size,
|
|
1110
1537
|
"status": receipt.get("status"),
|
|
1538
|
+
"rawDigest": "sha256:" + hashlib.sha256(raw).hexdigest(),
|
|
1539
|
+
"decodeStatus": decode_status,
|
|
1111
1540
|
}
|
|
1112
1541
|
|
|
1113
1542
|
|
|
@@ -1117,28 +1546,10 @@ def cancel_run_session(
|
|
|
1117
1546
|
*,
|
|
1118
1547
|
reason: str = "CANCEL_REQUESTED",
|
|
1119
1548
|
) -> dict[str, Any]:
|
|
1120
|
-
from harness_service import verify_process_identity
|
|
1121
|
-
|
|
1122
1549
|
receipt = _load_run_receipt(state_root, session_id)
|
|
1123
1550
|
if receipt.get("status") in RUN_TERMINAL_STATUSES:
|
|
1124
1551
|
return receipt
|
|
1125
|
-
|
|
1126
|
-
worker_session = {
|
|
1127
|
-
"pid": receipt.get("workerPid"),
|
|
1128
|
-
"startedAt": (
|
|
1129
|
-
worker_identity.get("startedAt")
|
|
1130
|
-
if isinstance(worker_identity, dict)
|
|
1131
|
-
else None
|
|
1132
|
-
),
|
|
1133
|
-
"processIdentity": {
|
|
1134
|
-
"executable": (
|
|
1135
|
-
worker_identity.get("executable")
|
|
1136
|
-
if isinstance(worker_identity, dict)
|
|
1137
|
-
else None
|
|
1138
|
-
)
|
|
1139
|
-
},
|
|
1140
|
-
}
|
|
1141
|
-
if verify_process_identity(worker_session) is not True:
|
|
1552
|
+
if _receipt_identity_state(receipt) is not True:
|
|
1142
1553
|
return {
|
|
1143
1554
|
**receipt,
|
|
1144
1555
|
"ok": False,
|