@danmoisan/drm-copilot-mcp 0.0.5 → 0.0.6
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 +93 -20
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/orchestrator.md +6 -2
- package/resources/claude-customizations/.claude/agents/pr-author.md +78 -0
- package/resources/claude-customizations/.claude/agents/task-researcher.md +10 -6
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +68 -10
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +28 -1
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +40 -14
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +210 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +153 -10
- package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +136 -0
- package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +22 -6
- package/resources/claude-customizations/.claude/settings.json +22 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +0 -1
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +76 -2
- package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +5 -3
- package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +69 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +96 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +77 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +64 -0
- package/resources/claude-customizations/pack-manifests/core.json +65 -0
- package/resources/claude-customizations/pack-manifests/csharp-legacy.json +11 -0
- package/resources/claude-customizations/pack-manifests/csharp-modern.json +12 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +14 -0
- package/resources/claude-customizations/pack-manifests/python.json +14 -0
- package/resources/claude-customizations/pack-manifests/typescript.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +30 -13
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +113 -10
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +33 -8
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +12 -7
- package/resources/codex-and-agents-customizations/.codex/config.toml +65 -6
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +303 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +300 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +148 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +336 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +83 -17
- package/resources/config/orchestration-routing.json +2 -10
- package/resources/customizations/.github/agents/pr-author.agent.md +25 -0
- package/resources/customizations/.github/agents/task-researcher.agent.md +4 -4
- package/resources/customizations/.github/prompts/fillout-prd-feature.prompt.md +1 -1
- package/resources/customizations/.github/prompts/research-issue.prompt.md +4 -3
- package/resources/scripts/dev_tools/push_down_claude_customizations.py +253 -224
- package/resources/scripts/dev_tools/push_down_claude_filesystem.py +472 -0
- package/resources/scripts/dev_tools/push_down_claude_pack_selection.py +401 -0
- package/resources/scripts/dev_tools/validate_orchestrator_state.py +106 -29
- package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +24 -0
- package/resources/templates/push_down_claude_customizations.py +128 -304
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Blocks implementation writes before Issue #232 orchestration readiness exists.
|
|
4
|
+
#>
|
|
5
|
+
[CmdletBinding()]
|
|
6
|
+
param()
|
|
7
|
+
|
|
8
|
+
$script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
9
|
+
$script:Issue232FeatureFolder = 'docs/features/active/2026-06-24-harden-orchestrate-skill-232'
|
|
10
|
+
|
|
11
|
+
function ConvertFrom-CheckpointJson {
|
|
12
|
+
[CmdletBinding()]
|
|
13
|
+
param([Parameter(Mandatory)][string] $Json)
|
|
14
|
+
|
|
15
|
+
return $Json | ConvertFrom-Json -ErrorAction Stop
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Get-StringProperty {
|
|
19
|
+
[CmdletBinding()]
|
|
20
|
+
[OutputType([string])]
|
|
21
|
+
param(
|
|
22
|
+
[Parameter(Mandatory)][AllowNull()] $Value,
|
|
23
|
+
[Parameter(Mandatory)][string] $Name
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if ($null -eq $Value -or -not ($Value.PSObject.Properties.Name -contains $Name)) {
|
|
27
|
+
return ''
|
|
28
|
+
}
|
|
29
|
+
return ([string]$Value.$Name).Trim()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Test-DocumentationOrEvidencePath {
|
|
33
|
+
[CmdletBinding()]
|
|
34
|
+
[OutputType([bool])]
|
|
35
|
+
param([Parameter(Mandatory)][string] $NormalizedPath)
|
|
36
|
+
|
|
37
|
+
return $NormalizedPath.StartsWith($script:Issue232FeatureFolder + '/')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Test-ImplementationPath {
|
|
41
|
+
[CmdletBinding()]
|
|
42
|
+
[OutputType([bool])]
|
|
43
|
+
param([Parameter(Mandatory)][string] $NormalizedPath)
|
|
44
|
+
|
|
45
|
+
if (Test-DocumentationOrEvidencePath -NormalizedPath $NormalizedPath) {
|
|
46
|
+
return $false
|
|
47
|
+
}
|
|
48
|
+
if ($NormalizedPath -eq $script:CheckpointPath) {
|
|
49
|
+
return $false
|
|
50
|
+
}
|
|
51
|
+
return $NormalizedPath -match '\.(py|ps1|psm1|ts|tsx|js|jsx|cs|json|yml|yaml)$'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Test-Issue232OrchestrationReady {
|
|
55
|
+
[CmdletBinding()]
|
|
56
|
+
[OutputType([bool])]
|
|
57
|
+
param([Parameter(Mandatory)][AllowNull()] $Payload)
|
|
58
|
+
|
|
59
|
+
if ($null -eq $Payload) {
|
|
60
|
+
return $false
|
|
61
|
+
}
|
|
62
|
+
$issueNum = Get-StringProperty -Value $Payload -Name 'issue-num'
|
|
63
|
+
$featureFolder = Get-StringProperty -Value $Payload -Name 'feature-folder'
|
|
64
|
+
$routeId = Get-StringProperty -Value $Payload -Name 'route_id'
|
|
65
|
+
if (-not $routeId) {
|
|
66
|
+
$routeId = Get-StringProperty -Value $Payload -Name 'path_selected'
|
|
67
|
+
}
|
|
68
|
+
$lifecycleReady = $false
|
|
69
|
+
if ($Payload.PSObject.Properties.Name -contains 'lifecycle_ready') {
|
|
70
|
+
$lifecycleReady = [bool]$Payload.lifecycle_ready
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
$issueNum -eq '232' -and
|
|
75
|
+
$featureFolder -eq $script:Issue232FeatureFolder -and
|
|
76
|
+
$routeId -and
|
|
77
|
+
$lifecycleReady
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function Get-CheckpointContent {
|
|
82
|
+
[CmdletBinding()]
|
|
83
|
+
[OutputType([string])]
|
|
84
|
+
param()
|
|
85
|
+
|
|
86
|
+
if (-not (Test-Path -LiteralPath $script:CheckpointPath)) {
|
|
87
|
+
return ''
|
|
88
|
+
}
|
|
89
|
+
return Get-Content -Raw -LiteralPath $script:CheckpointPath
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Invoke-OrchestrationPreimplementationGateDecision {
|
|
93
|
+
[CmdletBinding()]
|
|
94
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
95
|
+
param(
|
|
96
|
+
[string] $ToolInputRaw,
|
|
97
|
+
[string] $CheckpointRaw
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if (-not $ToolInputRaw) {
|
|
101
|
+
return [ordered]@{ decision = 'allow' }
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
105
|
+
} catch {
|
|
106
|
+
throw "enforce-orchestration-preimplementation-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
$filePath = $toolInput.file_path
|
|
110
|
+
if (-not $filePath) {
|
|
111
|
+
return [ordered]@{ decision = 'allow' }
|
|
112
|
+
}
|
|
113
|
+
$normalized = ([string]$filePath) -replace '\\', '/'
|
|
114
|
+
if (-not (Test-ImplementationPath -NormalizedPath $normalized)) {
|
|
115
|
+
return [ordered]@{ decision = 'allow' }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (-not $CheckpointRaw) {
|
|
119
|
+
$CheckpointRaw = Get-CheckpointContent
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
$checkpoint = ConvertFrom-CheckpointJson -Json $CheckpointRaw
|
|
123
|
+
} catch {
|
|
124
|
+
$checkpoint = $null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (Test-Issue232OrchestrationReady -Payload $checkpoint) {
|
|
128
|
+
return [ordered]@{ decision = 'allow' }
|
|
129
|
+
}
|
|
130
|
+
return [ordered]@{
|
|
131
|
+
decision = 'block'
|
|
132
|
+
reason = 'PREIMPLEMENTATION_GATE_BLOCKED: Issue #232 implementation writes require artifacts/orchestration/orchestrator-state.json to contain route metadata, lifecycle readiness, and checkpoint state before implementation begins.'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
$decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
142
|
+
} catch {
|
|
143
|
+
Write-Error $_
|
|
144
|
+
exit 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$decision | ConvertTo-Json -Compress | Write-Output
|
|
148
|
+
exit 0
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Converted hook
|
|
2
|
+
# Review the generated hook behavior before enabling it.
|
|
3
|
+
|
|
4
|
+
<#
|
|
5
|
+
.SYNOPSIS
|
|
6
|
+
Pre-tool-use hook that enforces the pr-author skill is used before gh pr create or gh pr edit.
|
|
7
|
+
|
|
8
|
+
.DESCRIPTION
|
|
9
|
+
Invoked by the Claude Code PreToolUse hook before any Bash command runs. Reads
|
|
10
|
+
tool input JSON from the CLAUDE_TOOL_INPUT environment variable, inspects the
|
|
11
|
+
attempted command, and blocks gh pr create / gh pr edit commands that bypass the
|
|
12
|
+
pr-author skill workflow.
|
|
13
|
+
|
|
14
|
+
Required sequence:
|
|
15
|
+
1. mcp__drm-copilot__collect_pr_context writes artifacts/pr_context.summary.txt
|
|
16
|
+
2. pr-author skill reads that file and writes artifacts/pr_body_<N>.md
|
|
17
|
+
3. gh pr create --body-file artifacts/pr_body_<N>.md
|
|
18
|
+
(or gh pr edit --body-file ...)
|
|
19
|
+
|
|
20
|
+
Block cases:
|
|
21
|
+
Case A - gh pr create or gh pr edit with --body (inline, no --body-file): blocked.
|
|
22
|
+
Case B - gh pr create with neither --body nor --body-file: blocked.
|
|
23
|
+
Case C - gh pr create or gh pr edit with --body-file but context artifact absent: blocked.
|
|
24
|
+
Case D - --body-file present, context artifact present, but the authorization sentinel
|
|
25
|
+
artifacts/pr_author_authorization.json is absent or empty: blocked
|
|
26
|
+
(PR_AGENT_AUTHORIZATION_MISSING).
|
|
27
|
+
Malformed - sentinel present but not valid JSON, or issued_at missing/unparseable: blocked
|
|
28
|
+
(PR_AGENT_AUTHORIZATION_MALFORMED).
|
|
29
|
+
Case E - sentinel valid JSON but issued_by != "pr-author": blocked
|
|
30
|
+
(PR_AGENT_AUTHORIZATION_INVALID).
|
|
31
|
+
Case F - sentinel issued_by == "pr-author" but elapsed time since issued_at exceeds
|
|
32
|
+
ttl_seconds: blocked (PR_AGENT_AUTHORIZATION_EXPIRED).
|
|
33
|
+
|
|
34
|
+
Authorization sentinel decision order on the --body-file-with-context path:
|
|
35
|
+
missing -> malformed -> invalid issuer -> expired -> allow.
|
|
36
|
+
|
|
37
|
+
.NOTES
|
|
38
|
+
Compatible with PowerShell 7+. No external module dependencies.
|
|
39
|
+
|
|
40
|
+
Enforcement strength: the authorization sentinel is a policy guardrail, not a cryptographic
|
|
41
|
+
or security control. Any actor with Write access to artifacts/ can forge
|
|
42
|
+
artifacts/pr_author_authorization.json, because all agents share the same filesystem and the
|
|
43
|
+
runtime exposes no native agent-identity signal at Bash PreToolUse time. The mechanism prevents
|
|
44
|
+
accidental bypass and requires a deliberate, documented act to circumvent. It MUST NOT be
|
|
45
|
+
described as tamper-proof or as a security boundary.
|
|
46
|
+
#>
|
|
47
|
+
[CmdletBinding()]
|
|
48
|
+
param()
|
|
49
|
+
|
|
50
|
+
$script:PrContextArtifactPath = 'artifacts/pr_context.summary.txt'
|
|
51
|
+
$script:PrAuthorAuthorizationPath = 'artifacts/pr_author_authorization.json'
|
|
52
|
+
$script:PrAuthorAuthorizationTtlSeconds = 120
|
|
53
|
+
|
|
54
|
+
function Get-PrContextArtifactExistence {
|
|
55
|
+
<#
|
|
56
|
+
.SYNOPSIS
|
|
57
|
+
Wrapper around Test-Path for the PR context artifact. Tests mock this function.
|
|
58
|
+
.OUTPUTS
|
|
59
|
+
System.Boolean
|
|
60
|
+
#>
|
|
61
|
+
[CmdletBinding()]
|
|
62
|
+
[OutputType([bool])]
|
|
63
|
+
param()
|
|
64
|
+
|
|
65
|
+
return [bool](Test-Path -LiteralPath $script:PrContextArtifactPath)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Get-PrAuthorAuthorizationContent {
|
|
69
|
+
<#
|
|
70
|
+
.SYNOPSIS
|
|
71
|
+
Read the raw text of the authorization sentinel. Tests mock this function (read seam).
|
|
72
|
+
.DESCRIPTION
|
|
73
|
+
Returns the raw text content of artifacts/pr_author_authorization.json, or $null when the
|
|
74
|
+
sentinel file is absent. This is the injectable boundary for sentinel content in tests; no
|
|
75
|
+
test writes the sentinel file to disk.
|
|
76
|
+
.OUTPUTS
|
|
77
|
+
System.String or $null
|
|
78
|
+
#>
|
|
79
|
+
[CmdletBinding()]
|
|
80
|
+
[OutputType([string])]
|
|
81
|
+
param()
|
|
82
|
+
|
|
83
|
+
if (-not (Test-Path -LiteralPath $script:PrAuthorAuthorizationPath)) {
|
|
84
|
+
return $null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (Get-Content -LiteralPath $script:PrAuthorAuthorizationPath -Raw)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Get-CurrentDateTimeUtc {
|
|
91
|
+
<#
|
|
92
|
+
.SYNOPSIS
|
|
93
|
+
Return the current UTC time. Tests mock this function (clock seam) for time-travel scenarios.
|
|
94
|
+
.OUTPUTS
|
|
95
|
+
System.DateTime
|
|
96
|
+
#>
|
|
97
|
+
[CmdletBinding()]
|
|
98
|
+
[OutputType([datetime])]
|
|
99
|
+
param()
|
|
100
|
+
|
|
101
|
+
return [DateTime]::UtcNow
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function Test-PrAuthorAuthorization {
|
|
105
|
+
<#
|
|
106
|
+
.SYNOPSIS
|
|
107
|
+
Validate the authorization sentinel and return a block-reason string, or $null when authorized.
|
|
108
|
+
.DESCRIPTION
|
|
109
|
+
Reads the sentinel via Get-PrAuthorAuthorizationContent and computes elapsed time via
|
|
110
|
+
Get-CurrentDateTimeUtc. Applies the decision order missing -> malformed -> invalid issuer ->
|
|
111
|
+
expired -> allow (spec FR-2 step 3):
|
|
112
|
+
- $null/empty contents -> PR_AGENT_AUTHORIZATION_MISSING
|
|
113
|
+
- not valid JSON, or issued_at
|
|
114
|
+
missing/unparseable -> PR_AGENT_AUTHORIZATION_MALFORMED
|
|
115
|
+
- issued_by != "pr-author" -> PR_AGENT_AUTHORIZATION_INVALID
|
|
116
|
+
- elapsed seconds > ttl_seconds -> PR_AGENT_AUTHORIZATION_EXPIRED
|
|
117
|
+
- all checks pass -> $null (allow)
|
|
118
|
+
The ttl_seconds value defaults to the named constant when absent from the sentinel.
|
|
119
|
+
This is a policy guardrail, not a cryptographic control; the sentinel is forgeable by any
|
|
120
|
+
actor with Write access to artifacts/.
|
|
121
|
+
.OUTPUTS
|
|
122
|
+
System.String or $null
|
|
123
|
+
#>
|
|
124
|
+
[CmdletBinding()]
|
|
125
|
+
[OutputType([string])]
|
|
126
|
+
param()
|
|
127
|
+
|
|
128
|
+
$contents = Get-PrAuthorAuthorizationContent
|
|
129
|
+
|
|
130
|
+
if ([string]::IsNullOrWhiteSpace($contents)) {
|
|
131
|
+
return "PR_AGENT_AUTHORIZATION_MISSING: ``$script:PrAuthorAuthorizationPath`` is absent or empty. The pr-author agent must write the authorization sentinel immediately before issuing ``gh pr create``/``gh pr edit --body*``."
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
$sentinel = $contents | ConvertFrom-Json -ErrorAction Stop
|
|
136
|
+
} catch {
|
|
137
|
+
return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` is not valid JSON. The pr-author agent must write a well-formed sentinel with ``issued_by``, ``issued_at``, and ``ttl_seconds``."
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
$issuedAtRaw = $sentinel.issued_at
|
|
141
|
+
if ([string]::IsNullOrWhiteSpace([string]$issuedAtRaw)) {
|
|
142
|
+
return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` is missing ``issued_at``. The pr-author agent must record a UTC ISO-8601 ``issued_at`` timestamp."
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
$issuedAt = [DateTime]::MinValue
|
|
146
|
+
$parsed = [DateTime]::TryParse(
|
|
147
|
+
[string]$issuedAtRaw,
|
|
148
|
+
[System.Globalization.CultureInfo]::InvariantCulture,
|
|
149
|
+
[System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
|
|
150
|
+
[ref] $issuedAt)
|
|
151
|
+
if (-not $parsed) {
|
|
152
|
+
return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` has an unparseable ``issued_at``. The pr-author agent must record a UTC ISO-8601 ``issued_at`` timestamp."
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if ($sentinel.issued_by -ne 'pr-author') {
|
|
156
|
+
return "PR_AGENT_AUTHORIZATION_INVALID: ``$script:PrAuthorAuthorizationPath`` ``issued_by`` is not ``pr-author``. Only the pr-author agent may authorize PR creation or body edits."
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
$ttlSeconds = $script:PrAuthorAuthorizationTtlSeconds
|
|
160
|
+
if ($null -ne $sentinel.ttl_seconds) {
|
|
161
|
+
$candidateTtl = 0
|
|
162
|
+
if ([int]::TryParse([string]$sentinel.ttl_seconds, [ref] $candidateTtl)) {
|
|
163
|
+
$ttlSeconds = $candidateTtl
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
$elapsedSeconds = ((Get-CurrentDateTimeUtc) - $issuedAt).TotalSeconds
|
|
168
|
+
if ($elapsedSeconds -gt $ttlSeconds) {
|
|
169
|
+
return "PR_AGENT_AUTHORIZATION_EXPIRED: ``$script:PrAuthorAuthorizationPath`` issued $([math]::Round($elapsedSeconds)) s ago, exceeding the ${ttlSeconds}s TTL. The pr-author agent must write a fresh sentinel immediately before the ``gh`` command."
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return $null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function Get-PrAuthorBypassReason {
|
|
176
|
+
<#
|
|
177
|
+
.SYNOPSIS
|
|
178
|
+
Inspect the command text and return a block reason string, or $null when the command is allowed.
|
|
179
|
+
.DESCRIPTION
|
|
180
|
+
Returns PR_AUTHOR_SKILL_BLOCKED when gh pr create or gh pr edit is run with --body (inline,
|
|
181
|
+
no --body-file), or when gh pr create is run with no body flag at all. Returns
|
|
182
|
+
PR_CONTEXT_MISSING when --body-file is present but the context artifact
|
|
183
|
+
does not exist on disk. When --body-file is present and the context artifact exists, evaluates
|
|
184
|
+
the authorization sentinel via Test-PrAuthorAuthorization and returns its block reason
|
|
185
|
+
(PR_AGENT_AUTHORIZATION_*) when authorization fails. Returns $null for all allowed patterns.
|
|
186
|
+
Cases A, B, and C are evaluated first and unchanged; the sentinel check only extends the
|
|
187
|
+
previously-allowed --body-file-with-context path.
|
|
188
|
+
.PARAMETER CommandText
|
|
189
|
+
The Bash command text extracted from CLAUDE_TOOL_INPUT.
|
|
190
|
+
.PARAMETER ContextExists
|
|
191
|
+
Whether artifacts/pr_context.summary.txt currently exists on disk.
|
|
192
|
+
.OUTPUTS
|
|
193
|
+
System.String or $null
|
|
194
|
+
#>
|
|
195
|
+
[CmdletBinding()]
|
|
196
|
+
[OutputType([string])]
|
|
197
|
+
param(
|
|
198
|
+
[Parameter(Mandatory)]
|
|
199
|
+
[string] $CommandText,
|
|
200
|
+
|
|
201
|
+
[Parameter(Mandatory)]
|
|
202
|
+
[bool] $ContextExists
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Only act on gh pr create or gh pr edit subcommands.
|
|
206
|
+
$isPrCreate = $CommandText -match '(?i)\bgh\s+pr\s+create\b'
|
|
207
|
+
$isPrEdit = $CommandText -match '(?i)\bgh\s+pr\s+edit\b'
|
|
208
|
+
|
|
209
|
+
if (-not $isPrCreate -and -not $isPrEdit) {
|
|
210
|
+
return $null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
$hasBodyFile = $CommandText -match '(?i)--body-file\b'
|
|
214
|
+
$hasInlineBody = $CommandText -match '(?i)--body(?!-file)\b'
|
|
215
|
+
|
|
216
|
+
# Case A: gh pr create OR gh pr edit with inline --body (not --body-file). Evaluated before the
|
|
217
|
+
# gh pr edit no-body allow short-circuit so inline-body edits are blocked, not allowed.
|
|
218
|
+
if (($isPrCreate -or $isPrEdit) -and $hasInlineBody -and -not $hasBodyFile) {
|
|
219
|
+
return "PR_AUTHOR_SKILL_BLOCKED: ``gh pr create`` and ``gh pr edit`` must use ``--body-file`` with a file produced by the pr-author skill from ``$script:PrContextArtifactPath``. Run ``mcp__drm-copilot__collect_pr_context`` to generate the context file, apply the pr-author skill to produce ``artifacts/pr_body_<N>.md``, then pass that file via ``--body-file``."
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if ($isPrCreate) {
|
|
223
|
+
# Case B: gh pr create with no body flag at all.
|
|
224
|
+
if (-not $hasInlineBody -and -not $hasBodyFile) {
|
|
225
|
+
return "PR_AUTHOR_SKILL_BLOCKED: New PRs require ``--body-file``. Run ``mcp__drm-copilot__collect_pr_context`` to generate ``$script:PrContextArtifactPath``, apply the pr-author skill to produce ``artifacts/pr_body_<N>.md``, then pass that file via ``--body-file``."
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if ($isPrEdit) {
|
|
230
|
+
# gh pr edit with no --body or --body-file (e.g., --title, --add-label, --reviewer) is allowed.
|
|
231
|
+
if (-not $hasInlineBody -and -not $hasBodyFile) {
|
|
232
|
+
return $null
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Case C: --body-file present but context artifact is absent.
|
|
237
|
+
if ($hasBodyFile -and -not $ContextExists) {
|
|
238
|
+
return "PR_CONTEXT_MISSING: ``$script:PrContextArtifactPath`` is absent. Run ``mcp__drm-copilot__collect_pr_context`` before creating or editing the PR body."
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Cases D/E/F and malformed: --body-file present and context artifact exists; verify the
|
|
242
|
+
# authorization sentinel. This extends, and does not replace, the previously-allowed path.
|
|
243
|
+
if ($hasBodyFile -and $ContextExists) {
|
|
244
|
+
$authorizationReason = Test-PrAuthorAuthorization
|
|
245
|
+
if ($authorizationReason) {
|
|
246
|
+
return $authorizationReason
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return $null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function Invoke-PrAuthorSkillDecision {
|
|
254
|
+
<#
|
|
255
|
+
.SYNOPSIS
|
|
256
|
+
Parse CLAUDE_TOOL_INPUT and return an allow-or-block decision.
|
|
257
|
+
.PARAMETER ToolInputRaw
|
|
258
|
+
The raw JSON tool payload supplied by Claude Code.
|
|
259
|
+
.OUTPUTS
|
|
260
|
+
System.Collections.Specialized.OrderedDictionary
|
|
261
|
+
.NOTES
|
|
262
|
+
Missing tool input or missing command text is treated as allow.
|
|
263
|
+
#>
|
|
264
|
+
[CmdletBinding()]
|
|
265
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
266
|
+
param(
|
|
267
|
+
[string] $ToolInputRaw
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if (-not $ToolInputRaw) {
|
|
271
|
+
return [ordered]@{ decision = 'allow' }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
276
|
+
} catch {
|
|
277
|
+
throw "enforce-pr-author-skill hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
$commandText = $toolInput.command
|
|
281
|
+
if (-not $commandText) {
|
|
282
|
+
return [ordered]@{ decision = 'allow' }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
$contextExists = Get-PrContextArtifactExistence
|
|
286
|
+
$reason = Get-PrAuthorBypassReason -CommandText $commandText -ContextExists $contextExists
|
|
287
|
+
|
|
288
|
+
if ($reason) {
|
|
289
|
+
return [ordered]@{
|
|
290
|
+
decision = 'block'
|
|
291
|
+
reason = $reason
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return [ordered]@{ decision = 'allow' }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function Test-PrAuthorBypassRequired {
|
|
299
|
+
<#
|
|
300
|
+
.SYNOPSIS
|
|
301
|
+
Return $true when a Bash command requires the pr-author skill to run first.
|
|
302
|
+
.PARAMETER CommandText
|
|
303
|
+
The Bash command text extracted from CLAUDE_TOOL_INPUT.
|
|
304
|
+
.PARAMETER ContextExists
|
|
305
|
+
Whether artifacts/pr_context.summary.txt currently exists on disk.
|
|
306
|
+
.OUTPUTS
|
|
307
|
+
System.Boolean
|
|
308
|
+
#>
|
|
309
|
+
[CmdletBinding()]
|
|
310
|
+
[OutputType([bool])]
|
|
311
|
+
param(
|
|
312
|
+
[Parameter(Mandatory)]
|
|
313
|
+
[string] $CommandText,
|
|
314
|
+
|
|
315
|
+
[Parameter(Mandatory)]
|
|
316
|
+
[bool] $ContextExists
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return ($null -ne (Get-PrAuthorBypassReason -CommandText $CommandText -ContextExists $ContextExists))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Allow dot-sourcing in tests without executing the entrypoint.
|
|
323
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
$decision = Invoke-PrAuthorSkillDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
329
|
+
} catch {
|
|
330
|
+
Write-Error $_
|
|
331
|
+
exit 1
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
$decision | ConvertTo-Json -Compress | Write-Output
|
|
335
|
+
|
|
336
|
+
exit 0
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
# Converted hook
|
|
2
|
-
# Review the generated hook behavior before enabling it.
|
|
3
|
-
|
|
4
1
|
<#
|
|
5
2
|
.SYNOPSIS
|
|
6
3
|
Pre-tool-use hook that blocks Bash promotion-script bypass attempts.
|
|
@@ -11,12 +8,18 @@
|
|
|
11
8
|
variable, inspects the attempted command text, and blocks direct promotion
|
|
12
9
|
script execution that would bypass the repository's MCP-only promotion path.
|
|
13
10
|
|
|
14
|
-
Forbidden command tokens:
|
|
11
|
+
Forbidden command tokens (legacy promotion-script bypass):
|
|
15
12
|
- new-potential-entry.ps1
|
|
16
13
|
- new_potential_bug_entry
|
|
17
14
|
- potential_to_issue
|
|
18
15
|
- new_active_feature_folder
|
|
19
16
|
|
|
17
|
+
Forbidden gh-CLI patterns (raw GitHub issue creation bypass):
|
|
18
|
+
- gh issue create (with any flag suffix)
|
|
19
|
+
- gh issue new
|
|
20
|
+
- gh api against repos/<owner>/<repo>/issues with explicit POST method
|
|
21
|
+
(-X POST or --method POST)
|
|
22
|
+
|
|
20
23
|
The hook is read-only: it inspects the attempted command and emits a JSON
|
|
21
24
|
allow-or-block decision without mutating the command text.
|
|
22
25
|
|
|
@@ -28,10 +31,12 @@ param()
|
|
|
28
31
|
|
|
29
32
|
$script:PromotionMcpOnlyBlockedReason = 'PROMOTION_MCP_ONLY_BLOCKED: Direct Bash promotion-script execution is not allowed in agent sessions. Use the drm-copilot MCP promotion tools instead.'
|
|
30
33
|
|
|
34
|
+
$script:PromotionMcpOnlyGhIssueBlockedReason = 'PROMOTION_MCP_ONLY_BLOCKED: Direct GitHub issue creation via `gh` bypasses the approved drm-copilot MCP promotion path (`mcp__drm-copilot__new_potential_entry` -> `mcp__drm-copilot__potential_to_issue` -> `mcp__drm-copilot__new_active_feature_folder`). Use those MCP tools instead.'
|
|
35
|
+
|
|
31
36
|
function Get-PromotionMcpOnlyBlockedReason {
|
|
32
37
|
<#
|
|
33
38
|
.SYNOPSIS
|
|
34
|
-
Return the canonical deny message for promotion-script bypass attempts.
|
|
39
|
+
Return the canonical deny message for legacy promotion-script bypass attempts.
|
|
35
40
|
.OUTPUTS
|
|
36
41
|
System.String
|
|
37
42
|
#>
|
|
@@ -42,24 +47,40 @@ function Get-PromotionMcpOnlyBlockedReason {
|
|
|
42
47
|
return $script:PromotionMcpOnlyBlockedReason
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
function
|
|
50
|
+
function Get-PromotionMcpOnlyGhIssueBlockedReason {
|
|
51
|
+
<#
|
|
52
|
+
.SYNOPSIS
|
|
53
|
+
Return the deny message for raw gh-CLI issue creation bypass attempts.
|
|
54
|
+
.OUTPUTS
|
|
55
|
+
System.String
|
|
56
|
+
#>
|
|
57
|
+
[CmdletBinding()]
|
|
58
|
+
[OutputType([string])]
|
|
59
|
+
param()
|
|
60
|
+
|
|
61
|
+
return $script:PromotionMcpOnlyGhIssueBlockedReason
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Get-PromotionBypassReason {
|
|
46
65
|
<#
|
|
47
66
|
.SYNOPSIS
|
|
48
|
-
|
|
67
|
+
Inspect the command text and return the specific deny reason, or $null when allowed.
|
|
68
|
+
.DESCRIPTION
|
|
69
|
+
Returns the legacy promotion-script reason when any forbidden token is present.
|
|
70
|
+
Returns the gh-CLI issue creation reason when a forbidden gh pattern is matched.
|
|
71
|
+
Returns $null when the command is allowed.
|
|
49
72
|
.PARAMETER CommandText
|
|
50
73
|
The Bash command text extracted from CLAUDE_TOOL_INPUT.
|
|
51
74
|
.OUTPUTS
|
|
52
|
-
System.
|
|
75
|
+
System.String or $null.
|
|
53
76
|
#>
|
|
54
77
|
[CmdletBinding()]
|
|
55
|
-
[OutputType([
|
|
78
|
+
[OutputType([string])]
|
|
56
79
|
param(
|
|
57
80
|
[Parameter(Mandatory)]
|
|
58
81
|
[string] $CommandText
|
|
59
82
|
)
|
|
60
83
|
|
|
61
|
-
# Inspect only the command text so the hook remains a narrow, non-mutating
|
|
62
|
-
# policy gate for direct promotion-script bypass attempts.
|
|
63
84
|
$forbiddenTokens = @(
|
|
64
85
|
'new-potential-entry.ps1',
|
|
65
86
|
'new_potential_bug_entry',
|
|
@@ -69,27 +90,71 @@ function Test-PromotionBypassToken {
|
|
|
69
90
|
|
|
70
91
|
foreach ($token in $forbiddenTokens) {
|
|
71
92
|
if ($CommandText.IndexOf($token, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
|
|
72
|
-
return
|
|
93
|
+
return (Get-PromotionMcpOnlyBlockedReason)
|
|
73
94
|
}
|
|
74
95
|
}
|
|
75
96
|
|
|
76
|
-
|
|
97
|
+
# `gh issue create` and `gh issue new` are direct bypasses of the MCP
|
|
98
|
+
# promotion path. Tolerate any flags after the subcommand.
|
|
99
|
+
if ($CommandText -match '(?i)\bgh\s+issue\s+(?:create|new)\b') {
|
|
100
|
+
return (Get-PromotionMcpOnlyGhIssueBlockedReason)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# `gh api repos/<owner>/<repo>/issues` is a write surface only when an
|
|
104
|
+
# explicit POST method is supplied. `gh api` defaults to GET, so we only
|
|
105
|
+
# block when -X POST or --method POST is present, to avoid false positives
|
|
106
|
+
# on issue read operations. Use a single regex with lookaheads against the
|
|
107
|
+
# whole command string.
|
|
108
|
+
$ghApiIssuesPostPattern = '(?i)(?=.*\bgh\s+api\b)(?=.*repos/[^/\s]+/[^/\s]+/issues(?:\b|/[^/\s]*$))(?=.*(?:-X\s+POST|--method\s+POST))'
|
|
109
|
+
if ($CommandText -match $ghApiIssuesPostPattern) {
|
|
110
|
+
return (Get-PromotionMcpOnlyGhIssueBlockedReason)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return $null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function Test-PromotionBypassToken {
|
|
117
|
+
<#
|
|
118
|
+
.SYNOPSIS
|
|
119
|
+
Return $true when a Bash command contains a forbidden promotion bypass pattern.
|
|
120
|
+
.PARAMETER CommandText
|
|
121
|
+
The Bash command text extracted from CLAUDE_TOOL_INPUT.
|
|
122
|
+
.OUTPUTS
|
|
123
|
+
System.Boolean
|
|
124
|
+
#>
|
|
125
|
+
[CmdletBinding()]
|
|
126
|
+
[OutputType([bool])]
|
|
127
|
+
param(
|
|
128
|
+
[Parameter(Mandatory)]
|
|
129
|
+
[string] $CommandText
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return ($null -ne (Get-PromotionBypassReason -CommandText $CommandText))
|
|
77
133
|
}
|
|
78
134
|
|
|
79
135
|
function Get-PromotionMcpOnlyBlockDecision {
|
|
80
136
|
<#
|
|
81
137
|
.SYNOPSIS
|
|
82
138
|
Construct the structured block decision for a forbidden Bash command.
|
|
139
|
+
.PARAMETER Reason
|
|
140
|
+
The specific deny reason to surface in the block decision. Defaults to the
|
|
141
|
+
legacy promotion-script reason for backward compatibility.
|
|
83
142
|
.OUTPUTS
|
|
84
143
|
System.Collections.Specialized.OrderedDictionary
|
|
85
144
|
#>
|
|
86
145
|
[CmdletBinding()]
|
|
87
146
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
88
|
-
param(
|
|
147
|
+
param(
|
|
148
|
+
[string] $Reason
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if (-not $Reason) {
|
|
152
|
+
$Reason = Get-PromotionMcpOnlyBlockedReason
|
|
153
|
+
}
|
|
89
154
|
|
|
90
155
|
return [ordered]@{
|
|
91
156
|
decision = 'block'
|
|
92
|
-
reason =
|
|
157
|
+
reason = $Reason
|
|
93
158
|
}
|
|
94
159
|
}
|
|
95
160
|
|
|
@@ -126,8 +191,9 @@ function Invoke-PromotionMcpOnlyDecision {
|
|
|
126
191
|
return [ordered]@{ decision = 'allow' }
|
|
127
192
|
}
|
|
128
193
|
|
|
129
|
-
|
|
130
|
-
|
|
194
|
+
$reason = Get-PromotionBypassReason -CommandText $commandText
|
|
195
|
+
if ($reason) {
|
|
196
|
+
return Get-PromotionMcpOnlyBlockDecision -Reason $reason
|
|
131
197
|
}
|
|
132
198
|
|
|
133
199
|
return [ordered]@{ decision = 'allow' }
|