Skip to content

Add allowlisted dynamic repository checkout lists - #68593

Open
pelikhan wants to merge 13 commits into
mainfrom
pelikhan-checkout-syntax
Open

pelikhan wants to merge 13 commits into
mainfrom
pelikhan-checkout-syntax

Conversation

@pelikhan

@pelikhan pelikhan commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Scheduled workers need to process a changing set of repositories without recompiling their workflows. This adds runtime checkout expansion while keeping repository selection constrained by an explicit policy and preserving per-checkout credentials.

Approach

  • Accept newline-separated repository slugs through GitHub Actions expressions in checkout.repository or its repo alias, with mandatory allowed-repos and a literal non-root path prefix for runtime expansion. The standalone ${{ github.repository }} expression is exempt and retains normal single-repository checkout behavior without an allowlist or prefix. Scoped wildcard patterns filter selections; repository selectors and unrestricted wildcard policies are rejected.
  • Validate the entire selected batch and API-resolved canonical names before generating SHA-pinned checkout actions. Public workflow repositories cannot select private or internal targets, and unverifiable metadata fails closed.
  • Expand paths as prefix/owner/repo, record the results in the checkout manifest, and share the layout between agent and safe-output jobs. Target metadata and follow-up fetches use the entry's credentials; agent checkouts remain credential-free.
  • Propagate checkout preparation failures into existing agent failure issues, including safe-output failures, without publishing raw dependency diagnostics. Parsing and compiler API entry points now propagate checkout validation errors instead of ignoring them.

Security and migration

Repository expressions require allowed-repos except for the standalone ${{ github.repository }} expression. To retain the workflow repository at the workspace root, omit repository or use that expression without an allowlist. Other expressions require a policy and prefix; compound expressions containing github.repository do not qualify for the exception. A major changeset and updated reference/specification docs describe the migration.

Runtime lists reject lfs: true at compilation and runtime because repository-controlled LFS endpoints can bypass the allowlist using the checkout token. Generated dynamic actions explicitly disable LFS; static checkout support is unchanged. Public-host runtime checkouts also reject submodules whose visibility cannot be verified. Policy expressions must resolve from trusted configuration, not attacker-controlled selection data.

Agent finalization explicitly cleans and verifies each expanded checkout's git config and nested submodule configs using validated checkout paths, independent of bounded filesystem discovery. This closes the deep-prefix credential-cleanup bypass, including with force-clean-git-credentials: true. Missing configs, unavailable helpers, symlinked configs, or residual credentials stop the job. Safe-output jobs retain their intended write credentials.

Validation

Focused Go compiler/parser/checkout and credential-verification tests, JavaScript type checking, and 592 runtime/manifest/failure-handler tests passed. Local Git regressions cover deep credential includes, nested submodules, failed and false-success cleanup, missing helpers/configs, symlinks, and safe-output credential retention. Standard Go lint, changed-file JavaScript lint, action shell checks, and schema freshness checks passed.

The publication gate remains blocked by pre-existing custom Go lint findings in changed files. Broader package testing also encountered macOS/Linux fixture limitations; no live GitHub Actions execution or authenticated end-to-end LFS reproduction was performed.

pelikhan and others added 5 commits October 13, 2026 08:38
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Require allowed-repos for repository expressions, keep wildcards exclusive to scoped allowlists, and propagate hard checkout rejections into agent failure issues. Fail compilation instead of silently dropping invalid main or imported checkout configurations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…pression

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread actions/setup/js/checkout_dynamic.cjs Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@pelikhan

Copy link
Copy Markdown
Collaborator Author

@copilot merge main and do a deep review as a GitHub actions security expert and compare to the security architecture of AW. Be adverserial.

@pelikhan

Copy link
Copy Markdown
Collaborator Author

@copilot add a max-repos to place a limit of the number of repos checked out.

Copilot AI and others added 2 commits October 14, 2026 14:43
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

