@danmoisan/drm-copilot-mcp 1.1.23 → 1.1.25

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.
Files changed (199) hide show
  1. package/out/mcp-server.js +13347 -7926
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/discovery-capability-sentinel.md +34 -0
  4. package/resources/claude-customizations/.claude/agents/legacy-parity-analyst.md +101 -43
  5. package/resources/claude-customizations/.claude/agents/migration-coverage-reviewer.md +107 -43
  6. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +14 -0
  7. package/resources/claude-customizations/.claude/agents/parallel-planner.md +21 -1
  8. package/resources/claude-customizations/.claude/agents/pr-author.md +17 -1
  9. package/resources/claude-customizations/.claude/agents/requirements-reconciler.md +117 -42
  10. package/resources/claude-customizations/.claude/agents/runtime-characterization-analyst.md +103 -43
  11. package/resources/claude-customizations/.claude/hooks/discovery-capability-probe.ps1 +102 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +69 -194
  13. package/resources/claude-customizations/.claude/hooks/enforce-discovery-planning-gate.ps1 +87 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +7 -1
  15. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +6 -3
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +168 -48
  17. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +1 -1
  18. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +59 -20
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +44 -3
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +60 -4
  21. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +4 -2
  22. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +59 -20
  23. package/resources/claude-customizations/.claude/hooks/record-discovery-agent-start.ps1 +132 -0
  24. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +56 -212
  25. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-rejection.ps1 +172 -0
  26. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +9 -1
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +9 -3
  28. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusAggregate.psm1 +154 -0
  29. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusCombination.psm1 +228 -0
  30. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +1 -0
  31. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +4 -2
  32. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +63 -16
  33. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +56 -1
  34. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +4 -1
  35. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +5 -1
  36. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
  37. package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +25 -2
  38. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
  39. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
  40. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
  41. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
  42. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
  43. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
  44. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
  45. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
  46. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
  47. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
  48. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
  49. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
  50. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
  51. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
  52. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +188 -341
  53. package/resources/claude-customizations/.claude/lib/worktree-resolution/HookTargetWorktree.psm1 +391 -0
  54. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +1 -0
  55. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -18
  56. package/resources/claude-customizations/.claude/settings.json +32 -3
  57. package/resources/claude-customizations/.claude/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
  58. package/resources/claude-customizations/.claude/skills/characterize-runtime-behavior/SKILL.md +134 -0
  59. package/resources/claude-customizations/.claude/skills/discovery-behavior-reconciliation/SKILL.md +28 -47
  60. package/resources/claude-customizations/.claude/skills/discovery-coverage-ledger/SKILL.md +32 -48
  61. package/resources/claude-customizations/.claude/skills/discovery-parity-matrix/SKILL.md +28 -45
  62. package/resources/claude-customizations/.claude/skills/discovery-repo-inventory/SKILL.md +28 -62
  63. package/resources/claude-customizations/.claude/skills/discovery-runtime-characterization/SKILL.md +28 -45
  64. package/resources/claude-customizations/.claude/skills/discovery-validate-artifacts/SKILL.md +28 -61
  65. package/resources/claude-customizations/.claude/skills/discovery-workflow/SKILL.md +55 -139
  66. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +24 -9
  67. package/resources/claude-customizations/.claude/skills/extract-feature-contract/SKILL.md +133 -0
  68. package/resources/claude-customizations/.claude/skills/generate-acceptance-scenarios/SKILL.md +132 -0
  69. package/resources/claude-customizations/.claude/skills/initialize-discovery-workspace/SKILL.md +131 -0
  70. package/resources/claude-customizations/.claude/skills/inventory-legacy-system/SKILL.md +131 -0
  71. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +5 -4
  72. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +21 -1
  73. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +11 -1
  74. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +34 -0
  75. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +69 -6
  76. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +15 -0
  77. package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +6 -0
  78. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +21 -4
  79. package/resources/claude-customizations/.claude/skills/reconcile-parity-matrix/SKILL.md +131 -0
  80. package/resources/claude-customizations/.claude/skills/review-discovery-coverage/SKILL.md +129 -0
  81. package/resources/claude-customizations/.claude/skills/validate-discovery-artifacts/SKILL.md +127 -0
  82. package/resources/claude-customizations/config/blast-radius.json +6 -5
  83. package/resources/claude-customizations/config/orchestration-routing.json +61 -1
  84. package/resources/claude-customizations/pack-manifests/core.json +33 -1
  85. package/resources/codex-and-agents-customizations/.agents/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
  86. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +7 -0
  87. package/resources/codex-and-agents-customizations/.agents/skills/characterize-runtime-behavior/SKILL.md +134 -0
  88. package/resources/codex-and-agents-customizations/.agents/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
  89. package/resources/codex-and-agents-customizations/.agents/skills/discovery-coverage-ledger/SKILL.md +50 -0
  90. package/resources/codex-and-agents-customizations/.agents/skills/discovery-parity-matrix/SKILL.md +46 -0
  91. package/resources/codex-and-agents-customizations/.agents/skills/discovery-repo-inventory/SKILL.md +46 -0
  92. package/resources/codex-and-agents-customizations/.agents/skills/discovery-runtime-characterization/SKILL.md +46 -0
  93. package/resources/codex-and-agents-customizations/.agents/skills/discovery-validate-artifacts/SKILL.md +46 -0
  94. package/resources/codex-and-agents-customizations/.agents/skills/discovery-workflow/SKILL.md +62 -0
  95. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +24 -9
  96. package/resources/codex-and-agents-customizations/.agents/skills/extract-feature-contract/SKILL.md +133 -0
  97. package/resources/codex-and-agents-customizations/.agents/skills/generate-acceptance-scenarios/SKILL.md +132 -0
  98. package/resources/codex-and-agents-customizations/.agents/skills/initialize-discovery-workspace/SKILL.md +131 -0
  99. package/resources/codex-and-agents-customizations/.agents/skills/inventory-legacy-system/SKILL.md +131 -0
  100. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +8 -0
  101. package/resources/codex-and-agents-customizations/.agents/skills/reconcile-parity-matrix/SKILL.md +131 -0
  102. package/resources/codex-and-agents-customizations/.agents/skills/review-discovery-coverage/SKILL.md +129 -0
  103. package/resources/codex-and-agents-customizations/.agents/skills/validate-discovery-artifacts/SKILL.md +127 -0
  104. package/resources/codex-and-agents-customizations/.codex/agents/discovery-capability-sentinel.toml +51 -0
  105. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c1.toml +148 -0
  106. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c2.toml +148 -0
  107. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3-elevated.toml +148 -0
  108. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3.toml +148 -0
  109. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c4.toml +148 -0
  110. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst.toml +148 -0
  111. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c1.toml +154 -0
  112. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c2.toml +154 -0
  113. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3-elevated.toml +154 -0
  114. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3.toml +154 -0
  115. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c4.toml +154 -0
  116. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer.toml +154 -0
  117. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c1.toml +164 -0
  118. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c2.toml +164 -0
  119. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3-elevated.toml +164 -0
  120. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3.toml +164 -0
  121. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c4.toml +164 -0
  122. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler.toml +164 -0
  123. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c1.toml +150 -0
  124. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c2.toml +150 -0
  125. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3-elevated.toml +150 -0
  126. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3.toml +150 -0
  127. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c4.toml +150 -0
  128. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst.toml +150 -0
  129. package/resources/codex-and-agents-customizations/.codex/config.toml +33 -3
  130. package/resources/codex-and-agents-customizations/.codex/hooks/codex-agent-profile-attestation.ps1 +22 -0
  131. package/resources/codex-and-agents-customizations/.codex/hooks/discovery-capability-probe.ps1 +97 -0
  132. package/resources/codex-and-agents-customizations/.codex/hooks/discovery-start-attestation.ps1 +171 -0
  133. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +4 -1
  134. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-artifact-gate.ps1 +187 -0
  135. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-planning-gate.ps1 +149 -0
  136. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +239 -2
  137. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -15
  138. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +54 -15
  139. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +54 -15
  140. package/resources/codex-and-agents-customizations/.codex/hooks/hook-target-worktree.ps1 +369 -0
  141. package/resources/codex-and-agents-customizations/.codex/hooks/record-subagent-routing-attestation.ps1 +8 -0
  142. package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +22 -1
  143. package/resources/codex-and-agents-customizations/.codex/hooks/validate-discovery-agent-output.ps1 +178 -0
  144. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +11 -1
  145. package/resources/codex-and-agents-customizations/pack-manifests/core.json +63 -1
  146. package/resources/config/orchestration-routing.json +61 -1
  147. package/resources/customizations/.github/agents/discovery-capability-sentinel.agent.md +34 -0
  148. package/resources/customizations/.github/agents/legacy-parity-analyst.agent.md +122 -0
  149. package/resources/customizations/.github/agents/migration-coverage-reviewer.agent.md +128 -0
  150. package/resources/customizations/.github/agents/requirements-reconciler.agent.md +138 -0
  151. package/resources/customizations/.github/agents/runtime-characterization-analyst.agent.md +124 -0
  152. package/resources/customizations/.github/hooks/discovery-agent-start.ps1 +139 -0
  153. package/resources/customizations/.github/hooks/discovery-agent-stop.ps1 +123 -0
  154. package/resources/customizations/.github/hooks/discovery-capability-probe.ps1 +98 -0
  155. package/resources/customizations/.github/hooks/discovery-planning.ps1 +91 -0
  156. package/resources/customizations/.github/hooks/discovery-pre-tool.ps1 +123 -0
  157. package/resources/customizations/.github/hooks/discovery.json +35 -0
  158. package/resources/customizations/.github/instructions/mermaid.instructions.md +85 -0
  159. package/resources/customizations/.github/prompts/execute-hard-lock.prompt.md +105 -0
  160. package/resources/customizations/.github/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
  161. package/resources/customizations/.github/skills/characterize-runtime-behavior/SKILL.md +134 -0
  162. package/resources/customizations/.github/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
  163. package/resources/customizations/.github/skills/discovery-coverage-ledger/SKILL.md +50 -0
  164. package/resources/customizations/.github/skills/discovery-parity-matrix/SKILL.md +46 -0
  165. package/resources/customizations/.github/skills/discovery-repo-inventory/SKILL.md +46 -0
  166. package/resources/customizations/.github/skills/discovery-runtime-characterization/SKILL.md +46 -0
  167. package/resources/customizations/.github/skills/discovery-validate-artifacts/SKILL.md +46 -0
  168. package/resources/customizations/.github/skills/discovery-workflow/SKILL.md +62 -0
  169. package/resources/customizations/.github/skills/execution-environment/SKILL.md +24 -9
  170. package/resources/customizations/.github/skills/extract-feature-contract/SKILL.md +133 -0
  171. package/resources/customizations/.github/skills/generate-acceptance-scenarios/SKILL.md +132 -0
  172. package/resources/customizations/.github/skills/initialize-discovery-workspace/SKILL.md +131 -0
  173. package/resources/customizations/.github/skills/inventory-legacy-system/SKILL.md +131 -0
  174. package/resources/customizations/.github/skills/reconcile-parity-matrix/SKILL.md +131 -0
  175. package/resources/customizations/.github/skills/review-discovery-coverage/SKILL.md +129 -0
  176. package/resources/customizations/.github/skills/validate-discovery-artifacts/SKILL.md +127 -0
  177. package/resources/lib/codex-routing/CodexDeployment.psm1 +5 -1
  178. package/resources/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
  179. package/resources/lib/codex-routing/CodexTopology.psm1 +25 -2
  180. package/resources/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
  181. package/resources/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
  182. package/resources/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
  183. package/resources/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
  184. package/resources/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
  185. package/resources/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
  186. package/resources/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
  187. package/resources/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
  188. package/resources/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
  189. package/resources/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
  190. package/resources/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
  191. package/resources/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
  192. package/resources/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
  193. package/resources/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
  194. package/resources/powershell/PoshQC/PoshQC.Parallel.psm1 +452 -0
  195. package/resources/powershell/PoshQC/PoshQC.Shard.psm1 +106 -8
  196. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -0
  197. package/resources/powershell/PoshQC/PoshQC.psd1 +6 -1
  198. package/resources/powershell/PoshQC/PoshQC.psm1 +7 -1
  199. package/resources/powershell/PoshQC/README.md +20 -1
