@hunter-harness/workflow-harness 0.2.42 → 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 +156 -1
- 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-archive/reference.md +1 -0
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +8 -0
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +28 -0
- 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-run/reference.md +6 -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 +8 -2
- package/harness/bundles/general/claude-code/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +24 -1
- package/harness/bundles/general/claude-code/protocols/verification-graph-protocol.md +6 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +271 -22
- package/harness/bundles/general/claude-code/scripts/harness_change.py +137 -2
- package/harness/bundles/general/claude-code/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/general/claude-code/scripts/harness_environment.py +760 -46
- package/harness/bundles/general/claude-code/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +742 -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 +278 -4
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +1576 -3
- package/harness/bundles/general/claude-code/scripts/harness_service.py +1528 -177
- package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +224 -146
- package/harness/bundles/general/claude-code/scripts/harness_verification.py +487 -8
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/build-profile-v3.schema.json +156 -1
- 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-archive/reference.md +1 -0
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +8 -0
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +28 -0
- 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-run/reference.md +6 -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 +8 -2
- package/harness/bundles/general/codebuddy/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +24 -1
- package/harness/bundles/general/codebuddy/protocols/verification-graph-protocol.md +6 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +271 -22
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +137 -2
- package/harness/bundles/general/codebuddy/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/general/codebuddy/scripts/harness_environment.py +760 -46
- package/harness/bundles/general/codebuddy/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +742 -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 +278 -4
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +1576 -3
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +1528 -177
- package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +224 -146
- package/harness/bundles/general/codebuddy/scripts/harness_verification.py +487 -8
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/build-profile-v3.schema.json +156 -1
- 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-archive/reference.md +1 -0
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +8 -0
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +28 -0
- 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-run/reference.md +6 -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 +8 -2
- package/harness/bundles/general/codex/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/general/codex/protocols/state-layout-protocol.md +24 -1
- package/harness/bundles/general/codex/protocols/verification-graph-protocol.md +6 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +271 -22
- package/harness/bundles/general/codex/scripts/harness_change.py +137 -2
- package/harness/bundles/general/codex/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/general/codex/scripts/harness_environment.py +760 -46
- package/harness/bundles/general/codex/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/codex/scripts/harness_fixback.py +742 -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 +278 -4
- package/harness/bundles/general/codex/scripts/harness_runtime.py +1576 -3
- package/harness/bundles/general/codex/scripts/harness_service.py +1528 -177
- package/harness/bundles/general/codex/scripts/harness_test_runner.py +224 -146
- package/harness/bundles/general/codex/scripts/harness_verification.py +487 -8
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/build-profile-v3.schema.json +156 -1
- 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-archive/reference.md +1 -0
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +8 -0
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +28 -0
- 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-run/reference.md +6 -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 +8 -2
- package/harness/bundles/general/cursor/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +24 -1
- package/harness/bundles/general/cursor/protocols/verification-graph-protocol.md +6 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +271 -22
- package/harness/bundles/general/cursor/scripts/harness_change.py +137 -2
- package/harness/bundles/general/cursor/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/general/cursor/scripts/harness_environment.py +760 -46
- package/harness/bundles/general/cursor/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +742 -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 +278 -4
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +1576 -3
- package/harness/bundles/general/cursor/scripts/harness_service.py +1528 -177
- package/harness/bundles/general/cursor/scripts/harness_test_runner.py +224 -146
- package/harness/bundles/general/cursor/scripts/harness_verification.py +487 -8
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/build-profile-v3.schema.json +156 -1
- 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-archive/reference.md +1 -0
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +8 -0
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +28 -0
- 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 +8 -2
- package/harness/bundles/java/claude-code/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +24 -1
- package/harness/bundles/java/claude-code/protocols/verification-graph-protocol.md +6 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +271 -22
- package/harness/bundles/java/claude-code/scripts/harness_change.py +137 -2
- package/harness/bundles/java/claude-code/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/java/claude-code/scripts/harness_environment.py +760 -46
- package/harness/bundles/java/claude-code/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +742 -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 +278 -4
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +1576 -3
- package/harness/bundles/java/claude-code/scripts/harness_service.py +1528 -177
- package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +224 -146
- package/harness/bundles/java/claude-code/scripts/harness_verification.py +487 -8
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/build-profile-v3.schema.json +156 -1
- 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-archive/reference.md +1 -0
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +8 -0
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +28 -0
- 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 +8 -2
- package/harness/bundles/java/codebuddy/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +24 -1
- package/harness/bundles/java/codebuddy/protocols/verification-graph-protocol.md +6 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +271 -22
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +137 -2
- package/harness/bundles/java/codebuddy/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/java/codebuddy/scripts/harness_environment.py +760 -46
- package/harness/bundles/java/codebuddy/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +742 -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 +278 -4
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +1576 -3
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +1528 -177
- package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +224 -146
- package/harness/bundles/java/codebuddy/scripts/harness_verification.py +487 -8
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/build-profile-v3.schema.json +156 -1
- 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-archive/reference.md +1 -0
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +8 -0
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +28 -0
- 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 +8 -2
- package/harness/bundles/java/codex/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/java/codex/protocols/state-layout-protocol.md +24 -1
- package/harness/bundles/java/codex/protocols/verification-graph-protocol.md +6 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +271 -22
- package/harness/bundles/java/codex/scripts/harness_change.py +137 -2
- package/harness/bundles/java/codex/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/java/codex/scripts/harness_environment.py +760 -46
- package/harness/bundles/java/codex/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/codex/scripts/harness_fixback.py +742 -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 +278 -4
- package/harness/bundles/java/codex/scripts/harness_runtime.py +1576 -3
- package/harness/bundles/java/codex/scripts/harness_service.py +1528 -177
- package/harness/bundles/java/codex/scripts/harness_test_runner.py +224 -146
- package/harness/bundles/java/codex/scripts/harness_verification.py +487 -8
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/build-profile-v3.schema.json +156 -1
- 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-archive/reference.md +1 -0
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +8 -0
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +28 -0
- 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 +8 -2
- package/harness/bundles/java/cursor/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +24 -1
- package/harness/bundles/java/cursor/protocols/verification-graph-protocol.md +6 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +271 -22
- package/harness/bundles/java/cursor/scripts/harness_change.py +137 -2
- package/harness/bundles/java/cursor/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/java/cursor/scripts/harness_environment.py +760 -46
- package/harness/bundles/java/cursor/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +742 -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 +278 -4
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +1576 -3
- package/harness/bundles/java/cursor/scripts/harness_service.py +1528 -177
- package/harness/bundles/java/cursor/scripts/harness_test_runner.py +224 -146
- package/harness/bundles/java/cursor/scripts/harness_verification.py +487 -8
- package/harness/manifests/general/claude-code.json +76 -31
- package/harness/manifests/general/codebuddy.json +76 -31
- package/harness/manifests/general/codex.json +76 -31
- package/harness/manifests/general/cursor.json +76 -31
- package/harness/manifests/java/claude-code.json +77 -32
- package/harness/manifests/java/codebuddy.json +77 -32
- package/harness/manifests/java/codex.json +77 -32
- package/harness/manifests/java/cursor.json +77 -32
- package/hunter-workflow-family.json +6 -4
- package/package.json +1 -1
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Platform process identity, spawn attestation, and owned-tree termination.
|
|
3
|
+
|
|
4
|
+
OS observations never accept expected argv/cwd/owner fields. Destructive
|
|
5
|
+
actions require both independently observed identity closure and a
|
|
6
|
+
platform-specific ownership proof.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import datetime as dt
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import signal
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
22
|
+
import uuid
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
IDENTITY_TOLERANCE_SECONDS = 1.0
|
|
26
|
+
PROVENANCE = {"OBSERVED", "ATTESTED", "UNAVAILABLE"}
|
|
27
|
+
_WINDOWS_JOB_HANDLES: dict[str, Any] = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def canonical_argv_hash(argv: Sequence[str]) -> str:
|
|
31
|
+
payload = json.dumps(
|
|
32
|
+
[str(item) for item in argv],
|
|
33
|
+
ensure_ascii=False,
|
|
34
|
+
separators=(",", ":"),
|
|
35
|
+
).encode("utf-8")
|
|
36
|
+
return "sha256:" + hashlib.sha256(payload).hexdigest()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _token_hash(value: str) -> str:
|
|
40
|
+
return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _iso(value: dt.datetime | None) -> str | None:
|
|
44
|
+
if value is None:
|
|
45
|
+
return None
|
|
46
|
+
return value.astimezone(dt.timezone.utc).isoformat(timespec="microseconds")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_iso(value: Any) -> dt.datetime | None:
|
|
50
|
+
if not isinstance(value, str) or not value.strip():
|
|
51
|
+
return None
|
|
52
|
+
text = value.strip()
|
|
53
|
+
if text.endswith("Z"):
|
|
54
|
+
text = text[:-1] + "+00:00"
|
|
55
|
+
try:
|
|
56
|
+
parsed = dt.datetime.fromisoformat(text)
|
|
57
|
+
except ValueError:
|
|
58
|
+
return None
|
|
59
|
+
if parsed.tzinfo is None:
|
|
60
|
+
parsed = parsed.astimezone()
|
|
61
|
+
return parsed.astimezone(dt.timezone.utc)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_pid_alive(pid: int) -> bool:
|
|
65
|
+
if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0:
|
|
66
|
+
return False
|
|
67
|
+
if os.name == "nt":
|
|
68
|
+
try:
|
|
69
|
+
import ctypes
|
|
70
|
+
from ctypes import wintypes
|
|
71
|
+
|
|
72
|
+
kernel32 = ctypes.windll.kernel32
|
|
73
|
+
handle = kernel32.OpenProcess(0x1000, False, pid)
|
|
74
|
+
if not handle:
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
exit_code = wintypes.DWORD()
|
|
78
|
+
if not kernel32.GetExitCodeProcess(
|
|
79
|
+
handle, ctypes.byref(exit_code)
|
|
80
|
+
):
|
|
81
|
+
return False
|
|
82
|
+
return int(exit_code.value) == 259
|
|
83
|
+
finally:
|
|
84
|
+
kernel32.CloseHandle(handle)
|
|
85
|
+
except Exception:
|
|
86
|
+
return False
|
|
87
|
+
try:
|
|
88
|
+
os.kill(pid, 0)
|
|
89
|
+
return True
|
|
90
|
+
except ProcessLookupError:
|
|
91
|
+
return False
|
|
92
|
+
except PermissionError:
|
|
93
|
+
return True
|
|
94
|
+
except OSError:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_process_create_time(pid: int) -> dt.datetime | None:
|
|
99
|
+
if not is_pid_alive(pid):
|
|
100
|
+
return None
|
|
101
|
+
if os.name == "nt":
|
|
102
|
+
try:
|
|
103
|
+
import ctypes
|
|
104
|
+
from ctypes import wintypes
|
|
105
|
+
|
|
106
|
+
kernel32 = ctypes.windll.kernel32
|
|
107
|
+
handle = kernel32.OpenProcess(0x1000, False, pid)
|
|
108
|
+
if not handle:
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
creation = wintypes.FILETIME()
|
|
112
|
+
exit_time = wintypes.FILETIME()
|
|
113
|
+
kernel_time = wintypes.FILETIME()
|
|
114
|
+
user_time = wintypes.FILETIME()
|
|
115
|
+
if not kernel32.GetProcessTimes(
|
|
116
|
+
handle,
|
|
117
|
+
ctypes.byref(creation),
|
|
118
|
+
ctypes.byref(exit_time),
|
|
119
|
+
ctypes.byref(kernel_time),
|
|
120
|
+
ctypes.byref(user_time),
|
|
121
|
+
):
|
|
122
|
+
return None
|
|
123
|
+
raw = (creation.dwHighDateTime << 32) | creation.dwLowDateTime
|
|
124
|
+
seconds = (raw - 116444736000000000) / 10_000_000
|
|
125
|
+
return dt.datetime.fromtimestamp(
|
|
126
|
+
seconds, tz=dt.timezone.utc
|
|
127
|
+
)
|
|
128
|
+
finally:
|
|
129
|
+
kernel32.CloseHandle(handle)
|
|
130
|
+
except Exception:
|
|
131
|
+
return None
|
|
132
|
+
stat = _read_proc_stat(pid)
|
|
133
|
+
if stat is None:
|
|
134
|
+
return None
|
|
135
|
+
boot = _linux_boot_time()
|
|
136
|
+
if boot is None:
|
|
137
|
+
return None
|
|
138
|
+
try:
|
|
139
|
+
ticks = int(os.sysconf("SC_CLK_TCK"))
|
|
140
|
+
return dt.datetime.fromtimestamp(
|
|
141
|
+
boot + int(stat["startTicks"]) / ticks,
|
|
142
|
+
tz=dt.timezone.utc,
|
|
143
|
+
)
|
|
144
|
+
except (OSError, TypeError, ValueError):
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_process_executable(pid: int) -> str | None:
|
|
149
|
+
if not is_pid_alive(pid):
|
|
150
|
+
return None
|
|
151
|
+
if os.name == "nt":
|
|
152
|
+
try:
|
|
153
|
+
import ctypes
|
|
154
|
+
from ctypes import wintypes
|
|
155
|
+
|
|
156
|
+
kernel32 = ctypes.windll.kernel32
|
|
157
|
+
handle = kernel32.OpenProcess(0x1000, False, pid)
|
|
158
|
+
if not handle:
|
|
159
|
+
return None
|
|
160
|
+
try:
|
|
161
|
+
capacity = wintypes.DWORD(32768)
|
|
162
|
+
buffer = ctypes.create_unicode_buffer(capacity.value)
|
|
163
|
+
if not kernel32.QueryFullProcessImageNameW(
|
|
164
|
+
handle, 0, buffer, ctypes.byref(capacity)
|
|
165
|
+
):
|
|
166
|
+
return None
|
|
167
|
+
return str(Path(buffer.value).resolve())
|
|
168
|
+
finally:
|
|
169
|
+
kernel32.CloseHandle(handle)
|
|
170
|
+
except Exception:
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
return str(Path(f"/proc/{pid}/exe").resolve(strict=True))
|
|
174
|
+
except OSError:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _linux_boot_time() -> float | None:
|
|
179
|
+
try:
|
|
180
|
+
for line in Path("/proc/stat").read_text(encoding="utf-8").splitlines():
|
|
181
|
+
if line.startswith("btime "):
|
|
182
|
+
return float(line.split()[1])
|
|
183
|
+
except (OSError, ValueError):
|
|
184
|
+
return None
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _read_proc_stat(pid: int) -> dict[str, int] | None:
|
|
189
|
+
try:
|
|
190
|
+
text = Path(f"/proc/{pid}/stat").read_text(
|
|
191
|
+
encoding="utf-8", errors="replace"
|
|
192
|
+
)
|
|
193
|
+
right = text.rfind(")")
|
|
194
|
+
if right < 0:
|
|
195
|
+
return None
|
|
196
|
+
fields = text[right + 2 :].split()
|
|
197
|
+
return {
|
|
198
|
+
"parentPid": int(fields[1]),
|
|
199
|
+
"groupId": int(fields[2]),
|
|
200
|
+
"sessionId": int(fields[3]),
|
|
201
|
+
"startTicks": int(fields[19]),
|
|
202
|
+
}
|
|
203
|
+
except (OSError, IndexError, ValueError):
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _proc_argv(pid: int) -> list[str] | None:
|
|
208
|
+
try:
|
|
209
|
+
raw = Path(f"/proc/{pid}/cmdline").read_bytes()
|
|
210
|
+
except OSError:
|
|
211
|
+
return None
|
|
212
|
+
if not raw:
|
|
213
|
+
return None
|
|
214
|
+
try:
|
|
215
|
+
return [
|
|
216
|
+
part.decode("utf-8", errors="strict")
|
|
217
|
+
for part in raw.rstrip(b"\0").split(b"\0")
|
|
218
|
+
]
|
|
219
|
+
except UnicodeDecodeError:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _proc_cwd(pid: int) -> str | None:
|
|
224
|
+
try:
|
|
225
|
+
return str(Path(f"/proc/{pid}/cwd").resolve(strict=True))
|
|
226
|
+
except OSError:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _enumerate_posix_members(
|
|
231
|
+
group_id: int,
|
|
232
|
+
session_id: int,
|
|
233
|
+
) -> tuple[list[int], bool]:
|
|
234
|
+
proc = Path("/proc")
|
|
235
|
+
if not proc.is_dir():
|
|
236
|
+
return [], False
|
|
237
|
+
members: list[int] = []
|
|
238
|
+
complete = True
|
|
239
|
+
for candidate in proc.iterdir():
|
|
240
|
+
if not candidate.name.isdigit():
|
|
241
|
+
continue
|
|
242
|
+
stat = _read_proc_stat(int(candidate.name))
|
|
243
|
+
if stat is None:
|
|
244
|
+
continue
|
|
245
|
+
if (
|
|
246
|
+
stat["groupId"] == group_id
|
|
247
|
+
and stat["sessionId"] == session_id
|
|
248
|
+
):
|
|
249
|
+
members.append(int(candidate.name))
|
|
250
|
+
return sorted(members), complete
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def normalize_process_observation(raw: Mapping[str, Any]) -> dict[str, Any]:
|
|
254
|
+
pid = int(raw.get("pid") or 0)
|
|
255
|
+
alive = bool(raw.get("alive"))
|
|
256
|
+
created = raw.get("createdAt")
|
|
257
|
+
executable = raw.get("executable")
|
|
258
|
+
argv_hash = raw.get("argvHash")
|
|
259
|
+
cwd = raw.get("workingDirectory")
|
|
260
|
+
parent = raw.get("parentIdentity")
|
|
261
|
+
tree = raw.get("treeIdentity")
|
|
262
|
+
return {
|
|
263
|
+
"schemaVersion": 1,
|
|
264
|
+
"pid": pid,
|
|
265
|
+
"alive": alive,
|
|
266
|
+
"createdAt": created if isinstance(created, str) else None,
|
|
267
|
+
"executable": executable if isinstance(executable, str) else None,
|
|
268
|
+
"argvHash": argv_hash if isinstance(argv_hash, str) else None,
|
|
269
|
+
"workingDirectory": cwd if isinstance(cwd, str) else None,
|
|
270
|
+
"parentIdentity": parent if isinstance(parent, dict) else None,
|
|
271
|
+
"ownerTokenHash": (
|
|
272
|
+
str(raw["ownerTokenHash"])
|
|
273
|
+
if isinstance(raw.get("ownerTokenHash"), str)
|
|
274
|
+
else None
|
|
275
|
+
),
|
|
276
|
+
"treeIdentity": tree if isinstance(tree, dict) else None,
|
|
277
|
+
"fieldProvenance": {
|
|
278
|
+
"pid": "OBSERVED",
|
|
279
|
+
"createdAt": "OBSERVED" if isinstance(created, str) else "UNAVAILABLE",
|
|
280
|
+
"executable": (
|
|
281
|
+
"OBSERVED" if isinstance(executable, str) else "UNAVAILABLE"
|
|
282
|
+
),
|
|
283
|
+
"argvHash": "OBSERVED" if isinstance(argv_hash, str) else "UNAVAILABLE",
|
|
284
|
+
"workingDirectory": "OBSERVED" if isinstance(cwd, str) else "UNAVAILABLE",
|
|
285
|
+
"parentIdentity": "OBSERVED" if isinstance(parent, dict) else "UNAVAILABLE",
|
|
286
|
+
"ownerTokenHash": (
|
|
287
|
+
"ATTESTED"
|
|
288
|
+
if isinstance(raw.get("ownerTokenHash"), str)
|
|
289
|
+
else "UNAVAILABLE"
|
|
290
|
+
),
|
|
291
|
+
"treeIdentity": "OBSERVED" if isinstance(tree, dict) else "UNAVAILABLE",
|
|
292
|
+
},
|
|
293
|
+
"capabilities": {
|
|
294
|
+
"canObserveCreateTime": isinstance(created, str),
|
|
295
|
+
"canObserveExecutable": isinstance(executable, str),
|
|
296
|
+
"canObserveArgv": isinstance(argv_hash, str),
|
|
297
|
+
"canObserveWorkingDirectory": isinstance(cwd, str),
|
|
298
|
+
"canObserveParent": isinstance(parent, dict),
|
|
299
|
+
"canEnumerateTree": bool(
|
|
300
|
+
isinstance(tree, dict) and tree.get("complete") is True
|
|
301
|
+
),
|
|
302
|
+
"canVerifyOwnership": bool(raw.get("canVerifyOwnership")),
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def observe_process_identity(pid: int) -> dict[str, Any]:
|
|
308
|
+
"""Observe OS facts only. Expected identity is intentionally not accepted."""
|
|
309
|
+
|
|
310
|
+
alive = is_pid_alive(pid)
|
|
311
|
+
if not alive:
|
|
312
|
+
return normalize_process_observation(
|
|
313
|
+
{"pid": pid, "alive": False, "platform": _platform_name()}
|
|
314
|
+
)
|
|
315
|
+
created = _iso(get_process_create_time(pid))
|
|
316
|
+
executable = get_process_executable(pid)
|
|
317
|
+
parent: dict[str, Any] | None = None
|
|
318
|
+
tree: dict[str, Any] | None = None
|
|
319
|
+
argv_hash: str | None = None
|
|
320
|
+
cwd: str | None = None
|
|
321
|
+
if os.name != "nt":
|
|
322
|
+
stat = _read_proc_stat(pid)
|
|
323
|
+
argv = _proc_argv(pid)
|
|
324
|
+
argv_hash = canonical_argv_hash(argv) if argv is not None else None
|
|
325
|
+
cwd = _proc_cwd(pid)
|
|
326
|
+
if stat is not None and stat["parentPid"] > 0:
|
|
327
|
+
parent = {
|
|
328
|
+
"pid": stat["parentPid"],
|
|
329
|
+
"createdAt": _iso(
|
|
330
|
+
get_process_create_time(stat["parentPid"])
|
|
331
|
+
),
|
|
332
|
+
"executable": get_process_executable(stat["parentPid"]),
|
|
333
|
+
}
|
|
334
|
+
members, complete = _enumerate_posix_members(
|
|
335
|
+
stat["groupId"], stat["sessionId"]
|
|
336
|
+
)
|
|
337
|
+
tree = {
|
|
338
|
+
"platform": "LINUX" if sys.platform.startswith("linux") else "POSIX",
|
|
339
|
+
"proofKind": "OBSERVED_SESSION",
|
|
340
|
+
"memberPids": members,
|
|
341
|
+
"complete": complete,
|
|
342
|
+
"groupId": stat["groupId"],
|
|
343
|
+
"sessionId": stat["sessionId"],
|
|
344
|
+
}
|
|
345
|
+
return normalize_process_observation(
|
|
346
|
+
{
|
|
347
|
+
"pid": pid,
|
|
348
|
+
"alive": True,
|
|
349
|
+
"createdAt": created,
|
|
350
|
+
"executable": executable,
|
|
351
|
+
"argvHash": argv_hash,
|
|
352
|
+
"workingDirectory": cwd,
|
|
353
|
+
"parentIdentity": parent,
|
|
354
|
+
"treeIdentity": tree,
|
|
355
|
+
"canVerifyOwnership": False,
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _platform_name() -> str:
|
|
361
|
+
if os.name == "nt":
|
|
362
|
+
return "WINDOWS"
|
|
363
|
+
return "LINUX" if sys.platform.startswith("linux") else "POSIX"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _normalized_path(value: str) -> str:
|
|
367
|
+
normalized = os.path.normcase(os.path.abspath(value))
|
|
368
|
+
return normalized.replace("\\", "/")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _created_at_matches(expected: Any, actual: Any) -> bool | None:
|
|
372
|
+
left = _parse_iso(expected)
|
|
373
|
+
right = _parse_iso(actual)
|
|
374
|
+
if left is None or right is None:
|
|
375
|
+
return None
|
|
376
|
+
return abs((left - right).total_seconds()) <= IDENTITY_TOLERANCE_SECONDS
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def verify_process_identity(
|
|
380
|
+
expected_attestation: Mapping[str, Any],
|
|
381
|
+
observed_identity: Mapping[str, Any],
|
|
382
|
+
) -> dict[str, Any]:
|
|
383
|
+
"""Compare spawn attestation to independent OS observation."""
|
|
384
|
+
|
|
385
|
+
if observed_identity.get("alive") is not True:
|
|
386
|
+
return _decision(False, "PROCESS_IDENTITY_MISMATCH", ["process-dead"])
|
|
387
|
+
if expected_attestation.get("pid") != observed_identity.get("pid"):
|
|
388
|
+
return _decision(False, "PROCESS_IDENTITY_MISMATCH", ["pid"])
|
|
389
|
+
checks = (
|
|
390
|
+
("createdAt", "PROCESS_CREATE_TIME_MISMATCH", _created_at_matches),
|
|
391
|
+
(
|
|
392
|
+
"executable",
|
|
393
|
+
"PROCESS_EXECUTABLE_MISMATCH",
|
|
394
|
+
lambda left, right: (
|
|
395
|
+
_normalized_path(left) == _normalized_path(right)
|
|
396
|
+
if isinstance(left, str) and isinstance(right, str)
|
|
397
|
+
else None
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
(
|
|
401
|
+
"argvHash",
|
|
402
|
+
"PROCESS_ARGV_MISMATCH",
|
|
403
|
+
lambda left, right: left == right
|
|
404
|
+
if isinstance(left, str) and isinstance(right, str)
|
|
405
|
+
else None,
|
|
406
|
+
),
|
|
407
|
+
(
|
|
408
|
+
"workingDirectory",
|
|
409
|
+
"PROCESS_CWD_MISMATCH",
|
|
410
|
+
lambda left, right: (
|
|
411
|
+
_normalized_path(left) == _normalized_path(right)
|
|
412
|
+
if isinstance(left, str) and isinstance(right, str)
|
|
413
|
+
else None
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
(
|
|
417
|
+
"ownerTokenHash",
|
|
418
|
+
"PROCESS_OWNER_MISMATCH",
|
|
419
|
+
lambda left, right: (
|
|
420
|
+
left == right
|
|
421
|
+
if isinstance(left, str) and isinstance(right, str)
|
|
422
|
+
else True if right is None else None
|
|
423
|
+
),
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
unavailable: list[str] = []
|
|
427
|
+
expected_provenance = expected_attestation.get("fieldProvenance")
|
|
428
|
+
if not isinstance(expected_provenance, Mapping):
|
|
429
|
+
expected_provenance = {}
|
|
430
|
+
observed_provenance = observed_identity.get("fieldProvenance")
|
|
431
|
+
if not isinstance(observed_provenance, Mapping):
|
|
432
|
+
observed_provenance = {}
|
|
433
|
+
for field, reason, comparator in checks:
|
|
434
|
+
expected = expected_attestation.get(field)
|
|
435
|
+
if expected is None:
|
|
436
|
+
continue
|
|
437
|
+
actual = observed_identity.get(field)
|
|
438
|
+
expected_source = expected_provenance.get(field)
|
|
439
|
+
observed_source = observed_provenance.get(field)
|
|
440
|
+
# Create time, executable and owner/parent facts must come from an
|
|
441
|
+
# independent OS observation. argv/cwd may remain spawn-attested on
|
|
442
|
+
# platforms that cannot read them back, but a value labelled
|
|
443
|
+
# UNAVAILABLE is never allowed to masquerade as an observation.
|
|
444
|
+
independent_field = field in {
|
|
445
|
+
"createdAt",
|
|
446
|
+
"executable",
|
|
447
|
+
"parentIdentity",
|
|
448
|
+
}
|
|
449
|
+
if actual is not None and observed_source not in {"OBSERVED", "ATTESTED"}:
|
|
450
|
+
unavailable.append(field)
|
|
451
|
+
continue
|
|
452
|
+
if actual is not None and independent_field and observed_source != "OBSERVED":
|
|
453
|
+
unavailable.append(field)
|
|
454
|
+
continue
|
|
455
|
+
compared = comparator(expected, actual)
|
|
456
|
+
if compared is False:
|
|
457
|
+
return _decision(False, reason, [field])
|
|
458
|
+
if compared is None:
|
|
459
|
+
if field in {"argvHash", "workingDirectory"} and expected_source == "ATTESTED" and actual is None:
|
|
460
|
+
# The attestation is the only trustworthy source on a
|
|
461
|
+
# platform without argv/cwd observation. Core identity and
|
|
462
|
+
# ownership proof checks below still have to close.
|
|
463
|
+
continue
|
|
464
|
+
unavailable.append(field)
|
|
465
|
+
expected_parent = expected_attestation.get("parentIdentity")
|
|
466
|
+
if isinstance(expected_parent, dict):
|
|
467
|
+
actual_parent = observed_identity.get("parentIdentity")
|
|
468
|
+
if not isinstance(actual_parent, dict):
|
|
469
|
+
unavailable.append("parentIdentity")
|
|
470
|
+
elif observed_provenance.get("parentIdentity") != "OBSERVED":
|
|
471
|
+
unavailable.append("parentIdentity")
|
|
472
|
+
elif expected_parent.get("pid") != actual_parent.get("pid"):
|
|
473
|
+
return _decision(
|
|
474
|
+
False, "PROCESS_PARENT_MISMATCH", ["parentIdentity.pid"]
|
|
475
|
+
)
|
|
476
|
+
else:
|
|
477
|
+
parent_time = _created_at_matches(
|
|
478
|
+
expected_parent.get("createdAt"),
|
|
479
|
+
actual_parent.get("createdAt"),
|
|
480
|
+
)
|
|
481
|
+
if parent_time is False:
|
|
482
|
+
return _decision(
|
|
483
|
+
False,
|
|
484
|
+
"PROCESS_PARENT_MISMATCH",
|
|
485
|
+
["parentIdentity.createdAt"],
|
|
486
|
+
)
|
|
487
|
+
if parent_time is None:
|
|
488
|
+
unavailable.append("parentIdentity.createdAt")
|
|
489
|
+
if unavailable:
|
|
490
|
+
return _decision(False, "IDENTITY_UNVERIFIABLE", unavailable)
|
|
491
|
+
return _decision(True, "PROCESS_IDENTITY_VERIFIED", [])
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _decision(ok: bool, reason: str, details: list[str]) -> dict[str, Any]:
|
|
495
|
+
return {
|
|
496
|
+
"ok": ok,
|
|
497
|
+
"authorized": ok,
|
|
498
|
+
"reasonCode": reason,
|
|
499
|
+
"details": details,
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def validate_ownership_proof(
|
|
504
|
+
expected_attestation: Mapping[str, Any],
|
|
505
|
+
ownership_proof: Mapping[str, Any] | None,
|
|
506
|
+
observed_members: Sequence[Mapping[str, Any]],
|
|
507
|
+
) -> dict[str, Any]:
|
|
508
|
+
if not isinstance(ownership_proof, Mapping):
|
|
509
|
+
return _decision(False, "IDENTITY_UNVERIFIABLE", ["proof-missing"])
|
|
510
|
+
if (
|
|
511
|
+
expected_attestation.get("ownerTokenHash") is None
|
|
512
|
+
or ownership_proof.get("ownershipTokenHash")
|
|
513
|
+
!= expected_attestation.get("ownerTokenHash")
|
|
514
|
+
):
|
|
515
|
+
return _decision(False, "PROCESS_OWNER_MISMATCH", ["ownerTokenHash"])
|
|
516
|
+
if ownership_proof.get("membersComplete") is not True:
|
|
517
|
+
return _decision(False, "IDENTITY_UNVERIFIABLE", ["members-incomplete"])
|
|
518
|
+
if ownership_proof.get("leaderExited") is True:
|
|
519
|
+
return _decision(False, "IDENTITY_UNVERIFIABLE", ["leader-exited"])
|
|
520
|
+
kind = ownership_proof.get("kind")
|
|
521
|
+
if kind not in {"WINDOWS_NAMED_JOB", "LINUX_PIDFD_SESSION", "POSIX_MEMBERS"}:
|
|
522
|
+
return _decision(False, "IDENTITY_UNVERIFIABLE", ["proof-kind"])
|
|
523
|
+
expected_members = ownership_proof.get("members")
|
|
524
|
+
if not isinstance(expected_members, list) or not expected_members:
|
|
525
|
+
return _decision(False, "IDENTITY_UNVERIFIABLE", ["attested-members"])
|
|
526
|
+
expected_by_pid = {
|
|
527
|
+
item.get("pid"): item
|
|
528
|
+
for item in expected_members
|
|
529
|
+
if isinstance(item, Mapping) and isinstance(item.get("pid"), int)
|
|
530
|
+
}
|
|
531
|
+
observed_by_pid = {
|
|
532
|
+
item.get("pid"): item
|
|
533
|
+
for item in observed_members
|
|
534
|
+
if isinstance(item, Mapping) and item.get("alive") is True
|
|
535
|
+
}
|
|
536
|
+
if set(expected_by_pid) != set(observed_by_pid):
|
|
537
|
+
return _decision(
|
|
538
|
+
False,
|
|
539
|
+
"IDENTITY_UNVERIFIABLE",
|
|
540
|
+
["foreign-or-missing-member"],
|
|
541
|
+
)
|
|
542
|
+
leader_pid = ownership_proof.get("leaderPid")
|
|
543
|
+
if leader_pid not in observed_by_pid:
|
|
544
|
+
return _decision(False, "IDENTITY_UNVERIFIABLE", ["leader-missing"])
|
|
545
|
+
for pid, expected in expected_by_pid.items():
|
|
546
|
+
observed = observed_by_pid[pid]
|
|
547
|
+
verified = verify_process_identity(expected, observed)
|
|
548
|
+
if not verified["ok"]:
|
|
549
|
+
return verified
|
|
550
|
+
expected_tree = expected.get("treeIdentity")
|
|
551
|
+
observed_tree = observed.get("treeIdentity")
|
|
552
|
+
if kind != "WINDOWS_NAMED_JOB":
|
|
553
|
+
if not isinstance(observed_tree, Mapping):
|
|
554
|
+
return _decision(
|
|
555
|
+
False, "IDENTITY_UNVERIFIABLE", ["tree-unavailable"]
|
|
556
|
+
)
|
|
557
|
+
for field in ("groupId", "sessionId"):
|
|
558
|
+
proof_value = ownership_proof.get(field)
|
|
559
|
+
if proof_value is not None and observed_tree.get(field) != proof_value:
|
|
560
|
+
return _decision(
|
|
561
|
+
False,
|
|
562
|
+
"IDENTITY_UNVERIFIABLE",
|
|
563
|
+
[f"{field}-reused"],
|
|
564
|
+
)
|
|
565
|
+
return {
|
|
566
|
+
"ok": True,
|
|
567
|
+
"authorized": True,
|
|
568
|
+
"reasonCode": "OWNERSHIP_PROOF_VERIFIED",
|
|
569
|
+
"details": [],
|
|
570
|
+
"memberPids": sorted(int(pid) for pid in observed_by_pid),
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _observe_proof_members(
|
|
575
|
+
ownership_proof: Mapping[str, Any] | None,
|
|
576
|
+
) -> list[dict[str, Any]]:
|
|
577
|
+
if not isinstance(ownership_proof, Mapping):
|
|
578
|
+
return []
|
|
579
|
+
members = ownership_proof.get("members")
|
|
580
|
+
if not isinstance(members, list):
|
|
581
|
+
return []
|
|
582
|
+
if ownership_proof.get("kind") == "WINDOWS_NAMED_JOB":
|
|
583
|
+
pids = _windows_job_member_pids(ownership_proof)
|
|
584
|
+
if pids is None:
|
|
585
|
+
if isinstance(ownership_proof, dict):
|
|
586
|
+
ownership_proof["membersComplete"] = False
|
|
587
|
+
return []
|
|
588
|
+
return [observe_process_identity(pid) for pid in pids]
|
|
589
|
+
observed = [
|
|
590
|
+
observe_process_identity(int(item["pid"]))
|
|
591
|
+
for item in members
|
|
592
|
+
if isinstance(item, Mapping) and isinstance(item.get("pid"), int)
|
|
593
|
+
]
|
|
594
|
+
if ownership_proof.get("kind") != "WINDOWS_NAMED_JOB":
|
|
595
|
+
group_id = ownership_proof.get("groupId")
|
|
596
|
+
session_id = ownership_proof.get("sessionId")
|
|
597
|
+
if isinstance(group_id, int) and isinstance(session_id, int):
|
|
598
|
+
pids, complete = _enumerate_posix_members(group_id, session_id)
|
|
599
|
+
ownership_proof["membersComplete"] = bool(complete)
|
|
600
|
+
known = {item["pid"] for item in observed}
|
|
601
|
+
observed.extend(
|
|
602
|
+
observe_process_identity(pid) for pid in pids if pid not in known
|
|
603
|
+
)
|
|
604
|
+
return observed
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def terminate_owned_tree(
|
|
608
|
+
expected_attestation: Mapping[str, Any],
|
|
609
|
+
ownership_proof: Mapping[str, Any] | None,
|
|
610
|
+
timeout_policy: Mapping[str, Any] | None = None,
|
|
611
|
+
*,
|
|
612
|
+
member_observer: Callable[
|
|
613
|
+
[Mapping[str, Any] | None], Sequence[Mapping[str, Any]]
|
|
614
|
+
] = _observe_proof_members,
|
|
615
|
+
signaler: Callable[[int, int], None] = os.kill,
|
|
616
|
+
) -> dict[str, Any]:
|
|
617
|
+
"""Terminate only after proof and every current member identity close."""
|
|
618
|
+
|
|
619
|
+
observed = list(member_observer(ownership_proof))
|
|
620
|
+
proof_result = validate_ownership_proof(
|
|
621
|
+
expected_attestation, ownership_proof, observed
|
|
622
|
+
)
|
|
623
|
+
if not proof_result["ok"]:
|
|
624
|
+
return {
|
|
625
|
+
**proof_result,
|
|
626
|
+
"terminatedPids": [],
|
|
627
|
+
"cleanupComplete": False,
|
|
628
|
+
}
|
|
629
|
+
assert ownership_proof is not None
|
|
630
|
+
pids = list(reversed(proof_result["memberPids"]))
|
|
631
|
+
terminated: list[int] = []
|
|
632
|
+
if ownership_proof.get("kind") == "WINDOWS_NAMED_JOB":
|
|
633
|
+
proof_id = str(ownership_proof.get("proofId") or "")
|
|
634
|
+
job = _WINDOWS_JOB_HANDLES.get(proof_id)
|
|
635
|
+
if job is None:
|
|
636
|
+
job = _WindowsNamedJob(
|
|
637
|
+
str(ownership_proof.get("jobName") or ""),
|
|
638
|
+
configure=False,
|
|
639
|
+
)
|
|
640
|
+
if job is None or not job.terminate():
|
|
641
|
+
return {
|
|
642
|
+
"ok": False,
|
|
643
|
+
"authorized": False,
|
|
644
|
+
"reasonCode": "IDENTITY_UNVERIFIABLE",
|
|
645
|
+
"details": ["named-job-unavailable"],
|
|
646
|
+
"terminatedPids": [],
|
|
647
|
+
"cleanupComplete": False,
|
|
648
|
+
}
|
|
649
|
+
terminated.extend(pids)
|
|
650
|
+
else:
|
|
651
|
+
for pid in pids:
|
|
652
|
+
try:
|
|
653
|
+
signaler(pid, signal.SIGTERM)
|
|
654
|
+
terminated.append(pid)
|
|
655
|
+
except ProcessLookupError:
|
|
656
|
+
terminated.append(pid)
|
|
657
|
+
except OSError as exc:
|
|
658
|
+
return {
|
|
659
|
+
"ok": False,
|
|
660
|
+
"authorized": True,
|
|
661
|
+
"reasonCode": "SERVICE_STOP_FAILED",
|
|
662
|
+
"details": [str(exc)],
|
|
663
|
+
"terminatedPids": terminated,
|
|
664
|
+
"cleanupComplete": False,
|
|
665
|
+
}
|
|
666
|
+
grace = float((timeout_policy or {}).get("graceSeconds", 3.0))
|
|
667
|
+
deadline = time.monotonic() + max(0.0, grace)
|
|
668
|
+
remaining = [
|
|
669
|
+
item
|
|
670
|
+
for item in member_observer(ownership_proof)
|
|
671
|
+
if item.get("alive") is True
|
|
672
|
+
]
|
|
673
|
+
while remaining and time.monotonic() < deadline:
|
|
674
|
+
time.sleep(min(0.05, max(0.0, deadline - time.monotonic())))
|
|
675
|
+
remaining = [
|
|
676
|
+
item
|
|
677
|
+
for item in member_observer(ownership_proof)
|
|
678
|
+
if item.get("alive") is True
|
|
679
|
+
]
|
|
680
|
+
if remaining and ownership_proof.get("kind") != "WINDOWS_NAMED_JOB":
|
|
681
|
+
for item in remaining:
|
|
682
|
+
try:
|
|
683
|
+
signaler(int(item["pid"]), signal.SIGKILL)
|
|
684
|
+
except OSError:
|
|
685
|
+
pass
|
|
686
|
+
remaining = [
|
|
687
|
+
item
|
|
688
|
+
for item in member_observer(ownership_proof)
|
|
689
|
+
if item.get("alive") is True
|
|
690
|
+
]
|
|
691
|
+
return {
|
|
692
|
+
"ok": not remaining,
|
|
693
|
+
"authorized": True,
|
|
694
|
+
"reasonCode": (
|
|
695
|
+
"SERVICE_STOPPED" if not remaining else "SERVICE_STOP_TIMEOUT"
|
|
696
|
+
),
|
|
697
|
+
"details": [],
|
|
698
|
+
"terminatedPids": sorted(set(terminated)),
|
|
699
|
+
"cleanupComplete": not remaining,
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
class _WindowsNamedJob:
|
|
704
|
+
def __init__(self, name: str, *, configure: bool = True) -> None:
|
|
705
|
+
self.name = name
|
|
706
|
+
self.handle: Any = None
|
|
707
|
+
if os.name != "nt":
|
|
708
|
+
return
|
|
709
|
+
try:
|
|
710
|
+
import ctypes
|
|
711
|
+
|
|
712
|
+
kernel32 = ctypes.windll.kernel32
|
|
713
|
+
kernel32.CreateJobObjectW.restype = ctypes.c_void_p
|
|
714
|
+
handle = kernel32.CreateJobObjectW(None, name)
|
|
715
|
+
self.handle = int(handle) if handle else None
|
|
716
|
+
if self.handle is not None and configure:
|
|
717
|
+
self._configure_kill_on_close()
|
|
718
|
+
except Exception:
|
|
719
|
+
if self.handle is not None:
|
|
720
|
+
try:
|
|
721
|
+
ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(self.handle))
|
|
722
|
+
except Exception:
|
|
723
|
+
pass
|
|
724
|
+
self.handle = None
|
|
725
|
+
|
|
726
|
+
def _configure_kill_on_close(self) -> None:
|
|
727
|
+
if self.handle is None:
|
|
728
|
+
return
|
|
729
|
+
import ctypes
|
|
730
|
+
from ctypes import wintypes
|
|
731
|
+
|
|
732
|
+
class BasicLimit(ctypes.Structure):
|
|
733
|
+
_fields_ = [
|
|
734
|
+
("PerProcessUserTimeLimit", ctypes.c_int64),
|
|
735
|
+
("PerJobUserTimeLimit", ctypes.c_int64),
|
|
736
|
+
("LimitFlags", wintypes.DWORD),
|
|
737
|
+
("MinimumWorkingSetSize", ctypes.c_size_t),
|
|
738
|
+
("MaximumWorkingSetSize", ctypes.c_size_t),
|
|
739
|
+
("ActiveProcessLimit", wintypes.DWORD),
|
|
740
|
+
("Affinity", ctypes.c_size_t),
|
|
741
|
+
("PriorityClass", wintypes.DWORD),
|
|
742
|
+
("SchedulingClass", wintypes.DWORD),
|
|
743
|
+
]
|
|
744
|
+
|
|
745
|
+
class IoCounters(ctypes.Structure):
|
|
746
|
+
_fields_ = [
|
|
747
|
+
("ReadOperationCount", ctypes.c_uint64),
|
|
748
|
+
("WriteOperationCount", ctypes.c_uint64),
|
|
749
|
+
("OtherOperationCount", ctypes.c_uint64),
|
|
750
|
+
("ReadTransferCount", ctypes.c_uint64),
|
|
751
|
+
("WriteTransferCount", ctypes.c_uint64),
|
|
752
|
+
("OtherTransferCount", ctypes.c_uint64),
|
|
753
|
+
]
|
|
754
|
+
|
|
755
|
+
class ExtendedLimit(ctypes.Structure):
|
|
756
|
+
_fields_ = [
|
|
757
|
+
("BasicLimitInformation", BasicLimit),
|
|
758
|
+
("IoInfo", IoCounters),
|
|
759
|
+
("ProcessMemoryLimit", ctypes.c_size_t),
|
|
760
|
+
("JobMemoryLimit", ctypes.c_size_t),
|
|
761
|
+
("PeakProcessMemoryUsed", ctypes.c_size_t),
|
|
762
|
+
("PeakJobMemoryUsed", ctypes.c_size_t),
|
|
763
|
+
]
|
|
764
|
+
|
|
765
|
+
info = ExtendedLimit()
|
|
766
|
+
info.BasicLimitInformation.LimitFlags = 0x00002000
|
|
767
|
+
if not ctypes.windll.kernel32.SetInformationJobObject(
|
|
768
|
+
ctypes.c_void_p(self.handle),
|
|
769
|
+
9,
|
|
770
|
+
ctypes.byref(info),
|
|
771
|
+
ctypes.sizeof(info),
|
|
772
|
+
):
|
|
773
|
+
raise OSError("SetInformationJobObject(KILL_ON_JOB_CLOSE) failed")
|
|
774
|
+
|
|
775
|
+
def assign(self, process: subprocess.Popen[Any]) -> bool:
|
|
776
|
+
if self.handle is None:
|
|
777
|
+
return False
|
|
778
|
+
try:
|
|
779
|
+
import ctypes
|
|
780
|
+
|
|
781
|
+
return bool(
|
|
782
|
+
ctypes.windll.kernel32.AssignProcessToJobObject(
|
|
783
|
+
ctypes.c_void_p(self.handle),
|
|
784
|
+
ctypes.c_void_p(int(process._handle)), # type: ignore[attr-defined]
|
|
785
|
+
)
|
|
786
|
+
)
|
|
787
|
+
except Exception:
|
|
788
|
+
return False
|
|
789
|
+
|
|
790
|
+
def terminate(self) -> bool:
|
|
791
|
+
opened = False
|
|
792
|
+
try:
|
|
793
|
+
import ctypes
|
|
794
|
+
|
|
795
|
+
if self.handle is None and os.name == "nt" and self.name:
|
|
796
|
+
kernel32 = ctypes.windll.kernel32
|
|
797
|
+
kernel32.OpenJobObjectW.restype = ctypes.c_void_p
|
|
798
|
+
raw = kernel32.OpenJobObjectW(0x0001, False, self.name)
|
|
799
|
+
if raw:
|
|
800
|
+
self.handle = int(raw)
|
|
801
|
+
opened = True
|
|
802
|
+
if self.handle is None:
|
|
803
|
+
return False
|
|
804
|
+
return bool(
|
|
805
|
+
ctypes.windll.kernel32.TerminateJobObject(
|
|
806
|
+
ctypes.c_void_p(self.handle), 1
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
except Exception:
|
|
810
|
+
return False
|
|
811
|
+
finally:
|
|
812
|
+
if opened and self.handle is not None:
|
|
813
|
+
try:
|
|
814
|
+
ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(self.handle))
|
|
815
|
+
except Exception:
|
|
816
|
+
pass
|
|
817
|
+
self.handle = None
|
|
818
|
+
|
|
819
|
+
def close(self) -> None:
|
|
820
|
+
if self.handle is None:
|
|
821
|
+
return
|
|
822
|
+
try:
|
|
823
|
+
import ctypes
|
|
824
|
+
|
|
825
|
+
ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(self.handle))
|
|
826
|
+
finally:
|
|
827
|
+
self.handle = None
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _windows_job_member_pids(
|
|
831
|
+
ownership_proof: Mapping[str, Any],
|
|
832
|
+
) -> list[int] | None:
|
|
833
|
+
if os.name != "nt":
|
|
834
|
+
return None
|
|
835
|
+
proof_id = str(ownership_proof.get("proofId") or "")
|
|
836
|
+
job = _WINDOWS_JOB_HANDLES.get(proof_id)
|
|
837
|
+
handle = job.handle if job is not None else None
|
|
838
|
+
opened = False
|
|
839
|
+
try:
|
|
840
|
+
import ctypes
|
|
841
|
+
|
|
842
|
+
kernel32 = ctypes.windll.kernel32
|
|
843
|
+
if handle is None:
|
|
844
|
+
name = ownership_proof.get("jobName")
|
|
845
|
+
if not isinstance(name, str) or not name:
|
|
846
|
+
return None
|
|
847
|
+
kernel32.OpenJobObjectW.restype = ctypes.c_void_p
|
|
848
|
+
raw = kernel32.OpenJobObjectW(0x0004 | 0x0008, False, name)
|
|
849
|
+
if not raw:
|
|
850
|
+
return None
|
|
851
|
+
handle = int(raw)
|
|
852
|
+
opened = True
|
|
853
|
+
pointer_size = ctypes.sizeof(ctypes.c_size_t)
|
|
854
|
+
capacity = 4096
|
|
855
|
+
buffer = ctypes.create_string_buffer(8 + pointer_size * capacity)
|
|
856
|
+
returned = ctypes.c_uint32()
|
|
857
|
+
if not kernel32.QueryInformationJobObject(
|
|
858
|
+
ctypes.c_void_p(handle),
|
|
859
|
+
3,
|
|
860
|
+
buffer,
|
|
861
|
+
ctypes.sizeof(buffer),
|
|
862
|
+
ctypes.byref(returned),
|
|
863
|
+
):
|
|
864
|
+
return None
|
|
865
|
+
assigned = ctypes.c_uint32.from_buffer_copy(buffer.raw[0:4]).value
|
|
866
|
+
count = ctypes.c_uint32.from_buffer_copy(buffer.raw[4:8]).value
|
|
867
|
+
if count > assigned or count > capacity:
|
|
868
|
+
return None
|
|
869
|
+
array_type = ctypes.c_size_t * count
|
|
870
|
+
members = array_type.from_buffer_copy(
|
|
871
|
+
buffer.raw[8 : 8 + pointer_size * count]
|
|
872
|
+
)
|
|
873
|
+
return sorted(int(pid) for pid in members)
|
|
874
|
+
except Exception:
|
|
875
|
+
return None
|
|
876
|
+
finally:
|
|
877
|
+
if opened and handle is not None:
|
|
878
|
+
try:
|
|
879
|
+
import ctypes
|
|
880
|
+
|
|
881
|
+
ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(handle))
|
|
882
|
+
except Exception:
|
|
883
|
+
pass
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@dataclass
|
|
887
|
+
class SpawnedProcess:
|
|
888
|
+
process: subprocess.Popen[Any]
|
|
889
|
+
attestation: dict[str, Any]
|
|
890
|
+
observedIdentity: dict[str, Any]
|
|
891
|
+
ownershipProof: dict[str, Any] | None
|
|
892
|
+
_job: _WindowsNamedJob | None = None
|
|
893
|
+
_pidfd: int | None = None
|
|
894
|
+
|
|
895
|
+
def close(self) -> None:
|
|
896
|
+
if self._pidfd is not None:
|
|
897
|
+
try:
|
|
898
|
+
os.close(self._pidfd)
|
|
899
|
+
except OSError:
|
|
900
|
+
pass
|
|
901
|
+
self._pidfd = None
|
|
902
|
+
if self._job is not None:
|
|
903
|
+
proof_id = (
|
|
904
|
+
str(self.ownershipProof.get("proofId") or "")
|
|
905
|
+
if isinstance(self.ownershipProof, dict)
|
|
906
|
+
else ""
|
|
907
|
+
)
|
|
908
|
+
if proof_id:
|
|
909
|
+
_WINDOWS_JOB_HANDLES.pop(proof_id, None)
|
|
910
|
+
# KILL_ON_JOB_CLOSE is the durable backstop, but explicitly
|
|
911
|
+
# terminating the owned job first closes a short race where a
|
|
912
|
+
# descendant is still being attached while the leader exits.
|
|
913
|
+
try:
|
|
914
|
+
self._job.terminate()
|
|
915
|
+
except Exception:
|
|
916
|
+
pass
|
|
917
|
+
self._job.close()
|
|
918
|
+
self._job = None
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def capture_owned_members(spawned: SpawnedProcess) -> dict[str, Any] | None:
|
|
922
|
+
"""Refresh a spawn proof with the provider's current owned members.
|
|
923
|
+
|
|
924
|
+
A process can create children between spawn and cleanup (notably console
|
|
925
|
+
hosts on Windows). The provider, rather than each consumer, must capture
|
|
926
|
+
that membership before identity verification and termination.
|
|
927
|
+
"""
|
|
928
|
+
|
|
929
|
+
proof = spawned.ownershipProof
|
|
930
|
+
leader = spawned.attestation
|
|
931
|
+
if not isinstance(proof, dict) or not isinstance(leader, dict):
|
|
932
|
+
return proof if isinstance(proof, dict) else None
|
|
933
|
+
observed = list(_observe_proof_members(proof))
|
|
934
|
+
if not observed:
|
|
935
|
+
return proof
|
|
936
|
+
leader_pid = leader.get("pid")
|
|
937
|
+
members: list[dict[str, Any]] = []
|
|
938
|
+
for item in observed:
|
|
939
|
+
if item.get("pid") == leader_pid:
|
|
940
|
+
members.append(json.loads(json.dumps(leader)))
|
|
941
|
+
else:
|
|
942
|
+
members.append(json.loads(json.dumps(item)))
|
|
943
|
+
if leader_pid in {item.get("pid") for item in members}:
|
|
944
|
+
proof["members"] = members
|
|
945
|
+
proof["membersComplete"] = True
|
|
946
|
+
return proof
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def spawn_structured_argv(
|
|
950
|
+
argv: Sequence[str],
|
|
951
|
+
*,
|
|
952
|
+
cwd: Path | str,
|
|
953
|
+
environment: Mapping[str, str] | None,
|
|
954
|
+
owner_token: str,
|
|
955
|
+
stdout: Any = None,
|
|
956
|
+
stderr: Any = None,
|
|
957
|
+
) -> SpawnedProcess:
|
|
958
|
+
"""Spawn with shell=False and return separate attestation/observation/proof."""
|
|
959
|
+
|
|
960
|
+
if (
|
|
961
|
+
not argv
|
|
962
|
+
or any(not isinstance(item, str) or "\0" in item for item in argv)
|
|
963
|
+
or not isinstance(owner_token, str)
|
|
964
|
+
or not owner_token
|
|
965
|
+
):
|
|
966
|
+
raise ValueError("ARGUMENT_INVALID: argv and owner token are required")
|
|
967
|
+
root = Path(cwd).expanduser().resolve()
|
|
968
|
+
child_env = os.environ.copy()
|
|
969
|
+
child_env.update({str(key): str(value) for key, value in (environment or {}).items()})
|
|
970
|
+
child_env.setdefault("PYTHONUTF8", "1")
|
|
971
|
+
child_env.setdefault("PYTHONIOENCODING", "utf-8")
|
|
972
|
+
kwargs: dict[str, Any] = {
|
|
973
|
+
"cwd": str(root),
|
|
974
|
+
"env": child_env,
|
|
975
|
+
"shell": False,
|
|
976
|
+
"stdout": stdout,
|
|
977
|
+
"stderr": stderr,
|
|
978
|
+
}
|
|
979
|
+
if os.name == "nt":
|
|
980
|
+
kwargs["creationflags"] = getattr(
|
|
981
|
+
subprocess, "CREATE_NEW_PROCESS_GROUP", 0
|
|
982
|
+
)
|
|
983
|
+
else:
|
|
984
|
+
kwargs["start_new_session"] = True
|
|
985
|
+
process = subprocess.Popen(list(argv), **kwargs)
|
|
986
|
+
observed = observe_process_identity(process.pid)
|
|
987
|
+
owner_hash = _token_hash(owner_token)
|
|
988
|
+
attestation = json.loads(json.dumps(observed))
|
|
989
|
+
attestation["argvHash"] = canonical_argv_hash(argv)
|
|
990
|
+
attestation["workingDirectory"] = str(root)
|
|
991
|
+
attestation["ownerTokenHash"] = owner_hash
|
|
992
|
+
attestation["fieldProvenance"]["argvHash"] = (
|
|
993
|
+
"OBSERVED" if observed.get("argvHash") is not None else "ATTESTED"
|
|
994
|
+
)
|
|
995
|
+
attestation["fieldProvenance"]["workingDirectory"] = (
|
|
996
|
+
"OBSERVED"
|
|
997
|
+
if observed.get("workingDirectory") is not None
|
|
998
|
+
else "ATTESTED"
|
|
999
|
+
)
|
|
1000
|
+
attestation["fieldProvenance"]["ownerTokenHash"] = "ATTESTED"
|
|
1001
|
+
proof_id = "proof-" + uuid.uuid4().hex
|
|
1002
|
+
job: _WindowsNamedJob | None = None
|
|
1003
|
+
pidfd: int | None = None
|
|
1004
|
+
proof: dict[str, Any] | None
|
|
1005
|
+
if os.name == "nt":
|
|
1006
|
+
job = _WindowsNamedJob(
|
|
1007
|
+
"Local\\HunterHarness-" + owner_hash.split(":", 1)[1][:16] + "-" + proof_id
|
|
1008
|
+
)
|
|
1009
|
+
if job.handle is not None and job.assign(process):
|
|
1010
|
+
_WINDOWS_JOB_HANDLES[proof_id] = job
|
|
1011
|
+
proof = {
|
|
1012
|
+
"schemaVersion": 1,
|
|
1013
|
+
"proofId": proof_id,
|
|
1014
|
+
"kind": "WINDOWS_NAMED_JOB",
|
|
1015
|
+
"jobName": job.name,
|
|
1016
|
+
"ownershipTokenHash": owner_hash,
|
|
1017
|
+
"leaderPid": process.pid,
|
|
1018
|
+
"leaderCreatedAt": attestation.get("createdAt"),
|
|
1019
|
+
"members": [attestation],
|
|
1020
|
+
"membersComplete": True,
|
|
1021
|
+
"leaderExited": False,
|
|
1022
|
+
}
|
|
1023
|
+
else:
|
|
1024
|
+
try:
|
|
1025
|
+
process.kill()
|
|
1026
|
+
process.wait(timeout=5)
|
|
1027
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
1028
|
+
pass
|
|
1029
|
+
job.close()
|
|
1030
|
+
raise RuntimeError("IDENTITY_UNVERIFIABLE: Windows ownership proof unavailable")
|
|
1031
|
+
else:
|
|
1032
|
+
tree = observed.get("treeIdentity") or {}
|
|
1033
|
+
kind = "POSIX_MEMBERS"
|
|
1034
|
+
if sys.platform.startswith("linux") and hasattr(os, "pidfd_open"):
|
|
1035
|
+
try:
|
|
1036
|
+
pidfd = os.pidfd_open(process.pid)
|
|
1037
|
+
kind = "LINUX_PIDFD_SESSION"
|
|
1038
|
+
except OSError:
|
|
1039
|
+
pidfd = None
|
|
1040
|
+
proof = {
|
|
1041
|
+
"schemaVersion": 1,
|
|
1042
|
+
"proofId": proof_id,
|
|
1043
|
+
"kind": kind,
|
|
1044
|
+
"ownershipTokenHash": owner_hash,
|
|
1045
|
+
"leaderPid": process.pid,
|
|
1046
|
+
"leaderCreatedAt": attestation.get("createdAt"),
|
|
1047
|
+
"groupId": tree.get("groupId", process.pid),
|
|
1048
|
+
"sessionId": tree.get("sessionId", process.pid),
|
|
1049
|
+
"members": [attestation],
|
|
1050
|
+
"membersComplete": bool(tree.get("complete", False)),
|
|
1051
|
+
"leaderExited": False,
|
|
1052
|
+
"pidfdHeld": pidfd is not None,
|
|
1053
|
+
}
|
|
1054
|
+
attestation["capabilities"]["canVerifyOwnership"] = bool(
|
|
1055
|
+
isinstance(proof, dict)
|
|
1056
|
+
and proof.get("membersComplete") is True
|
|
1057
|
+
)
|
|
1058
|
+
return SpawnedProcess(
|
|
1059
|
+
process=process,
|
|
1060
|
+
attestation=attestation,
|
|
1061
|
+
observedIdentity=observed,
|
|
1062
|
+
ownershipProof=proof,
|
|
1063
|
+
_job=job,
|
|
1064
|
+
_pidfd=pidfd,
|
|
1065
|
+
)
|