@danmoisan/drm-copilot-mcp 1.1.23 → 1.1.24

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,164 @@
1
+ <#
2
+ .SYNOPSIS
3
+ One-use root grants written by the native discovery PreToolUse adapters for root calls.
4
+
5
+ .DESCRIPTION
6
+ When the PreToolUse payload carries no agent identity, the caller is the root
7
+ of its session. For `prepare_discovery_agent`, `run_discovery_doctor` and every
8
+ controlled discovery tool called by the root, the gate writes one short-lived,
9
+ one-use `root-grant` record under `<authority root>/root-grants/`, bound to the
10
+ tool, the derived operation and the request digest. The MCP server consumes the
11
+ grant by tool, operation and digest and takes the host, session and parent from
12
+ it; nothing in tool input selects or supplies those values.
13
+
14
+ A root grant for prepare and for a controlled tool is written only when the
15
+ session's protected capability attestation is `supported` and unexpired, which
16
+ is the same reading the start decision performs. The doctor grant has no
17
+ precondition, because the probe is what produces that attestation.
18
+
19
+ Store access flows through injected seams; the module never invokes a process.
20
+
21
+ .NOTES
22
+ Issue #980. Compatible with PowerShell 7+. Depends only on the sibling
23
+ DiscoveryEncoding, DiscoveryAuthority and DiscoveryStartRecord modules.
24
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
25
+ #>
26
+
27
+ Set-StrictMode -Version Latest
28
+ $ErrorActionPreference = 'Stop'
29
+
30
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryEncoding.psm1') -ErrorAction Stop
31
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryAuthority.psm1') -ErrorAction Stop
32
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryStartRecord.psm1') -ErrorAction Stop
33
+
34
+ $script:RootGrantSchema = 'urn:drm-copilot:discovery:2.0.0:root-grant'
35
+ $script:RootGrantPartition = 'root-grants'
36
+ $script:CapabilityAttestationFile = 'capability-attestation.json'
37
+ # Schema timestamp spelling for issued_at and expires_at.
38
+ $script:TimestampFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
39
+
40
+ function Get-DiscoveryRootGrantDirectory {
41
+ <#
42
+ .SYNOPSIS
43
+ Returns `<authority root>/root-grants` for the resolved workspace location.
44
+ #>
45
+ [CmdletBinding()]
46
+ [OutputType([string])]
47
+ param([Parameter(Mandatory = $true)][pscustomobject]$Location)
48
+
49
+ return "$($Location.Directory)/$($script:RootGrantPartition)"
50
+ }
51
+
52
+ function ConvertTo-DiscoveryRootGrantRecord {
53
+ <#
54
+ .SYNOPSIS
55
+ Builds one `root-grant` record bound to the root principal, tool, operation and request digest.
56
+ .DESCRIPTION
57
+ State is always `issued`; the server consumes it exactly once. `issued_at`
58
+ and `expires_at` come from the injected instant and TTL, never from the
59
+ request. The record is state in the protected store; no file is changed here.
60
+ #>
61
+ [CmdletBinding()]
62
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
63
+ param(
64
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
65
+ [Parameter(Mandatory = $true)][string]$Tool,
66
+ [Parameter(Mandatory = $true)][string]$Operation,
67
+ [Parameter(Mandatory = $true)][string]$InputSha256,
68
+ [Parameter(Mandatory = $false)][AllowNull()][AllowEmptyString()][string]$ToolUseId = $null,
69
+ [Parameter(Mandatory = $true)][string]$HandlerSha256,
70
+ [Parameter(Mandatory = $true)][datetime]$Now,
71
+ [Parameter(Mandatory = $true)][string]$Nonce,
72
+ [Parameter(Mandatory = $false)][int]$TtlSeconds = 300
73
+ )
74
+
75
+ $toolUse = $(if ([string]::IsNullOrWhiteSpace($ToolUseId)) { $null } else { $ToolUseId })
76
+ return [ordered]@{
77
+ '$schema' = $script:RootGrantSchema
78
+ schema_version = '2.0.0'
79
+ artifact_kind = 'root-grant'
80
+ id = "grant-$Nonce"
81
+ grant_ref = "grant-$Nonce"
82
+ host = $Principal.HostName
83
+ session_id = $Principal.SessionId
84
+ parent_agent_id = $Principal.ParentAgentId
85
+ tool = $Tool
86
+ operation = $Operation
87
+ input_sha256 = $InputSha256
88
+ tool_use_id = $toolUse
89
+ handler_sha256 = $HandlerSha256
90
+ issued_at = $Now.ToUniversalTime().ToString($script:TimestampFormat, [System.Globalization.CultureInfo]::InvariantCulture)
91
+ expires_at = $Now.ToUniversalTime().AddSeconds($TtlSeconds).ToString($script:TimestampFormat, [System.Globalization.CultureInfo]::InvariantCulture)
92
+ state = 'issued'
93
+ }
94
+ }
95
+
96
+ function Test-DiscoveryRootGrantPrecondition {
97
+ <#
98
+ .SYNOPSIS
99
+ Returns the deficiency that forbids a root grant, or $null when the session capability is current.
100
+ .DESCRIPTION
101
+ Reads `<session>/capability-attestation.json` through the store seam, as the
102
+ start decision does, and requires a `capability-attestation` record whose
103
+ status is `supported`, whose expiry is after Now, and whose host, session and
104
+ workspace equal the principal's and the resolved location's.
105
+ #>
106
+ [CmdletBinding()]
107
+ [OutputType([string])]
108
+ param(
109
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
110
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
111
+ [Parameter(Mandatory = $true)][hashtable]$Store,
112
+ [Parameter(Mandatory = $true)][datetime]$Now
113
+ )
114
+
115
+ $path = "$(Get-DiscoverySessionDirectory -Location $Location -SessionId $Principal.SessionId)/$($script:CapabilityAttestationFile)"
116
+ $text = Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @($path)
117
+ if ($null -eq $text) { return 'the session has no readable protected capability attestation' }
118
+ try { $capability = ConvertFrom-Json -InputObject ([string]$text) -Depth 16 } catch { return 'the session capability attestation is not readable JSON' }
119
+ if ($capability -isnot [System.Management.Automation.PSCustomObject]) { return 'the session capability attestation is not an object' }
120
+ $names = @($capability.PSObject.Properties.Name)
121
+ foreach ($required in @('artifact_kind', 'status', 'host', 'session_id', 'workspace', 'expires_at')) {
122
+ if ($names -notcontains $required) { return "the capability attestation lacks '$required'" }
123
+ }
124
+ $expires = ConvertTo-DiscoveryUtcInstant -Value $capability.expires_at
125
+ # A grant needs a current supported attestation bound to this host, session and workspace.
126
+ if ($capability.artifact_kind -ne 'capability-attestation' -or $capability.status -ne 'supported' -or $null -eq $expires -or $expires -le $Now.ToUniversalTime()) {
127
+ return 'the capability attestation is not a current supported attestation'
128
+ }
129
+ if ($capability.host -ne $Principal.HostName -or $capability.session_id -ne $Principal.SessionId -or [string]$capability.workspace.workspace_sha256 -ne $Location.WorkspaceSha256) {
130
+ return 'the capability attestation is bound to another host, session or workspace'
131
+ }
132
+ return $null
133
+ }
134
+
135
+ function Write-DiscoveryRootGrant {
136
+ <#
137
+ .SYNOPSIS
138
+ Writes one root-grant record exclusively under `root-grants/` and returns its path.
139
+ .DESCRIPTION
140
+ The directory is created when absent and the record is written through the
141
+ exclusive WriteText seam, so a second writer for the same reference fails
142
+ instead of replacing a record. The seam's error propagates to the caller.
143
+ #>
144
+ [CmdletBinding(SupportsShouldProcess = $true)]
145
+ [OutputType([string])]
146
+ param(
147
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
148
+ [Parameter(Mandatory = $true)][System.Collections.Specialized.OrderedDictionary]$Record,
149
+ [Parameter(Mandatory = $true)][hashtable]$Store
150
+ )
151
+
152
+ $directory = Get-DiscoveryRootGrantDirectory -Location $Location
153
+ $path = "$directory/$($Record['grant_ref']).json"
154
+ if (-not $PSCmdlet.ShouldProcess($path, 'write root grant')) { return $path }
155
+ Invoke-DiscoveryStoreSeam -Seam $Store.EnsureDirectory -Arguments @($directory)
156
+ Invoke-DiscoveryStoreSeam -Seam $Store.WriteText -Arguments @($path, (($Record | ConvertTo-Json -Depth 8) + "`n"))
157
+ return $path
158
+ }
159
+
160
+ Export-ModuleMember -Function `
161
+ Get-DiscoveryRootGrantDirectory, `
162
+ ConvertTo-DiscoveryRootGrantRecord, `
163
+ Test-DiscoveryRootGrantPrecondition, `
164
+ Write-DiscoveryRootGrant
@@ -0,0 +1,440 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Native SubagentStart attestation decisions for discovery specialists.
4
+
5
+ .DESCRIPTION
6
+ Every native discovery SubagentStart adapter (Claude Code, Codex, GitHub
7
+ Copilot) takes its decision from this module. A start event whose agent type
8
+ is not a discovery specialist is skipped, so ordinary and epic starts keep
9
+ their existing attestation path. For a discovery specialist the module
10
+ resolves the explicit `discovery-invocation` receipt through the protected
11
+ store and the trusted parent launch context: the attested host, session and
12
+ parent must match the record, the activation must be active and bound to the
13
+ same workspace and profile, exactly one prepared invocation must name the
14
+ launched agent type, the actual role, model, effort and agent-profile hash
15
+ must equal the deployment the root prepared, the runtime pins must equal the
16
+ session's supported capability attestation, and a specialist may not launch
17
+ another specialist. Only then is the `start-attestation` recorded and the
18
+ invocation moved to `started`; no PreToolUse grant can bind before that.
19
+
20
+ Nothing in the start payload can supply identity or authority beyond the agent
21
+ identifier the host assigned; orchestrator checkpoints are never read or
22
+ written. Store access flows through injected seams; no process is invoked.
23
+
24
+ .NOTES
25
+ Issue #980. Compatible with PowerShell 7+. Depends only on the sibling
26
+ DiscoveryEncoding, DiscoveryAuthority, DiscoveryPayload and DiscoveryStartRecord modules.
27
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
28
+ #>
29
+
30
+ Set-StrictMode -Version Latest
31
+ $ErrorActionPreference = 'Stop'
32
+
33
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryEncoding.psm1') -ErrorAction Stop
34
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryAuthority.psm1') -ErrorAction Stop
35
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryPayload.psm1') -ErrorAction Stop
36
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryStartRecord.psm1') -ErrorAction Stop
37
+
38
+ $script:CapabilityAttestationFile = 'capability-attestation.json'
39
+ $script:RuntimeKeys = @('build_sha256', 'package_version', 'policy_sha256', 'policy_version', 'schema_version')
40
+ $script:CorrelationIdPattern = '^[A-Za-z0-9][A-Za-z0-9._:-]*$'
41
+
42
+ function Get-DiscoveryStartOutcome {
43
+ <#
44
+ .SYNOPSIS
45
+ Builds one start outcome (skip, deny or attest) with its code and reason.
46
+ #>
47
+ [CmdletBinding()]
48
+ [OutputType([pscustomobject])]
49
+ param(
50
+ [Parameter(Mandatory = $true)][ValidateSet('skip', 'deny', 'observe', 'attest')][string]$Decision,
51
+ [Parameter(Mandatory = $true)][string]$Code,
52
+ [Parameter(Mandatory = $true)][string]$Reason,
53
+ [Parameter(Mandatory = $false)][AllowNull()][object]$Attestation = $null,
54
+ [Parameter(Mandatory = $false)][AllowNull()][string]$AttestationPath = $null,
55
+ [Parameter(Mandatory = $false)][AllowNull()][object]$Invocation = $null,
56
+ [Parameter(Mandatory = $false)][AllowNull()][string]$Directory = $null
57
+ )
58
+
59
+ return [pscustomobject]@{ Decision = $Decision; Code = $Code; Reason = $Reason; Attestation = $Attestation; AttestationPath = $AttestationPath; Invocation = $Invocation; Directory = $Directory }
60
+ }
61
+
62
+ function Start-DiscoveryPreparedInvocation {
63
+ <#
64
+ .SYNOPSIS
65
+ Binds one agent to the single prepared invocation of its session, parent and agent type, and starts it.
66
+ .DESCRIPTION
67
+ Shared by the SubagentStart adapters and by the PreToolUse gate's first
68
+ controlled call. Exactly one prepared invocation must match (zero:
69
+ NO_INVOCATION; several: AMBIGUOUS); the record, deployment, agent profile
70
+ and capability must agree; then the start attestation is written exclusively
71
+ and the invocation record moves to `started`. The agent profile is read
72
+ through the Store's `ReadAgentProfile` seam when present (hosts with
73
+ non-Markdown profiles), else through ReadBytes under WorkspaceRoot.
74
+ .OUTPUTS
75
+ [pscustomobject] Decision (deny or attest), Code, Reason, Attestation,
76
+ AttestationPath, Invocation (the started record) and Directory.
77
+ #>
78
+ [CmdletBinding(SupportsShouldProcess = $true)]
79
+ [OutputType([pscustomobject])]
80
+ param(
81
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
82
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
83
+ [Parameter(Mandatory = $true)][string]$AgentId,
84
+ [Parameter(Mandatory = $true)][string]$AgentType,
85
+ [Parameter(Mandatory = $true)][pscustomobject]$Activation,
86
+ [Parameter(Mandatory = $true)][string]$WorkspaceRoot,
87
+ [Parameter(Mandatory = $true)][hashtable]$Store,
88
+ [Parameter(Mandatory = $true)][string]$HandlerSha256,
89
+ [Parameter(Mandatory = $true)][datetime]$Now,
90
+ [Parameter(Mandatory = $true)][string]$Nonce,
91
+ [Parameter(Mandatory = $false)][AllowNull()][pscustomobject]$AgentProfile = $null,
92
+ [Parameter(Mandatory = $false)][AllowNull()][string]$ActualModel = $null,
93
+ [Parameter(Mandatory = $false)][AllowNull()][string]$ActualReasoningEffort = $null,
94
+ [Parameter(Mandatory = $false)][AllowNull()][scriptblock]$ReadBytes = $null
95
+ )
96
+
97
+ $found = Find-DiscoveryPreparedInvocation -Location $Location -Principal $Principal -AgentType $AgentType -Store $Store
98
+ if ($found.Count -eq 0) {
99
+ return Get-DiscoveryStartOutcome -Decision 'deny' -Code 'NO_INVOCATION' -Reason "no prepared protected invocation binds agent type $AgentType to parent $($Principal.ParentAgentId) in this session"
100
+ }
101
+ if ($found.Count -gt 1) {
102
+ return Get-DiscoveryStartOutcome -Decision 'deny' -Code 'AMBIGUOUS' -Reason "$($found.Count) prepared invocations bind agent type $AgentType in this session; the start cannot be attributed to one"
103
+ }
104
+ $record = $found.Record
105
+ if ($null -eq $AgentProfile) {
106
+ try {
107
+ # Profile source order: the host's own reader seam, else the Markdown reader over ReadBytes.
108
+ if ($Store.ContainsKey('ReadAgentProfile')) {
109
+ $AgentProfile = Invoke-DiscoveryStoreSeam -Seam $Store.ReadAgentProfile -Arguments @($Principal.HostName, $AgentType, $WorkspaceRoot)
110
+ } else {
111
+ $readParameters = @{ HostName = $Principal.HostName; AgentType = $AgentType; WorkspaceRoot = $WorkspaceRoot }
112
+ if ($null -ne $ReadBytes) { $readParameters['ReadBytes'] = $ReadBytes }
113
+ elseif ($Store.ContainsKey('ReadBytes')) { $readParameters['ReadBytes'] = $Store.ReadBytes }
114
+ $AgentProfile = Read-DiscoveryAgentProfile @readParameters
115
+ }
116
+ } catch {
117
+ return Get-DiscoveryStartOutcome -Decision 'deny' -Code 'PROFILE' -Reason "the agent profile could not be read: $($_.Exception.Message)"
118
+ }
119
+ }
120
+ $mismatch = Get-DiscoveryDeploymentMismatch -Record $record -Activation $Activation -Location $Location -AgentType $AgentType `
121
+ -AgentProfile $AgentProfile -ActualModel $ActualModel -ActualReasoningEffort $ActualReasoningEffort
122
+ if ($null -ne $mismatch) { return Get-DiscoveryStartOutcome -Decision 'deny' -Code $mismatch.Code -Reason $mismatch.Reason }
123
+ $mismatch = Get-DiscoveryCapabilityMismatch -Location $Location -Principal $Principal -Record $record -Store $Store -Now $Now
124
+ if ($null -ne $mismatch) { return Get-DiscoveryStartOutcome -Decision 'deny' -Code $mismatch.Code -Reason $mismatch.Reason }
125
+ $attestation = ConvertTo-DiscoveryStartAttestationRecord -Principal $Principal -AgentId $AgentId -AgentType $AgentType -Record $record -AgentProfile $AgentProfile `
126
+ -ActualModel $ActualModel -ActualReasoningEffort $ActualReasoningEffort -HandlerSha256 $HandlerSha256 -Now $Now -Nonce $Nonce
127
+ $path = "$($found.Directory)/$(Get-DiscoveryStartAttestationFileName)"
128
+ if (-not $PSCmdlet.ShouldProcess($path, 'write start attestation')) {
129
+ return Get-DiscoveryStartOutcome -Decision 'deny' -Code 'STORE' -Reason 'the start attestation write was not confirmed'
130
+ }
131
+ try {
132
+ # The attestation is the exclusive latch; the record transition follows it.
133
+ Invoke-DiscoveryStoreSeam -Seam $Store.WriteText -Arguments @($path, (($attestation | ConvertTo-Json -Depth 8) + "`n"))
134
+ $startedText = ConvertTo-DiscoveryStartedInvocationText -Text $found.Text -AgentId $AgentId -StartAttestationRef ([string]$attestation['start_attestation_ref'])
135
+ Invoke-DiscoveryStoreSeam -Seam $Store.ReplaceText -Arguments @("$($found.Directory)/invocation.json", $startedText)
136
+ } catch {
137
+ return Get-DiscoveryStartOutcome -Decision 'deny' -Code 'STORE' -Reason "the start attestation could not be recorded: $($_.Exception.Message)"
138
+ }
139
+ $started = ConvertFrom-Json -InputObject $startedText -Depth 16
140
+ return Get-DiscoveryStartOutcome -Decision 'attest' -Code 'ATTESTED' -Reason "attested $AgentType as $($record.role) for invocation $($record.invocation_id)" `
141
+ -Attestation $attestation -AttestationPath $path -Invocation $started -Directory $found.Directory
142
+ }
143
+
144
+ function Read-DiscoveryStoreJson {
145
+ <#
146
+ .SYNOPSIS
147
+ Reads one JSON object from the store, returning $null for a missing or unreadable record.
148
+ #>
149
+ [CmdletBinding()]
150
+ [OutputType([pscustomobject])]
151
+ param(
152
+ [Parameter(Mandatory = $true)][hashtable]$Store,
153
+ [Parameter(Mandatory = $true)][string]$Path
154
+ )
155
+
156
+ $text = Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @($Path)
157
+ if ($null -eq $text) { return $null }
158
+ try { $record = ConvertFrom-Json -InputObject ([string]$text) -Depth 16 } catch { return $null }
159
+ if ($record -isnot [System.Management.Automation.PSCustomObject]) { return $null }
160
+ return $record
161
+ }
162
+
163
+ function Find-DiscoveryPreparedInvocation {
164
+ <#
165
+ .SYNOPSIS
166
+ Finds the prepared discovery invocations the root bound to AgentType in the attested session.
167
+ .DESCRIPTION
168
+ Enumerates `<session>/invocations/*/invocation.json` and keeps every
169
+ `discovery-invocation` record whose host, session and parent equal the
170
+ principal's, whose state is `prepared` and whose deployment names AgentType.
171
+ An unreadable record is skipped so a corrupt neighbour cannot admit a start.
172
+ .OUTPUTS
173
+ [pscustomobject] Count, Record, Directory, Text (the single match, or $null members).
174
+ #>
175
+ [CmdletBinding()]
176
+ [OutputType([pscustomobject])]
177
+ param(
178
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
179
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
180
+ [Parameter(Mandatory = $true)][string]$AgentType,
181
+ [Parameter(Mandatory = $true)][hashtable]$Store
182
+ )
183
+
184
+ $directory = "$(Get-DiscoverySessionDirectory -Location $Location -SessionId $Principal.SessionId)/invocations"
185
+ $bindings = [System.Collections.Generic.List[object]]::new()
186
+ foreach ($entry in @(Invoke-DiscoveryStoreSeam -Seam $Store.ListEntries -Arguments @($directory))) {
187
+ $path = "$directory/$entry/invocation.json"
188
+ $text = Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @($path)
189
+ if ($null -eq $text) { continue }
190
+ try { $record = ConvertFrom-Json -InputObject ([string]$text) -Depth 16 } catch { continue }
191
+ if ($record -isnot [System.Management.Automation.PSCustomObject]) { continue }
192
+ $names = @($record.PSObject.Properties.Name)
193
+ $missing = @(@('artifact_kind', 'host', 'session_id', 'parent_agent_id', 'state', 'deployment') | Where-Object { $names -notcontains $_ })
194
+ if ($missing.Count -gt 0 -or $record.deployment -isnot [System.Management.Automation.PSCustomObject]) { continue }
195
+ $bound = ($record.artifact_kind -eq 'discovery-invocation') -and ($record.host -eq $Principal.HostName) -and
196
+ ($record.session_id -eq $Principal.SessionId) -and ($record.parent_agent_id -eq $Principal.ParentAgentId) -and
197
+ ($record.state -eq 'prepared') -and ([string]$record.deployment.agent_name -ceq $AgentType)
198
+ if ($bound) { $bindings.Add([pscustomobject]@{ Record = $record; Directory = "$directory/$entry"; Text = [string]$text }) }
199
+ }
200
+ if ($bindings.Count -ne 1) { return [pscustomobject]@{ Count = $bindings.Count; Record = $null; Directory = $null; Text = $null } }
201
+ return [pscustomobject]@{ Count = 1; Record = $bindings[0].Record; Directory = $bindings[0].Directory; Text = $bindings[0].Text }
202
+ }
203
+
204
+ function Get-DiscoveryDeploymentMismatch {
205
+ <#
206
+ .SYNOPSIS
207
+ Compares the launched agent against the deployment and binding the root prepared.
208
+ .DESCRIPTION
209
+ Returns $null when every leg agrees, otherwise an object with Code (RECEIPT,
210
+ ROLE, MODEL, EFFORT, PROFILE or BINDING) and Reason. Model and effort are
211
+ compared only when the host exposes them; the agent profile name must always
212
+ equal the launched agent type and the invocation's workspace binding must
213
+ equal the current activation.
214
+ #>
215
+ [CmdletBinding()]
216
+ [OutputType([pscustomobject])]
217
+ param(
218
+ [Parameter(Mandatory = $true)][pscustomobject]$Record,
219
+ [Parameter(Mandatory = $true)][pscustomobject]$Activation,
220
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
221
+ [Parameter(Mandatory = $true)][string]$AgentType,
222
+ [Parameter(Mandatory = $true)][pscustomobject]$AgentProfile,
223
+ [Parameter(Mandatory = $false)][AllowNull()][string]$ActualModel,
224
+ [Parameter(Mandatory = $false)][AllowNull()][string]$ActualReasoningEffort
225
+ )
226
+
227
+ $names = @($Record.PSObject.Properties.Name)
228
+ foreach ($required in @('schema_version', 'invocation_id', 'role', 'workspace', 'runtime', 'capability_ref', 'production_write_count')) {
229
+ if ($names -notcontains $required) { return [pscustomobject]@{ Code = 'RECEIPT'; Reason = "the protected invocation record lacks '$required'" } }
230
+ }
231
+ if ([string]$Record.schema_version -ne '2.0.0' -or [string]$Record.invocation_id -notmatch $script:CorrelationIdPattern -or [int]$Record.production_write_count -ne 0) {
232
+ return [pscustomobject]@{ Code = 'RECEIPT'; Reason = 'the protected invocation record is not a schema 2.0.0 discovery-invocation with zero production writes' }
233
+ }
234
+ $role = Get-DiscoveryAgentRole -AgentType $AgentType
235
+ if ([string]$Record.role -cne $role) {
236
+ return [pscustomobject]@{ Code = 'ROLE'; Reason = "agent type $AgentType acts as $role but the invocation was prepared for role $($Record.role)" }
237
+ }
238
+ $deployment = $Record.deployment
239
+ # A [string] parameter renders an absent value as ''; an empty value means the host exposed none.
240
+ if (-not [string]::IsNullOrEmpty($ActualModel) -and $ActualModel -cne [string]$deployment.model) {
241
+ return [pscustomobject]@{ Code = 'MODEL'; Reason = "the agent started with model $ActualModel but the invocation was prepared for $($deployment.model)" }
242
+ }
243
+ if (-not [string]::IsNullOrEmpty($ActualReasoningEffort) -and $ActualReasoningEffort -cne [string]$deployment.reasoning_effort) {
244
+ return [pscustomobject]@{ Code = 'EFFORT'; Reason = "the agent profile declares reasoning effort $ActualReasoningEffort but the invocation was prepared for $($deployment.reasoning_effort)" }
245
+ }
246
+ if ([string]$AgentProfile.ProfileName -cne $AgentType -or [string]$AgentProfile.ProfileSha256 -notmatch '^[0-9a-f]{64}$') {
247
+ return [pscustomobject]@{ Code = 'PROFILE'; Reason = "the agent profile at $($AgentProfile.ProfilePath) does not declare the launched agent $AgentType" }
248
+ }
249
+ if (-not [string]::IsNullOrEmpty([string]$AgentProfile.ProfileModel) -and [string]::IsNullOrEmpty($ActualModel) -and [string]$AgentProfile.ProfileModel -cne [string]$deployment.model) {
250
+ return [pscustomobject]@{ Code = 'MODEL'; Reason = "the agent profile declares model $($AgentProfile.ProfileModel) but the invocation was prepared for $($deployment.model)" }
251
+ }
252
+ $sameBinding = ([string]$Record.workspace.workspace_sha256 -eq $Location.WorkspaceSha256) -and
253
+ ([string]$Record.workspace.profile_path -eq [string]$Activation.profile_path) -and
254
+ ([string]$Record.workspace.profile_sha256 -eq [string]$Activation.profile_sha256) -and
255
+ ([string]$Record.runtime.schema_version -eq [string]$Activation.schema_version)
256
+ if (-not $sameBinding) {
257
+ return [pscustomobject]@{ Code = 'BINDING'; Reason = 'the invocation is bound to another workspace, profile or schema than the current activation' }
258
+ }
259
+ return $null
260
+ }
261
+
262
+ function Get-DiscoveryCapabilityMismatch {
263
+ <#
264
+ .SYNOPSIS
265
+ Verifies the session's protected capability attestation against the invocation's pins.
266
+ .DESCRIPTION
267
+ The attestation referenced by the record must exist in the protected session
268
+ directory, be `supported`, unexpired, bound to the same host, session and
269
+ workspace, and carry exactly the runtime identity (package, schema, build,
270
+ policy) the invocation was prepared with. Returns $null or an object with Code
271
+ (CAPABILITY or RUNTIME) and Reason.
272
+ #>
273
+ [CmdletBinding()]
274
+ [OutputType([pscustomobject])]
275
+ param(
276
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
277
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
278
+ [Parameter(Mandatory = $true)][pscustomobject]$Record,
279
+ [Parameter(Mandatory = $true)][hashtable]$Store,
280
+ [Parameter(Mandatory = $true)][datetime]$Now
281
+ )
282
+
283
+ $path = "$(Get-DiscoverySessionDirectory -Location $Location -SessionId $Principal.SessionId)/$($script:CapabilityAttestationFile)"
284
+ $capability = Read-DiscoveryStoreJson -Store $Store -Path $path
285
+ if ($null -eq $capability) {
286
+ return [pscustomobject]@{ Code = 'CAPABILITY'; Reason = 'the session has no readable protected capability attestation' }
287
+ }
288
+ $names = @($capability.PSObject.Properties.Name)
289
+ foreach ($required in @('artifact_kind', 'attestation_ref', 'status', 'host', 'session_id', 'workspace', 'runtime', 'expires_at')) {
290
+ if ($names -notcontains $required) { return [pscustomobject]@{ Code = 'CAPABILITY'; Reason = "the capability attestation lacks '$required'" } }
291
+ }
292
+ $expires = ConvertTo-DiscoveryUtcInstant -Value $capability.expires_at
293
+ if ($capability.artifact_kind -ne 'capability-attestation' -or [string]$capability.attestation_ref -cne [string]$Record.capability_ref) {
294
+ return [pscustomobject]@{ Code = 'CAPABILITY'; Reason = "the capability attestation does not match the invocation reference $($Record.capability_ref)" }
295
+ }
296
+ if ($capability.status -ne 'supported' -or $null -eq $expires -or $expires -le $Now.ToUniversalTime()) {
297
+ return [pscustomobject]@{ Code = 'CAPABILITY'; Reason = 'the capability attestation is not a current supported attestation' }
298
+ }
299
+ if ($capability.host -ne $Principal.HostName -or $capability.session_id -ne $Principal.SessionId -or [string]$capability.workspace.workspace_sha256 -ne $Location.WorkspaceSha256) {
300
+ return [pscustomobject]@{ Code = 'CAPABILITY'; Reason = 'the capability attestation is bound to another host, session or workspace' }
301
+ }
302
+ $recordRuntime = $Record.runtime
303
+ $capabilityRuntime = $capability.runtime
304
+ foreach ($runtime in @($recordRuntime, $capabilityRuntime)) {
305
+ if ($runtime -isnot [System.Management.Automation.PSCustomObject]) { return [pscustomobject]@{ Code = 'RUNTIME'; Reason = 'a runtime identity is not an object' } }
306
+ $keys = @($runtime.PSObject.Properties.Name | Sort-Object)
307
+ if (($keys -join ',') -ne ($script:RuntimeKeys -join ',')) { return [pscustomobject]@{ Code = 'RUNTIME'; Reason = 'a runtime identity does not carry exactly the five pinned keys' } }
308
+ }
309
+ foreach ($key in $script:RuntimeKeys) {
310
+ if ([string]$recordRuntime.$key -cne [string]$capabilityRuntime.$key) {
311
+ return [pscustomobject]@{ Code = 'RUNTIME'; Reason = "runtime pin $key of the invocation differs from the supported capability" }
312
+ }
313
+ }
314
+ return $null
315
+ }
316
+
317
+ function Get-DiscoveryStartDecision {
318
+ <#
319
+ .SYNOPSIS
320
+ Decides one SubagentStart event for a native discovery adapter.
321
+ .DESCRIPTION
322
+ The start is observational: no host lets a SubagentStart hook prevent the
323
+ agent from running, so the decision records the attestation when it can and
324
+ reports why it could not otherwise; the PreToolUse gate completes the
325
+ binding at the first controlled call. Order: a non-discovery agent type is
326
+ skipped (the existing ordinary and epic attestation applies); an envelope
327
+ anomaly, a missing host agent identifier, an inactive activation, zero or
328
+ several prepared invocations for this host, session, parent and agent type,
329
+ or a record, deployment, agent-profile or capability disagreement yields
330
+ `observe` with the same codes the gate uses; otherwise the attestation is
331
+ written exclusively, the invocation record moves to `started` and the
332
+ decision is `attest`. The principal (host, session, parent, agent) comes
333
+ from the hook payload alone.
334
+ .PARAMETER AgentProfile
335
+ The host's agent profile reading (ProfilePath, ProfileSha256, ProfileName,
336
+ ProfileModel, ProfileReasoningEffort). When omitted the Markdown profile under
337
+ the event's workspace is read through ReadBytes.
338
+ .PARAMETER Store
339
+ Seams ReadText, ListEntries, WriteText, ReplaceText; defaults to the real filesystem.
340
+ .OUTPUTS
341
+ [pscustomobject] Decision (skip, observe or attest), Code, Reason, Attestation, AttestationPath, Invocation, Directory.
342
+ #>
343
+ [CmdletBinding()]
344
+ [OutputType([pscustomobject])]
345
+ param(
346
+ [Parameter(Mandatory = $true)][pscustomobject]$Normalized,
347
+ [Parameter(Mandatory = $true)][hashtable]$HostEnvironment,
348
+ [Parameter(Mandatory = $true)][string]$HandlerSha256,
349
+ [Parameter(Mandatory = $false)][hashtable]$Store = (Get-DiscoveryStartStore),
350
+ [Parameter(Mandatory = $false)][datetime]$Now = [datetime]::UtcNow,
351
+ [Parameter(Mandatory = $false)][string]$Nonce = (Get-DiscoveryAuthorizationNonceText),
352
+ [Parameter(Mandatory = $false)][AllowNull()][pscustomobject]$AgentProfile = $null,
353
+ [Parameter(Mandatory = $false)][AllowNull()][string]$ActualModel = $null,
354
+ [Parameter(Mandatory = $false)][AllowNull()][string]$ActualReasoningEffort = $null,
355
+ [Parameter(Mandatory = $false)][AllowNull()][scriptblock]$ReadBytes = $null
356
+ )
357
+
358
+ if ($null -ne $Normalized.Anomaly) {
359
+ return Get-DiscoveryStartOutcome -Decision 'observe' -Code 'ENVELOPE' -Reason ('payload anomaly - ' + (Get-DiscoveryPayloadAnomalyReason -Code $Normalized.Anomaly) + '. The start attestation cannot read this envelope.')
360
+ }
361
+ $agentType = [string]$Normalized.AgentType
362
+ $role = Get-DiscoveryAgentRole -AgentType $agentType
363
+ if ($null -eq $role) {
364
+ return Get-DiscoveryStartOutcome -Decision 'skip' -Code 'NOT_DISCOVERY_AGENT' -Reason 'the started agent is not a discovery specialist; the ordinary start attestation applies'
365
+ }
366
+ $principal = Get-DiscoveryTrustedPrincipal -Normalized $Normalized
367
+ if ($null -eq $principal.AgentId) {
368
+ return Get-DiscoveryStartOutcome -Decision 'observe' -Code 'IDENTITY' -Reason 'the native start event carries no agent identifier for the discovery specialist; the first controlled tool call binds the prepared invocation instead'
369
+ }
370
+ if ([string]::IsNullOrWhiteSpace([string]$Normalized.Cwd)) {
371
+ return Get-DiscoveryStartOutcome -Decision 'observe' -Code 'WORKSPACE' -Reason 'the native event carries no workspace directory'
372
+ }
373
+ try {
374
+ $location = Resolve-DiscoveryAuthorityLocation -WorkspaceRoot ([string]$Normalized.Cwd) -Platform $HostEnvironment.Platform `
375
+ -UserHome $HostEnvironment.UserHome -LocalAppData $HostEnvironment.LocalAppData -XdgStateHome $HostEnvironment.XdgStateHome
376
+ } catch {
377
+ return Get-DiscoveryStartOutcome -Decision 'observe' -Code 'WORKSPACE' -Reason $_.Exception.Message
378
+ }
379
+ $activation = Read-DiscoveryActivationState -Location $location -ReadText $Store.ReadText
380
+ if ($activation.Status -ne 'active') {
381
+ return Get-DiscoveryStartOutcome -Decision 'observe' -Code 'ACTIVATION' -Reason "discovery is not active for this workspace ($($activation.Status): $($activation.Reason))"
382
+ }
383
+ $started = Start-DiscoveryPreparedInvocation -Location $location -Principal $principal -AgentId $principal.AgentId -AgentType $agentType -Activation $activation.Record `
384
+ -WorkspaceRoot ([string]$Normalized.Cwd) -Store $Store -HandlerSha256 $HandlerSha256 -Now $Now -Nonce $Nonce `
385
+ -AgentProfile $AgentProfile -ActualModel $ActualModel -ActualReasoningEffort $ActualReasoningEffort -ReadBytes $ReadBytes
386
+ # The start hook cannot block a host; a binding that did not complete is observed with its code.
387
+ if ($started.Decision -eq 'deny') { $started.Decision = 'observe' }
388
+ return $started
389
+ }
390
+
391
+ function Get-DiscoveryAuthorizationNonceText {
392
+ <#
393
+ .SYNOPSIS
394
+ Returns 20 lowercase hex characters from the platform random generator.
395
+ #>
396
+ [CmdletBinding()]
397
+ [OutputType([string])]
398
+ param()
399
+
400
+ $bytes = [byte[]]::new(10)
401
+ [System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
402
+ return [Convert]::ToHexString($bytes).ToLowerInvariant()
403
+ }
404
+
405
+ function ConvertTo-DiscoveryStartHookOutput {
406
+ <#
407
+ .SYNOPSIS
408
+ Renders a start outcome as the SubagentStart hook output, or $null when the hook stays silent.
409
+ .DESCRIPTION
410
+ A skipped or attested start produces no output. An observed start carries
411
+ the reason as a system message with the attestation prefix and tells the
412
+ agent that its first controlled discovery call binds the prepared
413
+ invocation; the output never carries a blocking field, because no host lets
414
+ a SubagentStart hook prevent the agent from running.
415
+ #>
416
+ [CmdletBinding()]
417
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
418
+ param(
419
+ [Parameter(Mandatory = $true)][pscustomobject]$Outcome,
420
+ [Parameter(Mandatory = $true)][string]$ReasonPrefix
421
+ )
422
+
423
+ if ($Outcome.Decision -ne 'observe') { return $null }
424
+ return [ordered]@{
425
+ systemMessage = "$ReasonPrefix $($Outcome.Reason)"
426
+ hookSpecificOutput = [ordered]@{
427
+ hookEventName = 'SubagentStart'
428
+ additionalContext = "$ReasonPrefix the start was not attested at launch. The first controlled discovery tool call binds the prepared invocation; every other tool is denied until then."
429
+ }
430
+ }
431
+ }
432
+
433
+ Export-ModuleMember -Function `
434
+ Find-DiscoveryPreparedInvocation, `
435
+ Get-DiscoveryDeploymentMismatch, `
436
+ Get-DiscoveryCapabilityMismatch, `
437
+ Start-DiscoveryPreparedInvocation, `
438
+ Get-DiscoveryStartDecision, `
439
+ Get-DiscoveryAuthorizationNonceText, `
440
+ ConvertTo-DiscoveryStartHookOutput