@copilot merge main and do a deep review as a GitHub actions security expert and compare to the security architecture of AW. Be adverserial.

Merged current main in cb1d3d58eb and completed an adversarial security review against AW’s repository policy, expression trust, credential lifecycle, agent/safe-output isolation, and firewall boundaries.

Confirmed HIGH finding, fixed in bafe428338: private/internal workflows could fetch non-allowlisted repositories through repository-controlled submodules using the checkout token. Credential cleanup cannot undo that read, and a GitHub domain allowlist cannot enforce repository-level policy. Runtime lists now reject enabled submodules at compilation and runtime for every host visibility; generated checkout actions hard-disable submodules. Static checkout behavior is unchanged.

Independent security re-review verified 33 host/setting combinations; focused code review found no further significant issues. Allowlist expressions must still come from trusted policy sources.

Validation: 615 targeted JavaScript tests, type checking, focused Go checkout/credential/parser tests, and final impacted tests passed; secret scanning passed. The publication gate remains blocked by inherited custom Go lint findings. Automated review/CodeQL timed out twice generating the merge diff and is incomplete. No live authenticated Actions checkout or end-to-end disclosure test was performed.

@pelikhan
pelikhan marked this pull request as ready for review October 14, 2026 14:53
Copilot AI balanced review requested due to automatic review settings October 14, 2026 14:53
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

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

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

Commit pushed: 9366956

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Ponytail Reviewer

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Ponytail Reviewer · gpt56 · 13.8 AIC · ⌖ 5.31 AIC · ⊞ 14.2K · ◷

Generated by Ponytail Reviewer for #68593

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Test Quality Sentinel · haiku45 · 5.1 AIC · ⌖ 11.7 AIC · ◇ 1.69 AIC · ⊞ 8.8K · ◷

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (692 new lines across pkg/ and actions/, >100 threshold) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/68593-allowlisted-runtime-repository-checkout-lists.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 and description
  2. Complete the missing sections — add context the AI couldn't infer, refine the decision rationale, and list real alternatives you 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-68593: Allowlisted Runtime Repository Checkout Lists

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

🔍 Evidence used
  • No implementation label; enforcement triggered by code volume: 692 additions in default business-logic directories (threshold 100), 35 changed files, no .design-gate.yml override.
  • No ADR link or Context/Decision/Alternatives/Consequences section found in the PR body; no docs/adr/68593-*.md on the branch; no closing-keyword linked issue in the PR body.
  • Decision inferred from pkg/workflow/checkout_dynamic.go, checkout_config_parser.go, checkout_step_generator.go, actions/setup/js/checkout_dynamic.cjs, actions/setup/sh/clean_git_credentials.sh, the schema change, and .changeset/major-runtime-repository-checkout.md.
❓ 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.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · pi · opus50 · 45.3 AIC · ⌖ 50.9 AIC · ⊞ 1.8K · ◷
Comment /review to run again

Copilot AI left a comment

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.

🟡 Changes recommended

Dynamic allowlist expressions currently permit caller-controlled policy expansion, defeating the intended trust boundary.

1 open finding
What changed in this PR

Adds runtime-expanded, allowlisted repository checkouts while preserving credential isolation, manifest generation, and failure reporting.

Changes:

  • Adds dynamic checkout validation, expansion, metadata checks, and credential cleanup.
  • Propagates checkout parsing and runtime failures through compiler and failure-reporting paths.
  • Updates schemas, documentation, migration notes, and tests.

Security review found one must-fix issue: caller-controlled expressions can currently define both repository selection and its allowlist.

