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