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