File Description
pkg/​workflow/​workflow_builder.go Propagates checkout parsing errors.
pkg/​workflow/​side_repo_maintenance_integration_test.go Updates dynamic checkout fixture.
pkg/​workflow/​pull_request_target_validation.go Revises trusted-checkout guidance.
pkg/​workflow/​pull_request_target_validation_test.go Updates pull-request-target cases.
pkg/​workflow/​pr_test.go Updates multi-checkout fixtures.
pkg/​workflow/​pr_ready_for_review_checkout_test.go Simplifies workflow-repository checkout.
pkg/​workflow/​notify_comment_conclusion_helpers.go Forwards dynamic checkout failures.
pkg/​workflow/​inline_imports_test.go Handles builder errors.
pkg/​workflow/​frontmatter_types_test.go Tests invalid checkout booleans.
pkg/​workflow/​frontmatter_parsing.go Returns checkout parsing errors.
pkg/​workflow/​compiler.go Validates runtime checkout configurations.
pkg/​workflow/​compiler_string_api.go Propagates builder failures.
pkg/​workflow/​compiler_safe_outputs_job.go Exposes safe-output checkout errors.
pkg/​workflow/​compiler_orchestrator_workflow.go Handles builder errors.
pkg/​workflow/​compiler_orchestrator_workflow_test.go Updates builder tests.
pkg/​workflow/​compiler_main_job_helpers.go Exposes agent checkout errors.
pkg/​workflow/​checkout_step_generator.go Generates dynamic checkout and manifest steps.
pkg/​workflow/​checkout_path_validation.go Updates dynamic-path documentation.
pkg/​workflow/​checkout_manifest_compile_test.go Tests dynamic manifest wiring.
pkg/​workflow/​checkout_manager.go Stores runtime allowlists.
pkg/​workflow/​checkout_dynamic.go Implements dynamic checkout generation.
pkg/​workflow/​checkout_dynamic_test.go Covers compiler-side dynamic behavior.
pkg/​workflow/​checkout_credentials_render.go Excludes dynamic entries from static cleanup.
pkg/​workflow/​checkout_config_parser.go Parses and validates runtime lists.
pkg/​parser/​schemas/​main_workflow_schema.json Adds alias and allowlist schema.
docs/​src/​content/​docs/​specs/​checkout-behavior-specification.md Specifies runtime checkout behavior.
docs/​src/​content/​docs/​reference/​frontmatter-full.md Documents new frontmatter fields.
docs/​src/​content/​docs/​reference/​checkout.md Adds runtime checkout guidance.
actions/​setup/​sh/​clean_git_credentials.sh Supports explicit config cleanup.
actions/​setup/​js/​handle_agent_failure.test.cjs Tests checkout failure reporting.
actions/​setup/​js/​handle_agent_failure.cjs Reports runtime checkout rejections.
actions/​setup/​js/​checkout_dynamic.test.cjs Tests runtime validation and cleanup.
actions/​setup/​js/​checkout_dynamic.cjs Implements runtime checkout expansion.
actions/​setup/​js/​build_checkout_manifest.cjs Incorporates dynamic checkout plans.
.changeset/​major-runtime-repository-checkout.md Records the breaking release change.

🧠 Review effort: Balanced

Comment thread pkg/workflow/checkout_dynamic.go Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

Peek at saved memory (pr-code-quality-reviewer)
reviewed_at: 2026-10-14T14:57:03.914+00:00
review_event: REQUEST_CHANGES
top_themes:
  - missing safe_outputs checkout manifest for runtime-selected repos
  - dynamic safe_outputs checkout token diverges from effective PR auth
files_reviewed:
  - actions/setup/js/checkout_dynamic.cjs
  - actions/setup/js/handle_agent_failure.cjs
  - actions/setup/sh/clean_git_credentials.sh
  - pkg/workflow/checkout_config_parser.go
  - pkg/workflow/checkout_dynamic.go
  - pkg/workflow/checkout_step_generator.go
  - pkg/workflow/workflow_builder.go
comment_count: 2

Note

This comment is managed by comment memory.

Expand the saved memory block to view or edit the persistent context for this thread.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 119.2 AIC · ⌖ 5.93 AIC · ⊞ 21.4K · ◷
Comment /review to run again

