Skip to content

Enforce manifest-level rejection of workflows that declare private: true#36227

Merged
pelikhan merged 6 commits into
mainfrom
copilot/check-aw-yml-agentic-workflows
Jun 1, 2026
Merged

Enforce manifest-level rejection of workflows that declare private: true#36227
pelikhan merged 6 commits into
mainfrom
copilot/check-aw-yml-agentic-workflows

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jun 1, 2026

aw.yml packages can include installable workflows, but only private: true should disable add/package behavior. This change ensures manifest-backed installs and compile-time validation consistently reject workflows that explicitly set private: true, while allowing private: false.

  • Manifest-backed add / add-wizard

    • Reject workflows selected from aw.yml when their frontmatter sets private: true
    • Fail with manifest-scoped errors that point to the offending workflow path
    • Preserve existing standalone workflow behavior where private: true blocks installation
  • Compile-time manifest validation

    • Validate manifest-listed installable workflows in local aw.yml packages
    • Fail compilation when a listed workflow sets private: true
    • Apply the same rule whether workflows are declared explicitly or discovered from package directories
  • Parsing and error semantics

    • Keep presence-aware parsing of the private field
    • Enforce blocking behavior only for private: true in manifest-listed workflows
  • Coverage

    • Update focused CLI tests for:
      • manifest-backed resolution rejecting private: true
      • compile-time validation rejecting local manifest-listed workflows with private: true

Example of a now-invalid manifest-listed workflow:

---
private: true
---

# Review

When that file is included by aw.yml, gh aw add, gh aw add-wizard, and compile-time manifest validation reject it.

Copilot AI and others added 2 commits June 1, 2026 11:37
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Validate private workflows in aw manifests Enforce manifest-level rejection of workflows that declare private Jun 1, 2026
Copilot AI requested a review from pelikhan June 1, 2026 11:42
@pelikhan pelikhan marked this pull request as ready for review June 1, 2026 11:44
Copilot AI review requested due to automatic review settings June 1, 2026 11:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens Agentic Workflow package manifest semantics by rejecting any manifest-listed installable workflow that declares a private frontmatter field (including private: false), both for remote manifest-backed installs (gh aw add / add-wizard) and for local compile-time manifest validation.

Changes:

  • Add presence-aware parsing for the private frontmatter field (ExtractWorkflowPrivateSetting) and use it to block manifest-listed workflows that declare private.
  • Extend local manifest compilation validation to inspect manifest-listed (or auto-discovered) installable workflows and fail compilation when any declares private.
  • Add CLI-focused tests covering rejection of manifest-listed workflows that set private: false in both remote resolution and local compilation.
Show a summary per file
File Description
pkg/cli/packages.go Introduces presence-aware private extraction helper used by manifest-specific validation.
pkg/cli/compile_repository_manifest.go Extends local manifest validation to read/installable workflow sources and reject private declarations.
pkg/cli/compile_repository_manifest_test.go Adds compilation test ensuring manifest-listed workflow with private: false is rejected.
pkg/cli/add_workflow_resolution.go Rejects manifest-backed workflow selections when the workflow frontmatter declares private.
pkg/cli/add_package_manifest.go Adds shared helper to validate privacy rules across manifest-listed installable workflows.
pkg/cli/add_package_manifest_test.go Adds resolution test ensuring repository package manifests reject private: false.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 6/6 changed files
  • Comments generated: 1

Comment thread pkg/cli/packages.go
Comment on lines 241 to +248
if private, ok := result.Frontmatter["private"]; ok {
if privateBool, ok := private.(bool); ok {
return privateBool
return privateBool, true
}
}

