diff --git a/docs/src/content/docs/specs/model-alias-specification.md b/docs/src/content/docs/specs/model-alias-specification.md index 5ea07361978..31694a10026 100644 --- a/docs/src/content/docs/specs/model-alias-specification.md +++ b/docs/src/content/docs/specs/model-alias-specification.md @@ -734,7 +734,14 @@ This section defines normative safeguards that conforming implementations MUST a ### 13.1 Alias Chain Depth Limit -**R-MAF-S001**: Implementations MUST enforce a maximum alias chain resolution depth. The default maximum chain depth is **10** hops. If recursive resolution exceeds this depth, the implementation MUST abort resolution with a descriptive error that names the alias key that triggered the depth limit and MUST NOT silently return an empty candidate list. +**R-MAF-S001**: Implementations MUST enforce a maximum alias chain resolution depth. The default maximum chain depth is **10** hops. If recursive resolution exceeds this depth, the implementation MUST abort resolution with error code **V-MAF-008** and a descriptive error message that names the alias key that triggered the depth limit and the configured depth ceiling. Implementations MUST NOT silently return an empty candidate list. + +Error message format (informative): +``` +model alias chain exceeded maximum depth (10): resolution path reached '{{alias-key}}' after 10 hops +``` + +Test coverage for this requirement is provided by test case **T-MAF-055** in `pkg/workflow/model_alias_validation_test.go`, which constructs a synthetic alias map of depth 11 and asserts that resolution fails with a V-MAF-008 error that identifies the terminal alias key. This limit prevents runaway resolution in pathological alias maps and bounds the worst-case cost of compile-time alias expansion. @@ -803,6 +810,8 @@ The compile-time loop-detection safeguard (§8.6.1 / V-MAF-010) is tested in: ## 15. Norms +The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and **OPTIONAL** in the requirement column of this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). + This section provides a normative reference table for all MUST/SHALL requirements defined in §§4–13 of this specification. The table covers both validation rule identifiers (`V-MAF-*`, defined in §11) and safeguard identifiers (`R-MAF-S*`, defined in §13). Use this section as a quick-reference index for verifying implementation compliance or mapping a requirement to its definitive section. ### 15.1 Validation Rule Norms (§11) @@ -825,7 +834,7 @@ This section provides a normative reference table for all MUST/SHALL requirement | ID | Section | Normative Requirement | |---|---|---| -| R-MAF-S001 | §13.1 | MUST enforce a maximum alias chain resolution depth of 10 hops; MUST abort with a descriptive error on overflow | +| R-MAF-S001 | §13.1 | MUST enforce a maximum alias chain resolution depth of 10 hops; MUST abort with error code V-MAF-008 and a descriptive error naming the terminal alias key and depth ceiling on overflow | | R-MAF-S002 | §13.2 | MUST reject model identifier strings containing ill-formed UTF-8 bytes | | R-MAF-S003 | §13.2 | MUST reject valid-UTF-8 characters outside the §4.1 allowed code-point set as a V-MAF-006 violation | | R-MAF-S004 | §13.3 | MUST reject `effort` values not in `{low, medium, high}` at compile time with a message identifying the offending value | diff --git a/docs/src/content/docs/specs/repository-package-manifest-specification.md b/docs/src/content/docs/specs/repository-package-manifest-specification.md index 3844c9eacee..2917ae5037a 100644 --- a/docs/src/content/docs/specs/repository-package-manifest-specification.md +++ b/docs/src/content/docs/specs/repository-package-manifest-specification.md @@ -92,6 +92,8 @@ Each entry MUST be resolved relative to the package root and MUST match one of t Duplicate entries SHOULD be ignored after normalization. +**Path-traversal safety**: Each entry in `files` MUST NOT contain a path-traversal sequence. Specifically, any entry that contains `../` (or `..\` on Windows-style paths), begins with `../`, or resolves to a path outside the package root after normalization MUST be rejected with a validation error. Implementations MUST NOT follow symlinks that would escape the package root during file resolution. This rule applies regardless of the number of traversal components in the path (e.g., `../../etc/passwd` and `workflows/../../hidden` are both prohibited). + ## 5. Installable file resolution Supported installable paths are: @@ -125,7 +127,7 @@ The install lifecycle (invoked by `gh aw add`) MUST proceed in the following ord 4. **Compile** each agentic workflow markdown file into the target repository's workflow directory. Raw `.yml` files are copied verbatim without compilation. 5. **Write** all output files atomically before reporting success. -If any step fails, the implementation MUST abort and MUST NOT leave partial output files in the target directory. The implementation SHOULD emit an actionable error identifying the failing step. +If any step fails, the implementation MUST abort and MUST NOT leave partial output files in the target directory. The implementation SHOULD emit an actionable error identifying the failing step. See §10 (Safeguards) for the normative rollback and permission-error requirements that apply to this lifecycle (R-PKG-003, R-PKG-004, R-PKG-006, R-PKG-007). ### 5.2 Update @@ -147,7 +149,7 @@ The remove lifecycle uninstalls a previously installed package by deleting its i **R-PKG-R002**: If a file to be removed has been modified since installation (detected by checksum or modification timestamp comparison), the implementation SHOULD warn the user and MUST NOT delete the file without explicit confirmation. -**R-PKG-R003**: If deletion of any installed file fails (for example, due to a filesystem permission error), the implementation MUST emit an error identifying the file and reason, and MUST continue attempting to remove the remaining files rather than aborting immediately. The implementation MUST report a final summary listing all files that could not be removed. +**R-PKG-R003**: If deletion of any installed file fails (for example, due to a filesystem permission error), the implementation MUST emit an error identifying the file and reason, and MUST continue attempting to remove the remaining files rather than aborting immediately. The implementation MUST report a final summary listing all files that could not be removed. See §10.4 (Safeguards — Filesystem Permission Errors) for the normative requirement on permission-error reporting (R-PKG-007). **R-PKG-R004**: After removal, if the target workflow directory is empty, the implementation MAY remove the empty directory. The implementation MUST NOT remove non-empty directories. @@ -280,6 +282,7 @@ This section provides a normative reference table for all MUST/SHALL requirement | — | §4.3 | `min-version` MUST use `vMAJOR.minor.patch` form; MUST fail if compiler version is lower | | — | §4.4 | `name` MUST be present and non-empty after trimming whitespace | | — | §4.7 | Each `files` entry MUST be resolved relative to the package root and MUST match a supported installable path | +| — | §4.8 | Each `files` entry MUST NOT contain a path-traversal sequence (`../`); entries that escape the package root MUST be rejected | | — | §4 (preamble) | Unknown top-level fields MUST be rejected | ### 11.2 File Resolution Norms (§5) diff --git a/scratchpad/github-mcp-access-control-specification.md b/scratchpad/github-mcp-access-control-specification.md index c85307df0ea..cdf29b18d1f 100644 --- a/scratchpad/github-mcp-access-control-specification.md +++ b/scratchpad/github-mcp-access-control-specification.md @@ -2136,6 +2136,20 @@ make test-github-mcp-blocked-users make test-github-mcp-approval-labels ``` +### 11.4 Compliance Fixture Stubs + +The following fixture files in [`specs/github-mcp-access-control-compliance/`](../../specs/github-mcp-access-control-compliance/) define normative test scenarios for the five core access-control categories. Each fixture is a YAML document specifying an input tool configuration, a simulated access request, and the required access-control decision. Implementations MUST produce the `expected.decision` outcome for every scenario in each fixture. + +| Fixture File | Scenario | Test IDs | +|---|---|---| +| [`exact-match-allow.yaml`](../../specs/github-mcp-access-control-compliance/exact-match-allow.yaml) | Exact repository pattern allows matching repo; denies non-matching | T-GH-011, T-GH-012 | +| [`wildcard-deny.yaml`](../../specs/github-mcp-access-control-compliance/wildcard-deny.yaml) | Owner-wildcard allows same-owner repos; denies different-owner repos | T-GH-013, T-GH-014 | +| [`role-deny.yaml`](../../specs/github-mcp-access-control-compliance/role-deny.yaml) | Role filter allows matching role; denies insufficient role | T-GH-019, T-GH-020, T-GH-023 | +| [`private-repo-block.yaml`](../../specs/github-mcp-access-control-compliance/private-repo-block.yaml) | `private-repos: false` blocks private repo; allows public repo | T-GH-024, T-GH-025, T-GH-026 | +| [`integrity-level-block.yaml`](../../specs/github-mcp-access-control-compliance/integrity-level-block.yaml) | `min-integrity` allows content at/above threshold; blocks content below | T-GH-051, T-GH-052, T-GH-054 | + +See [`specs/github-mcp-access-control-compliance/README.md`](../../specs/github-mcp-access-control-compliance/README.md) for fixture schema documentation and instructions for adding new scenarios. + --- ## Appendices diff --git a/scratchpad/guard-policies-specification.md b/scratchpad/guard-policies-specification.md index f6227a536c6..c6008371c40 100644 --- a/scratchpad/guard-policies-specification.md +++ b/scratchpad/guard-policies-specification.md @@ -422,6 +422,61 @@ The implementation follows established patterns in the codebase and integrates w --- +## Entities + +This section defines the normative data entities of the guard policies framework. Implementations MUST represent each entity with the fields, types, and constraints described below. + +### Entity: `GitHubReposScope` + +`GitHubReposScope` defines the repository access scope for a GitHub guard policy. It MUST be one of: + +| Value | Type | Meaning | +|---|---|---| +| `"all"` | String scalar | All repositories accessible by the token; no restriction | +| `"public"` | String scalar | Public repositories only; private repositories are denied | +| Array of patterns | `[]string` | Explicit allowlist of repository patterns (see §GP-03 for pattern syntax) | + +Implementations MUST reject any other type (e.g., integers, booleans, nested maps) with a descriptive compilation error. + +**Deprecated alias**: The YAML field `repos` is a deprecated alias for `allowed-repos` with identical semantics. Implementations MUST accept `repos` for backwards compatibility and SHOULD emit a deprecation warning when `repos` is used. New authoring MUST use `allowed-repos`. See [Deprecation: `repos` Field](#deprecation-repos-field). + +### Entity: `GitHubIntegrityLevel` + +`GitHubIntegrityLevel` represents the minimum content integrity level required before an AI agent is permitted to act on a GitHub object. It MUST be one of: + +| Value | Meaning | +|---|---| +| `"none"` | No integrity requirement; all objects are permitted (lowest trust) | +| `"unapproved"` | Objects from open, non-approved pull requests are permitted | +| `"approved"` | Objects from pull requests that have been reviewed and approved | +| `"merged"` | Objects reachable from the main branch (highest trust) | + +The trust ordering MUST be: `merged` > `approved` > `unapproved` > `none`. + +Any value outside the four literals above MUST be rejected with a compilation error. + +### Entity: `GitHubToolConfig` (guard-policy fields) + +`GitHubToolConfig` is the workflow-level struct that carries GitHub-specific configuration under the `tools.github` frontmatter key. The guard-policy subset of fields is: + +| Field | YAML Key | Type | Required | Description | +|---|---|---|---|---| +| `AllowedRepos` | `allowed-repos` | `GitHubReposScope` | No | Repository access scope. Defaults to `"all"` when `min-integrity` is present. | +| `Repos` | `repos` | `GitHubReposScope` | No | **Deprecated** alias for `allowed-repos`. | +| `MinIntegrity` | `min-integrity` | `GitHubIntegrityLevel` | Conditionally | Required when `allowed-repos` is set to a non-`"all"` scope or to any explicit pattern array. | + +Implementations MUST ensure `AllowedRepos` and `Repos` are not both set simultaneously; if both are present, implementations SHOULD error or use `AllowedRepos` and warn. + +### Deprecation: `repos` Field + +The YAML key `repos` under `tools.github` is **deprecated** as of guard-policy specification version 0.2.0. It was renamed to `allowed-repos` to avoid collision with the `repos` toolset name. + +**Migration path**: Use `gh aw fix` to automatically migrate `repos:` to `allowed-repos:` in workflow frontmatter. + +**Removal target**: The `repos` alias SHOULD be removed in a future major version of the spec (tentatively v2.0.0). When the alias is removed, implementations MUST reject `repos` as an unknown field with an error message that suggests `allowed-repos`. + +--- + ## Conformance The key words in this section are to be interpreted as described in RFC 2119 (see [Requirements Notation](#requirements-notation) above). @@ -449,3 +504,73 @@ A conforming implementation of the guard policies framework **MUST** satisfy all **GP-10**: When `lockdown: true` is set in the same workflow, implementations MUST treat `lockdown` as taking absolute precedence. Guard policy fields (`allowed-repos`, `min-integrity`) MUST NOT widen access beyond the single triggering repository when lockdown is active. The compiler SHOULD emit a warning when both `lockdown: true` and guard policy fields are present. **GP-11**: When `allowed-repos` is configured explicitly, implementations MUST require `min-integrity` to be present. In particular, any non-`"all"` `allowed-repos` scope MUST NOT be accepted without `min-integrity`, and implementations MAY enforce the same requirement for explicit `allowed-repos: "all"` for consistency with the general guard-policy validation rule. + +--- + +## Safeguards + +This section defines normative safeguards that conforming implementations MUST apply to prevent misconfiguration, privilege escalation, and silent policy-bypass in the guard policies framework. + +### GP-S001: Empty Allowlist Prevention + +Implementations MUST reject an empty `allowed-repos` array (`allowed-repos: []`) with a compilation error. An empty allowlist provides no access and is almost always a misconfiguration. The error message MUST identify the field and indicate that an empty array is not a valid scope value. A `MUST` sentinel such as `"all"` or `"public"` MUST be used instead. + +### GP-S002: Lockdown Supremacy + +When `lockdown: true` is present on the same workflow, guard policy fields (`allowed-repos`, `min-integrity`, `blocked-users`, `approval-labels`) MUST NOT be evaluated for access-widening purposes. Implementations MUST treat lockdown as taking absolute precedence and MUST NOT combine lockdown with guard policies in any way that permits access beyond the single triggering repository. + +Implementations MUST emit a compilation warning when both `lockdown: true` and any guard-policy field are present simultaneously, because the combination is almost certainly a misconfiguration (the guard-policy fields become inert). + +### GP-S003: Cross-Field Consistency + +When `allowed-repos` is set to an explicit pattern array or `"public"`, implementations MUST require `min-integrity` to also be present. Permitting a restricted repository scope without a minimum integrity level could allow low-integrity content to reach restricted repositories undetected. + +Implementations MUST reject the combination `{ allowed-repos: , min-integrity: (absent) }` with a compilation error that names both the missing field and the reason it is required. + +### GP-S004: Legacy Field Isolation + +When the deprecated `repos` field is used alongside `allowed-repos` in the same `tools.github` block, implementations MUST NOT silently merge the two values. Implementations MUST either: (a) reject the combination with an error explaining that `repos` and `allowed-repos` cannot both be set, or (b) use `allowed-repos` and emit a warning that `repos` is ignored when `allowed-repos` is present. + +In no case MUST the deprecated `repos` field silently override or supplement the normative `allowed-repos` field. + +### GP-S005: Absent Policy is Not Permissive + +When no guard-policy fields are present on `tools.github`, the derived safe-outputs `write-sink` policy MUST be `nil`. The absence of a guard policy is not equivalent to `accept: ["*"]`. Implementations MUST NOT add a default `accept: ["*"]` when the user has not configured any guard-policy. + +--- + +## Sync Notes + +This section maps normative sections of this specification to the implementation files that realise each requirement. Use this mapping to identify which files must be reviewed or updated when specification sections change. + +**Last verified**: 2026-07-03 + +### Guard Policy Validation + +| Spec Requirement | Description | Implementation File(s) | +|---|---|---| +| GP-01 `allowed-repos` parsing | Flat `allowed-repos` field extraction and type validation | `pkg/workflow/tools_parser.go` (`parseGitHubTool`) | +| GP-01, GP-03 pattern validation | Repository pattern format validation (exact, wildcard, prefix) | `pkg/workflow/tools_validation_github.go` (`validateReposScope`, `validateRepoPattern`, `isValidOwnerOrRepo`) | +| GP-02 `min-integrity` validation | Enum value check for `none`/`unapproved`/`approved`/`merged` | `pkg/workflow/tools_validation_github.go` (`validateGitHubGuardPolicy`) | +| GP-04 empty array rejection | Empty `allowed-repos` array detection and error | `pkg/workflow/tools_validation_github.go` (`validateGitHubGuardPolicy`) | +| GP-10 lockdown precedence | Lockdown + guard-policy conflict detection and warning | `pkg/workflow/tools_validation_github.go` (`validateGitHubGuardPolicy`, `emitGitHubLockdownGuardPolicyWarning`) | + +### Safe-Outputs Guard Policy Derivation + +| Spec Requirement | Description | Implementation File(s) | +|---|---|---| +| GP-05 through GP-08 | Deriving the safe-outputs `write-sink` policy from GitHub guard policy | `pkg/workflow/mcp_github_config.go` (`deriveSafeOutputsGuardPolicyFromGitHub`) | +| GP-06 scalar mapping | `"all"` / `"public"` → `accept: ["*"]` mapping | `pkg/workflow/mcp_github_config.go` (`deriveSafeOutputsGuardPolicyFromGitHub`) | +| GP-07 pattern transformation | Array patterns → `private:`-prefixed accept entries | `pkg/workflow/mcp_github_config.go` (`normalizeGitHubRepositoryInReposScope`) | +| GP-05 through GP-08 tests | Derivation tests including nil-return, scalar, and array cases | `pkg/workflow/safeoutputs_guard_policy_test.go` (`TestDeriveSafeOutputsGuardPolicyFromGitHub`) | + +### Legacy `repos` Field Migration + +The deprecated `repos` field (YAML key: `repos`) is handled alongside `allowed-repos` in: + +- **`pkg/workflow/mcp_github_config.go`** — The `deriveSafeOutputsGuardPolicyFromGitHub()` function reads `"allowed-repos"` first and falls back to `"repos"` when `"allowed-repos"` is absent (lines: `repos, hasRepos := githubTool["allowed-repos"]` then `repos, hasRepos = githubTool["repos"]`). +- **`pkg/workflow/tools_types.go`** — `GitHubToolConfig` declares both `AllowedRepos` (`yaml:"allowed-repos,omitempty"`) and the deprecated `Repos` (`yaml:"repos,omitempty"`) fields. + +**Migration command**: `gh aw fix` applies a codemod that replaces `repos:` with `allowed-repos:` in workflow frontmatter. The codemod is idempotent and safe to run multiple times. + +**Removal tracking**: The `repos` alias is tracked for removal. When it is removed, update `pkg/workflow/tools_types.go` (delete the `Repos` field), `pkg/workflow/mcp_github_config.go` (remove the fallback lookup), and `pkg/workflow/tools_validation_github.go` (adjust any `repos`-specific validation paths). Update doc-comments in `pkg/workflow/tools_types.go` to reference this spec version after the removal. diff --git a/specs/github-mcp-access-control-compliance/README.md b/specs/github-mcp-access-control-compliance/README.md new file mode 100644 index 00000000000..7cfb8b308a7 --- /dev/null +++ b/specs/github-mcp-access-control-compliance/README.md @@ -0,0 +1,59 @@ +# GitHub MCP Access Control Compliance Fixtures + +This directory contains fixture stubs for the Section 11 compliance tests of the +[GitHub MCP Access Control Specification](../../scratchpad/github-mcp-access-control-specification.md). + +Each fixture describes a test scenario with an input tool configuration and the expected +access-control decision. Fixtures are consumed by the compliance test runner to verify +that implementations satisfy the normative requirements in §§4–10 of the specification. + +## Fixture Files + +| Filename | Scenario | Spec Coverage | +|---|---|---| +| `exact-match-allow.yaml` | Exact repository pattern allows matching repo | T-GH-011, T-GH-012 | +| `wildcard-deny.yaml` | Owner-wildcard pattern denies non-matching owner | T-GH-013, T-GH-014 | +| `role-deny.yaml` | Role filter denies access when user role is insufficient | T-GH-019, T-GH-020 | +| `private-repo-block.yaml` | `private-repos: false` blocks access to private repository | T-GH-024, T-GH-025 | +| `integrity-level-block.yaml` | `min-integrity: approved` blocks content below the threshold | T-GH-051, T-GH-052 | + +## Fixture Schema + +Each fixture file is a YAML document with the following top-level keys: + +```yaml +fixture_id: string # Unique identifier matching the test IDs in §11.1 +description: string # Human-readable scenario description +spec_refs: # Normative requirements under test (§ references) + - string +input: + tool_config: object # Compiled GitHub MCP tool configuration under test + request: object # Simulated access request (repository, user, content) +expected: + decision: allow | deny # Required access-control outcome + error_code: integer | null # Expected MCP JSON-RPC error code on deny (e.g., -32001) + reason: string # Expected denial reason substring (informative) +``` + +## Adding New Fixtures + +1. Copy the most relevant existing fixture file. +2. Change `fixture_id` to a new unique identifier. +3. Update `input.tool_config` and `input.request` to reflect the new scenario. +4. Update `expected` fields to match the required outcome. +5. Register the new fixture in the table above and link it from §11.4 of the specification. + +## Running Compliance Tests + +Compliance tests that consume these fixtures are located in (or will be added to): + +``` +pkg/workflow/tools_validation_test.go — §11.1.1 configuration validation +pkg/workflow/tools_validation_test.go — §11.1.8 blocked-user tests +``` + +To run all related tests: + +```bash +go test -v -run "TestValidateGitHubGuardPolicy" ./pkg/workflow/ +``` diff --git a/specs/github-mcp-access-control-compliance/exact-match-allow.yaml b/specs/github-mcp-access-control-compliance/exact-match-allow.yaml new file mode 100644 index 00000000000..f64bfd92994 --- /dev/null +++ b/specs/github-mcp-access-control-compliance/exact-match-allow.yaml @@ -0,0 +1,58 @@ +# Exact-Match Allow — Compliance Fixture +# Tests: T-GH-011, T-GH-012 +# Spec: §5 Repository Scoping, §5.1 Exact Pattern Matching + +fixture_id: "exact-match-allow" +description: > + An exact repository pattern (e.g., "github/gh-aw") MUST allow access when the requested + repository exactly matches the pattern, and MUST deny access when the requested repository + does not match (T-GH-012). + +spec_refs: + - "§5.1 — Exact repository pattern allows matching repository" + - "§5.1 — Exact repository pattern rejects non-matching repository" + - "§9.1 — Access decision logged at INFO level on allow" + - "§9.2 — Access decision logged at WARN level on deny" + +# --- Scenario A: exact match is allowed --- +scenarios: + - scenario_id: "exact-match-allow-A" + description: "Exact pattern 'github/gh-aw' allows request for repository 'github/gh-aw'" + input: + tool_config: + repos: + - "github/gh-aw" + roles: + - "write" + private-repos: true + min-integrity: "unapproved" + request: + repository: "github/gh-aw" + user_role: "write" + is_private: true + content_integrity: "unapproved" + expected: + decision: allow + error_code: null + reason: "" + + # --- Scenario B: non-matching repository is denied --- + - scenario_id: "exact-match-allow-B" + description: "Exact pattern 'github/gh-aw' denies request for repository 'github/other-repo'" + input: + tool_config: + repos: + - "github/gh-aw" + roles: + - "write" + private-repos: true + min-integrity: "unapproved" + request: + repository: "github/other-repo" + user_role: "write" + is_private: true + content_integrity: "unapproved" + expected: + decision: deny + error_code: -32001 + reason: "repository not in allowed list" diff --git a/specs/github-mcp-access-control-compliance/integrity-level-block.yaml b/specs/github-mcp-access-control-compliance/integrity-level-block.yaml new file mode 100644 index 00000000000..f91e989350d --- /dev/null +++ b/specs/github-mcp-access-control-compliance/integrity-level-block.yaml @@ -0,0 +1,99 @@ +# Integrity Level Block — Compliance Fixture +# Tests: T-GH-051, T-GH-052, T-GH-054, T-GH-059, T-GH-060 +# Spec: §8 Integrity Level Management + +fixture_id: "integrity-level-block" +description: > + The `min-integrity` field enforces a minimum content integrity level. Requests whose + effective content integrity meets or exceeds `min-integrity` MUST be allowed (T-GH-051). + Requests whose effective content integrity is below `min-integrity` MUST be denied with + error code -32006 (T-GH-052, T-GH-054). When no `min-integrity` is configured, all + non-blocked items pass the integrity check regardless of their effective level (T-GH-059). + The integrity ordering MUST be: none < unapproved < approved < merged (T-GH-060). + +spec_refs: + - "§8.1 — min-integrity allows content at or above the configured threshold" + - "§8.2 — min-integrity denies content below the configured threshold" + - "§8.3 — Integrity ordinal order: none < unapproved < approved < merged" + - "§11.1.9 — T-GH-051 through T-GH-060" + +scenarios: + # --- Scenario A: content meets threshold → allowed --- + - scenario_id: "integrity-level-block-A" + description: "Content with integrity 'approved' is allowed when min-integrity: approved" + input: + tool_config: + repos: + - "*/*" + min-integrity: "approved" + request: + repository: "example/repo" + content_integrity: "approved" + expected: + decision: allow + error_code: null + reason: "" + + # --- Scenario B: content exceeds threshold → allowed --- + - scenario_id: "integrity-level-block-B" + description: "Content with integrity 'merged' is allowed when min-integrity: approved" + input: + tool_config: + repos: + - "*/*" + min-integrity: "approved" + request: + repository: "example/repo" + content_integrity: "merged" + expected: + decision: allow + error_code: null + reason: "" + + # --- Scenario C: content below threshold → denied --- + - scenario_id: "integrity-level-block-C" + description: "Content with integrity 'unapproved' is denied when min-integrity: approved" + input: + tool_config: + repos: + - "*/*" + min-integrity: "approved" + request: + repository: "example/repo" + content_integrity: "unapproved" + expected: + decision: deny + error_code: -32006 + reason: "content integrity below minimum required level" + + # --- Scenario D: content at lowest level below threshold → denied --- + - scenario_id: "integrity-level-block-D" + description: "Content with integrity 'none' is denied when min-integrity: merged" + input: + tool_config: + repos: + - "*/*" + min-integrity: "merged" + request: + repository: "example/repo" + content_integrity: "none" + expected: + decision: deny + error_code: -32006 + reason: "content integrity below minimum required level" + + # --- Scenario E: no min-integrity configured → any content level passes --- + - scenario_id: "integrity-level-block-E" + description: "Without min-integrity, content at any integrity level is permitted" + input: + tool_config: + repos: + - "*/*" + # no min-integrity field + request: + repository: "example/repo" + content_integrity: "none" + expected: + decision: allow + error_code: null + reason: "" diff --git a/specs/github-mcp-access-control-compliance/private-repo-block.yaml b/specs/github-mcp-access-control-compliance/private-repo-block.yaml new file mode 100644 index 00000000000..1b97de7d20a --- /dev/null +++ b/specs/github-mcp-access-control-compliance/private-repo-block.yaml @@ -0,0 +1,73 @@ +# Private Repository Block — Compliance Fixture +# Tests: T-GH-024, T-GH-025, T-GH-026, T-GH-027 +# Spec: §7 Private Repository Controls + +fixture_id: "private-repo-block" +description: > + The `private-repos` field controls whether private repositories are accessible through + the GitHub MCP tool. When `private-repos: false`, any request targeting a private repository + MUST be denied (T-GH-025). When `private-repos: true`, private repositories are permitted + subject to other active restrictions (T-GH-024). Public repositories MUST always be + accessible when `private-repos: false` (T-GH-026). Repository visibility is obtained + from the GitHub API (T-GH-027). + +spec_refs: + - "§7.1 — private-repos: true allows access to private repositories" + - "§7.2 — private-repos: false denies access to private repositories" + - "§7.3 — private-repos: false does not affect public repository access" + - "§7.4 — Repository visibility is queried from GitHub API" + +scenarios: + # --- Scenario A: private-repos: true → private repo allowed --- + - scenario_id: "private-repo-block-A" + description: "private-repos: true allows request for a private repository" + input: + tool_config: + repos: + - "myorg/*" + private-repos: true + min-integrity: "none" + request: + repository: "myorg/internal-tool" + is_private: true + content_integrity: "none" + expected: + decision: allow + error_code: null + reason: "" + + # --- Scenario B: private-repos: false → private repo denied --- + - scenario_id: "private-repo-block-B" + description: "private-repos: false denies request for a private repository" + input: + tool_config: + repos: + - "myorg/*" + private-repos: false + min-integrity: "none" + request: + repository: "myorg/internal-tool" + is_private: true + content_integrity: "none" + expected: + decision: deny + error_code: -32003 + reason: "private repository access denied" + + # --- Scenario C: private-repos: false → public repo still allowed --- + - scenario_id: "private-repo-block-C" + description: "private-repos: false does not block access to a public repository" + input: + tool_config: + repos: + - "myorg/*" + private-repos: false + min-integrity: "none" + request: + repository: "myorg/public-docs" + is_private: false + content_integrity: "none" + expected: + decision: allow + error_code: null + reason: "" diff --git a/specs/github-mcp-access-control-compliance/role-deny.yaml b/specs/github-mcp-access-control-compliance/role-deny.yaml new file mode 100644 index 00000000000..eae1ca0a3ea --- /dev/null +++ b/specs/github-mcp-access-control-compliance/role-deny.yaml @@ -0,0 +1,76 @@ +# Role Deny — Compliance Fixture +# Tests: T-GH-019, T-GH-020, T-GH-023 +# Spec: §6 Role-Based Filtering + +fixture_id: "role-deny" +description: > + Role-based filtering MUST allow access when the authenticated user's effective role + in the repository matches one of the configured roles (T-GH-019), and MUST deny access + when the user's role does not match any of the configured roles (T-GH-020). Multiple + roles are evaluated with OR logic — any matching role grants access (T-GH-023). + +spec_refs: + - "§6.1 — Role-based access control allows users with matching role" + - "§6.2 — Role-based access control denies users with non-matching role" + - "§6.3 — Multiple role values evaluated with OR logic" + - "§6.4 — Role is obtained from GitHub API repository permission level" + +scenarios: + # --- Scenario A: user role matches → allowed --- + - scenario_id: "role-deny-A" + description: "User with 'write' role is allowed when roles: [write, admin]" + input: + tool_config: + repos: + - "*/*" + roles: + - "write" + - "admin" + min-integrity: "none" + request: + repository: "example/repo" + user_role: "write" + content_integrity: "none" + expected: + decision: allow + error_code: null + reason: "" + + # --- Scenario B: user role does not match → denied --- + - scenario_id: "role-deny-B" + description: "User with 'read' role is denied when roles: [write, admin]" + input: + tool_config: + repos: + - "*/*" + roles: + - "write" + - "admin" + min-integrity: "none" + request: + repository: "example/repo" + user_role: "read" + content_integrity: "none" + expected: + decision: deny + error_code: -32002 + reason: "insufficient role" + + # --- Scenario C: user has no role in the repository → denied --- + - scenario_id: "role-deny-C" + description: "User with no role (outside collaborator) is denied when any roles restriction is set" + input: + tool_config: + repos: + - "*/*" + roles: + - "triage" + min-integrity: "none" + request: + repository: "example/private-repo" + user_role: "" + content_integrity: "none" + expected: + decision: deny + error_code: -32002 + reason: "insufficient role" diff --git a/specs/github-mcp-access-control-compliance/wildcard-deny.yaml b/specs/github-mcp-access-control-compliance/wildcard-deny.yaml new file mode 100644 index 00000000000..82b8ededdc8 --- /dev/null +++ b/specs/github-mcp-access-control-compliance/wildcard-deny.yaml @@ -0,0 +1,63 @@ +# Wildcard Deny — Compliance Fixture +# Tests: T-GH-013, T-GH-014 +# Spec: §5 Repository Scoping, §5.2 Wildcard Pattern Matching + +fixture_id: "wildcard-deny" +description: > + An owner-wildcard pattern (e.g., "github/*") MUST allow access to any repository under the + specified owner (T-GH-013), and MUST deny access to repositories under a different owner + (T-GH-014). The wildcard matches all repository names under the given owner and does not + match across owners. + +spec_refs: + - "§5.2 — Owner wildcard matches all repositories under the specified owner" + - "§5.2 — Owner wildcard rejects repositories under a different owner" + +scenarios: + # --- Scenario A: owner wildcard matches repository under same owner --- + - scenario_id: "wildcard-deny-A" + description: "Pattern 'github/*' allows request for repository 'github/any-repo'" + input: + tool_config: + repos: + - "github/*" + min-integrity: "none" + request: + repository: "github/any-repo" + content_integrity: "none" + expected: + decision: allow + error_code: null + reason: "" + + # --- Scenario B: owner wildcard denies repository under different owner --- + - scenario_id: "wildcard-deny-B" + description: "Pattern 'github/*' denies request for repository 'microsoft/vscode'" + input: + tool_config: + repos: + - "github/*" + min-integrity: "none" + request: + repository: "microsoft/vscode" + content_integrity: "none" + expected: + decision: deny + error_code: -32001 + reason: "repository not in allowed list" + + # --- Scenario C: prefix wildcard within owner --- + - scenario_id: "wildcard-deny-C" + description: "Pattern 'github/gh-*' denies repository 'github/copilot' (no 'gh-' prefix)" + input: + tool_config: + repos: + - "github/gh-*" + min-integrity: "none" + request: + repository: "github/copilot" + content_integrity: "none" + expected: + decision: deny + error_code: -32001 + reason: "repository not in allowed list"