@github-actions github-actions Bot left a comment

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.

Request changes

The runtime checkout work is not wired end to end yet: the agent path knows about the dynamic manifest and cleanup rules, but the safe_outputs path still loses runtime-selected repositories and can keep a different credential than the PR handlers actually use.

Blocking themes
  • safe_outputs never emits the checkout manifest for runtime-selected repos, so PR handlers fall back to the bounded workspace scan and can miss valid deep checkouts.
  • Dynamic safe_outputs checkouts fall back to pushToken, but that path does not carry the global safe-outputs GitHub App step output, so runtime-selected repos can fetch or push with the wrong credential.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 119.2 AIC · ⌖ 5.93 AIC · ⊞ 21.4K
Comment /review to run again

Comment thread pkg/workflow/checkout_step_generator.go
Comment thread pkg/workflow/checkout_dynamic.go

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /codebase-design to this high-risk runtime-checkout feature. The design is unusually thorough (fail-closed metadata checks, SHA-pinned actions, path-traversal/symlink defense, credential cleanup verification, 592 passing tests), and the LFS/submodule rejection logic correctly blocks the direct allowlist-bypass vector.

📋 Key Themes & Highlights

Key Themes

  • One open finding from GitHub Advanced Security (flagged on checkout_dynamic.cjs:150): submodule rejection is based solely on the checkout.submodules frontmatter setting; a repository-controlled .gitmodules file is never inspected. Today this is mitigated because the generated actions/checkout step hard-codes submodules: false, closing the direct bypass — but the surrounding comments/error text ("submodule URLs can bypass allowed-repos") promise a broader guarantee than is explicitly tested. Left an inline comment requesting either an explicit test/comment confirming no code path ever acts on .gitmodules for these checkouts, or a defense-in-depth guard if one exists.
  • Everything else reviewed (allowlist parsing/merging, path validation, credential cleanup, failure propagation, compiler validation ordering) is well-tested and consistent with existing patterns (resolvedCheckout, CheckoutManager, writeYAMLEnv escaping).

Positive Highlights

  • ✅ Fail-closed API visibility checks before any checkout action is generated, including canonical-name re-validation after redirects.
  • ✅ allowedRepos merge semantics (first-seen wins) are explicitly tested against import-based policy broadening.
  • ✅ Credential cleanup (cleanCheckoutCredentials) walks real filesystem state rather than relying on bounded find discovery, closing the deep-prefix bypass described in the PR body, with a dedicated regression test for nested submodule configs beyond the old depth cap.
  • ✅ Thorough test coverage across both the Go compiler layer and the JS runtime layer for LFS/submodule/path/symlink edge cases.

Given the single open finding is already acknowledged by tooling and has a clear closure path, this does not block approval but should be resolved before merge.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 153.8 AIC · ⌖ 15.6 AIC · ⊞ 10.3K
Comment /matt to run again

Comment thread actions/setup/js/checkout_dynamic.cjs
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

@copilot add a max-repos to place a limit of the number of repos checked out.

Added checkout.max-repos in 4741e30. It accepts a positive literal integer and defaults to 10 unique repositories per runtime checkout entry. Oversized lists fail before metadata requests or checkout, rather than being truncated. Matching is case-insensitive; merged entries retain the smallest effective cap, and agent/safe-output jobs enforce the same limit.