return false, false
}
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (>100 new lines in pkg/cli/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/36227-reject-private-field-in-manifest-workflows.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff (presence-aware private-field rejection in manifest-listed workflows).
  2. Complete the missing sections — add context the AI couldn't infer, refine the decision rationale, and confirm the alternatives reflect what you actually considered.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-36227: Reject the private Field in Manifest-Listed Installable Workflows

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 0042-use-postgresql.md for PR #42).

🔒 This gate is blocking: the PR cannot merge until an ADR is linked in the PR body.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · opus48 566.4K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking review — two medium-priority issues worth addressing before the code grows more test coverage on top of the current structure.

### Findings summary

Duplicate error-message strings (add_workflow_resolution.go and add_package_manifest.go) — identical fmt.Errorf format strings are copy-pasted across the runtime and compile-time validation paths. Extract them into package-level constants so both sites stay in sync and tests don't need to be updated in two places.

Missing test for private: true manifest pathTestResolveWorkflows_RepositoryPackageRejectsPrivateFalse exists but the private: true branch in ResolveWorkflows produces a distinct error message that is never asserted. A regression in that branch would go undetected.

(The non-bool private value bypass — e.g. private: "false" — is already called out in a prior review comment.)

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 1.6M

Comment thread pkg/cli/add_workflow_resolution.go Outdated
if hasPrivate {
manifestPath := joinRepositoryPackagePath(spec.PackagePath, repositoryPackageManifestFileName)
if privateValue {
return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: workflow %q sets private: true and cannot be included because private workflows cannot be added", manifestPath, resolvedSpec.WorkflowPath)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate error-message strings will silently drift from the compile-time counterpart: the two error templates for private: true and private: false are copy-pasted verbatim from validateManifestInstallableWorkflowPrivacy, so any future wording fix applied to one site won't propagate to the other.

💡 Suggested fix

Extract both templates into package-level constants (or a tiny shared helper) so both validation paths stay in sync:

const (
    errManifestPrivateTrue = "invalid Agentic Workflow manifest %q: workflow %q sets private: true and cannot be included because private workflows cannot be added"
    errManifestPrivateFalse = "invalid Agentic Workflow manifest %q: workflow %q sets private: false; remove the private field because manifest-listed workflows must not declare it"
)

Both validateManifestInstallableWorkflowPrivacy and the block added here then reference the same constants, and a test that asserts on the exact string only needs to be updated in one place.

Comment thread pkg/cli/add_workflow_resolution.go Outdated

if spec.FromRepositoryManifest {
privateValue, hasPrivate := ExtractWorkflowPrivateSetting(string(fetched.Content))
if hasPrivate {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The private: true manifest-rejection path has no test: the new private: false case is covered by TestResolveWorkflows_RepositoryPackageRejectsPrivateFalse, but the private: true branch (line 220) produces a different error message ("sets private: true and cannot be included") that is never asserted. If that branch were accidentally deleted or its message changed, no test would catch it.

💡 Suggested fix

Add a companion test that sets private: true in the fetched workflow content and asserts the "sets private: true" error string:

func TestResolveWorkflows_RepositoryPackageRejectsPrivateTrue(t *testing.T) {
    // ... same stub setup as the PrivateFalse test ...
    // workflow content: "---\nprivate: true\n---\n\n# Review\n"
    _, err := ResolveWorkflows(context.Background(), []string{"owner/repo"}, false)
    require.Error(t, err)
    assert.Contains(t, err.Error(), `workflow "workflows/review.md" sets private: true`)
}

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd and /diagnose — commenting with non-blocking suggestions on test coverage gaps and a potential robustness issue in the new filesystem scanner.

📋 Key Themes & Highlights

Key Themes

  • Test coverage gaps: The scanLocalRepositoryPackageInstallablePaths auto-discovery fallback has no test; the private: true branch of the manifest-backed install check also lacks a dedicated case.
  • Duplicated enforcement logic: The install-time path inlines the same two error messages that validateManifestInstallableWorkflowPrivacy already encapsulates, creating a maintenance surface.
  • WalkDir robustness: Returning walkErr immediately on any entry error means a single unreadable file aborts the whole compile-time scan.

Positive Highlights

  • ✅ Correct root-cause fix: splitting ExtractWorkflowPrivate into presence-aware ExtractWorkflowPrivateSetting eliminates the boolean-blindness bug cleanly without breaking existing callers.
  • ✅ Defense-in-depth: two independent enforcement layers (install-time + compile-time) are the right approach.
  • ✅ Clear, actionable error messages that tell the author exactly what to change (remove the private field).
  • ✅ Tests use real filesystem fixtures and integration-style setup, which gives high confidence for the covered paths.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 1.4M

`), 0o644))

_, err = CompileWorkflows(context.Background(), CompileConfig{})
require.Error(t, err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The new scanLocalRepositoryPackageInstallablePaths fallback (triggered when manifest.Files and manifest.Includes are both empty) has no test coverage. This is the only code path that actually walks the filesystem, applies deduplication, and skips missing source directories — all independently from the files:-listing path tested here.

💡 Suggested test sketch

Add a test variant that omits files: from aw.yml so the compile-time path falls through to the scanner:

func TestCompileWorkflows_RejectsAutoDiscoveredWorkflowWithPrivate(t *testing.T) {
    // Same setup, but aw.yml has no `files:` key
    os.WriteFile(..., []byte(`manifest-version: "1"\nname: Repo Assist\n`), 0o644)
    // review.md with private: true or private: false lives under workflows/
    _, err = CompileWorkflows(context.Background(), CompileConfig{})
    require.Error(t, err)
    assert.Contains(t, err.Error(), `sets private:`)
}

Without this, a regression in WalkDir path normalization or deduplication would be invisible.

}

_, err := ResolveWorkflows(context.Background(), []string{"owner/repo"}, false)
require.Error(t, err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The new test covers private: false through a manifest-backed install, but there's no corresponding test for private: true arriving via FromRepositoryManifest. The check in add_workflow_resolution.go has two branches (privateValue true vs false) — only the false branch is exercised here.

💡 Suggested addition

Add a TestResolveWorkflows_RepositoryPackageRejectsPrivateTrue mirror of this test where workflows/review.md contains private: true. This confirms both branches of the inline check in ResolveWorkflows and guards against the error message being accidentally removed from the true branch.

Comment thread pkg/cli/add_workflow_resolution.go Outdated
if privateValue {
return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: workflow %q sets private: true and cannot be included because private workflows cannot be added", manifestPath, resolvedSpec.WorkflowPath)
}
return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: workflow %q sets private: false; remove the private field because manifest-listed workflows must not declare it", manifestPath, resolvedSpec.WorkflowPath)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnose] The install-time check here is inlined, while the compile-time path delegates to validateManifestInstallableWorkflowPrivacy. This means the same two error message strings exist in two places. If one is updated (e.g. to improve wording), the other is easy to miss.

💡 Suggestion

Consider calling validateManifestInstallableWorkflowPrivacy (or extracting a small checkWorkflowPrivacyForManifest(manifestPath, workflowPath string, content []byte) error helper) from both sites. The inline check here could become:

if spec.FromRepositoryManifest {
    manifestPath := joinRepositoryPackagePath(spec.PackagePath, repositoryPackageManifestFileName)
    if err := checkWorkflowPrivacyForManifest(manifestPath, resolvedSpec.WorkflowPath, fetched.Content); err != nil {
        return nil, err
    }
}

This keeps the two enforcement layers in sync automatically.

}

relativePath, err := filepath.Rel(packageDir, currentPath)
if err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnose] filepath.WalkDir returns walkErr to the callback for entries it could not stat (e.g. a broken symlink or a permission-denied file inside the directory). Returning walkErr immediately aborts the entire scan — a single unreadable file fails the whole compilation.

💡 Suggestion

Consider skipping non-fatal walk errors on individual files rather than aborting:

if walkErr != nil {
    if d != nil && !d.IsDir() {
        return nil // skip unreadable file, keep scanning
    }
    return walkErr // propagate errors opening directories
}

This keeps the scan robust in the presence of broken symlinks or permission quirks in the package tree.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 80/100 — Acceptable

Analyzed 2 test(s): 2 design (behavioral contracts), 0 implementation, 0 guideline violations — minor assertion message gaps noted.

📊 Metrics & Test Classification (2 tests analyzed)
Metric Value
New/modified tests analyzed 2
✅ Design tests (behavioral contracts) 2 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (100%)
Duplicate test clusters 0
Test inflation detected Yes (1 file)
🚨 Coding-guideline violations 0 (no mock libraries, build tags present)

Test Classification Details

Test File Classification Issues Detected
TestResolveWorkflows_RepositoryPackageRejectsPrivateFalse pkg/cli/add_package_manifest_test.go:473 ✅ Design Bare assertions (no message args); test inflation (54 vs 24 lines)
TestCompileWorkflows_RejectsManifestWorkflowWithPrivateFalse pkg/cli/compile_repository_manifest_test.go:180 ✅ Design Bare assertions (no message args)

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 2 tests — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests
⚠️ Flagged Tests — Requires Review (2 issue(s))

⚠️ TestResolveWorkflows_RepositoryPackageRejectsPrivateFalse (pkg/cli/add_package_manifest_test.go:473)

Classification: Design test ✅ — correctly verifies the behavioral contract that ResolveWorkflows rejects workflows declaring private: false
Issue 1 — Bare assertions: require.Error(t, err) and assert.Contains(t, err.Error(), ...) lack descriptive message arguments.
Suggested improvement: Add context to each assertion:

require.Error(t, err, "ResolveWorkflows should reject a workflow with private: false")
assert.Contains(t, err.Error(), `workflow "workflows/review.md" sets private: false`, "error message should identify the offending workflow")

Issue 2 — Test inflation: add_package_manifest_test.go gained +54 lines while add_package_manifest.go gained only +24 lines (ratio ≈ 2.25:1, threshold 2:1). This triggered the inflation penalty but is borderline and not a concern given the tests are legitimately replacing broken code that was embedded inside another test's body.


⚠️ TestCompileWorkflows_RejectsManifestWorkflowWithPrivateFalse (pkg/cli/compile_repository_manifest_test.go:180)

Classification: Design test ✅ — end-to-end test that writes real files to a temp directory and verifies CompileWorkflows rejects a manifest referencing a workflow with private: false.
Issue — Bare assertions: Multiple require.NoError, require.Error, and assert.Contains calls lack message arguments.
Suggested improvement: Add message context to key assertions:

require.Error(t, err, "CompileWorkflows should fail when a manifest workflow sets private: false")
assert.Contains(t, err.Error(), `workflow "workflows/review.md" sets private: false`, "error should identify the offending workflow")

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). Both new tests directly verify the behavioral contract that manifest-listed workflows declaring private: false are rejected. Minor suggestion: add descriptive message arguments to assertions per project guidelines.

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §26752819254

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 1.4M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 80/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). Both tests enforce the behavioral contract that manifest workflows with private: false are rejected. Minor suggestion: add descriptive message arguments to assertions.

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented Jun 1, 2026

@copilot apply copilot-review skill

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 1, 2026

@copilot apply copilot-review skill

Applied. I reviewed the PR feedback using the copilot-review workflow; there are no new actionable @copilot code-change requests in the current comment set.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented Jun 1, 2026

@copilot it should be "private: true" instead of "private: false" to disable add/package.

@github-actions github-actions Bot mentioned this pull request Jun 1, 2026
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 1, 2026

@copilot it should be "private: true" instead of "private: false" to disable add/package.

Fixed in 351ea82. Manifest-backed add/package and compile validation now only block workflows with private: true; private: false no longer triggers rejection. I also updated the related tests and ADR draft wording to match.

Copilot AI changed the title Enforce manifest-level rejection of workflows that declare private Enforce manifest-level rejection of workflows that declare private: true Jun 1, 2026
@pelikhan pelikhan merged commit 0d26639 into main Jun 1, 2026
@pelikhan pelikhan deleted the copilot/check-aw-yml-agentic-workflows branch June 1, 2026 14:39
Copilot stopped work on behalf of pelikhan due to an error June 1, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants