@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.
- package/out/mcp-server.js +13347 -7926
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/discovery-capability-sentinel.md +34 -0
- package/resources/claude-customizations/.claude/agents/legacy-parity-analyst.md +101 -43
- package/resources/claude-customizations/.claude/agents/migration-coverage-reviewer.md +107 -43
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +14 -0
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +21 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +17 -1
- package/resources/claude-customizations/.claude/agents/requirements-reconciler.md +117 -42
- package/resources/claude-customizations/.claude/agents/runtime-characterization-analyst.md +103 -43
- package/resources/claude-customizations/.claude/hooks/discovery-capability-probe.ps1 +102 -0
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +69 -194
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-planning-gate.ps1 +87 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +7 -1
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +6 -3
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +168 -48
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +1 -1
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +59 -20
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +44 -3
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +60 -4
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +4 -2
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +59 -20
- package/resources/claude-customizations/.claude/hooks/record-discovery-agent-start.ps1 +132 -0
- package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +56 -212
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-rejection.ps1 +172 -0
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +9 -1
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +9 -3
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusAggregate.psm1 +154 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusCombination.psm1 +228 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +1 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +63 -16
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +56 -1
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +4 -1
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +5 -1
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +25 -2
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +188 -341
- package/resources/claude-customizations/.claude/lib/worktree-resolution/HookTargetWorktree.psm1 +391 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +1 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -18
- package/resources/claude-customizations/.claude/settings.json +32 -3
- package/resources/claude-customizations/.claude/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
- package/resources/claude-customizations/.claude/skills/characterize-runtime-behavior/SKILL.md +134 -0
- package/resources/claude-customizations/.claude/skills/discovery-behavior-reconciliation/SKILL.md +28 -47
- package/resources/claude-customizations/.claude/skills/discovery-coverage-ledger/SKILL.md +32 -48
- package/resources/claude-customizations/.claude/skills/discovery-parity-matrix/SKILL.md +28 -45
- package/resources/claude-customizations/.claude/skills/discovery-repo-inventory/SKILL.md +28 -62
- package/resources/claude-customizations/.claude/skills/discovery-runtime-characterization/SKILL.md +28 -45
- package/resources/claude-customizations/.claude/skills/discovery-validate-artifacts/SKILL.md +28 -61
- package/resources/claude-customizations/.claude/skills/discovery-workflow/SKILL.md +55 -139
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +24 -9
- package/resources/claude-customizations/.claude/skills/extract-feature-contract/SKILL.md +133 -0
- package/resources/claude-customizations/.claude/skills/generate-acceptance-scenarios/SKILL.md +132 -0
- package/resources/claude-customizations/.claude/skills/initialize-discovery-workspace/SKILL.md +131 -0
- package/resources/claude-customizations/.claude/skills/inventory-legacy-system/SKILL.md +131 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +5 -4
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +21 -1
- package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +11 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +34 -0
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +69 -6
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +21 -4
- package/resources/claude-customizations/.claude/skills/reconcile-parity-matrix/SKILL.md +131 -0
- package/resources/claude-customizations/.claude/skills/review-discovery-coverage/SKILL.md +129 -0
- package/resources/claude-customizations/.claude/skills/validate-discovery-artifacts/SKILL.md +127 -0
- package/resources/claude-customizations/config/blast-radius.json +6 -5
- package/resources/claude-customizations/config/orchestration-routing.json +61 -1
- package/resources/claude-customizations/pack-manifests/core.json +33 -1
- package/resources/codex-and-agents-customizations/.agents/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +7 -0
- package/resources/codex-and-agents-customizations/.agents/skills/characterize-runtime-behavior/SKILL.md +134 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-coverage-ledger/SKILL.md +50 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-parity-matrix/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-repo-inventory/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-runtime-characterization/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-validate-artifacts/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-workflow/SKILL.md +62 -0
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +24 -9
- package/resources/codex-and-agents-customizations/.agents/skills/extract-feature-contract/SKILL.md +133 -0
- package/resources/codex-and-agents-customizations/.agents/skills/generate-acceptance-scenarios/SKILL.md +132 -0
- package/resources/codex-and-agents-customizations/.agents/skills/initialize-discovery-workspace/SKILL.md +131 -0
- package/resources/codex-and-agents-customizations/.agents/skills/inventory-legacy-system/SKILL.md +131 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/reconcile-parity-matrix/SKILL.md +131 -0
- package/resources/codex-and-agents-customizations/.agents/skills/review-discovery-coverage/SKILL.md +129 -0
- package/resources/codex-and-agents-customizations/.agents/skills/validate-discovery-artifacts/SKILL.md +127 -0
- package/resources/codex-and-agents-customizations/.codex/agents/discovery-capability-sentinel.toml +51 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c1.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c2.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3-elevated.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c4.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c1.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c2.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3-elevated.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c4.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c1.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c2.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3-elevated.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c4.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c1.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c2.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3-elevated.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c4.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +33 -3
- package/resources/codex-and-agents-customizations/.codex/hooks/codex-agent-profile-attestation.ps1 +22 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/discovery-capability-probe.ps1 +97 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/discovery-start-attestation.ps1 +171 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +4 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-artifact-gate.ps1 +187 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-planning-gate.ps1 +149 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +239 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -15
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +54 -15
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +54 -15
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-target-worktree.ps1 +369 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/record-subagent-routing-attestation.ps1 +8 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +22 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-discovery-agent-output.ps1 +178 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +11 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +63 -1
- package/resources/config/orchestration-routing.json +61 -1
- package/resources/customizations/.github/agents/discovery-capability-sentinel.agent.md +34 -0
- package/resources/customizations/.github/agents/legacy-parity-analyst.agent.md +122 -0
- package/resources/customizations/.github/agents/migration-coverage-reviewer.agent.md +128 -0
- package/resources/customizations/.github/agents/requirements-reconciler.agent.md +138 -0
- package/resources/customizations/.github/agents/runtime-characterization-analyst.agent.md +124 -0
- package/resources/customizations/.github/hooks/discovery-agent-start.ps1 +139 -0
- package/resources/customizations/.github/hooks/discovery-agent-stop.ps1 +123 -0
- package/resources/customizations/.github/hooks/discovery-capability-probe.ps1 +98 -0
- package/resources/customizations/.github/hooks/discovery-planning.ps1 +91 -0
- package/resources/customizations/.github/hooks/discovery-pre-tool.ps1 +123 -0
- package/resources/customizations/.github/hooks/discovery.json +35 -0
- package/resources/customizations/.github/instructions/mermaid.instructions.md +85 -0
- package/resources/customizations/.github/prompts/execute-hard-lock.prompt.md +105 -0
- package/resources/customizations/.github/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
- package/resources/customizations/.github/skills/characterize-runtime-behavior/SKILL.md +134 -0
- package/resources/customizations/.github/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-coverage-ledger/SKILL.md +50 -0
- package/resources/customizations/.github/skills/discovery-parity-matrix/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-repo-inventory/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-runtime-characterization/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-validate-artifacts/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-workflow/SKILL.md +62 -0
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +24 -9
- package/resources/customizations/.github/skills/extract-feature-contract/SKILL.md +133 -0
- package/resources/customizations/.github/skills/generate-acceptance-scenarios/SKILL.md +132 -0
- package/resources/customizations/.github/skills/initialize-discovery-workspace/SKILL.md +131 -0
- package/resources/customizations/.github/skills/inventory-legacy-system/SKILL.md +131 -0
- package/resources/customizations/.github/skills/reconcile-parity-matrix/SKILL.md +131 -0
- package/resources/customizations/.github/skills/review-discovery-coverage/SKILL.md +129 -0
- package/resources/customizations/.github/skills/validate-discovery-artifacts/SKILL.md +127 -0
- package/resources/lib/codex-routing/CodexDeployment.psm1 +5 -1
- package/resources/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +25 -2
- package/resources/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
- package/resources/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
- package/resources/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
- package/resources/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
- package/resources/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
- package/resources/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
- package/resources/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
- package/resources/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
- package/resources/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
- package/resources/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
- package/resources/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
- package/resources/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
- package/resources/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
- package/resources/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
- package/resources/powershell/PoshQC/PoshQC.Parallel.psm1 +452 -0
- package/resources/powershell/PoshQC/PoshQC.Shard.psm1 +106 -8
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -0
- package/resources/powershell/PoshQC/PoshQC.psd1 +6 -1
- package/resources/powershell/PoshQC/PoshQC.psm1 +7 -1
- package/resources/powershell/PoshQC/README.md +20 -1
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Native host capability observation for discovery agents: registration, handler hashes and the harmless sentinel.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Every native capability probe (Claude Code, Codex, GitHub Copilot) takes its
|
|
7
|
+
observation from this module. The probe reads the host's hook registration
|
|
8
|
+
(`.claude/settings.json`, `.codex/config.toml` or `.github/hooks/discovery.json`),
|
|
9
|
+
confirms that the start, tool and stop handlers are registered for the four
|
|
10
|
+
discovery agent names and the controlled tools, hashes the configuration and
|
|
11
|
+
every handler, and reads the host observations the native PreToolUse gate
|
|
12
|
+
recorded while the discovery-capability-sentinel agent ran: a sentinel tool call
|
|
13
|
+
that carried the agent identity and was consumed by the sentinel MCP tool, and
|
|
14
|
+
the denied sentinel shell command from the same agent. Consumption is the
|
|
15
|
+
exclusive `<record>.json.consumed` latch the MCP server writes beside the
|
|
16
|
+
observation and never a rewrite of the record: the gate writes the record once
|
|
17
|
+
with state `issued`, and the probe accepts it only when the latch beside it names
|
|
18
|
+
the same observation_ref. The result is a `capability-attestation`-shaped document
|
|
19
|
+
whose status is derived, never supplied: `supported` requires every control,
|
|
20
|
+
every handler and observation evidence for attribution and the denied sentinel.
|
|
21
|
+
No in-process decision, caller Boolean or repository text can substitute for
|
|
22
|
+
these observations; the gateway alone writes the protected attestation.
|
|
23
|
+
|
|
24
|
+
.NOTES
|
|
25
|
+
Issue #980. Compatible with PowerShell 7+. Depends only on the sibling
|
|
26
|
+
DiscoveryEncoding, DiscoveryAuthority, DiscoveryPayload and DiscoveryObservation 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 'DiscoveryObservation.psm1') -ErrorAction Stop
|
|
37
|
+
|
|
38
|
+
$script:CapabilitySchema = 'urn:drm-copilot:discovery:2.0.0:capability-attestation'
|
|
39
|
+
$script:CapabilityAttestationFile = 'capability-attestation.json'
|
|
40
|
+
$script:DiscoveryAgentNames = @('legacy-parity-analyst', 'runtime-characterization-analyst', 'requirements-reconciler', 'migration-coverage-reviewer')
|
|
41
|
+
$script:ControlledToolNames = @('Bash', 'Write', 'mcp__drm-copilot__run_discovery_repo_inventory', 'Agent')
|
|
42
|
+
$script:ControlNames = @('workspace_trusted', 'hooks_enabled', 'trusted_attribution', 'start_event', 'tool_event', 'stop_event', 'forbidden_tools_denied')
|
|
43
|
+
$script:HandlerEvents = @('start', 'tool', 'stop')
|
|
44
|
+
$script:EventNames = @{ start = 'SubagentStart'; tool = 'PreToolUse'; stop = 'SubagentStop' }
|
|
45
|
+
# Per-host registration file, handler directory and the handler each lifecycle event names.
|
|
46
|
+
$script:HostLayout = @{
|
|
47
|
+
'claude-code' = @{ Configuration = '.claude/settings.json'; Directory = '.claude/hooks'; start = 'record-discovery-agent-start.ps1'; tool = 'enforce-discovery-artifact-gate.ps1'; stop = 'validate-discovery-artifact-gate.ps1' }
|
|
48
|
+
'codex' = @{ Configuration = '.codex/config.toml'; Directory = '.codex/hooks'; start = 'record-subagent-routing-attestation.ps1'; tool = 'enforce-discovery-artifact-gate.ps1'; stop = 'validate-discovery-agent-output.ps1' }
|
|
49
|
+
'github-copilot' = @{ Configuration = '.github/hooks/discovery.json'; Directory = '.github/hooks'; start = 'discovery-agent-start.ps1'; tool = 'discovery-pre-tool.ps1'; stop = 'discovery-agent-stop.ps1' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function Get-DiscoverySentinelRequest {
|
|
53
|
+
<#
|
|
54
|
+
.SYNOPSIS
|
|
55
|
+
Returns the harmless sentinel command text the native hook must deny and never execute.
|
|
56
|
+
#>
|
|
57
|
+
[CmdletBinding()]
|
|
58
|
+
[OutputType([string])]
|
|
59
|
+
param([Parameter(Mandatory = $true)][string]$SentinelId)
|
|
60
|
+
|
|
61
|
+
return "drm-copilot discovery capability probe $SentinelId" + ': this command must be denied by the native hook and never executed'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Get-DiscoverySentinelEnvelope {
|
|
65
|
+
<#
|
|
66
|
+
.SYNOPSIS
|
|
67
|
+
Builds the host-shaped PreToolUse envelope of the sentinel: an attested specialist running a shell command.
|
|
68
|
+
#>
|
|
69
|
+
[CmdletBinding()]
|
|
70
|
+
[OutputType([string])]
|
|
71
|
+
param(
|
|
72
|
+
[Parameter(Mandatory = $true)][ValidateSet('claude-code', 'codex', 'github-copilot')][string]$HostName,
|
|
73
|
+
[Parameter(Mandatory = $true)][string]$SessionId,
|
|
74
|
+
[Parameter(Mandatory = $true)][string]$SentinelId,
|
|
75
|
+
[Parameter(Mandatory = $true)][string]$WorkspaceRoot
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
$command = Get-DiscoverySentinelRequest -SentinelId $SentinelId
|
|
79
|
+
if ($HostName -eq 'github-copilot') {
|
|
80
|
+
$envelope = [ordered]@{ sessionId = $SessionId; hookEventName = 'PreToolUse'; toolName = 'Bash'; toolInput = [ordered]@{ command = $command }; cwd = $WorkspaceRoot; toolUseId = $SentinelId }
|
|
81
|
+
} else {
|
|
82
|
+
$envelope = [ordered]@{ session_id = $SessionId; hook_event_name = 'PreToolUse'; tool_name = 'Bash'; tool_input = [ordered]@{ command = $command }; cwd = $WorkspaceRoot; tool_use_id = $SentinelId }
|
|
83
|
+
}
|
|
84
|
+
return ($envelope | ConvertTo-Json -Compress -Depth 4)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ConvertTo-DiscoveryObservationInstant {
|
|
88
|
+
<#
|
|
89
|
+
.SYNOPSIS
|
|
90
|
+
Returns a UTC instant from a parsed observation timestamp (string or DateTime), or $null when unreadable.
|
|
91
|
+
#>
|
|
92
|
+
[CmdletBinding()]
|
|
93
|
+
[OutputType([datetime])]
|
|
94
|
+
param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
|
|
95
|
+
|
|
96
|
+
if ($Value -is [datetime]) { return $Value.ToUniversalTime() }
|
|
97
|
+
$parsed = [datetime]::MinValue
|
|
98
|
+
$styles = [System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal
|
|
99
|
+
if ($Value -is [string] -and [datetime]::TryParseExact($Value, "yyyy-MM-dd'T'HH:mm:ss'Z'", [System.Globalization.CultureInfo]::InvariantCulture, $styles, [ref]$parsed)) { return $parsed }
|
|
100
|
+
return $null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function Get-DiscoveryConsumedLatch {
|
|
104
|
+
<#
|
|
105
|
+
.SYNOPSIS
|
|
106
|
+
Reads the exclusive `.consumed` latch beside a one-use record through the ReadText seam.
|
|
107
|
+
.DESCRIPTION
|
|
108
|
+
The MCP server consumes a sentinel grant by creating `<record>.json.consumed`
|
|
109
|
+
with a JSON object naming the record's observation_ref; the record itself is
|
|
110
|
+
never rewritten. Returns the parsed latch as a hashtable, or $null when the
|
|
111
|
+
latch is absent, unreadable or not a JSON object.
|
|
112
|
+
#>
|
|
113
|
+
[CmdletBinding()]
|
|
114
|
+
[OutputType([hashtable])]
|
|
115
|
+
param(
|
|
116
|
+
[Parameter(Mandatory = $true)][hashtable]$Store,
|
|
117
|
+
[Parameter(Mandatory = $true)][string]$Path
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
$text = Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @($Path)
|
|
122
|
+
$latch = $(if ($null -ne $text) { ConvertFrom-Json -InputObject $text -AsHashtable -Depth 4 } else { $null })
|
|
123
|
+
} catch { $latch = $null }
|
|
124
|
+
if ($latch -is [hashtable]) { return $latch }
|
|
125
|
+
return $null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function Get-DiscoverySessionObservation {
|
|
129
|
+
<#
|
|
130
|
+
.SYNOPSIS
|
|
131
|
+
Reads every parseable host-observation record of the session from the protected store, with its consumer latch.
|
|
132
|
+
.DESCRIPTION
|
|
133
|
+
Entries that cannot be read or parsed are skipped; an absent directory yields
|
|
134
|
+
an empty list. Records are returned as hashtables keyed by the schema names
|
|
135
|
+
plus the reader-side entry `consumed_latch`: the sibling `<name>.consumed`
|
|
136
|
+
the sentinel MCP tool writes when it consumes the grant, read by path through
|
|
137
|
+
the same ReadText seam, as a hashtable when it parses as a JSON object and
|
|
138
|
+
otherwise $null.
|
|
139
|
+
#>
|
|
140
|
+
[CmdletBinding()]
|
|
141
|
+
[OutputType([object[]])]
|
|
142
|
+
param(
|
|
143
|
+
[Parameter(Mandatory = $true)][pscustomobject]$Location,
|
|
144
|
+
[Parameter(Mandatory = $true)][string]$SessionId,
|
|
145
|
+
[Parameter(Mandatory = $true)][hashtable]$Store
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
$directory = Get-DiscoveryObservationDirectory -Location $Location -SessionId $SessionId
|
|
149
|
+
$names = @()
|
|
150
|
+
try { $names = @(Invoke-DiscoveryStoreSeam -Seam $Store.ListEntries -Arguments @($directory)) } catch { $names = @() }
|
|
151
|
+
$records = [System.Collections.Generic.List[object]]::new()
|
|
152
|
+
# Each entry is one observation file; unreadable or non-object entries carry no evidence.
|
|
153
|
+
foreach ($name in @($names | Where-Object { $_ -is [string] -and $_.EndsWith('.json') })) {
|
|
154
|
+
try {
|
|
155
|
+
$text = Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @("$directory/$name")
|
|
156
|
+
$record = $(if ($null -ne $text) { ConvertFrom-Json -InputObject $text -AsHashtable -Depth 8 } else { $null })
|
|
157
|
+
} catch { $record = $null }
|
|
158
|
+
if ($record -is [hashtable] -and [string]$record['artifact_kind'] -eq 'host-observation') {
|
|
159
|
+
# The latch is the consumption marker; it is attached here so the evidence derivation never touches the store.
|
|
160
|
+
$record['consumed_latch'] = Get-DiscoveryConsumedLatch -Store $Store -Path "$directory/$name.consumed"
|
|
161
|
+
$records.Add($record)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return $records.ToArray()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function Get-DiscoveryObservationEvidence {
|
|
168
|
+
<#
|
|
169
|
+
.SYNOPSIS
|
|
170
|
+
Derives trusted_attribution and forbidden_tools_denied from the session's host observations.
|
|
171
|
+
.DESCRIPTION
|
|
172
|
+
trusted_attribution holds only for a sentinel-tool observation that carries a
|
|
173
|
+
non-empty agent_id, the sentinel agent type, state `issued` beside a consumer
|
|
174
|
+
latch (`consumed_latch`, the parsed `<record>.json.consumed` the sentinel MCP
|
|
175
|
+
tool writes) whose observation_ref equals the record's, the hash of the
|
|
176
|
+
registered tool handler and an observed_at within TtlSeconds of Now. A record
|
|
177
|
+
rewritten as `consumed`, a missing latch or a latch naming another observation
|
|
178
|
+
is reported as not consumed, because the record is never rewritten and only
|
|
179
|
+
the latch beside it proves consumption. forbidden_tools_denied holds only for
|
|
180
|
+
a SHELL deny observation from the same agent_id. Every rejected or missing
|
|
181
|
+
observation yields one deficiency message.
|
|
182
|
+
.OUTPUTS
|
|
183
|
+
[pscustomobject] TrustedAttribution, ForbiddenDenied, Deficiencies, SentinelRef, ShellRef.
|
|
184
|
+
#>
|
|
185
|
+
[CmdletBinding()]
|
|
186
|
+
[OutputType([pscustomobject])]
|
|
187
|
+
param(
|
|
188
|
+
[Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$Observations,
|
|
189
|
+
[Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][string]$ToolHandlerSha256,
|
|
190
|
+
[Parameter(Mandatory = $true)][datetime]$Now,
|
|
191
|
+
[Parameter(Mandatory = $true)][int]$TtlSeconds
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
$deficiencies = [System.Collections.Generic.List[object]]::new()
|
|
195
|
+
$accepted = $null
|
|
196
|
+
$candidates = @($Observations | Where-Object { [string]$_['code'] -ceq 'SENTINEL' -and [string]$_['tool'] -ceq 'discovery_capability_sentinel' })
|
|
197
|
+
if ($candidates.Count -eq 0) { $deficiencies.Add([pscustomobject]@{ Control = 'trusted_attribution'; Message = 'no sentinel tool observation was recorded for this session' }) }
|
|
198
|
+
# Each sentinel-tool observation must pass every check; the first that does supplies the attributed agent.
|
|
199
|
+
foreach ($candidate in $candidates) {
|
|
200
|
+
$reference = [string]$candidate['observation_ref']
|
|
201
|
+
$observedAt = ConvertTo-DiscoveryObservationInstant -Value $candidate['observed_at']
|
|
202
|
+
$problem = $null
|
|
203
|
+
# Consumption is proved by the latch alone: present and naming this record, or absent, or naming another record.
|
|
204
|
+
$latch = $candidate['consumed_latch']
|
|
205
|
+
$latchState = $(if ($latch -isnot [hashtable]) { 'absent' } elseif ([string]$latch['observation_ref'] -ceq $reference) { 'present' } else { 'names another observation' })
|
|
206
|
+
if ([string]::IsNullOrWhiteSpace([string]$candidate['agent_id']) -or [string]$candidate['agent_type'] -cne 'discovery-capability-sentinel') { $problem = 'carries no sentinel agent identity' }
|
|
207
|
+
elseif ([string]$candidate['state'] -cne 'issued' -or $latchState -ne 'present') { $problem = "was not consumed by the sentinel tool (state $([string]$candidate['state']), latch $latchState)" }
|
|
208
|
+
elseif ([string]::IsNullOrEmpty($ToolHandlerSha256) -or [string]$candidate['handler_sha256'] -cne $ToolHandlerSha256) { $problem = 'was recorded by a tool handler whose hash differs from the registered handler' }
|
|
209
|
+
elseif ($null -eq $observedAt -or ($Now.ToUniversalTime() - $observedAt).TotalSeconds -gt $TtlSeconds -or $observedAt -gt $Now.ToUniversalTime()) { $problem = 'is stale' }
|
|
210
|
+
if ($null -eq $problem) { if ($null -eq $accepted) { $accepted = $candidate } }
|
|
211
|
+
else { $deficiencies.Add([pscustomobject]@{ Control = 'trusted_attribution'; Message = "sentinel observation $reference $problem" }) }
|
|
212
|
+
}
|
|
213
|
+
$shell = $null
|
|
214
|
+
if ($null -ne $accepted) {
|
|
215
|
+
$shell = @($Observations | Where-Object { [string]$_['code'] -ceq 'SHELL' -and [string]$_['decision'] -ceq 'deny' -and [string]$_['agent_id'] -ceq [string]$accepted['agent_id'] }) | Select-Object -First 1
|
|
216
|
+
}
|
|
217
|
+
if ($null -eq $shell) { $deficiencies.Add([pscustomobject]@{ Control = 'forbidden_tools_denied'; Message = 'no SHELL deny observation from the attributed sentinel agent was recorded' }) }
|
|
218
|
+
return [pscustomobject]@{
|
|
219
|
+
TrustedAttribution = ($null -ne $accepted)
|
|
220
|
+
ForbiddenDenied = ($null -ne $shell)
|
|
221
|
+
Deficiencies = @($deficiencies)
|
|
222
|
+
SentinelRef = $(if ($null -ne $accepted) { [string]$accepted['observation_ref'] } else { $null })
|
|
223
|
+
ShellRef = $(if ($null -ne $shell) { [string]$shell['observation_ref'] } else { $null })
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function Get-DiscoveryHostRegistration {
|
|
228
|
+
<#
|
|
229
|
+
.SYNOPSIS
|
|
230
|
+
Parses a host registration text into event entries (Event, Matcher, Command).
|
|
231
|
+
.DESCRIPTION
|
|
232
|
+
Claude settings and Copilot hook files are JSON: an event maps to groups
|
|
233
|
+
(`matcher` plus `hooks[]`) or directly to hook objects. A hook object names
|
|
234
|
+
its handler through `powershell`, `bash` or `command`, read in that order, so
|
|
235
|
+
the documented Copilot shape (`version` 1 with `powershell` and `bash`) and
|
|
236
|
+
the Claude `command` shape both parse. The Codex file is TOML scanned line by
|
|
237
|
+
line: a `[[hooks.<Event>]]` table starts a group, `matcher` sets its regex and
|
|
238
|
+
each `command` names a handler. Parsed is $false when the text cannot be read
|
|
239
|
+
as the host's format.
|
|
240
|
+
#>
|
|
241
|
+
[CmdletBinding()]
|
|
242
|
+
[OutputType([pscustomobject])]
|
|
243
|
+
param(
|
|
244
|
+
[Parameter(Mandatory = $true)][ValidateSet('claude-code', 'codex', 'github-copilot')][string]$HostName,
|
|
245
|
+
[Parameter(Mandatory = $false)][AllowNull()][AllowEmptyString()][string]$ConfigurationText
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
$entries = [System.Collections.Generic.List[object]]::new()
|
|
249
|
+
if ([string]::IsNullOrWhiteSpace($ConfigurationText)) { return [pscustomobject]@{ Parsed = $false; Entries = @() } }
|
|
250
|
+
if ($HostName -eq 'codex') {
|
|
251
|
+
$currentEvent = $null
|
|
252
|
+
$currentMatcher = $null
|
|
253
|
+
foreach ($line in ($ConfigurationText -split "`r?`n")) {
|
|
254
|
+
if ($line -match '^\[\[hooks\.(?<event>[A-Za-z]+)\]\]\s*$') { $currentEvent = $Matches['event']; $currentMatcher = $null; continue }
|
|
255
|
+
if ($null -ne $currentEvent -and $line -match '^matcher\s*=\s*"(?<matcher>.+)"\s*$') { $currentMatcher = $Matches['matcher']; continue }
|
|
256
|
+
if ($null -ne $currentEvent -and $line -match '^command\s*=\s*''(?<command>.+)''\s*$') {
|
|
257
|
+
$entries.Add([pscustomobject]@{ Event = $currentEvent; Matcher = $currentMatcher; Command = $Matches['command'] })
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return [pscustomobject]@{ Parsed = ($entries.Count -gt 0); Entries = @($entries) }
|
|
261
|
+
}
|
|
262
|
+
try { $document = ConvertFrom-Json -InputObject $ConfigurationText -Depth 16 } catch { return [pscustomobject]@{ Parsed = $false; Entries = @() } }
|
|
263
|
+
if ($document -isnot [System.Management.Automation.PSCustomObject] -or $null -eq $document.PSObject.Properties['hooks'] -or $document.hooks -isnot [System.Management.Automation.PSCustomObject]) {
|
|
264
|
+
return [pscustomobject]@{ Parsed = $false; Entries = @() }
|
|
265
|
+
}
|
|
266
|
+
foreach ($eventProperty in $document.hooks.PSObject.Properties) {
|
|
267
|
+
foreach ($item in @($eventProperty.Value)) {
|
|
268
|
+
if ($item -isnot [System.Management.Automation.PSCustomObject]) { continue }
|
|
269
|
+
$matcher = $(if ($null -ne $item.PSObject.Properties['matcher']) { [string]$item.matcher } else { $null })
|
|
270
|
+
$hooks = $(if ($null -ne $item.PSObject.Properties['hooks']) { @($item.hooks) } else { @($item) })
|
|
271
|
+
foreach ($hook in $hooks) {
|
|
272
|
+
if ($hook -isnot [System.Management.Automation.PSCustomObject]) { continue }
|
|
273
|
+
# The first present handler key wins: the Copilot shape names powershell and bash, the Claude shape names command.
|
|
274
|
+
$command = $null
|
|
275
|
+
foreach ($key in @('powershell', 'bash', 'command')) {
|
|
276
|
+
if ($null -ne $hook.PSObject.Properties[$key] -and -not [string]::IsNullOrWhiteSpace([string]$hook.$key)) { $command = [string]$hook.$key; break }
|
|
277
|
+
}
|
|
278
|
+
if ($null -ne $command) { $entries.Add([pscustomobject]@{ Event = $eventProperty.Name; Matcher = $matcher; Command = $command }) }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return [pscustomobject]@{ Parsed = $true; Entries = @($entries) }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function Test-DiscoveryHandlerRegistered {
|
|
286
|
+
<#
|
|
287
|
+
.SYNOPSIS
|
|
288
|
+
True when Event registers HandlerName with a matcher that admits every name in Admitted (or no matcher).
|
|
289
|
+
#>
|
|
290
|
+
[CmdletBinding()]
|
|
291
|
+
[OutputType([bool])]
|
|
292
|
+
param(
|
|
293
|
+
[Parameter(Mandatory = $true)][pscustomobject]$Registration,
|
|
294
|
+
[Parameter(Mandatory = $true)][string]$Event,
|
|
295
|
+
[Parameter(Mandatory = $true)][string]$HandlerName,
|
|
296
|
+
[Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]]$Admitted
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
$entries = @($Registration.Entries | Where-Object { $_.Event -eq $Event -and ([string]$_.Command).Contains($HandlerName) })
|
|
300
|
+
if ($entries.Count -eq 0) { return $false }
|
|
301
|
+
# The handler may be registered under several matcher groups; their union must admit every name.
|
|
302
|
+
foreach ($name in $Admitted) {
|
|
303
|
+
$covered = @($entries | Where-Object { [string]::IsNullOrEmpty($_.Matcher) -or $name -match $_.Matcher })
|
|
304
|
+
if ($covered.Count -eq 0) { return $false }
|
|
305
|
+
}
|
|
306
|
+
return $true
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function Get-DiscoveryCapabilityObservation {
|
|
310
|
+
<#
|
|
311
|
+
.SYNOPSIS
|
|
312
|
+
Observes one host's discovery controls and returns the capability-attestation-shaped document.
|
|
313
|
+
.DESCRIPTION
|
|
314
|
+
Controls: hooks_enabled (the registration file is present and parseable and
|
|
315
|
+
names a discovery handler), start_event, tool_event and stop_event (the
|
|
316
|
+
handler is registered for the discovery agents or controlled tools and its
|
|
317
|
+
file exists), trusted_attribution and forbidden_tools_denied (proved only
|
|
318
|
+
from the session's host observations recorded by the native PreToolUse gate
|
|
319
|
+
while the capability sentinel agent ran; see Get-DiscoveryObservationEvidence),
|
|
320
|
+
workspace_trusted (supplied by the host probe, which the host only runs inside
|
|
321
|
+
a trusted workspace). The protected activation supplies the workspace binding;
|
|
322
|
+
without one the status is unsupported. A session without observations is
|
|
323
|
+
unsupported, so a supported result requires the session the sentinel ran in.
|
|
324
|
+
#>
|
|
325
|
+
[CmdletBinding()]
|
|
326
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
327
|
+
param(
|
|
328
|
+
[Parameter(Mandatory = $true)][ValidateSet('claude-code', 'codex', 'github-copilot')][string]$HostName,
|
|
329
|
+
[Parameter(Mandatory = $true)][string]$WorkspaceRoot,
|
|
330
|
+
[Parameter(Mandatory = $true)][string]$SessionId,
|
|
331
|
+
[Parameter(Mandatory = $true)][string]$Nonce,
|
|
332
|
+
[Parameter(Mandatory = $true)][System.Collections.IDictionary]$Runtime,
|
|
333
|
+
[Parameter(Mandatory = $true)][hashtable]$HostEnvironment,
|
|
334
|
+
[Parameter(Mandatory = $false)][hashtable]$Store = (Get-DiscoveryDefaultStore),
|
|
335
|
+
[Parameter(Mandatory = $false)][bool]$WorkspaceTrusted = $true,
|
|
336
|
+
[Parameter(Mandatory = $false)][datetime]$Now = [datetime]::UtcNow,
|
|
337
|
+
[Parameter(Mandatory = $false)][int]$TtlSeconds = 3600,
|
|
338
|
+
[Parameter(Mandatory = $false)][scriptblock]$ReadBytes = {
|
|
339
|
+
param([string]$Path)
|
|
340
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
|
|
341
|
+
return [System.IO.File]::ReadAllBytes($Path)
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
$layout = $script:HostLayout[$HostName]
|
|
346
|
+
$root = $WorkspaceRoot.TrimEnd('/', '\')
|
|
347
|
+
$findings = [System.Collections.Generic.List[object]]::new()
|
|
348
|
+
$controls = [ordered]@{}
|
|
349
|
+
foreach ($name in $script:ControlNames) { $controls[$name] = $false }
|
|
350
|
+
$controls['workspace_trusted'] = $WorkspaceTrusted
|
|
351
|
+
$configurationBytes = & $ReadBytes "$root/$($layout.Configuration)"
|
|
352
|
+
$configurationText = $null
|
|
353
|
+
if ($null -ne $configurationBytes) {
|
|
354
|
+
[byte[]]$configurationBytes = @($configurationBytes)
|
|
355
|
+
try { $configurationText = [System.Text.UTF8Encoding]::new($false, $true).GetString($configurationBytes) } catch { $configurationText = $null }
|
|
356
|
+
} else { [byte[]]$configurationBytes = @() }
|
|
357
|
+
$registration = Get-DiscoveryHostRegistration -HostName $HostName -ConfigurationText $configurationText
|
|
358
|
+
$handlers = [System.Collections.Generic.List[object]]::new()
|
|
359
|
+
foreach ($lifecycle in $script:HandlerEvents) {
|
|
360
|
+
$handlerName = $layout[$lifecycle]
|
|
361
|
+
$admitted = $(if ($lifecycle -eq 'tool') { $script:ControlledToolNames } else { $script:DiscoveryAgentNames })
|
|
362
|
+
$registered = $registration.Parsed -and (Test-DiscoveryHandlerRegistered -Registration $registration -Event $script:EventNames[$lifecycle] -HandlerName $handlerName -Admitted $admitted)
|
|
363
|
+
$relativePath = "$($layout.Directory)/$handlerName"
|
|
364
|
+
$bytes = & $ReadBytes "$root/$relativePath"
|
|
365
|
+
if ($registered -and $null -ne $bytes) {
|
|
366
|
+
$handlers.Add([ordered]@{ event = $lifecycle; path = $relativePath; sha256 = (Get-DiscoveryBytesSha256Hex -Bytes ([byte[]]@($bytes))) })
|
|
367
|
+
$controls["$($lifecycle)_event"] = $true
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
$controls['hooks_enabled'] = $registration.Parsed -and ($handlers.Count -gt 0)
|
|
371
|
+
# The workspace binding comes from the protected activation, never from the caller.
|
|
372
|
+
$location = Resolve-DiscoveryAuthorityLocation -WorkspaceRoot $WorkspaceRoot -Platform $HostEnvironment.Platform -UserHome $HostEnvironment.UserHome -LocalAppData $HostEnvironment.LocalAppData -XdgStateHome $HostEnvironment.XdgStateHome
|
|
373
|
+
# Attribution and the forbidden-tool deny are proved only from what the gate observed for the sentinel agent.
|
|
374
|
+
$toolHandler = @($handlers | Where-Object { $_['event'] -eq 'tool' }) | Select-Object -First 1
|
|
375
|
+
$observations = @(Get-DiscoverySessionObservation -Location $location -SessionId $SessionId -Store $Store)
|
|
376
|
+
$evidence = Get-DiscoveryObservationEvidence -Observations $observations -ToolHandlerSha256 $(if ($null -ne $toolHandler) { [string]$toolHandler['sha256'] } else { $null }) -Now $Now -TtlSeconds $TtlSeconds
|
|
377
|
+
$controls['trusted_attribution'] = $evidence.TrustedAttribution
|
|
378
|
+
$controls['forbidden_tools_denied'] = $evidence.ForbiddenDenied
|
|
379
|
+
$denied = $evidence.TrustedAttribution -and $evidence.ForbiddenDenied
|
|
380
|
+
$probe = [ordered]@{
|
|
381
|
+
sentinel_id = $(if ($null -ne $evidence.SentinelRef) { $evidence.SentinelRef } else { "sentinel-$Nonce" })
|
|
382
|
+
result = $(if ($denied) { 'denied' } else { 'unavailable' })
|
|
383
|
+
evidence_ref = $(if ($denied) { $evidence.ShellRef } else { $null })
|
|
384
|
+
}
|
|
385
|
+
foreach ($deficiency in $evidence.Deficiencies) {
|
|
386
|
+
$findings.Add([ordered]@{ code = 'DISCOVERY_ENFORCEMENT_UNAVAILABLE'; severity = 'error'; artifact_path = $null; pointer = '/controls'; related_ids = @(); related_paths = @(); message = "Native control $($deficiency.Control) is not proved: $($deficiency.Message)."; next_action = 'Run the discovery-capability-sentinel agent in this session, then re-run the capability probe with the same session.' })
|
|
387
|
+
}
|
|
388
|
+
$activation = Read-DiscoveryActivationState -Location $location -ReadText $Store.ReadText
|
|
389
|
+
$workspace = [ordered]@{ workspace_root = $location.Workspace; workspace_sha256 = $location.WorkspaceSha256; profile_path = 'config/discovery.json'; profile_sha256 = ('0' * 64) }
|
|
390
|
+
if ($activation.Status -eq 'active') {
|
|
391
|
+
$workspace['profile_path'] = [string]$activation.Record.profile_path
|
|
392
|
+
$workspace['profile_sha256'] = [string]$activation.Record.profile_sha256
|
|
393
|
+
} else {
|
|
394
|
+
$findings.Add([ordered]@{ code = 'DISCOVERY_ENFORCEMENT_UNAVAILABLE'; severity = 'error'; artifact_path = $null; pointer = '/workspace'; related_ids = @(); related_paths = @(); message = "No active protected activation binds this workspace ($($activation.Status))."; next_action = 'Initialize or configure discovery for this workspace, then re-run the capability probe.' })
|
|
395
|
+
}
|
|
396
|
+
foreach ($name in $script:ControlNames) {
|
|
397
|
+
if (-not $controls[$name]) { $findings.Add([ordered]@{ code = 'DISCOVERY_ENFORCEMENT_UNAVAILABLE'; severity = 'error'; artifact_path = $null; pointer = '/controls'; related_ids = @(); related_paths = @(); message = "Native control $name is not available."; next_action = 'Enable the native hook controls for this host and re-run the capability probe.' }) }
|
|
398
|
+
}
|
|
399
|
+
foreach ($lifecycle in $script:HandlerEvents) {
|
|
400
|
+
if (-not ($handlers | Where-Object { $_['event'] -eq $lifecycle })) { $findings.Add([ordered]@{ code = 'DISCOVERY_ENFORCEMENT_UNAVAILABLE'; severity = 'error'; artifact_path = $null; pointer = '/handlers'; related_ids = @(); related_paths = @(); message = "No $lifecycle hook handler was observed."; next_action = 'Install and register the discovery hook handlers and re-run the capability probe.' }) }
|
|
401
|
+
}
|
|
402
|
+
if (-not $denied) { $findings.Add([ordered]@{ code = 'DISCOVERY_ENFORCEMENT_UNAVAILABLE'; severity = 'error'; artifact_path = $null; pointer = '/probe'; related_ids = @(); related_paths = @(); message = "Sentinel $($probe['sentinel_id']) has no attributed denial in the recorded host observations."; next_action = 'Confirm the native hook denies forbidden operations and records protected evidence.' }) }
|
|
403
|
+
$stamp = $Now.ToUniversalTime()
|
|
404
|
+
return [ordered]@{
|
|
405
|
+
'$schema' = $script:CapabilitySchema
|
|
406
|
+
schema_version = '2.0.0'
|
|
407
|
+
artifact_kind = 'capability-attestation'
|
|
408
|
+
id = "capability-$Nonce"
|
|
409
|
+
attestation_ref = "capability-$Nonce"
|
|
410
|
+
workspace = $workspace
|
|
411
|
+
host = $HostName
|
|
412
|
+
session_id = $SessionId
|
|
413
|
+
runtime = $Runtime
|
|
414
|
+
configuration_sha256 = Get-DiscoveryBytesSha256Hex -Bytes $configurationBytes
|
|
415
|
+
handlers = @($handlers)
|
|
416
|
+
controls = $controls
|
|
417
|
+
probe = $probe
|
|
418
|
+
status = $(if ($findings.Count -eq 0) { 'supported' } else { 'unsupported' })
|
|
419
|
+
findings = @($findings)
|
|
420
|
+
observed_at = $stamp.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
|
421
|
+
expires_at = $stamp.AddSeconds($TtlSeconds).ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function Write-DiscoveryCapabilityAttestation {
|
|
426
|
+
<#
|
|
427
|
+
.SYNOPSIS
|
|
428
|
+
Persists an observation as the session's protected capability attestation and returns its path.
|
|
429
|
+
.DESCRIPTION
|
|
430
|
+
The doctor, the agent preparation and the start attestation read only the protected
|
|
431
|
+
session copy at `<sessions>/<session>/capability-attestation.json`, so a probe that
|
|
432
|
+
only prints its observation leaves every control unverifiable. The record is written
|
|
433
|
+
exclusively on first observation and replaced on a later probe of the same session
|
|
434
|
+
when the store offers ReplaceText; a store without it keeps the first record.
|
|
435
|
+
#>
|
|
436
|
+
[CmdletBinding()]
|
|
437
|
+
[OutputType([string])]
|
|
438
|
+
param(
|
|
439
|
+
[Parameter(Mandatory = $true)][System.Collections.IDictionary]$Observation,
|
|
440
|
+
[Parameter(Mandatory = $true)][string]$WorkspaceRoot,
|
|
441
|
+
[Parameter(Mandatory = $true)][hashtable]$HostEnvironment,
|
|
442
|
+
[Parameter(Mandatory = $true)][hashtable]$Store
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
$location = Resolve-DiscoveryAuthorityLocation -WorkspaceRoot $WorkspaceRoot -Platform $HostEnvironment.Platform -UserHome $HostEnvironment.UserHome -LocalAppData $HostEnvironment.LocalAppData -XdgStateHome $HostEnvironment.XdgStateHome
|
|
446
|
+
$directory = Get-DiscoverySessionDirectory -Location $location -SessionId ([string]$Observation['session_id'])
|
|
447
|
+
$path = "$directory/$($script:CapabilityAttestationFile)"
|
|
448
|
+
$text = (($Observation | ConvertTo-Json -Depth 8) + "`n")
|
|
449
|
+
Invoke-DiscoveryStoreSeam -Seam $Store.EnsureDirectory -Arguments @($directory)
|
|
450
|
+
# A re-probe of the same session replaces its earlier record; the exclusive write guards the first.
|
|
451
|
+
$existing = Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @($path)
|
|
452
|
+
if ($null -ne $existing -and $Store.ContainsKey('ReplaceText')) {
|
|
453
|
+
Invoke-DiscoveryStoreSeam -Seam $Store.ReplaceText -Arguments @($path, $text)
|
|
454
|
+
} else {
|
|
455
|
+
Invoke-DiscoveryStoreSeam -Seam $Store.WriteText -Arguments @($path, $text)
|
|
456
|
+
}
|
|
457
|
+
return $path
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
Export-ModuleMember -Function `
|
|
461
|
+
Get-DiscoverySentinelRequest, `
|
|
462
|
+
Get-DiscoverySentinelEnvelope, `
|
|
463
|
+
Get-DiscoverySessionObservation, `
|
|
464
|
+
Get-DiscoveryObservationEvidence, `
|
|
465
|
+
Get-DiscoveryHostRegistration, `
|
|
466
|
+
Test-DiscoveryHandlerRegistered, `
|
|
467
|
+
Get-DiscoveryCapabilityObservation, `
|
|
468
|
+
Write-DiscoveryCapabilityAttestation
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Hashing and canonical JSON encoding shared by the native discovery hooks.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Renders JSON values with sorted keys, no whitespace, a trailing LF and non-ASCII
|
|
7
|
+
kept literal, exactly as `canonical_json` in the Python runtime and
|
|
8
|
+
`canonicalJson` in the TypeScript gateway do, and returns lowercase hex SHA-256
|
|
9
|
+
digests of text and bytes. Every discovery digest (tool/input authorization,
|
|
10
|
+
input manifest, receipt) is computed over this encoding, so a hook that
|
|
11
|
+
recomputes a digest with these functions agrees with the runtime byte for byte.
|
|
12
|
+
|
|
13
|
+
Numbers must be safe integers because a fractional value cannot be normalized
|
|
14
|
+
identically across runtimes; anything outside JSON is refused rather than
|
|
15
|
+
guessed. The module performs no I/O.
|
|
16
|
+
|
|
17
|
+
.NOTES
|
|
18
|
+
Issue #980. Compatible with PowerShell 7+. No external module dependencies.
|
|
19
|
+
Mirrored byte-identically under extensions/drm-copilot/resources/claude-customizations/
|
|
20
|
+
and published to the Codex bundle under .codex/lib/discovery-validation/.
|
|
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
|
+
function Get-DiscoverySha256Hex {
|
|
28
|
+
<#
|
|
29
|
+
.SYNOPSIS
|
|
30
|
+
Returns the lowercase hex SHA-256 of the UTF-8 encoding of Text.
|
|
31
|
+
#>
|
|
32
|
+
[CmdletBinding()]
|
|
33
|
+
[OutputType([string])]
|
|
34
|
+
param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Text)
|
|
35
|
+
|
|
36
|
+
return Get-DiscoveryBytesSha256Hex -Bytes ([System.Text.Encoding]::UTF8.GetBytes($Text))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Get-DiscoveryBytesSha256Hex {
|
|
40
|
+
<#
|
|
41
|
+
.SYNOPSIS
|
|
42
|
+
Returns the lowercase hex SHA-256 of raw bytes (an empty array is allowed).
|
|
43
|
+
#>
|
|
44
|
+
[CmdletBinding()]
|
|
45
|
+
[OutputType([string])]
|
|
46
|
+
param([Parameter(Mandatory = $true)][AllowEmptyCollection()][byte[]]$Bytes)
|
|
47
|
+
|
|
48
|
+
return [Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData($Bytes)).ToLowerInvariant()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ConvertTo-DiscoveryJsonString {
|
|
52
|
+
<#
|
|
53
|
+
.SYNOPSIS
|
|
54
|
+
Quotes one string the way JSON.stringify and json.dumps(ensure_ascii=False) do.
|
|
55
|
+
.DESCRIPTION
|
|
56
|
+
Only the quote, the backslash and C0 controls are escaped; every other code
|
|
57
|
+
point stays literal so digests agree across the three runtimes.
|
|
58
|
+
#>
|
|
59
|
+
[CmdletBinding()]
|
|
60
|
+
[OutputType([string])]
|
|
61
|
+
param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Value)
|
|
62
|
+
|
|
63
|
+
$builder = [System.Text.StringBuilder]::new('"')
|
|
64
|
+
# Escape per code point (culture-aware string comparison treats control
|
|
65
|
+
# characters as ignorable, so the switch is on the integer value): the short
|
|
66
|
+
# forms first, then \u00XX for other controls.
|
|
67
|
+
foreach ($character in $Value.ToCharArray()) {
|
|
68
|
+
$code = [int]$character
|
|
69
|
+
switch ($code) {
|
|
70
|
+
34 { [void]$builder.Append('\"') }
|
|
71
|
+
92 { [void]$builder.Append('\\') }
|
|
72
|
+
8 { [void]$builder.Append('\b') }
|
|
73
|
+
12 { [void]$builder.Append('\f') }
|
|
74
|
+
10 { [void]$builder.Append('\n') }
|
|
75
|
+
13 { [void]$builder.Append('\r') }
|
|
76
|
+
9 { [void]$builder.Append('\t') }
|
|
77
|
+
default {
|
|
78
|
+
if ($code -lt 0x20) {
|
|
79
|
+
[void]$builder.Append('\u' + $code.ToString('x4'))
|
|
80
|
+
} else {
|
|
81
|
+
[void]$builder.Append($character)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return $builder.Append('"').ToString()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ConvertTo-DiscoveryCanonicalJson {
|
|
90
|
+
<#
|
|
91
|
+
.SYNOPSIS
|
|
92
|
+
Renders a JSON value with sorted keys, no whitespace and a trailing LF.
|
|
93
|
+
.DESCRIPTION
|
|
94
|
+
Mirrors `canonical_json` in the Python runtime and `canonicalJson` in the
|
|
95
|
+
TypeScript gateway. Objects (hashtables, ordered dictionaries and
|
|
96
|
+
PSCustomObjects) sort keys by ordinal code point; arrays keep order; numbers
|
|
97
|
+
must be integers because a fractional value cannot be normalized identically.
|
|
98
|
+
.OUTPUTS
|
|
99
|
+
[string] The canonical text, ending with a single LF.
|
|
100
|
+
#>
|
|
101
|
+
[CmdletBinding()]
|
|
102
|
+
[OutputType([string])]
|
|
103
|
+
param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
|
|
104
|
+
|
|
105
|
+
return (ConvertTo-DiscoveryCanonicalFragment -Value $Value) + "`n"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ConvertTo-DiscoveryCanonicalFragment {
|
|
109
|
+
<#
|
|
110
|
+
.SYNOPSIS
|
|
111
|
+
Renders one JSON value without the trailing LF; recursive helper.
|
|
112
|
+
#>
|
|
113
|
+
[CmdletBinding()]
|
|
114
|
+
[OutputType([string])]
|
|
115
|
+
param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
|
|
116
|
+
|
|
117
|
+
# Routing by JSON type; anything outside JSON is refused rather than guessed.
|
|
118
|
+
if ($null -eq $Value) { return 'null' }
|
|
119
|
+
if ($Value -is [bool]) { return $(if ($Value) { 'true' } else { 'false' }) }
|
|
120
|
+
if ($Value -is [string]) { return ConvertTo-DiscoveryJsonString -Value $Value }
|
|
121
|
+
if ($Value -is [byte] -or $Value -is [int16] -or $Value -is [int32] -or $Value -is [int64] -or $Value -is [uint16] -or $Value -is [uint32]) {
|
|
122
|
+
return ([int64]$Value).ToString([System.Globalization.CultureInfo]::InvariantCulture)
|
|
123
|
+
}
|
|
124
|
+
if ($Value -is [double] -or $Value -is [decimal] -or $Value -is [single]) {
|
|
125
|
+
if ([math]::Floor([double]$Value) -ne [double]$Value -or [math]::Abs([double]$Value) -gt 9007199254740991) {
|
|
126
|
+
throw 'AUTHORIZATION_INPUT_INVALID: canonical JSON accepts only safe integers as numbers.'
|
|
127
|
+
}
|
|
128
|
+
return ([int64][double]$Value).ToString([System.Globalization.CultureInfo]::InvariantCulture)
|
|
129
|
+
}
|
|
130
|
+
if ($Value -is [System.Collections.IDictionary]) {
|
|
131
|
+
$members = @()
|
|
132
|
+
# Ordinal key order is what both reference runtimes emit.
|
|
133
|
+
[string[]]$sortedKeys = @($Value.Keys | ForEach-Object { [string]$_ })
|
|
134
|
+
[Array]::Sort($sortedKeys, [System.StringComparer]::Ordinal)
|
|
135
|
+
foreach ($key in $sortedKeys) {
|
|
136
|
+
$members += (ConvertTo-DiscoveryJsonString -Value ([string]$key)) + ':' + (ConvertTo-DiscoveryCanonicalFragment -Value $Value[$key])
|
|
137
|
+
}
|
|
138
|
+
return '{' + ($members -join ',') + '}'
|
|
139
|
+
}
|
|
140
|
+
if ($Value -is [System.Collections.IEnumerable]) {
|
|
141
|
+
$items = @()
|
|
142
|
+
foreach ($item in $Value) { $items += ConvertTo-DiscoveryCanonicalFragment -Value $item }
|
|
143
|
+
return '[' + ($items -join ',') + ']'
|
|
144
|
+
}
|
|
145
|
+
if ($Value -is [System.Management.Automation.PSCustomObject]) {
|
|
146
|
+
$table = [ordered]@{}
|
|
147
|
+
foreach ($property in $Value.PSObject.Properties) { $table[$property.Name] = $property.Value }
|
|
148
|
+
return ConvertTo-DiscoveryCanonicalFragment -Value $table
|
|
149
|
+
}
|
|
150
|
+
throw "AUTHORIZATION_INPUT_INVALID: canonical JSON cannot render a value of type $($Value.GetType().FullName)."
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Export-ModuleMember -Function `
|
|
154
|
+
Get-DiscoverySha256Hex, `
|
|
155
|
+
Get-DiscoveryBytesSha256Hex, `
|
|
156
|
+
ConvertTo-DiscoveryCanonicalJson
|