Updated schema, documentation, and regression tests. Focused Go tests, JavaScript type checking, and 628 final JavaScript tests passed. Independent code review found no significant issues; the publication gate still has inherited custom Go lint failures. Automated validation reported success but skipped unchanged review/Go analysis.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot address the following outstanding work in one pass:

  1. Update this branch with the latest main using make merge-main, resolving any conflicts and preserving the intended changes.
  2. Review (actions/setup/js/checkout_dynamic.cjs:150): Submodules are only blocked for public workflow hosts, but repository-controlled .gitmodules URLs bypass allowed-repos in exactly the same way the code rejects lfs: true for all hosts. In a private/internal workflow, any repository matching a scoped allowlist pattern (e.g. org/*, where an attacker may be able to create or modify a repo) can declare a submodule pointing at any other GitHub repository the checkout token can read; actions/checkout will clone it with the token's credentials into the workspace, placing non-allowlisted... - Add allowlisted dynamic repository checkout lists #68593 (comment)
  3. Review (pkg/workflow/checkout_dynamic.go:15): Do not skip validation for every allowed-repos expression. This currently accepts caller-controlled policies such as ${{ inputs.allowed }} (and the new test explicitly compiles one), so the same caller can choose both the repository list and a scoped pattern that authorizes it, defeating the mandatory allowlist. Restrict policy expressions to trusted configuration contexts (for example repository/org variables), or reject dynamic policies whose provenance cannot be established; only the selected repository list should be attacker-influenced. - Add allowlisted dynamic repository checkout lists #68593 (comment)
  4. Review (pkg/workflow/checkout_step_generator.go:171): This only feeds the manifest builder in the agent job, so the safe_outputs job still has no checkout-manifest.json for runtime-selected repos and falls back to the bounded workspace scan. - Add allowlisted dynamic repository checkout lists #68593 (comment)
  5. Review (pkg/workflow/checkout_dynamic.go:77): Falling back to pushToken here persists the wrong credential for dynamic safe_outputs checkouts when the effective PR auth comes from the global safe-outputs GitHub App. - Add allowlisted dynamic repository checkout lists #68593 (comment)
  6. Review (actions/setup/js/checkout_dynamic.cjs:158): [/diagnosing-bugs] This is the same bypass GHAS already flagged: the submodule rejection check (GH_AW_DYNAMIC_SUBMODULES) only validates the frontmatter checkout.submodules setting, but nothing here inspects a repository-controlled .gitmodules file. Since submodules: false is hard-coded into the generated actions/checkout step, the direct bypass via the action's own submodules input is closed — but the code comment and error message ("submodule URLs can bypass allowed-repos") imply a broader guarantee that isn't fully delivered: if a later finish() step or any follow-up tooli... - Add allowlisted dynamic repository checkout lists #68593 (comment)

Push the necessary fixes, reply to each listed review thread and resolve it when addressed. Ignore feedback already answered or resolved. Use the pr-finisher skill and stop when only human review or CI remains; do not trigger CI.

Sous-chef head: 4741e30
Sous-chef work: 1ff8cb849c058765721a3a4e8af0c064e119a72570671265b27fe6c76261e218 2ab473f5d249e327d2e8ad6fc7e139253328fba6329d07f9df0979b600bfc6d3 4e60461fbab824fab9282b9bb7cb55a0ad851cb2dccfbc8ca60b1ce323ca50ed e247c2b02c4f5d5f1792dcaf74af2864fa9dcc59cae68ee558763e522673c63b f9f636138ff1d3dc7d64aae6d381771c456dda7d32cb5acb736656f2dd8e96c5
Sous-chef state: 1cbcbaed1d2f1ccd75a41f43760477324074a1804cf2816e9f082cc1f011dcec

Generated by 👨‍🍳 PR Sous Chef · pi · haiku45 · 5.53 AIC · ⌖ 6.54 AIC · ⊞ 1.1K · ◷
Comment /souschef to run again

pelikhan and others added 2 commits October 14, 2026 08:32
Restrict allowlist expression provenance, disable recursive submodule fetches, and wire safe-output manifests and effective checkout authentication with regression coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Align safe-output requirements with trusted runtime policies, bounded preflight, job-local manifests, credential finalization, staged isolation, and failure reporting. Add an executable bounded TLA+ model with checker integration, negative controls, and reachability witnesses.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

5 participants