@@ -0,0 +1,384 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Input-manifest validation and freshness recomputation for native discovery hooks.
4
+
5
+ .DESCRIPTION
6
+ A validation receipt carries the complete sorted set of inputs the runtime
7
+ read (`service-result#/$defs/input-manifest`). Before a native stop or
8
+ planning check trusts that receipt it re-enumerates the authorized input set,
9
+ re-reads every path through an injected byte reader, recomputes the digests
10
+ and sizes, confirms that paths reported missing are still missing, checks the
11
+ protected profile binding, and classifies every difference as drift:
12
+ `edited`, `deleted`, `appeared`, `renamed`, `evidence-replaced` or
13
+ `profile-changed`. The rules mirror `discovery-input-manifest.ts` so the hook,
14
+ the gateway and the runtime agree on the same digest for the same inputs.
15
+
16
+ The module performs no writes, holds no authority and never reimplements
17
+ schema or graph semantics; it only decides whether the inputs a receipt names
18
+ are still the inputs on disk.
19
+
20
+ .NOTES
21
+ Issue #980. Compatible with PowerShell 7+. Depends only on the sibling
22
+ DiscoveryAuthority module for hashing and canonical JSON.
23
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
24
+ #>
25
+
26
+ Set-StrictMode -Version Latest
27
+ $ErrorActionPreference = 'Stop'
28
+
29
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryEncoding.psm1') -ErrorAction Stop
30
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryAuthority.psm1') -ErrorAction Stop
31
+
32
+ $script:InputCategories = @(
33
+ 'profile', 'activation', 'scope', 'inventory', 'artifact', 'evidence',
34
+ 'source-snapshot', 'extension-schema', 'extension-result', 'source', 'test', 'documentation'
35
+ )
36
+ $script:ManifestKeys = 'entries,missing_paths,sha256,source_repositories,target_repositories'
37
+ $script:EntryKeys = 'category,path,sha256,size'
38
+ $script:RepositoryKeys = 'id,revision'
39
+ $script:IdentifierPattern = '^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$'
40
+ $script:RevisionPattern = '^(?:[0-9a-f]{40}|[0-9a-f]{64})$'
41
+ $script:DigestPattern = '^[0-9a-f]{64}$'
42
+
43
+ function Get-DiscoveryManifestFailureMessage {
44
+ <#
45
+ .SYNOPSIS
46
+ Builds the typed INPUT_MANIFEST_INVALID message for one defect.
47
+ #>
48
+ [CmdletBinding()]
49
+ [OutputType([string])]
50
+ param([Parameter(Mandatory = $true)][string]$Detail)
51
+
52
+ return "INPUT_MANIFEST_INVALID: Input manifest is malformed: $Detail"
53
+ }
54
+
55
+ function Get-DiscoverySortedPropertyNameList {
56
+ <#
57
+ .SYNOPSIS
58
+ Returns the ordinal-sorted member names of a hashtable or PSCustomObject joined by commas.
59
+ #>
60
+ [CmdletBinding()]
61
+ [OutputType([string])]
62
+ param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
63
+
64
+ if ($Value -is [System.Collections.IDictionary]) {
65
+ [string[]]$names = @($Value.Keys | ForEach-Object { [string]$_ })
66
+ } elseif ($Value -is [System.Management.Automation.PSCustomObject]) {
67
+ [string[]]$names = @($Value.PSObject.Properties.Name)
68
+ } else {
69
+ return $null
70
+ }
71
+ [Array]::Sort($names, [System.StringComparer]::Ordinal)
72
+ return ($names -join ',')
73
+ }
74
+
75
+ function Get-DiscoveryMemberValue {
76
+ <#
77
+ .SYNOPSIS
78
+ Reads one member from a hashtable or PSCustomObject uniformly.
79
+ #>
80
+ [CmdletBinding()]
81
+ [OutputType([object], [object[]])]
82
+ param(
83
+ [Parameter(Mandatory = $true)][object]$Value,
84
+ [Parameter(Mandatory = $true)][string]$Name
85
+ )
86
+
87
+ # A direct assignment (no subexpression) and the unary comma keep an array
88
+ # member intact across the function boundary instead of unrolling it.
89
+ if ($Value -is [System.Collections.IDictionary]) {
90
+ $member = $Value[$Name]
91
+ } else {
92
+ $member = $Value.PSObject.Properties[$Name].Value
93
+ }
94
+ if ($null -eq $member) { return $null }
95
+ return , $member
96
+ }
97
+
98
+ function Test-DiscoveryIntegerValue {
99
+ <#
100
+ .SYNOPSIS
101
+ True when Value is a non-negative integer (int32, int64, or an integral double from JSON).
102
+ #>
103
+ [CmdletBinding()]
104
+ [OutputType([bool])]
105
+ param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
106
+
107
+ if ($Value -is [int32] -or $Value -is [int64]) { return [int64]$Value -ge 0 }
108
+ if ($Value -is [double]) { return ([math]::Floor($Value) -eq $Value) -and $Value -ge 0 }
109
+ return $false
110
+ }
111
+
112
+ function ConvertFrom-DiscoveryManifestEntry {
113
+ <#
114
+ .SYNOPSIS
115
+ Decodes one manifest entry strictly (exact keys, digest, size, category, relative path).
116
+ #>
117
+ [CmdletBinding()]
118
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
119
+ param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
120
+
121
+ if ((Get-DiscoverySortedPropertyNameList -Value $Value) -ne $script:EntryKeys) {
122
+ throw (Get-DiscoveryManifestFailureMessage -Detail 'entry keys')
123
+ }
124
+ $path = Get-DiscoveryMemberValue -Value $Value -Name 'path'
125
+ $sha256 = Get-DiscoveryMemberValue -Value $Value -Name 'sha256'
126
+ $size = Get-DiscoveryMemberValue -Value $Value -Name 'size'
127
+ $category = Get-DiscoveryMemberValue -Value $Value -Name 'category'
128
+ if ($sha256 -isnot [string] -or $sha256 -cnotmatch $script:DigestPattern -or
129
+ -not (Test-DiscoveryIntegerValue -Value $size) -or
130
+ $category -isnot [string] -or $script:InputCategories -notcontains $category) {
131
+ throw (Get-DiscoveryManifestFailureMessage -Detail 'entry fields')
132
+ }
133
+ if ($path -isnot [string] -or -not (Test-DiscoveryRelativePath -Path $path)) {
134
+ throw (Get-DiscoveryManifestFailureMessage -Detail 'entry path')
135
+ }
136
+ return [ordered]@{ category = $category; path = $path; sha256 = $sha256; size = [int64]$size }
137
+ }
138
+
139
+ function ConvertFrom-DiscoveryRepositoryReference {
140
+ <#
141
+ .SYNOPSIS
142
+ Decodes one `{id, revision}` repository reference strictly.
143
+ #>
144
+ [CmdletBinding()]
145
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
146
+ param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
147
+
148
+ if ((Get-DiscoverySortedPropertyNameList -Value $Value) -ne $script:RepositoryKeys) {
149
+ throw (Get-DiscoveryManifestFailureMessage -Detail 'repository reference')
150
+ }
151
+ $id = Get-DiscoveryMemberValue -Value $Value -Name 'id'
152
+ $revision = Get-DiscoveryMemberValue -Value $Value -Name 'revision'
153
+ if ($id -isnot [string] -or $id -cnotmatch $script:IdentifierPattern -or
154
+ $revision -isnot [string] -or $revision -cnotmatch $script:RevisionPattern) {
155
+ throw (Get-DiscoveryManifestFailureMessage -Detail 'repository reference')
156
+ }
157
+ return [ordered]@{ id = $id; revision = $revision }
158
+ }
159
+
160
+ function Assert-DiscoveryUniqueValueSet {
161
+ <#
162
+ .SYNOPSIS
163
+ Throws the manifest failure when Values contains a duplicate.
164
+ #>
165
+ [CmdletBinding()]
166
+ param(
167
+ [Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]]$Values,
168
+ [Parameter(Mandatory = $true)][string]$Detail
169
+ )
170
+
171
+ $distinct = @($Values | Select-Object -Unique)
172
+ if ($distinct.Count -ne $Values.Count) { throw (Get-DiscoveryManifestFailureMessage -Detail $Detail) }
173
+ }
174
+
175
+ function ConvertFrom-DiscoveryInputManifest {
176
+ <#
177
+ .SYNOPSIS
178
+ Parses a runtime-reported input manifest strictly.
179
+ .DESCRIPTION
180
+ Accepts the parsed JSON (PSCustomObject or hashtable) of
181
+ `service-result#/$defs/input-manifest` and returns a normalized ordered
182
+ manifest, or throws `INPUT_MANIFEST_INVALID` naming the defect. A path cannot
183
+ be both read and missing; entry paths, missing paths and repository ids are
184
+ unique.
185
+ .OUTPUTS
186
+ [System.Collections.Specialized.OrderedDictionary] entries, missing_paths,
187
+ source_repositories, target_repositories, sha256.
188
+ #>
189
+ [CmdletBinding()]
190
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
191
+ param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
192
+
193
+ if ((Get-DiscoverySortedPropertyNameList -Value $Value) -ne $script:ManifestKeys) {
194
+ throw (Get-DiscoveryManifestFailureMessage -Detail 'manifest keys')
195
+ }
196
+ $entries = Get-DiscoveryMemberValue -Value $Value -Name 'entries'
197
+ $missing = Get-DiscoveryMemberValue -Value $Value -Name 'missing_paths'
198
+ $sources = Get-DiscoveryMemberValue -Value $Value -Name 'source_repositories'
199
+ $targets = Get-DiscoveryMemberValue -Value $Value -Name 'target_repositories'
200
+ $sha256 = Get-DiscoveryMemberValue -Value $Value -Name 'sha256'
201
+ # Arrays must be real arrays; a string is also IEnumerable, so it is excluded.
202
+ foreach ($member in @($entries, $missing, $sources, $targets)) {
203
+ if ($null -eq $member -or $member -is [string] -or $member -isnot [System.Collections.IEnumerable]) {
204
+ throw (Get-DiscoveryManifestFailureMessage -Detail 'manifest fields')
205
+ }
206
+ }
207
+ if ($sha256 -isnot [string] -or $sha256 -cnotmatch $script:DigestPattern) {
208
+ throw (Get-DiscoveryManifestFailureMessage -Detail 'manifest fields')
209
+ }
210
+ $decodedEntries = @(foreach ($entry in $entries) { ConvertFrom-DiscoveryManifestEntry -Value $entry })
211
+ $missingPaths = @(foreach ($path in $missing) {
212
+ if ($path -isnot [string] -or -not (Test-DiscoveryRelativePath -Path $path)) {
213
+ throw (Get-DiscoveryManifestFailureMessage -Detail 'missing path')
214
+ }
215
+ [string]$path
216
+ })
217
+ Assert-DiscoveryUniqueValueSet -Values @($decodedEntries | ForEach-Object { $_.path }) -Detail 'duplicate entry paths'
218
+ Assert-DiscoveryUniqueValueSet -Values $missingPaths -Detail 'duplicate missing paths'
219
+ # A path cannot be both read and missing in one manifest.
220
+ $listed = @($decodedEntries | ForEach-Object { $_.path })
221
+ foreach ($path in $missingPaths) {
222
+ if ($listed -contains $path) { throw (Get-DiscoveryManifestFailureMessage -Detail 'missing path also listed as an entry') }
223
+ }
224
+ $sourceRefs = @(foreach ($row in $sources) { ConvertFrom-DiscoveryRepositoryReference -Value $row })
225
+ $targetRefs = @(foreach ($row in $targets) { ConvertFrom-DiscoveryRepositoryReference -Value $row })
226
+ Assert-DiscoveryUniqueValueSet -Values @($sourceRefs | ForEach-Object { $_.id }) -Detail 'duplicate source repositories'
227
+ Assert-DiscoveryUniqueValueSet -Values @($targetRefs | ForEach-Object { $_.id }) -Detail 'duplicate target repositories'
228
+ return [ordered]@{
229
+ entries = $decodedEntries
230
+ missing_paths = $missingPaths
231
+ source_repositories = $sourceRefs
232
+ target_repositories = $targetRefs
233
+ sha256 = $sha256
234
+ }
235
+ }
236
+
237
+ function Get-DiscoveryInputManifestDigest {
238
+ <#
239
+ .SYNOPSIS
240
+ Returns the digest of the four manifest members in canonical JSON.
241
+ #>
242
+ [CmdletBinding()]
243
+ [OutputType([string])]
244
+ param([Parameter(Mandatory = $true)][System.Collections.IDictionary]$Manifest)
245
+
246
+ $body = [ordered]@{
247
+ entries = @($Manifest['entries'])
248
+ missing_paths = @($Manifest['missing_paths'])
249
+ source_repositories = @($Manifest['source_repositories'])
250
+ target_repositories = @($Manifest['target_repositories'])
251
+ }
252
+ return Get-DiscoverySha256Hex -Text (ConvertTo-DiscoveryCanonicalJson -Value $body)
253
+ }
254
+
255
+ function Get-DiscoveryWorkspacePath {
256
+ <#
257
+ .SYNOPSIS
258
+ Joins a workspace-relative path to the absolute root with forward slashes.
259
+ #>
260
+ [CmdletBinding()]
261
+ [OutputType([string])]
262
+ param(
263
+ [Parameter(Mandatory = $true)][string]$WorkspaceRoot,
264
+ [Parameter(Mandatory = $true)][string]$RelativePath
265
+ )
266
+
267
+ return $WorkspaceRoot.TrimEnd('/', '\') + '/' + $RelativePath
268
+ }
269
+
270
+ function Compare-DiscoveryInputManifest {
271
+ <#
272
+ .SYNOPSIS
273
+ Re-reads every input and classifies every difference from the manifest as drift.
274
+ .DESCRIPTION
275
+ Returns the recomputed manifest (same entry set with fresh digests and sizes,
276
+ digest recomputed) together with the drift list. A changed digest or size is
277
+ `edited` (`evidence-replaced` for the evidence category), an absent file is
278
+ `deleted`, a missing path that now exists is `appeared` unless its bytes equal
279
+ a deleted input (`renamed`), and a profile binding that is absent or differs
280
+ from the activation digest is `profile-changed`.
281
+ .PARAMETER ReadBytes
282
+ Seam returning the raw bytes of an absolute path, or $null when no regular
283
+ file exists. The default reads the real filesystem.
284
+ .OUTPUTS
285
+ [pscustomobject] Manifest (ordered dictionary with sha256) and Drift (array of
286
+ kind/path/detail objects).
287
+ #>
288
+ [CmdletBinding()]
289
+ [OutputType([pscustomobject])]
290
+ param(
291
+ [Parameter(Mandatory = $true)][System.Collections.IDictionary]$Manifest,
292
+ [Parameter(Mandatory = $true)][string]$WorkspaceRoot,
293
+ [Parameter(Mandatory = $true)][string]$ProfilePath,
294
+ [Parameter(Mandatory = $true)][string]$ProfileSha256,
295
+ [Parameter(Mandatory = $false)][scriptblock]$ReadBytes = {
296
+ param([string]$Path)
297
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
298
+ return [System.IO.File]::ReadAllBytes($Path)
299
+ }
300
+ )
301
+
302
+ $drift = [System.Collections.Generic.List[object]]::new()
303
+ $entries = [System.Collections.Generic.List[object]]::new()
304
+ $deletedDigests = @{}
305
+ # Every listed input is re-read; a changed digest or size is an edit, an
306
+ # absent file is a deletion, and evidence edits are named separately.
307
+ foreach ($entry in @($Manifest['entries'])) {
308
+ $bytes = & $ReadBytes (Get-DiscoveryWorkspacePath -WorkspaceRoot $WorkspaceRoot -RelativePath $entry['path'])
309
+ if ($null -eq $bytes) {
310
+ $deletedDigests[$entry['sha256']] = $entry['path']
311
+ $drift.Add([pscustomobject]@{ kind = 'deleted'; path = $entry['path']; detail = 'input no longer exists' })
312
+ continue
313
+ }
314
+ [byte[]]$bytes = @($bytes)
315
+ $digest = Get-DiscoveryBytesSha256Hex -Bytes $bytes
316
+ if ($digest -ne $entry['sha256'] -or $bytes.Length -ne $entry['size']) {
317
+ $kind = $(if ($entry['category'] -eq 'evidence') { 'evidence-replaced' } else { 'edited' })
318
+ $drift.Add([pscustomobject]@{ kind = $kind; path = $entry['path']; detail = "digest $($entry['sha256']) is now $digest" })
319
+ }
320
+ $entries.Add([ordered]@{ category = $entry['category']; path = $entry['path']; sha256 = $digest; size = [int64]$bytes.Length })
321
+ }
322
+ $stillMissing = [System.Collections.Generic.List[string]]::new()
323
+ # A path the runtime found absent that now exists was created after the
324
+ # result; when its bytes equal a deleted input it is a rename.
325
+ foreach ($path in @($Manifest['missing_paths'])) {
326
+ $bytes = & $ReadBytes (Get-DiscoveryWorkspacePath -WorkspaceRoot $WorkspaceRoot -RelativePath $path)
327
+ if ($null -eq $bytes) { $stillMissing.Add($path); continue }
328
+ $digest = Get-DiscoveryBytesSha256Hex -Bytes ([byte[]]@($bytes))
329
+ if ($deletedDigests.ContainsKey($digest)) {
330
+ $drift.Add([pscustomobject]@{ kind = 'renamed'; path = $path; detail = "moved from $($deletedDigests[$digest])" })
331
+ } else {
332
+ $drift.Add([pscustomobject]@{ kind = 'appeared'; path = $path; detail = 'missing input now exists' })
333
+ }
334
+ }
335
+ # The protected activation binding must be one of the inputs and unchanged.
336
+ $profileEntry = @($entries | Where-Object { $_['path'] -eq $ProfilePath } | Select-Object -First 1)
337
+ if ($profileEntry.Count -eq 0) {
338
+ $drift.Add([pscustomobject]@{ kind = 'profile-changed'; path = $ProfilePath; detail = 'profile was not among the inputs read' })
339
+ } elseif ($profileEntry[0]['sha256'] -ne $ProfileSha256) {
340
+ $drift.Add([pscustomobject]@{ kind = 'profile-changed'; path = $ProfilePath; detail = "profile digest $($profileEntry[0]['sha256']) differs from activation $ProfileSha256" })
341
+ }
342
+ $recomputed = [ordered]@{
343
+ entries = @($entries)
344
+ missing_paths = @($stillMissing)
345
+ source_repositories = @($Manifest['source_repositories'])
346
+ target_repositories = @($Manifest['target_repositories'])
347
+ }
348
+ $recomputed['sha256'] = Get-DiscoveryInputManifestDigest -Manifest $recomputed
349
+ return [pscustomobject]@{ Manifest = $recomputed; Drift = @($drift) }
350
+ }
351
+
352
+ function Assert-DiscoveryInputsFresh {
353
+ <#
354
+ .SYNOPSIS
355
+ Recomputes the manifest and throws INPUT_DRIFT when any input moved.
356
+ .OUTPUTS
357
+ [System.Collections.Specialized.OrderedDictionary] The recomputed manifest.
358
+ #>
359
+ [CmdletBinding()]
360
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
361
+ param(
362
+ [Parameter(Mandatory = $true)][System.Collections.IDictionary]$Manifest,
363
+ [Parameter(Mandatory = $true)][string]$WorkspaceRoot,
364
+ [Parameter(Mandatory = $true)][string]$ProfilePath,
365
+ [Parameter(Mandatory = $true)][string]$ProfileSha256,
366
+ [Parameter(Mandatory = $false)][scriptblock]$ReadBytes
367
+ )
368
+
369
+ $arguments = @{ Manifest = $Manifest; WorkspaceRoot = $WorkspaceRoot; ProfilePath = $ProfilePath; ProfileSha256 = $ProfileSha256 }
370
+ if ($PSBoundParameters.ContainsKey('ReadBytes')) { $arguments['ReadBytes'] = $ReadBytes }
371
+ $result = Compare-DiscoveryInputManifest @arguments
372
+ if ($result.Drift.Count -gt 0) {
373
+ $summary = @($result.Drift | ForEach-Object { "$($_.kind):$($_.path)" }) -join ', '
374
+ throw "INPUT_DRIFT: Inputs changed after the runtime result: $summary"
375
+ }
376
+ return $result.Manifest
377
+ }
378
+
379
+ Export-ModuleMember -Function `
380
+ ConvertFrom-DiscoveryInputManifest, `
381
+ Get-DiscoveryInputManifestDigest, `
382
+ Get-DiscoveryWorkspacePath, `
383
+ Compare-DiscoveryInputManifest, `
384
+ Assert-DiscoveryInputsFresh
@@ -0,0 +1,217 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Host observations and the sentinel decision of the native discovery PreToolUse adapters.
4
+
5
+ .DESCRIPTION
6
+ The capability probe cannot assume what a host places in its hook payloads.
7
+ Instead, a sentinel agent calls the sentinel MCP tool and runs one harmless
8
+ shell command, and the PreToolUse gate records what it received as a
9
+ `host-observation` under `sessions/<sha256(session id)>/observations/`: the
10
+ payload identity fields as received (`agent_id` and `agent_type` may be null),
11
+ whether a tool-call identifier was present, the decision and its code, and the
12
+ handler hash. The sentinel tool observation also carries the one-use sentinel
13
+ grant fields the MCP server consumes. The probe then proves attribution from
14
+ these records and reports `unsupported` otherwise.
15
+
16
+ Store access flows through injected seams; the module never invokes a process.
17
+
18
+ .NOTES
19
+ Issue #980. Compatible with PowerShell 7+. Depends only on the sibling
20
+ DiscoveryEncoding, DiscoveryAuthority and DiscoveryToolRouting modules.
21
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
22
+ #>
23
+
24
+ Set-StrictMode -Version Latest
25
+ $ErrorActionPreference = 'Stop'
26
+
27
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryEncoding.psm1') -ErrorAction Stop
28
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryAuthority.psm1') -ErrorAction Stop
29
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryToolRouting.psm1') -ErrorAction Stop
30
+
31
+ $script:ObservationSchema = 'urn:drm-copilot:discovery:2.0.0:host-observation'
32
+ $script:ObservationPartition = 'observations'
33
+ $script:SentinelAgentType = 'discovery-capability-sentinel'
34
+ $script:SentinelToolName = 'discovery_capability_sentinel'
35
+ $script:SentinelOperation = 'sentinel'
36
+ $script:SentinelCommandPrefix = 'drm-copilot discovery capability probe '
37
+ $script:CorrelationIdPattern = '^[A-Za-z0-9][A-Za-z0-9._:-]*$'
38
+ # Schema timestamp spelling for observed_at and expires_at.
39
+ $script:TimestampFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
40
+
41
+ function Get-DiscoverySentinelCommandPrefix {
42
+ <#
43
+ .SYNOPSIS
44
+ Returns the prefix of the harmless sentinel shell command the gate must deny.
45
+ #>
46
+ [CmdletBinding()]
47
+ [OutputType([string])]
48
+ param()
49
+
50
+ return $script:SentinelCommandPrefix
51
+ }
52
+
53
+ function Get-DiscoveryObservationDirectory {
54
+ <#
55
+ .SYNOPSIS
56
+ Returns `<session>/observations` for SessionId under the resolved location.
57
+ #>
58
+ [CmdletBinding()]
59
+ [OutputType([string])]
60
+ param(
61
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
62
+ [Parameter(Mandatory = $true)][string]$SessionId
63
+ )
64
+
65
+ return "$(Get-DiscoverySessionDirectory -Location $Location -SessionId $SessionId)/$($script:ObservationPartition)"
66
+ }
67
+
68
+ function Test-DiscoverySentinelEnvelope {
69
+ <#
70
+ .SYNOPSIS
71
+ True when the envelope comes from the sentinel agent type, names the sentinel tool, or runs the sentinel command.
72
+ #>
73
+ [CmdletBinding()]
74
+ [OutputType([bool])]
75
+ param(
76
+ [Parameter(Mandatory = $true)][pscustomobject]$Normalized,
77
+ [Parameter(Mandatory = $true)][pscustomobject]$Tool
78
+ )
79
+
80
+ if ($Normalized.AgentType -is [string] -and $Normalized.AgentType -ceq $script:SentinelAgentType) { return $true }
81
+ if ($Tool.Family -eq 'mcp-discovery' -and $Tool.Name -ceq $script:SentinelToolName) { return $true }
82
+ if ($Tool.Family -eq 'shell' -and $null -ne $Normalized.ToolInput) {
83
+ $command = Get-DiscoveryInputMember -ToolInput $Normalized.ToolInput -Name 'command'
84
+ if ($command -is [string] -and $command.StartsWith($script:SentinelCommandPrefix, [System.StringComparison]::Ordinal)) { return $true }
85
+ }
86
+ return $false
87
+ }
88
+
89
+ function ConvertTo-DiscoveryHostObservationRecord {
90
+ <#
91
+ .SYNOPSIS
92
+ Builds one `host-observation` record from the payload as received and the gate's decision.
93
+ .DESCRIPTION
94
+ The reference is derived from the host's tool-call identifier when one is
95
+ present (one record per tool call), else from the nonce. When
96
+ SentinelInputSha256 is supplied the record also carries the one-use sentinel
97
+ grant fields (tool, input digest, expiry, state `issued`).
98
+ #>
99
+ [CmdletBinding()]
100
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
101
+ param(
102
+ [Parameter(Mandatory = $true)][pscustomobject]$Normalized,
103
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
104
+ [Parameter(Mandatory = $true)][string]$ToolName,
105
+ [Parameter(Mandatory = $true)][ValidateSet('allow', 'deny')][string]$Decision,
106
+ [Parameter(Mandatory = $true)][string]$Code,
107
+ [Parameter(Mandatory = $true)][string]$HandlerSha256,
108
+ [Parameter(Mandatory = $true)][datetime]$Now,
109
+ [Parameter(Mandatory = $true)][string]$Nonce,
110
+ [Parameter(Mandatory = $false)][AllowNull()][string]$SentinelInputSha256 = $null,
111
+ [Parameter(Mandatory = $false)][int]$TtlSeconds = 300
112
+ )
113
+
114
+ $toolUseId = $(if ($Normalized.ToolUseId -is [string] -and $Normalized.ToolUseId -cmatch $script:CorrelationIdPattern) { [string]$Normalized.ToolUseId } else { $null })
115
+ $suffix = $(if ($null -ne $toolUseId) { (Get-DiscoverySha256Hex -Text $toolUseId).Substring(0, 20) } else { $Nonce })
116
+ $record = [ordered]@{
117
+ '$schema' = $script:ObservationSchema
118
+ schema_version = '2.0.0'
119
+ artifact_kind = 'host-observation'
120
+ id = "observation-$suffix"
121
+ observation_ref = "observation-$suffix"
122
+ host = $Principal.HostName
123
+ session_id = $Principal.SessionId
124
+ agent_id = $(if ($Normalized.AgentId -is [string] -and $Normalized.AgentId -cmatch $script:CorrelationIdPattern) { [string]$Normalized.AgentId } else { $null })
125
+ agent_type = $(if ($Normalized.AgentType -is [string] -and $Normalized.AgentType -cmatch $script:CorrelationIdPattern) { [string]$Normalized.AgentType } else { $null })
126
+ parent_agent_id = $Principal.ParentAgentId
127
+ tool_name = $ToolName
128
+ tool_use_id_present = ($null -ne $toolUseId)
129
+ decision = $Decision
130
+ code = $Code
131
+ handler_sha256 = $HandlerSha256
132
+ observed_at = $Now.ToUniversalTime().ToString($script:TimestampFormat, [System.Globalization.CultureInfo]::InvariantCulture)
133
+ }
134
+ if (-not [string]::IsNullOrEmpty($SentinelInputSha256)) {
135
+ $record['tool'] = $script:SentinelToolName
136
+ $record['input_sha256'] = $SentinelInputSha256
137
+ $record['expires_at'] = $Now.ToUniversalTime().AddSeconds($TtlSeconds).ToString($script:TimestampFormat, [System.Globalization.CultureInfo]::InvariantCulture)
138
+ $record['state'] = 'issued'
139
+ }
140
+ return $record
141
+ }
142
+
143
+ function Write-DiscoveryHostObservation {
144
+ <#
145
+ .SYNOPSIS
146
+ Writes one observation exclusively under `<session>/observations` and returns its path.
147
+ #>
148
+ [CmdletBinding(SupportsShouldProcess = $true)]
149
+ [OutputType([string])]
150
+ param(
151
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
152
+ [Parameter(Mandatory = $true)][System.Collections.Specialized.OrderedDictionary]$Record,
153
+ [Parameter(Mandatory = $true)][hashtable]$Store
154
+ )
155
+
156
+ $directory = Get-DiscoveryObservationDirectory -Location $Location -SessionId ([string]$Record['session_id'])
157
+ $path = "$directory/$($Record['observation_ref']).json"
158
+ if (-not $PSCmdlet.ShouldProcess($path, 'write host observation')) { return $path }
159
+ Invoke-DiscoveryStoreSeam -Seam $Store.EnsureDirectory -Arguments @($directory)
160
+ Invoke-DiscoveryStoreSeam -Seam $Store.WriteText -Arguments @($path, (($Record | ConvertTo-Json -Depth 8) + "`n"))
161
+ return $path
162
+ }
163
+
164
+ function Get-DiscoverySentinelDecision {
165
+ <#
166
+ .SYNOPSIS
167
+ Decides a sentinel envelope: deny the sentinel shell command, allow the sentinel tool with a grant, deny anything else.
168
+ .DESCRIPTION
169
+ Every branch records an observation; the sentinel tool observation carries the
170
+ one-use grant fields. A failed observation write denies with STORE so no
171
+ unobserved sentinel call proceeds. No root grant and no tool authorization is
172
+ ever written for a sentinel envelope.
173
+ .OUTPUTS
174
+ [pscustomobject] Decision, Code, Reason, Record, RecordPath.
175
+ #>
176
+ [CmdletBinding()]
177
+ [OutputType([pscustomobject])]
178
+ param(
179
+ [Parameter(Mandatory = $true)][pscustomobject]$Normalized,
180
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
181
+ [Parameter(Mandatory = $true)][pscustomobject]$Tool,
182
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
183
+ [Parameter(Mandatory = $true)][hashtable]$Store,
184
+ [Parameter(Mandatory = $true)][string]$HandlerSha256,
185
+ [Parameter(Mandatory = $true)][datetime]$Now,
186
+ [Parameter(Mandatory = $true)][string]$Nonce,
187
+ [Parameter(Mandatory = $false)][int]$TtlSeconds = 300
188
+ )
189
+
190
+ $toolName = [string]$Normalized.ToolName
191
+ $digest = $null
192
+ # Routing: the shell sentinel is denied, the sentinel tool is allowed with a grant, anything else is denied.
193
+ if ($Tool.Family -eq 'shell') {
194
+ $decision = 'deny'; $code = 'SHELL'; $reason = "the sentinel shell command is denied; shell tools are not available to $($script:SentinelAgentType)"
195
+ } elseif ($Tool.Family -eq 'mcp-discovery' -and $Tool.Name -ceq $script:SentinelToolName) {
196
+ $decision = 'allow'; $code = 'SENTINEL'; $reason = 'the sentinel tool call is observed and granted once'
197
+ $digest = Get-DiscoveryInputDigest -Tool $script:SentinelToolName -Operation $script:SentinelOperation -Inputs $Normalized.ToolInput
198
+ } else {
199
+ $decision = 'deny'; $code = 'UNKNOWN_TOOL'; $reason = "tool $toolName is outside the sentinel grant"
200
+ }
201
+ $record = ConvertTo-DiscoveryHostObservationRecord -Normalized $Normalized -Principal $Principal -ToolName $toolName -Decision $decision -Code $code `
202
+ -HandlerSha256 $HandlerSha256 -Now $Now -Nonce $Nonce -SentinelInputSha256 $digest -TtlSeconds $TtlSeconds
203
+ try {
204
+ $path = Write-DiscoveryHostObservation -Location $Location -Record $record -Store $Store
205
+ } catch {
206
+ return [pscustomobject]@{ Decision = 'deny'; Code = 'STORE'; Reason = "the host observation could not be recorded: $($_.Exception.Message)"; Record = $null; RecordPath = $null }
207
+ }
208
+ return [pscustomobject]@{ Decision = $decision; Code = $code; Reason = $reason; Record = $record; RecordPath = $path }
209
+ }
210
+
211
+ Export-ModuleMember -Function `
212
+ Get-DiscoverySentinelCommandPrefix, `
213
+ Get-DiscoveryObservationDirectory, `
214
+ Test-DiscoverySentinelEnvelope, `
215
+ ConvertTo-DiscoveryHostObservationRecord, `
216
+ Write-DiscoveryHostObservation, `
217
+ Get-DiscoverySentinelDecision