@hunter-harness/workflow-harness 0.2.43 → 0.2.45
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 +324 -28
- 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 +646 -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 +324 -28
- 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 +646 -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 +324 -28
- 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 +646 -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 +324 -28
- 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 +646 -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 +324 -28
- 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 +646 -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 +324 -28
- 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 +646 -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 +324 -28
- 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 +646 -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 +324 -28
- 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 +646 -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,351 @@ 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 _build_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
|
+
return {
|
|
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
|
+
|
|
240
|
+
|
|
241
|
+
def _write_sensitive_scan_receipt(
|
|
242
|
+
change_root: Path,
|
|
243
|
+
*,
|
|
244
|
+
entries: list[dict[str, Any]],
|
|
245
|
+
unresolved: list[dict[str, Any]],
|
|
246
|
+
status: str,
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
change_root = change_root.expanduser().resolve()
|
|
249
|
+
receipt = _build_sensitive_scan_receipt(
|
|
250
|
+
change_root,
|
|
251
|
+
entries=entries,
|
|
252
|
+
unresolved=unresolved,
|
|
253
|
+
status=status,
|
|
254
|
+
)
|
|
255
|
+
atomic_write_json(_sensitive_evidence_receipt_path(change_root), receipt)
|
|
256
|
+
return receipt
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def quarantine_sensitive_evidence(
|
|
260
|
+
source: Path,
|
|
261
|
+
*,
|
|
262
|
+
change_root: Path,
|
|
263
|
+
reason: str = "sensitive evidence",
|
|
264
|
+
private_root: Path | None = None,
|
|
265
|
+
) -> dict[str, Any]:
|
|
266
|
+
"""Atomically move legacy plaintext evidence outside any publishable tree."""
|
|
267
|
+
source = source.expanduser().resolve()
|
|
268
|
+
change_root = change_root.expanduser().resolve()
|
|
269
|
+
private = _private_evidence_root(private_root)
|
|
270
|
+
if not source.is_file():
|
|
271
|
+
return {"ok": False, "reasonCode": "SENSITIVE_EVIDENCE_SOURCE_MISSING", "sourcePath": str(source)}
|
|
272
|
+
if private == change_root or private.is_relative_to(change_root):
|
|
273
|
+
return {
|
|
274
|
+
"ok": False,
|
|
275
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
276
|
+
"error": "private quarantine root must be outside the publishable change root",
|
|
277
|
+
}
|
|
278
|
+
try:
|
|
279
|
+
raw_digest = "sha256:" + hashlib.sha256(source.read_bytes()).hexdigest()
|
|
280
|
+
quarantine_dir = private / uuid.uuid4().hex
|
|
281
|
+
quarantine_dir.mkdir(parents=True, exist_ok=False)
|
|
282
|
+
acl_dir = _secure_private_path(quarantine_dir, directory=True)
|
|
283
|
+
target = quarantine_dir / "payload.bin"
|
|
284
|
+
os.replace(source, target)
|
|
285
|
+
acl_file = _secure_private_path(target, directory=False)
|
|
286
|
+
if os.name == "nt" and (
|
|
287
|
+
acl_dir != "WINDOWS_CURRENT_USER_ONLY"
|
|
288
|
+
or acl_file != "WINDOWS_CURRENT_USER_ONLY"
|
|
289
|
+
):
|
|
290
|
+
os.replace(target, source)
|
|
291
|
+
shutil.rmtree(quarantine_dir, ignore_errors=True)
|
|
292
|
+
return {
|
|
293
|
+
"ok": False,
|
|
294
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
295
|
+
"error": "private quarantine ACL could not be restricted to the current user",
|
|
296
|
+
}
|
|
297
|
+
moved_digest = "sha256:" + hashlib.sha256(target.read_bytes()).hexdigest()
|
|
298
|
+
if moved_digest != raw_digest:
|
|
299
|
+
os.replace(target, source)
|
|
300
|
+
shutil.rmtree(quarantine_dir, ignore_errors=True)
|
|
301
|
+
return {
|
|
302
|
+
"ok": False,
|
|
303
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
304
|
+
"error": "quarantine digest verification failed",
|
|
305
|
+
}
|
|
306
|
+
try:
|
|
307
|
+
prior = json.loads(
|
|
308
|
+
_sensitive_evidence_receipt_path(change_root).read_text(
|
|
309
|
+
encoding="utf-8-sig"
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
except (OSError, json.JSONDecodeError):
|
|
313
|
+
prior = {}
|
|
314
|
+
entries = list(prior.get("entries") or []) if isinstance(prior, dict) else []
|
|
315
|
+
entries.append({
|
|
316
|
+
"sourcePath": (
|
|
317
|
+
source.relative_to(change_root).as_posix()
|
|
318
|
+
if source.is_relative_to(change_root)
|
|
319
|
+
else source.name
|
|
320
|
+
),
|
|
321
|
+
"sourceDigest": raw_digest,
|
|
322
|
+
"privatePath": str(target),
|
|
323
|
+
"reason": reason[:160],
|
|
324
|
+
"status": "QUARANTINED",
|
|
325
|
+
"acl": {"directory": acl_dir, "file": acl_file},
|
|
326
|
+
"movedAt": now_iso(),
|
|
327
|
+
})
|
|
328
|
+
try:
|
|
329
|
+
receipt = _write_sensitive_scan_receipt(
|
|
330
|
+
change_root,
|
|
331
|
+
entries=entries,
|
|
332
|
+
unresolved=[],
|
|
333
|
+
status="QUARANTINED",
|
|
334
|
+
)
|
|
335
|
+
except BaseException as exc:
|
|
336
|
+
try:
|
|
337
|
+
os.replace(target, source)
|
|
338
|
+
except OSError:
|
|
339
|
+
pass
|
|
340
|
+
shutil.rmtree(quarantine_dir, ignore_errors=True)
|
|
341
|
+
return {
|
|
342
|
+
"ok": False,
|
|
343
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
344
|
+
"error": str(exc),
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
"ok": True,
|
|
348
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINED",
|
|
349
|
+
"sourceDigest": raw_digest,
|
|
350
|
+
"privatePath": str(target),
|
|
351
|
+
"receiptPath": str(_sensitive_evidence_receipt_path(change_root)),
|
|
352
|
+
"receipt": receipt,
|
|
353
|
+
}
|
|
354
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
355
|
+
# No copy is retained on failure. If os.replace crossed filesystems it
|
|
356
|
+
# fails before removing the source, which is the required fail-closed path.
|
|
357
|
+
return {
|
|
358
|
+
"ok": False,
|
|
359
|
+
"reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
|
|
360
|
+
"error": str(exc),
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def ensure_sensitive_evidence_scan_receipt(change_root: Path) -> dict[str, Any]:
|
|
365
|
+
"""Create a digest-bound scan receipt for a tree with no plaintext findings."""
|
|
366
|
+
change_root = change_root.expanduser().resolve()
|
|
367
|
+
unresolved = _sensitive_candidates(change_root)
|
|
368
|
+
if unresolved:
|
|
369
|
+
return _write_sensitive_scan_receipt(
|
|
370
|
+
change_root,
|
|
371
|
+
entries=[],
|
|
372
|
+
unresolved=unresolved,
|
|
373
|
+
status="FAIL",
|
|
374
|
+
)
|
|
375
|
+
return _write_sensitive_scan_receipt(
|
|
376
|
+
change_root,
|
|
377
|
+
entries=[],
|
|
378
|
+
unresolved=[],
|
|
379
|
+
status="OK",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def refresh_sensitive_evidence_scan_receipt(
|
|
384
|
+
change_root: Path,
|
|
385
|
+
*,
|
|
386
|
+
persist: bool = True,
|
|
387
|
+
) -> dict[str, Any]:
|
|
388
|
+
"""Rescan current bytes while preserving valid quarantine audit entries."""
|
|
389
|
+
change_root = change_root.expanduser().resolve()
|
|
390
|
+
receipt_path = _sensitive_evidence_receipt_path(change_root)
|
|
391
|
+
entries: list[dict[str, Any]] = []
|
|
392
|
+
if receipt_path.is_file():
|
|
393
|
+
try:
|
|
394
|
+
prior = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
|
|
395
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
396
|
+
return {
|
|
397
|
+
"ok": False,
|
|
398
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
|
|
399
|
+
"receiptPath": str(receipt_path),
|
|
400
|
+
"error": str(exc),
|
|
401
|
+
}
|
|
402
|
+
if not isinstance(prior, dict):
|
|
403
|
+
return {
|
|
404
|
+
"ok": False,
|
|
405
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
|
|
406
|
+
"receiptPath": str(receipt_path),
|
|
407
|
+
"error": "receipt must be an object",
|
|
408
|
+
}
|
|
409
|
+
prior_entries = prior.get("entries")
|
|
410
|
+
prior_unresolved = prior.get("unresolvedFailures")
|
|
411
|
+
valid_identity = (
|
|
412
|
+
prior.get("schemaVersion") == 1
|
|
413
|
+
and prior.get("rulesVersion") == SECRET_SCAN_RULES_VERSION
|
|
414
|
+
and prior.get("changeId") == change_root.name
|
|
415
|
+
and prior.get("publicationExcluded") is True
|
|
416
|
+
and str(prior.get("status") or "").upper()
|
|
417
|
+
in {"OK", "QUARANTINED", "FAIL"}
|
|
418
|
+
)
|
|
419
|
+
if (
|
|
420
|
+
not valid_identity
|
|
421
|
+
or not isinstance(prior_entries, list)
|
|
422
|
+
or not isinstance(prior_unresolved, list)
|
|
423
|
+
):
|
|
424
|
+
return {
|
|
425
|
+
"ok": False,
|
|
426
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
|
|
427
|
+
"receiptPath": str(receipt_path),
|
|
428
|
+
"error": "receipt identity or collection fields are invalid",
|
|
429
|
+
}
|
|
430
|
+
entries = list(prior_entries)
|
|
431
|
+
|
|
432
|
+
unresolved = _sensitive_candidates(change_root)
|
|
433
|
+
status = "FAIL" if unresolved else "QUARANTINED" if entries else "OK"
|
|
434
|
+
if persist:
|
|
435
|
+
try:
|
|
436
|
+
receipt = _write_sensitive_scan_receipt(
|
|
437
|
+
change_root,
|
|
438
|
+
entries=entries,
|
|
439
|
+
unresolved=unresolved,
|
|
440
|
+
status=status,
|
|
441
|
+
)
|
|
442
|
+
except OSError as exc:
|
|
443
|
+
return {
|
|
444
|
+
"ok": False,
|
|
445
|
+
"reasonCode": "SECRET_SCAN_RECEIPT_REFRESH_FAILED",
|
|
446
|
+
"receiptPath": str(receipt_path),
|
|
447
|
+
"error": str(exc),
|
|
448
|
+
}
|
|
449
|
+
else:
|
|
450
|
+
receipt = _build_sensitive_scan_receipt(
|
|
451
|
+
change_root,
|
|
452
|
+
entries=entries,
|
|
453
|
+
unresolved=unresolved,
|
|
454
|
+
status=status,
|
|
455
|
+
)
|
|
456
|
+
return {
|
|
457
|
+
"ok": not unresolved,
|
|
458
|
+
"reasonCode": (
|
|
459
|
+
"SENSITIVE_EVIDENCE_UNQUARANTINED"
|
|
460
|
+
if unresolved
|
|
461
|
+
else "SECRET_SCAN_RECEIPT_REFRESHED"
|
|
462
|
+
),
|
|
463
|
+
"receiptPath": str(receipt_path),
|
|
464
|
+
"receipt": receipt,
|
|
465
|
+
"persisted": persist,
|
|
466
|
+
"unresolvedFailures": unresolved,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
119
470
|
def _run_sessions_root(state_root: Path) -> Path:
|
|
120
471
|
return state_root.expanduser().resolve() / "runtime" / "run-sessions"
|
|
121
472
|
|
|
@@ -130,17 +481,114 @@ def _run_receipt_path(state_root: Path, session_id: str) -> Path:
|
|
|
130
481
|
return _run_session_root(state_root, session_id) / "session.json"
|
|
131
482
|
|
|
132
483
|
|
|
484
|
+
def _resource_lock_secrets_path(state_root: Path, session_id: str) -> Path:
|
|
485
|
+
return _run_session_root(state_root, session_id) / "resource-lock-secrets.json"
|
|
486
|
+
|
|
487
|
+
|
|
133
488
|
def _load_run_receipt(state_root: Path, session_id: str) -> dict[str, Any]:
|
|
134
489
|
path = _run_receipt_path(state_root, session_id)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
490
|
+
last_error: OSError | None = None
|
|
491
|
+
for attempt in range(20):
|
|
492
|
+
try:
|
|
493
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
494
|
+
break
|
|
495
|
+
except FileNotFoundError as exc:
|
|
496
|
+
raise ValueError(f"RUN_SESSION_NOT_FOUND: {session_id}") from exc
|
|
497
|
+
except PermissionError as exc:
|
|
498
|
+
last_error = exc
|
|
499
|
+
if attempt == 19:
|
|
500
|
+
raise ValueError(
|
|
501
|
+
f"RUN_SESSION_RECEIPT_BUSY: {session_id}"
|
|
502
|
+
) from exc
|
|
503
|
+
time.sleep(0.01 * (attempt + 1))
|
|
504
|
+
else:
|
|
505
|
+
raise ValueError(f"RUN_SESSION_RECEIPT_BUSY: {session_id}") from last_error
|
|
139
506
|
if not isinstance(value, dict):
|
|
140
507
|
raise ValueError(f"RUN_SESSION_CORRUPT: {session_id}")
|
|
141
508
|
return value
|
|
142
509
|
|
|
143
510
|
|
|
511
|
+
def _load_resource_lock_tokens(
|
|
512
|
+
state_root: Path,
|
|
513
|
+
session_id: str,
|
|
514
|
+
receipt: dict[str, Any],
|
|
515
|
+
) -> dict[str, str]:
|
|
516
|
+
"""Load lock tokens from a private sidecar; legacy receipts may inline them."""
|
|
517
|
+
path = _resource_lock_secrets_path(state_root, session_id)
|
|
518
|
+
try:
|
|
519
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
520
|
+
if isinstance(value, dict) and isinstance(value.get("tokens"), dict):
|
|
521
|
+
return {
|
|
522
|
+
str(name): str(token)
|
|
523
|
+
for name, token in value["tokens"].items()
|
|
524
|
+
if isinstance(name, str) and isinstance(token, str)
|
|
525
|
+
}
|
|
526
|
+
except (OSError, json.JSONDecodeError, TypeError):
|
|
527
|
+
pass
|
|
528
|
+
legacy = receipt.get("resourceLockTokens")
|
|
529
|
+
if isinstance(legacy, dict):
|
|
530
|
+
return {
|
|
531
|
+
str(name): str(token)
|
|
532
|
+
for name, token in legacy.items()
|
|
533
|
+
if isinstance(name, str) and isinstance(token, str)
|
|
534
|
+
}
|
|
535
|
+
return {}
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _receipt_identity_state(
|
|
539
|
+
receipt: dict[str, Any],
|
|
540
|
+
*,
|
|
541
|
+
field: str = "workerIdentity",
|
|
542
|
+
) -> bool | None:
|
|
543
|
+
"""Verify a canonical provider attestation, with legacy facade fallback."""
|
|
544
|
+
identity = receipt.get(field)
|
|
545
|
+
pid = identity.get("pid") if isinstance(identity, dict) else receipt.get("workerPid")
|
|
546
|
+
if isinstance(identity, dict) and {
|
|
547
|
+
"schemaVersion",
|
|
548
|
+
"createdAt",
|
|
549
|
+
"fieldProvenance",
|
|
550
|
+
"capabilities",
|
|
551
|
+
} <= set(identity):
|
|
552
|
+
from harness_process import observe_process_identity, verify_process_identity
|
|
553
|
+
|
|
554
|
+
observed = observe_process_identity(pid)
|
|
555
|
+
decision = verify_process_identity(identity, observed)
|
|
556
|
+
if decision.get("ok") is True:
|
|
557
|
+
return True
|
|
558
|
+
if decision.get("reasonCode") == "IDENTITY_UNVERIFIABLE":
|
|
559
|
+
return None
|
|
560
|
+
return False
|
|
561
|
+
from harness_service import verify_process_identity
|
|
562
|
+
|
|
563
|
+
legacy = {
|
|
564
|
+
"pid": pid,
|
|
565
|
+
"startedAt": (
|
|
566
|
+
identity.get("startedAt")
|
|
567
|
+
if isinstance(identity, dict)
|
|
568
|
+
else None
|
|
569
|
+
),
|
|
570
|
+
"processIdentity": {
|
|
571
|
+
"executable": (
|
|
572
|
+
identity.get("executable")
|
|
573
|
+
if isinstance(identity, dict)
|
|
574
|
+
else None
|
|
575
|
+
)
|
|
576
|
+
},
|
|
577
|
+
}
|
|
578
|
+
return verify_process_identity(legacy)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _capture_provider_members(spawned: Any) -> None:
|
|
582
|
+
"""Persist current provider-owned members before a later cleanup."""
|
|
583
|
+
proof = getattr(spawned, "ownershipProof", None)
|
|
584
|
+
leader = getattr(spawned, "attestation", None)
|
|
585
|
+
if not isinstance(proof, dict) or not isinstance(leader, dict):
|
|
586
|
+
return
|
|
587
|
+
from harness_process import capture_owned_members
|
|
588
|
+
|
|
589
|
+
capture_owned_members(spawned)
|
|
590
|
+
|
|
591
|
+
|
|
144
592
|
def _resource_lock_root() -> Path:
|
|
145
593
|
return Path(
|
|
146
594
|
os.environ.get(
|
|
@@ -366,25 +814,86 @@ def _acquire_resource_locks(
|
|
|
366
814
|
|
|
367
815
|
|
|
368
816
|
def _write_run_receipt(state_root: Path, receipt: dict[str, Any]) -> None:
|
|
817
|
+
_update_receipt_contract(receipt)
|
|
818
|
+
publishable = json.loads(json.dumps(receipt, ensure_ascii=False))
|
|
819
|
+
raw_tokens = publishable.pop("resourceLockTokens", {})
|
|
820
|
+
if isinstance(raw_tokens, dict):
|
|
821
|
+
publishable["resourceLockTokenHashes"] = {
|
|
822
|
+
str(name): _sha256_json(str(token))
|
|
823
|
+
for name, token in raw_tokens.items()
|
|
824
|
+
if isinstance(name, str) and isinstance(token, str)
|
|
825
|
+
}
|
|
369
826
|
atomic_write_json(
|
|
370
827
|
_run_receipt_path(state_root, str(receipt["sessionId"])),
|
|
371
|
-
|
|
828
|
+
publishable,
|
|
372
829
|
)
|
|
373
830
|
|
|
374
831
|
|
|
375
832
|
def _process_identity(pid: int, executable_hint: str) -> dict[str, Any]:
|
|
376
|
-
from
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
833
|
+
from harness_process import observe_process_identity
|
|
834
|
+
|
|
835
|
+
observed = observe_process_identity(pid)
|
|
836
|
+
if not observed.get("executable"):
|
|
837
|
+
observed["executable"] = _absolute_executable(executable_hint)
|
|
838
|
+
observed["fieldProvenance"]["executable"] = "ATTESTED"
|
|
839
|
+
if not observed.get("createdAt"):
|
|
840
|
+
observed["createdAt"] = now_iso()
|
|
841
|
+
observed["fieldProvenance"]["createdAt"] = "ATTESTED"
|
|
842
|
+
# ``startedAt`` is a compatibility alias used by legacy session readers;
|
|
843
|
+
# canonical identity comparisons use ``createdAt`` and provenance.
|
|
844
|
+
observed["startedAt"] = observed.get("createdAt")
|
|
845
|
+
return observed
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _log_contract(path: Path) -> dict[str, Any]:
|
|
849
|
+
raw = b""
|
|
850
|
+
if path.is_file():
|
|
851
|
+
try:
|
|
852
|
+
raw = path.read_bytes()
|
|
853
|
+
except OSError:
|
|
854
|
+
raw = b""
|
|
855
|
+
try:
|
|
856
|
+
raw.decode("utf-8")
|
|
857
|
+
decode_status = "OK"
|
|
858
|
+
except UnicodeDecodeError:
|
|
859
|
+
decode_status = "LOG_DECODE_DEGRADED"
|
|
380
860
|
return {
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
861
|
+
"cursor": len(raw),
|
|
862
|
+
"rawDigest": "sha256:" + hashlib.sha256(raw).hexdigest(),
|
|
863
|
+
"decodeStatus": decode_status,
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _update_receipt_contract(receipt: dict[str, Any]) -> None:
|
|
868
|
+
worker = receipt.get("workerIdentity")
|
|
869
|
+
writer_source = worker if isinstance(worker, dict) else {
|
|
870
|
+
"pid": receipt.get("workerPid") or receipt.get("launcherPid")
|
|
871
|
+
}
|
|
872
|
+
heartbeat_kind = "WORKER" if isinstance(worker, dict) else "SUPERVISOR"
|
|
873
|
+
receipt["heartbeat"] = {
|
|
874
|
+
"kind": heartbeat_kind,
|
|
875
|
+
"writerIdentity": _sha256_json(writer_source),
|
|
876
|
+
"lastSeenAt": str(receipt.get("lastHeartbeatAt") or now_iso()),
|
|
877
|
+
"ttlSeconds": max(1, int(float(receipt.get("heartbeatSeconds") or 30))),
|
|
878
|
+
"staleReason": (
|
|
879
|
+
"SERVICE_HEARTBEAT_STALE"
|
|
880
|
+
if receipt.get("reasonCode") == "HEARTBEAT_LOST"
|
|
881
|
+
else None
|
|
386
882
|
),
|
|
387
|
-
|
|
883
|
+
}
|
|
884
|
+
receipt["logs"] = {
|
|
885
|
+
"stdout": _log_contract(Path(str(receipt.get("stdoutPath") or ""))),
|
|
886
|
+
"stderr": _log_contract(Path(str(receipt.get("stderrPath") or ""))),
|
|
887
|
+
}
|
|
888
|
+
cleanup_status = str(receipt.get("cleanupStatus") or "")
|
|
889
|
+
complete = cleanup_status in {
|
|
890
|
+
"NO_CHILD",
|
|
891
|
+
"PROCESS_EXITED",
|
|
892
|
+
"PROCESS_TREE_TERMINATED",
|
|
893
|
+
}
|
|
894
|
+
receipt["cleanup"] = {
|
|
895
|
+
"complete": complete,
|
|
896
|
+
"reasonCode": None if complete else receipt.get("reasonCode"),
|
|
388
897
|
}
|
|
389
898
|
|
|
390
899
|
|
|
@@ -537,6 +1046,14 @@ def start_run_session(
|
|
|
537
1046
|
_write_run_receipt(state_root, receipt)
|
|
538
1047
|
return receipt
|
|
539
1048
|
receipt["resourceLockTokens"] = lock_tokens
|
|
1049
|
+
atomic_write_json(
|
|
1050
|
+
_resource_lock_secrets_path(state_root, session_id),
|
|
1051
|
+
{"schemaVersion": 1, "tokens": lock_tokens},
|
|
1052
|
+
)
|
|
1053
|
+
try:
|
|
1054
|
+
_resource_lock_secrets_path(state_root, session_id).chmod(0o600)
|
|
1055
|
+
except OSError:
|
|
1056
|
+
pass
|
|
540
1057
|
_write_run_receipt(state_root, receipt)
|
|
541
1058
|
atomic_write_json(
|
|
542
1059
|
spec_path,
|
|
@@ -575,6 +1092,7 @@ def start_run_session(
|
|
|
575
1092
|
)
|
|
576
1093
|
except OSError as exc:
|
|
577
1094
|
spec_path.unlink(missing_ok=True)
|
|
1095
|
+
_resource_lock_secrets_path(state_root, session_id).unlink(missing_ok=True)
|
|
578
1096
|
locks_released = _release_resource_locks(
|
|
579
1097
|
state_root,
|
|
580
1098
|
session_id,
|
|
@@ -613,22 +1131,32 @@ def start_run_session(
|
|
|
613
1131
|
def _terminate_managed_child(
|
|
614
1132
|
process: subprocess.Popen[bytes],
|
|
615
1133
|
job: Any,
|
|
1134
|
+
*,
|
|
1135
|
+
attestation: dict[str, Any] | None = None,
|
|
1136
|
+
ownership_proof: dict[str, Any] | None = None,
|
|
616
1137
|
) -> bool:
|
|
617
|
-
from
|
|
1138
|
+
from harness_process import terminate_owned_tree
|
|
618
1139
|
|
|
619
|
-
if
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1140
|
+
if isinstance(attestation, dict) and isinstance(ownership_proof, dict):
|
|
1141
|
+
result = terminate_owned_tree(
|
|
1142
|
+
attestation,
|
|
1143
|
+
ownership_proof,
|
|
1144
|
+
{"graceSeconds": 3.0},
|
|
1145
|
+
)
|
|
1146
|
+
if not result.get("cleanupComplete"):
|
|
624
1147
|
return False
|
|
625
|
-
|
|
626
|
-
|
|
1148
|
+
elif job is not None and getattr(job, "handle", None) is not None:
|
|
1149
|
+
# Compatibility for callers still passing the old Windows job object;
|
|
1150
|
+
# this branch is only used for an already-owned handle, never a PID.
|
|
1151
|
+
job.terminate_and_wait()
|
|
1152
|
+
else:
|
|
1153
|
+
# PID-only cleanup is intentionally forbidden by the process provider.
|
|
1154
|
+
return False
|
|
627
1155
|
try:
|
|
628
1156
|
process.wait(timeout=5)
|
|
629
1157
|
except subprocess.TimeoutExpired:
|
|
630
1158
|
return False
|
|
631
|
-
return
|
|
1159
|
+
return process.poll() is not None
|
|
632
1160
|
|
|
633
1161
|
|
|
634
1162
|
def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
@@ -637,6 +1165,11 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
637
1165
|
session_root = _run_session_root(state_root, session_id)
|
|
638
1166
|
spec_path = session_root / "launch-spec.json"
|
|
639
1167
|
receipt = _load_run_receipt(state_root, session_id)
|
|
1168
|
+
receipt["resourceLockTokens"] = _load_resource_lock_tokens(
|
|
1169
|
+
state_root,
|
|
1170
|
+
session_id,
|
|
1171
|
+
receipt,
|
|
1172
|
+
)
|
|
640
1173
|
try:
|
|
641
1174
|
spec = json.loads(spec_path.read_text(encoding="utf-8-sig"))
|
|
642
1175
|
except (OSError, json.JSONDecodeError) as exc:
|
|
@@ -690,27 +1223,32 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
690
1223
|
stderr_path = Path(str(receipt["stderrPath"]))
|
|
691
1224
|
stdout_path.parent.mkdir(parents=True, exist_ok=True)
|
|
692
1225
|
job = None
|
|
1226
|
+
spawned = None
|
|
1227
|
+
ownership_proof: dict[str, Any] | None = None
|
|
1228
|
+
identity: dict[str, Any] | None = None
|
|
693
1229
|
process: subprocess.Popen[bytes] | None = None
|
|
694
1230
|
try:
|
|
695
1231
|
with stdout_path.open("wb", buffering=0) as stdout, stderr_path.open(
|
|
696
1232
|
"wb", buffering=0
|
|
697
1233
|
) 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
1234
|
try:
|
|
712
|
-
|
|
713
|
-
|
|
1235
|
+
from harness_process import spawn_structured_argv
|
|
1236
|
+
|
|
1237
|
+
spawned = spawn_structured_argv(
|
|
1238
|
+
argv,
|
|
1239
|
+
cwd=Path(str(receipt["workingDirectory"])),
|
|
1240
|
+
environment={
|
|
1241
|
+
"PYTHONUTF8": "1",
|
|
1242
|
+
"PYTHONIOENCODING": "utf-8",
|
|
1243
|
+
},
|
|
1244
|
+
owner_token=f"run:{session_id}",
|
|
1245
|
+
stdout=stdout,
|
|
1246
|
+
stderr=stderr,
|
|
1247
|
+
)
|
|
1248
|
+
process = spawned.process
|
|
1249
|
+
ownership_proof = spawned.ownershipProof
|
|
1250
|
+
_capture_provider_members(spawned)
|
|
1251
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
714
1252
|
receipt.update(
|
|
715
1253
|
{
|
|
716
1254
|
"status": "INCOMPLETE",
|
|
@@ -723,56 +1261,19 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
723
1261
|
}
|
|
724
1262
|
)
|
|
725
1263
|
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
|
|
1264
|
+
assert spawned is not None and process is not None
|
|
1265
|
+
identity = spawned.attestation
|
|
1266
|
+
receipt["processOwnershipProof"] = ownership_proof
|
|
766
1267
|
_stage_transition(receipt, "execute")
|
|
767
1268
|
receipt.update(
|
|
768
1269
|
{
|
|
769
1270
|
"servicePid": process.pid,
|
|
770
1271
|
"processIdentity": identity,
|
|
771
|
-
"startedAt": identity
|
|
1272
|
+
"startedAt": identity.get("createdAt"),
|
|
772
1273
|
"status": "RUNNING",
|
|
773
1274
|
"reasonCode": "CHILD_RUNNING",
|
|
774
1275
|
"testProcessStarted": True,
|
|
775
|
-
"processTreeIsolated":
|
|
1276
|
+
"processTreeIsolated": ownership_proof is not None,
|
|
776
1277
|
}
|
|
777
1278
|
)
|
|
778
1279
|
_write_run_receipt(state_root, receipt)
|
|
@@ -791,15 +1292,18 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
791
1292
|
termination_reason = "CANCEL_REQUESTED"
|
|
792
1293
|
if termination_reason is not None:
|
|
793
1294
|
diagnostic_path = session_root / "diagnostic.json"
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1295
|
+
from harness_process import (
|
|
1296
|
+
observe_process_identity,
|
|
1297
|
+
verify_process_identity as verify_attestation,
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
verified = verify_attestation(
|
|
1301
|
+
identity,
|
|
1302
|
+
observe_process_identity(process.pid),
|
|
1303
|
+
).get("ok") is True
|
|
1304
|
+
job_owned = ownership_proof is not None
|
|
1305
|
+
if spawned is not None:
|
|
1306
|
+
_capture_provider_members(spawned)
|
|
803
1307
|
diagnostic = {
|
|
804
1308
|
"schemaVersion": 1,
|
|
805
1309
|
"sessionId": session_id,
|
|
@@ -825,8 +1329,13 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
825
1329
|
}
|
|
826
1330
|
atomic_write_json(diagnostic_path, diagnostic)
|
|
827
1331
|
receipt["diagnosticPath"] = str(diagnostic_path)
|
|
828
|
-
if job_owned
|
|
829
|
-
terminated = _terminate_managed_child(
|
|
1332
|
+
if job_owned and verified:
|
|
1333
|
+
terminated = _terminate_managed_child(
|
|
1334
|
+
process,
|
|
1335
|
+
job,
|
|
1336
|
+
attestation=identity,
|
|
1337
|
+
ownership_proof=ownership_proof,
|
|
1338
|
+
)
|
|
830
1339
|
job = None
|
|
831
1340
|
if not terminated:
|
|
832
1341
|
receipt.update(
|
|
@@ -864,7 +1373,14 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
864
1373
|
"cleanupStatus": "PROCESS_TREE_TERMINATION_REQUIRED",
|
|
865
1374
|
}
|
|
866
1375
|
)
|
|
867
|
-
|
|
1376
|
+
if spawned is not None:
|
|
1377
|
+
_capture_provider_members(spawned)
|
|
1378
|
+
_terminate_managed_child(
|
|
1379
|
+
process,
|
|
1380
|
+
job,
|
|
1381
|
+
attestation=identity,
|
|
1382
|
+
ownership_proof=ownership_proof,
|
|
1383
|
+
)
|
|
868
1384
|
job = None
|
|
869
1385
|
break
|
|
870
1386
|
_write_run_receipt(state_root, receipt)
|
|
@@ -914,8 +1430,15 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
914
1430
|
}
|
|
915
1431
|
)
|
|
916
1432
|
finally:
|
|
917
|
-
if
|
|
918
|
-
|
|
1433
|
+
if process is not None and process.poll() is None and ownership_proof is not None:
|
|
1434
|
+
if spawned is not None:
|
|
1435
|
+
_capture_provider_members(spawned)
|
|
1436
|
+
tree_drained = _terminate_managed_child(
|
|
1437
|
+
process,
|
|
1438
|
+
job,
|
|
1439
|
+
attestation=identity if isinstance(identity, dict) else None,
|
|
1440
|
+
ownership_proof=ownership_proof,
|
|
1441
|
+
)
|
|
919
1442
|
if not tree_drained:
|
|
920
1443
|
receipt.update(
|
|
921
1444
|
{
|
|
@@ -944,6 +1467,10 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
|
|
|
944
1467
|
session_id,
|
|
945
1468
|
dict(receipt.get("resourceLockTokens") or {}),
|
|
946
1469
|
)
|
|
1470
|
+
try:
|
|
1471
|
+
_resource_lock_secrets_path(state_root, session_id).unlink(missing_ok=True)
|
|
1472
|
+
except OSError:
|
|
1473
|
+
locks_released = False
|
|
947
1474
|
if not locks_released:
|
|
948
1475
|
receipt.update(
|
|
949
1476
|
{
|
|
@@ -977,32 +1504,17 @@ def _run_session_worker_with_log(state_root: Path, session_id: str) -> int:
|
|
|
977
1504
|
|
|
978
1505
|
|
|
979
1506
|
def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
|
|
980
|
-
from harness_service import is_pid_alive
|
|
1507
|
+
from harness_service import is_pid_alive
|
|
981
1508
|
|
|
982
1509
|
receipt = _load_run_receipt(state_root, session_id)
|
|
983
1510
|
worker_pid = receipt.get("workerPid")
|
|
984
1511
|
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
1512
|
if receipt.get("status") in RUN_TERMINAL_STATUSES:
|
|
1001
1513
|
if (
|
|
1002
1514
|
isinstance(worker_pid, int)
|
|
1003
1515
|
and worker_pid > 0
|
|
1004
1516
|
and is_pid_alive(worker_pid)
|
|
1005
|
-
and
|
|
1517
|
+
and _receipt_identity_state(receipt) is True
|
|
1006
1518
|
):
|
|
1007
1519
|
finalizing = dict(receipt)
|
|
1008
1520
|
finalizing.update(
|
|
@@ -1036,7 +1548,7 @@ def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
|
|
|
1036
1548
|
and heartbeat_age < startup_grace
|
|
1037
1549
|
):
|
|
1038
1550
|
return receipt
|
|
1039
|
-
identity_state =
|
|
1551
|
+
identity_state = _receipt_identity_state(receipt)
|
|
1040
1552
|
worker_gone = (
|
|
1041
1553
|
isinstance(worker_pid, int)
|
|
1042
1554
|
and worker_pid > 0
|
|
@@ -1079,6 +1591,7 @@ def read_run_session_log(
|
|
|
1079
1591
|
receipt = _load_run_receipt(state_root, session_id)
|
|
1080
1592
|
path = Path(str(receipt[f"{stream}Path"]))
|
|
1081
1593
|
raw = b""
|
|
1594
|
+
decode_status = "OK"
|
|
1082
1595
|
size = path.stat().st_size if path.is_file() else 0
|
|
1083
1596
|
if path.is_file() and cursor < size:
|
|
1084
1597
|
with path.open("rb") as handle:
|
|
@@ -1090,8 +1603,25 @@ def read_run_session_log(
|
|
|
1090
1603
|
break
|
|
1091
1604
|
except UnicodeDecodeError as exc:
|
|
1092
1605
|
if exc.end != len(raw):
|
|
1606
|
+
# An invalid sequence in the middle is durable evidence,
|
|
1607
|
+
# so consume it and expose replacement text instead of
|
|
1608
|
+
# silently dropping bytes from the cursor stream.
|
|
1609
|
+
text = raw.decode("utf-8", errors="replace")
|
|
1610
|
+
decode_status = "LOG_DECODE_DEGRADED"
|
|
1611
|
+
break
|
|
1612
|
+
if (
|
|
1613
|
+
receipt.get("status") in RUN_TERMINAL_STATUSES
|
|
1614
|
+
and cursor + len(raw) >= size
|
|
1615
|
+
):
|
|
1616
|
+
# Once the producer is terminal, an incomplete trailing
|
|
1617
|
+
# sequence cannot be completed by a later write. Preserve
|
|
1618
|
+
# and consume it with an explicit degraded status.
|
|
1093
1619
|
text = raw.decode("utf-8", errors="replace")
|
|
1620
|
+
decode_status = "LOG_DECODE_DEGRADED"
|
|
1094
1621
|
break
|
|
1622
|
+
# A live writer may have split a multibyte character across
|
|
1623
|
+
# this read. Keep the incomplete suffix for the next cursor
|
|
1624
|
+
# request so callers never see duplicate replacement glyphs.
|
|
1095
1625
|
raw = raw[: exc.start]
|
|
1096
1626
|
else:
|
|
1097
1627
|
text = ""
|
|
@@ -1108,6 +1638,8 @@ def read_run_session_log(
|
|
|
1108
1638
|
"text": text,
|
|
1109
1639
|
"eof": terminal and next_cursor >= size,
|
|
1110
1640
|
"status": receipt.get("status"),
|
|
1641
|
+
"rawDigest": "sha256:" + hashlib.sha256(raw).hexdigest(),
|
|
1642
|
+
"decodeStatus": decode_status,
|
|
1111
1643
|
}
|
|
1112
1644
|
|
|
1113
1645
|
|
|
@@ -1117,28 +1649,10 @@ def cancel_run_session(
|
|
|
1117
1649
|
*,
|
|
1118
1650
|
reason: str = "CANCEL_REQUESTED",
|
|
1119
1651
|
) -> dict[str, Any]:
|
|
1120
|
-
from harness_service import verify_process_identity
|
|
1121
|
-
|
|
1122
1652
|
receipt = _load_run_receipt(state_root, session_id)
|
|
1123
1653
|
if receipt.get("status") in RUN_TERMINAL_STATUSES:
|
|
1124
1654
|
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:
|
|
1655
|
+
if _receipt_identity_state(receipt) is not True:
|
|
1142
1656
|
return {
|
|
1143
1657
|
**receipt,
|
|
1144
1658
|
"ok": False,
|