diff --git a/.github/config/disclaimers.yml b/.github/config/disclaimers.yml deleted file mode 100644 index 5d0b117b5..000000000 --- a/.github/config/disclaimers.yml +++ /dev/null @@ -1,52 +0,0 @@ -# Disclaimer text configurations for AI-generated artifacts -# Referenced by instruction files and validated by CI -# -# Each planner section defines a full disclaimer and the artifacts it applies to. -# The validation script (Validate-PlannerArtifacts.ps1) reads this file as the -# single source of truth for Tier 2 disclaimer text. - -version: "1.0" - -disclaimers: - rai-planner: - id: rai-full-disclaimer - label: "RAI Planner Full Disclaimer" - scope: - - .github/instructions/rai-planning/** - applies-to: - - handoff-summary - - compact-handoff-summary - text: >- - > **Disclaimer** — This agent is an assistive tool only. It does not - provide legal, regulatory, or compliance advice and does not replace - Responsible AI review boards, ethics committees, legal counsel, - compliance teams, or other qualified human reviewers. The output consists - of suggested actions and considerations to support a user's own internal - review and decision‑making. All RAI assessments, risk classification - screenings, security models, and mitigation recommendations generated - by this tool must be independently reviewed and validated by appropriate - legal and compliance reviewers before use. Outputs from this tool do not - constitute legal approval, compliance certification, or regulatory - sign‑off. - - sssc-planner: - id: sssc-full-disclaimer - label: "SSSC Planner Full Disclaimer" - scope: - - .github/instructions/security/sssc-*.instructions.md - applies-to: - - handoff-summary - - compact-handoff-summary - text: >- - > **Disclaimer** — This agent is an assistive tool only. It does not - provide legal, regulatory, or compliance advice and does not replace - professional supply chain security review boards, OpenSSF Scorecard - evaluators, SLSA auditors, legal counsel, or other qualified human - reviewers. The output consists of suggested actions and considerations - to support a user's own internal supply chain security review and - decision‑making. All supply chain assessments, gap analyses, backlog - items, and mitigation recommendations generated by this tool must be - independently reviewed and validated by appropriate security and - compliance reviewers before use. Outputs from this tool do not - constitute security approval, compliance certification, or regulatory - sign‑off. diff --git a/.github/instructions/security/identity.instructions.md b/.github/instructions/security/identity.instructions.md index 505e5c0cd..7e6ec03ea 100644 --- a/.github/instructions/security/identity.instructions.md +++ b/.github/instructions/security/identity.instructions.md @@ -16,6 +16,23 @@ Core responsibilities: Voice: clear, methodical, and security-focused. Communicate with professional authority while keeping guidance accessible and actionable. +## Disclaimer and Attribution Protocol + +### Session Start Display + +On the first turn of any Security Planner session, display the canonical Security Planning disclaimer block defined in [.github/instructions/shared/disclaimer-language.instructions.md](../shared/disclaimer-language.instructions.md) verbatim. Record the display by setting `state.disclaimerShownAt` to an ISO 8601 timestamp. Do not advance to any phase work before the disclaimer is shown for the session. + +### Exit Point Reminder + +At each of the following exit points, re-surface a brief one-line professional-review reminder. Use the canonical wording in [.github/instructions/shared/disclaimer-language.instructions.md](../shared/disclaimer-language.instructions.md) (Security Planning section) for the reminder text. + +1. **Phase 6 completion (handoff success path)** — Display the reminder immediately before presenting the final handoff summary. +2. **Compact handoff** — Display the reminder when the orchestrator hands off to ADO or GitHub backlog workflows. +3. **Error exit** — Display the reminder on any unrecoverable error path before terminating the session. +4. **User-initiated exit** — Display the reminder when the user explicitly stops the session or switches agents. + +Each reminder must state that the generated plan is AI-assisted and requires professional security review before execution. + ## Six-Phase Definitions Each phase has entry criteria, activities, exit criteria, artifacts produced, and a defined transition. diff --git a/.github/instructions/shared/disclaimer-language.instructions.md b/.github/instructions/shared/disclaimer-language.instructions.md index d0d3b11da..81142e716 100644 --- a/.github/instructions/shared/disclaimer-language.instructions.md +++ b/.github/instructions/shared/disclaimer-language.instructions.md @@ -7,6 +7,20 @@ applyTo: '**/.copilot-tracking/rai-plans/**, **/.copilot-tracking/security-plans Planning agents that generate assessments requiring professional review display a CAUTION block during startup. Each section contains the verbatim disclaimer for the corresponding planner. Prompt files and agents reference the appropriate section via `#file:` to ensure consistent presentation across all entry points. + + + ## RAI Planning > [!CAUTION] diff --git a/plugins/hve-core-all/README.md b/plugins/hve-core-all/README.md index b10af4ba9..93024fc02 100644 --- a/plugins/hve-core-all/README.md +++ b/plugins/hve-core-all/README.md @@ -141,10 +141,10 @@ Use this edition when you want access to everything without choosing a focused c | **security-review-llm** | Runs OWASP LLM and Agentic vulnerability assessments with codebase profiling for context | | **security-review-sbd** | Runs a Secure by Design principles assessment based on UK and Australian government guidance | | **security-review-web** | Runs an OWASP Top 10 web vulnerability assessment without codebase profiling | -| **sssc-capture** | Start a new SSSC assessment via guided conversation using the SSSC Planner agent in capture mode | -| **sssc-from-brd** | Start an SSSC assessment from existing BRD artifacts using the SSSC Planner agent | -| **sssc-from-prd** | Start an SSSC assessment from existing PRD artifacts using the SSSC Planner agent | -| **sssc-from-security-plan** | Extend a Security Planner assessment with supply chain coverage using the SSSC Planner agent | +| **sssc-capture** | Initiate supply chain security planning from existing knowledge using the SSSC Planner agent in capture mode | +| **sssc-from-brd** | Initiate supply chain security planning from existing BRD artifacts using the SSSC Planner agent in from-brd mode | +| **sssc-from-prd** | Initiate supply chain security planning from existing PRD artifacts using the SSSC Planner agent in from-prd mode | +| **sssc-from-security-plan** | Extend a Security Planner assessment with supply chain coverage using the SSSC Planner agent in from-security-plan mode | | **synth-data-generate** | Generate comprehensive synthetic data for any specified subject with realistic patterns and relationships | | **task-challenge** | Adversarial What/Why/How interrogation of completed implementation artifacts | | **task-implement** | Locates and executes implementation plans using Task Implementor | @@ -255,7 +255,7 @@ Use this edition when you want access to everything without choosing a focused c | **security/sssc-gap-analysis** | Phase 4 gap comparison, adoption categorization, and effort sizing for SSSC Planner. | | **security/sssc-handoff** | Phase 6 backlog handoff protocol with Scorecard projections and dual-format output for SSSC Planner. | | **security/sssc-identity** | Identity and orchestration instructions for the SSSC Planner agent. Contains six-phase workflow, state.json schema, session recovery, and question cadence. | -| **security/sssc-standards** | Phase 3 OpenSSF Scorecard, SLSA, Best Practices Badge, Sigstore, and SBOM standards mapping for SSSC Planner. | +| **security/sssc-standards** | Phase 3 OpenSSF Scorecard, SLSA v1.0, OpenSSF Best Practices Badge, Sigstore (cosign), and NTIA SBOM minimum elements standards mapping for SSSC Planner. | | **security/standards-mapping** | Embedded OWASP and NIST security standards with researcher subagent delegation for CIS, WAF, CAF, and other runtime lookups | | **shared/coaching-patterns** | Shared exploration-first coaching patterns for planning agents (RAI, security, SSSC) adapted from Design Thinking research methods | | **shared/disclaimer-language** | Centralized disclaimer language for AI-assisted planning agents requiring professional review acknowledgment | diff --git a/plugins/project-planning/README.md b/plugins/project-planning/README.md index c6388c0e3..c92d69ec4 100644 --- a/plugins/project-planning/README.md +++ b/plugins/project-planning/README.md @@ -46,10 +46,10 @@ Create architecture decision records, requirements documents, and diagrams - all | **risk-register** | Creates a concise and well-structured qualitative risk register using a Probability × Impact (P×I) risk matrix. | | **security-capture** | Initiate security planning from existing notes or knowledge using the Security Planner agent in capture mode | | **security-plan-from-prd** | Initiate security planning from PRD/BRD artifacts using the Security Planner agent in from-prd mode | -| **sssc-capture** | Start a new SSSC assessment via guided conversation using the SSSC Planner agent in capture mode | -| **sssc-from-brd** | Start an SSSC assessment from existing BRD artifacts using the SSSC Planner agent | -| **sssc-from-prd** | Start an SSSC assessment from existing PRD artifacts using the SSSC Planner agent | -| **sssc-from-security-plan** | Extend a Security Planner assessment with supply chain coverage using the SSSC Planner agent | +| **sssc-capture** | Initiate supply chain security planning from existing knowledge using the SSSC Planner agent in capture mode | +| **sssc-from-brd** | Initiate supply chain security planning from existing BRD artifacts using the SSSC Planner agent in from-brd mode | +| **sssc-from-prd** | Initiate supply chain security planning from existing PRD artifacts using the SSSC Planner agent in from-prd mode | +| **sssc-from-security-plan** | Extend a Security Planner assessment with supply chain coverage using the SSSC Planner agent in from-security-plan mode | ### Instructions @@ -71,7 +71,7 @@ Create architecture decision records, requirements documents, and diagrams - all | **security/sssc-gap-analysis** | Phase 4 gap comparison, adoption categorization, and effort sizing for SSSC Planner. | | **security/sssc-handoff** | Phase 6 backlog handoff protocol with Scorecard projections and dual-format output for SSSC Planner. | | **security/sssc-identity** | Identity and orchestration instructions for the SSSC Planner agent. Contains six-phase workflow, state.json schema, session recovery, and question cadence. | -| **security/sssc-standards** | Phase 3 OpenSSF Scorecard, SLSA, Best Practices Badge, Sigstore, and SBOM standards mapping for SSSC Planner. | +| **security/sssc-standards** | Phase 3 OpenSSF Scorecard, SLSA v1.0, OpenSSF Best Practices Badge, Sigstore (cosign), and NTIA SBOM minimum elements standards mapping for SSSC Planner. | | **security/standards-mapping** | Embedded OWASP and NIST security standards with researcher subagent delegation for CIS, WAF, CAF, and other runtime lookups | | **shared/coaching-patterns** | Shared exploration-first coaching patterns for planning agents (RAI, security, SSSC) adapted from Design Thinking research methods | | **shared/disclaimer-language** | Centralized disclaimer language for AI-assisted planning agents requiring professional review acknowledgment | diff --git a/plugins/security/README.md b/plugins/security/README.md index d69beaf40..27ee2bd6f 100644 --- a/plugins/security/README.md +++ b/plugins/security/README.md @@ -46,10 +46,10 @@ Security review, planning, incident response, risk assessment, vulnerability ana | **security-review-llm** | Runs OWASP LLM and Agentic vulnerability assessments with codebase profiling for context | | **security-review-sbd** | Runs a Secure by Design principles assessment based on UK and Australian government guidance | | **security-review-web** | Runs an OWASP Top 10 web vulnerability assessment without codebase profiling | -| **sssc-capture** | Start a new SSSC assessment via guided conversation using the SSSC Planner agent in capture mode | -| **sssc-from-brd** | Start an SSSC assessment from existing BRD artifacts using the SSSC Planner agent | -| **sssc-from-prd** | Start an SSSC assessment from existing PRD artifacts using the SSSC Planner agent | -| **sssc-from-security-plan** | Extend a Security Planner assessment with supply chain coverage using the SSSC Planner agent | +| **sssc-capture** | Initiate supply chain security planning from existing knowledge using the SSSC Planner agent in capture mode | +| **sssc-from-brd** | Initiate supply chain security planning from existing BRD artifacts using the SSSC Planner agent in from-brd mode | +| **sssc-from-prd** | Initiate supply chain security planning from existing PRD artifacts using the SSSC Planner agent in from-prd mode | +| **sssc-from-security-plan** | Extend a Security Planner assessment with supply chain coverage using the SSSC Planner agent in from-security-plan mode | ### Instructions @@ -71,7 +71,7 @@ Security review, planning, incident response, risk assessment, vulnerability ana | **security/sssc-gap-analysis** | Phase 4 gap comparison, adoption categorization, and effort sizing for SSSC Planner. | | **security/sssc-handoff** | Phase 6 backlog handoff protocol with Scorecard projections and dual-format output for SSSC Planner. | | **security/sssc-identity** | Identity and orchestration instructions for the SSSC Planner agent. Contains six-phase workflow, state.json schema, session recovery, and question cadence. | -| **security/sssc-standards** | Phase 3 OpenSSF Scorecard, SLSA, Best Practices Badge, Sigstore, and SBOM standards mapping for SSSC Planner. | +| **security/sssc-standards** | Phase 3 OpenSSF Scorecard, SLSA v1.0, OpenSSF Best Practices Badge, Sigstore (cosign), and NTIA SBOM minimum elements standards mapping for SSSC Planner. | | **security/standards-mapping** | Embedded OWASP and NIST security standards with researcher subagent delegation for CIS, WAF, CAF, and other runtime lookups | | **shared/coaching-patterns** | Shared exploration-first coaching patterns for planning agents (RAI, security, SSSC) adapted from Design Thinking research methods | | **shared/disclaimer-language** | Centralized disclaimer language for AI-assisted planning agents requiring professional review acknowledgment | diff --git a/scripts/linting/Validate-PlannerArtifacts.ps1 b/scripts/linting/Validate-PlannerArtifacts.ps1 index 6e1c44927..798927a75 100644 --- a/scripts/linting/Validate-PlannerArtifacts.ps1 +++ b/scripts/linting/Validate-PlannerArtifacts.ps1 @@ -9,8 +9,9 @@ Validates AI artifact footer and disclaimer presence in instruction templates. .DESCRIPTION - Reads footer-with-review.yml and disclaimers.yml config files as the single source - of truth, then scans instruction files for required footer text based on artifact + Reads footer-with-review.yml for footer text and artifact-classification rules, and + parses shared/disclaimer-language.instructions.md as the canonical disclaimer source. + Scans instruction files for required footer and disclaimer text based on artifact classification rules. Outputs results as JSON and sets CI environment variables on failure. @@ -23,8 +24,9 @@ .PARAMETER FooterConfigPath Path to the footer-with-review.yml config file. -.PARAMETER DisclaimerConfigPath - Path to the disclaimers.yml config file. +.PARAMETER DisclaimerSourcePath + Path to the shared disclaimer-language instructions markdown file. The validator + parses H2 sections and their CAUTION blockquote bodies to derive disclaimer text. .PARAMETER FailOnMissing When specified, treats missing footers and disclaimers as validation failures. @@ -51,7 +53,7 @@ param( [string]$FooterConfigPath = '.github/config/footer-with-review.yml', [Parameter(Mandatory = $false)] - [string]$DisclaimerConfigPath = '.github/config/disclaimers.yml', + [string]$DisclaimerSourcePath = '.github/instructions/shared/disclaimer-language.instructions.md', [Parameter(Mandatory = $false)] [switch]$FailOnMissing, @@ -107,40 +109,78 @@ function Import-FooterConfig { return $config } -function Import-DisclaimerConfig { +function Import-DisclaimerSource { <# .SYNOPSIS - Loads and validates disclaimers.yml. + Parses the shared disclaimer-language instructions markdown as the canonical disclaimer source. - .PARAMETER ConfigPath - Absolute path to the disclaimer config YAML file. + .DESCRIPTION + Reads the markdown file, splits on H2 headings to identify planner sections, + and extracts the verbatim disclaimer prose from each section's CAUTION blockquote. + The first word of each heading (lowercased) maps to the planner key and disclaimer id + convention: 'RAI Planning' -> 'rai-planner' / 'rai-full-disclaimer'. + + .PARAMETER SourcePath + Absolute path to the disclaimer-language.instructions.md markdown file. .OUTPUTS - [hashtable] Parsed disclaimer config. + [hashtable] Parsed config shaped as @{ version; source; disclaimers = @{ key = @{ id; label; text } } }. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string]$ConfigPath + [string]$SourcePath ) - if (-not (Test-Path $ConfigPath)) { - throw "Disclaimer config not found: $ConfigPath" + if (-not (Test-Path $SourcePath)) { + throw "Disclaimer source not found: $SourcePath" } - $content = Get-Content -Path $ConfigPath -Raw -Encoding utf8 - $config = ConvertFrom-Yaml -Yaml $content + $raw = Get-Content -Path $SourcePath -Raw -Encoding utf8 + # Strip YAML frontmatter + $body = $raw -replace '(?s)\A---.*?\r?\n---\s*\r?\n', '' - if (-not $config.version) { - throw "Disclaimer config missing 'version' field: $ConfigPath" + $disclaimers = @{} + $sectionRegex = [regex]'(?ms)^##[ \t]+(?[^\r\n]+?)[ \t]*\r?\n(?.*?)(?=^##[ \t]|\z)' + $cautionRegex = [regex]'(?m)^>[ \t]*\[!CAUTION\][ \t]*\r?\n(?(?:^>.*\r?\n?)+)' + $prefixRegex = [regex]'^\*\*Disclaimer:?\*\*[\s:\-\u2014]*' + + foreach ($match in $sectionRegex.Matches($body)) { + $heading = $match.Groups['heading'].Value.Trim() + $sectionBody = $match.Groups['body'].Value + + $cautionMatch = $cautionRegex.Match($sectionBody) + if (-not $cautionMatch.Success) { continue } + + $blockLines = $cautionMatch.Groups['block'].Value -split '\r?\n' + $proseParts = foreach ($line in $blockLines) { + $stripped = $line -replace '^>[ \t]?', '' + if ($stripped.Trim().Length -gt 0) { $stripped.Trim() } + } + $prose = ($proseParts -join ' ').Trim() + $prose = $prefixRegex.Replace($prose, '', 1) + if ([string]::IsNullOrWhiteSpace($prose)) { continue } + + $slug = ($heading -split '\s+' | Select-Object -First 1).ToLowerInvariant() + $key = "$slug-planner" + $disclaimers[$key] = @{ + id = "$slug-full-disclaimer" + label = "$heading Disclaimer" + text = $prose + } } - if (-not $config.disclaimers) { - throw "Disclaimer config missing 'disclaimers' section: $ConfigPath" + + if ($disclaimers.Count -eq 0) { + throw "No disclaimer sections found in source: $SourcePath" } - return $config + return @{ + version = 'markdown-source' + source = $SourcePath + disclaimers = $disclaimers + } } function Get-FooterSearchText { @@ -318,7 +358,7 @@ function Test-AIArtifactCompliance { Parsed footer-with-review.yml config. .PARAMETER DisclaimerConfig - Parsed disclaimers.yml config. + Parsed disclaimer source (see Import-DisclaimerSource). .PARAMETER RepoRoot Repository root for relative path display. @@ -422,8 +462,8 @@ function Test-AIArtifactValidation { .PARAMETER FooterConfigPath Path to footer-with-review.yml relative to repo root. - .PARAMETER DisclaimerConfigPath - Path to disclaimers.yml relative to repo root. + .PARAMETER DisclaimerSourcePath + Path to the shared disclaimer-language instructions markdown file, relative to repo root. .PARAMETER FailOnMissing When set, missing footers cause a non-zero exit code. @@ -447,7 +487,7 @@ function Test-AIArtifactValidation { [string]$FooterConfigPath, [Parameter(Mandatory = $true)] - [string]$DisclaimerConfigPath, + [string]$DisclaimerSourcePath, [Parameter(Mandatory = $false)] [switch]$FailOnMissing, @@ -464,7 +504,7 @@ function Test-AIArtifactValidation { # Load configs $footerConfig = Import-FooterConfig -ConfigPath (Join-Path $repoRoot $FooterConfigPath) - $disclaimerConfig = Import-DisclaimerConfig -ConfigPath (Join-Path $repoRoot $DisclaimerConfigPath) + $disclaimerConfig = Import-DisclaimerSource -SourcePath (Join-Path $repoRoot $DisclaimerSourcePath) # Collect instruction files $allFiles = @() @@ -616,7 +656,7 @@ if ($MyInvocation.InvocationName -ne '.') { -Paths $Paths ` -ExcludePaths $ExcludePaths ` -FooterConfigPath $FooterConfigPath ` - -DisclaimerConfigPath $DisclaimerConfigPath ` + -DisclaimerSourcePath $DisclaimerSourcePath ` -FailOnMissing:$FailOnMissing ` -OutputPath $OutputPath diff --git a/scripts/tests/linting/Test-DisclaimerArtifacts.Tests.ps1 b/scripts/tests/linting/Test-DisclaimerArtifacts.Tests.ps1 new file mode 100644 index 000000000..bad532f5d --- /dev/null +++ b/scripts/tests/linting/Test-DisclaimerArtifacts.Tests.ps1 @@ -0,0 +1,62 @@ +#Requires -Modules Pester +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT +<# +.SYNOPSIS + Asserts disclaimer Note + reviewer checkbox patterns are present in both backlog + instruction files, and the `### Exit Point Reminder` heading with all four named + exit points exists in both planner identity files. +#> + +BeforeDiscovery { + $script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path + + $script:noteLiteral = '> **Note:** This work item was generated by the Security Planner AI agent. Review for accuracy and adjust before assigning to a person.' + $script:checkboxLiteral = '- [ ] Reviewed by a human security professional before execution' + + $script:backlogFiles = @( + (Join-Path $script:repoRoot '.github/instructions/security/backlog-handoff.instructions.md'), + (Join-Path $script:repoRoot '.github/instructions/security/sssc-backlog.instructions.md') + ) + $script:identityFiles = @( + (Join-Path $script:repoRoot '.github/instructions/security/identity.instructions.md'), + (Join-Path $script:repoRoot '.github/instructions/security/sssc-identity.instructions.md') + ) + + $script:exitPoints = @( + 'Phase 6 completion', + 'Compact handoff', + 'Error exit', + 'User-initiated exit' + ) +} + +Describe 'Backlog instruction files include disclaimer Note and reviewer checkbox in every WI template variant' { + It 'Backlog file <_> contains the Note literal at least twice' -ForEach $script:backlogFiles { + Test-Path $_ | Should -BeTrue + $content = Get-Content -Path $_ -Raw + $count = ([regex]::Matches($content, [regex]::Escape($script:noteLiteral))).Count + $count | Should -BeGreaterOrEqual 2 -Because "Note literal must appear in both ADO and GitHub WI variants in $_" + } + + It 'Backlog file <_> contains the reviewer checkbox literal at least twice' -ForEach $script:backlogFiles { + $content = Get-Content -Path $_ -Raw + $count = ([regex]::Matches($content, [regex]::Escape($script:checkboxLiteral))).Count + $count | Should -BeGreaterOrEqual 2 -Because "reviewer checkbox must appear in both ADO and GitHub WI variants in $_" + } +} + +Describe 'Identity files include Exit Point Reminder section with four named exit points' { + It 'Identity file <_> contains ### Exit Point Reminder heading' -ForEach $script:identityFiles { + Test-Path $_ | Should -BeTrue + $content = Get-Content -Path $_ -Raw + $content | Should -Match '(?m)^###\s+Exit Point Reminder\s*$' + } + + It 'Identity file <_> mentions all four exit-point labels' -ForEach $script:identityFiles { + $content = Get-Content -Path $_ -Raw + foreach ($label in $script:exitPoints) { + $content | Should -BeLike "*$label*" -Because "exit point '$label' must be listed in $_" + } + } +} diff --git a/scripts/tests/linting/Validate-PlannerArtifacts.Tests.ps1 b/scripts/tests/linting/Validate-PlannerArtifacts.Tests.ps1 index e715d77e3..394202063 100644 --- a/scripts/tests/linting/Validate-PlannerArtifacts.Tests.ps1 +++ b/scripts/tests/linting/Validate-PlannerArtifacts.Tests.ps1 @@ -20,7 +20,6 @@ BeforeAll { $script:Tier1Text = '> **Note** — The author created this content with assistance from AI. All outputs should be reviewed and validated before use.' $script:CheckboxText = '> - [ ] Reviewed and validated by a human reviewer' $script:DisclaimerText = '> **Disclaimer** — This agent is an assistive tool only. It does not provide legal, regulatory, or compliance advice.' - $script:SsscDisclaimerText = '> **Disclaimer** — This SSSC agent is an assistive tool only. It does not provide security, legal, or compliance advice.' # Create valid footer-with-review.yml $script:FooterConfigContent = @" @@ -53,7 +52,7 @@ artifact-classification: artifacts: - rai-review-summary - rai-handoff-with-disclaimer: + human-facing-with-disclaimer: required-footers: - ai-content-note - human-review-checkbox @@ -61,51 +60,30 @@ artifact-classification: disclaimer-ref: rai-full-disclaimer artifacts: - handoff-summary - - sssc-handoff-with-disclaimer: - scope: - - .github/instructions/security/sssc-*.instructions.md - required-footers: - - ai-content-note - - human-review-checkbox - requires-disclaimer: true - disclaimer-ref: sssc-full-disclaimer - artifacts: - - sssc-handoff-summary "@ - # Create valid disclaimers.yml - $script:DisclaimerConfigContent = @" -version: "1.0" + # Create valid disclaimer-language.instructions.md (markdown source of truth) + $script:DisclaimerSourceContent = @" +--- +description: "Test disclaimer source" +--- -disclaimers: - rai-planner: - id: rai-full-disclaimer - label: "RAI Planner Full Disclaimer" - applies-to: - - handoff-summary - text: >- - > **Disclaimer** — This agent is an assistive tool only. It does not - provide legal, regulatory, or compliance advice. - - sssc-planner: - id: sssc-full-disclaimer - label: "SSSC Planner Full Disclaimer" - applies-to: - - sssc-handoff-summary - text: >- - > **Disclaimer** — This SSSC agent is an assistive tool only. It does - not provide security, legal, or compliance advice. +# Disclaimer Language + +## RAI Planning + +> [!CAUTION] +> **Disclaimer:** This agent is an assistive tool only. It does not provide legal, regulatory, or compliance advice. "@ $script:FooterConfigPath = Join-Path $script:ConfigDir 'footer-with-review.yml' - $script:DisclaimerConfigPath = Join-Path $script:ConfigDir 'disclaimers.yml' Set-Content -Path $script:FooterConfigPath -Value $script:FooterConfigContent -Encoding utf8 - Set-Content -Path $script:DisclaimerConfigPath -Value $script:DisclaimerConfigContent -Encoding utf8 - # Pre-create bad config files for negative tests (avoids Pester-context YAML parsing issues) - $script:BadDisclaimerSectionPath = Join-Path $script:TempTestDir 'bad-disclaimer-section.yml' - [System.IO.File]::WriteAllText($script:BadDisclaimerSectionPath, "version: '1.0'`nplaceholder: true`n") + $script:DisclaimerSourceDir = Join-Path $script:TempTestDir '.github/instructions/shared' + New-Item -ItemType Directory -Path $script:DisclaimerSourceDir -Force | Out-Null + $script:DisclaimerSourcePath = Join-Path $script:DisclaimerSourceDir 'disclaimer-language.instructions.md' + $script:DisclaimerSourceRelative = '.github/instructions/shared/disclaimer-language.instructions.md' + Set-Content -Path $script:DisclaimerSourcePath -Value $script:DisclaimerSourceContent -Encoding utf8 } AfterAll { @@ -133,34 +111,129 @@ Describe 'Import-FooterConfig' -Tag 'Unit' { } } -Describe 'Import-DisclaimerConfig' -Tag 'Unit' { - It 'Loads a valid disclaimer config' { - $config = Import-DisclaimerConfig -ConfigPath $script:DisclaimerConfigPath - $config.version | Should -Be '1.0' +Describe 'Import-DisclaimerSource' -Tag 'Unit' { + It 'Loads a valid disclaimer markdown source' { + $config = Import-DisclaimerSource -SourcePath $script:DisclaimerSourcePath + $config.version | Should -Be 'markdown-source' $config.disclaimers | Should -Not -BeNullOrEmpty + $config.disclaimers['rai-planner'].id | Should -Be 'rai-full-disclaimer' + $config.disclaimers['rai-planner'].label | Should -Be 'RAI Planning Disclaimer' + } + + It 'Extracts disclaimer prose with the **Disclaimer** prefix stripped' { + $config = Import-DisclaimerSource -SourcePath $script:DisclaimerSourcePath + $config.disclaimers['rai-planner'].text | Should -BeLike 'This agent is an assistive tool only*' } It 'Throws when file does not exist' { - { Import-DisclaimerConfig -ConfigPath (Join-Path $script:TempTestDir 'nonexistent.yml') } | Should -Throw '*not found*' + { Import-DisclaimerSource -SourcePath (Join-Path $script:TempTestDir 'nonexistent.md') } | Should -Throw '*not found*' } - It 'Throws when version is missing' { - $badConfig = Join-Path $script:TempTestDir 'bad-disclaimer-version.yml' - @" -disclaimers: - rai-planner: - id: test -"@ | Set-Content -Path $badConfig -Encoding utf8 - { Import-DisclaimerConfig -ConfigPath $badConfig } | Should -Throw "*missing 'version'*" - } - - It 'Throws when disclaimers section is missing' { - $badConfig = Join-Path $script:TempTestDir 'bad-disclaimer-section.yml' - @" -version: '1.0' -placeholder: true -"@ | Set-Content -Path $badConfig -Encoding utf8 - { Import-DisclaimerConfig -ConfigPath $badConfig } | Should -Throw "*missing 'disclaimers'*" + It 'Throws when markdown contains no disclaimer sections' { + $badSource = Join-Path $script:TempTestDir 'no-sections.md' + Set-Content -Path $badSource -Value "# Empty`n`nNo H2 sections here." -Encoding utf8 + { Import-DisclaimerSource -SourcePath $badSource } | Should -Throw '*No disclaimer sections*' + } + + It 'Retains prose verbatim when the **Disclaimer:** prefix is absent' { + $source = Join-Path $script:TempTestDir 'no-prefix.md' + $content = @" +# Disclaimer Language + +## RAI Planning + +> [!CAUTION] +> This text has no bolded prefix and should be retained verbatim. +"@ + Set-Content -Path $source -Value $content -Encoding utf8 + $config = Import-DisclaimerSource -SourcePath $source + $config.disclaimers['rai-planner'].text | Should -Be 'This text has no bolded prefix and should be retained verbatim.' + } + + It 'Joins multi-line CAUTION blockquote prose with single spaces' { + $source = Join-Path $script:TempTestDir 'multi-line.md' + $content = @" +# Disclaimer Language + +## RAI Planning + +> [!CAUTION] +> **Disclaimer:** First sentence continues +> across multiple +> blockquote lines. +"@ + Set-Content -Path $source -Value $content -Encoding utf8 + $config = Import-DisclaimerSource -SourcePath $source + $config.disclaimers['rai-planner'].text | Should -Be 'First sentence continues across multiple blockquote lines.' + } + + It 'Uses only the first CAUTION block when an H2 section contains multiple' { + $source = Join-Path $script:TempTestDir 'multi-caution.md' + $content = @" +# Disclaimer Language + +## RAI Planning + +> [!CAUTION] +> **Disclaimer:** First caution wins. + +Some prose between blocks. + +> [!CAUTION] +> **Disclaimer:** Second caution is ignored. +"@ + Set-Content -Path $source -Value $content -Encoding utf8 + $config = Import-DisclaimerSource -SourcePath $source + $config.disclaimers['rai-planner'].text | Should -Be 'First caution wins.' + } + + It 'Parses multiple H2 sections into distinct planner keys using the first heading word as slug' { + $source = Join-Path $script:TempTestDir 'multi-section.md' + $content = @" +# Disclaimer Language + +## RAI Planning + +> [!CAUTION] +> **Disclaimer:** RAI text. + +## Security Engineering + +> [!CAUTION] +> **Disclaimer:** Security text. + +## SSSC + +> [!CAUTION] +> **Disclaimer:** SSSC text. +"@ + Set-Content -Path $source -Value $content -Encoding utf8 + $config = Import-DisclaimerSource -SourcePath $source + $config.disclaimers.Keys | Sort-Object | Should -Be @('rai-planner', 'security-planner', 'sssc-planner') + $config.disclaimers['security-planner'].id | Should -Be 'security-full-disclaimer' + $config.disclaimers['security-planner'].label | Should -Be 'Security Engineering Disclaimer' + $config.disclaimers['sssc-planner'].text | Should -Be 'SSSC text.' + } + + It 'Silently skips H2 sections that contain no CAUTION blockquote' { + $source = Join-Path $script:TempTestDir 'empty-section.md' + $content = @" +# Disclaimer Language + +## RAI Planning + +Some prose with no caution block here. + +## Security Engineering + +> [!CAUTION] +> **Disclaimer:** Security text only. +"@ + Set-Content -Path $source -Value $content -Encoding utf8 + $config = Import-DisclaimerSource -SourcePath $source + $config.disclaimers.ContainsKey('rai-planner') | Should -BeFalse + $config.disclaimers.Keys | Should -Be @('security-planner') + $config.disclaimers['security-planner'].text | Should -Be 'Security text only.' } } @@ -224,7 +297,7 @@ Describe 'Find-ArtifactReferences' -Tag 'Unit' { $refs[0].Tier | Should -Be 'agentic' } - It 'Finds rai-handoff-with-disclaimer artifact references' { + It 'Finds human-facing-with-disclaimer artifact references' { $refs = Find-ArtifactReferences -ArtifactClassification $script:FooterConfig.'artifact-classification' -RelativePath 'rai-planning/handoff-summary.md' $refs.Count | Should -Be 1 $refs[0].RequiresDisclaimer | Should -BeTrue @@ -256,7 +329,7 @@ Describe 'Find-ArtifactReferences' -Tag 'Unit' { Describe 'Test-AIArtifactCompliance' -Tag 'Unit' { BeforeAll { $script:FooterConfig = Import-FooterConfig -ConfigPath $script:FooterConfigPath - $script:DisclaimerConfig = Import-DisclaimerConfig -ConfigPath $script:DisclaimerConfigPath + $script:DisclaimerConfig = Import-DisclaimerSource -SourcePath $script:DisclaimerSourcePath } Context 'Agentic tier (Tier 1 only)' { @@ -339,7 +412,7 @@ $($script:Tier1Text) } } - Context 'Rai-handoff-with-disclaimer tier (Tier 1 + checkbox + disclaimer)' { + Context 'Human-facing-with-disclaimer tier (Tier 1 + checkbox + disclaimer)' { It 'Passes when all three elements are present' { $filePath = Join-Path $script:InstructionDir 'handoff-summary.instructions.md' $content = @" @@ -385,56 +458,6 @@ $($script:CheckboxText) } } - Context 'SSSC handoff with disclaimer tier (Tier 1 + checkbox + SSSC disclaimer)' { - It 'Passes when all three elements are present in an SSSC handoff' { - $ssscDir = Join-Path $script:TempTestDir '.github/instructions/security' - New-Item -ItemType Directory -Path $ssscDir -Force | Out-Null - $filePath = Join-Path $ssscDir 'sssc-handoff-summary.instructions.md' - $content = @" ---- -description: SSSC handoff ---- - -# Template for sssc-handoff-summary - -Content here. - -$($script:Tier1Text) - -$($script:CheckboxText) - -$($script:SsscDisclaimerText) -"@ - Set-Content -Path $filePath -Value $content -Encoding utf8 - $result = Test-AIArtifactCompliance -FilePath $filePath -FooterConfig $script:FooterConfig -DisclaimerConfig $script:DisclaimerConfig -RepoRoot $script:TempTestDir - $result.Passed | Should -BeTrue - } - - It 'Fails when SSSC disclaimer is missing from an SSSC handoff' { - $ssscDir = Join-Path $script:TempTestDir '.github/instructions/security' - New-Item -ItemType Directory -Path $ssscDir -Force | Out-Null - $filePath = Join-Path $ssscDir 'sssc-handoff-summary.instructions.md' - $content = @" ---- -description: SSSC handoff ---- - -# Template for sssc-handoff-summary - -Content here. - -$($script:Tier1Text) - -$($script:CheckboxText) -"@ - Set-Content -Path $filePath -Value $content -Encoding utf8 - $result = Test-AIArtifactCompliance -FilePath $filePath -FooterConfig $script:FooterConfig -DisclaimerConfig $script:DisclaimerConfig -RepoRoot $script:TempTestDir - $result.Passed | Should -BeFalse - $result.Issues | Should -HaveCount 1 - $result.Issues[0] | Should -BeLike '*SSSC*Disclaimer*' - } - } - Context 'Files without artifact references' { It 'Skips files with no matching artifacts' { $filePath = Join-Path $script:InstructionDir 'unrelated.instructions.md' @@ -456,7 +479,7 @@ description: Unrelated instruction Describe 'Test-AIArtifactValidation' -Tag 'Unit' { BeforeAll { $script:FooterConfig = Import-FooterConfig -ConfigPath $script:FooterConfigPath - $script:DisclaimerConfig = Import-DisclaimerConfig -ConfigPath $script:DisclaimerConfigPath + $script:DisclaimerConfig = Import-DisclaimerSource -SourcePath $script:DisclaimerSourcePath } BeforeEach { @@ -501,7 +524,7 @@ $($script:Tier1Text) $result = Test-AIArtifactValidation ` -Paths @('.github/instructions') ` -FooterConfigPath '.github/config/footer-with-review.yml' ` - -DisclaimerConfigPath '.github/config/disclaimers.yml' + -DisclaimerSourcePath '.github/instructions/shared/disclaimer-language.instructions.md' $result.TotalFiles | Should -BeGreaterOrEqual 2 $result.FilesWithArtifacts | Should -BeGreaterOrEqual 2 @@ -528,7 +551,7 @@ $($script:Tier1Text) $result = Test-AIArtifactValidation ` -Paths @('.github/instructions') ` -FooterConfigPath '.github/config/footer-with-review.yml' ` - -DisclaimerConfigPath '.github/config/disclaimers.yml' ` + -DisclaimerSourcePath '.github/instructions/shared/disclaimer-language.instructions.md' ` -ExcludePaths @('**/excluded/**') $excludedResults = $result.Results | Where-Object { $_.RelativePath -like '*excluded*' } @@ -550,7 +573,7 @@ $($script:Tier1Text) $result = Test-AIArtifactValidation ` -Paths @('.github/instructions') ` -FooterConfigPath '.github/config/footer-with-review.yml' ` - -DisclaimerConfigPath '.github/config/disclaimers.yml' ` + -DisclaimerSourcePath '.github/instructions/shared/disclaimer-language.instructions.md' ` -FailOnMissing $result.HasFailures | Should -BeTrue @@ -569,7 +592,7 @@ $($script:Tier1Text) $result = Test-AIArtifactValidation ` -Paths @('.github/instructions') ` -FooterConfigPath '.github/config/footer-with-review.yml' ` - -DisclaimerConfigPath '.github/config/disclaimers.yml' + -DisclaimerSourcePath '.github/instructions/shared/disclaimer-language.instructions.md' $result.HasFailures | Should -BeFalse } @@ -592,7 +615,7 @@ $($script:Tier1Text) Test-AIArtifactValidation ` -Paths @('.github/instructions') ` -FooterConfigPath '.github/config/footer-with-review.yml' ` - -DisclaimerConfigPath '.github/config/disclaimers.yml' ` + -DisclaimerSourcePath '.github/instructions/shared/disclaimer-language.instructions.md' ` -OutputPath $outputPath $outputFullPath | Should -Exist