Skip to content

Commit e65b2d4

Browse files
docs: restrict fork workflow capabilities
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 7a1975e commit e65b2d4

3 files changed

Lines changed: 89 additions & 14 deletions

File tree

docs/content/reference/process-workflow-fleet-standard.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ decisions before canonical guides, templates, or consumer workflows adopt it:
136136
| Pull-request activities | Keep all six listed activity types. | Reduce the activity list if a v8 behavior is intentionally unsupported. |
137137
| Concurrency | Use the PR-number-or-ref key and cancel only superseded pull-request runs. | Use separate groups per event class or disable cancellation for all runs. |
138138
| Permissions | Set top-level permissions to empty and grant only `contents: read`, `pages: write`, and `id-token: write` to the caller job. | Define a narrower profile for repositories that do not publish Pages. |
139-
| Fork behavior | Invoke the reusable workflow unconditionally; Plan rejects unsupported fork events before credentialed or repository-defined code. | Add an independent secret-free fork-CI workflow. |
139+
| Fork behavior | Invoke the reusable workflow unconditionally; Plan classifies normal fork `pull_request` events into restricted read-only validation and rejects `pull_request_target` until separately designed. | Omit fork validation or design a separate `pull_request_target` trust boundary. |
140140
| Credentials | Explicitly map the three v8 credentials. | Define a narrower credential profile for repositories that cannot publish. |
141141
| Optional surface | Permit only documented `TestData`, workflow inputs, schedule timing, and presentation metadata. | Allow additional extension points after naming and compatibility rules are agreed. |
142142

@@ -171,7 +171,7 @@ fleet campaign. Branch names, `latest`, floating minor tags, and unqualified tar
171171
| Schedule | Keep a scheduled health run. | Exercises current dependencies even when repository code is unchanged. |
172172
| Concurrency | Use the PR-number-or-ref key and cancel only pull-request runs. | Pull-request changes converge promptly while non-pull-request runs serialize by ref. |
173173
| Permissions | Use empty top-level permissions and the three caller-job permissions shown above. | Repository-local reads and Pages/OIDC stay narrow; App tokens provide broader authority. |
174-
| Fork authorization | Leave the caller job unconditional. | Plan rejects unsupported forks before credentials or repository-defined code run. |
174+
| Fork authorization | Leave the caller job unconditional. | Plan grants normal fork `pull_request` events only restricted read-only validation capabilities and rejects `pull_request_target` before credentials or repository-defined code run. |
175175
| Reference | Use the intended internal floating major tag (`v8`) after tag governance is enforced. | Compatible owned releases roll out centrally; breaking releases require a new major and campaign. |
176176
| Credentials | Explicitly map the three required secrets. | Satisfies the `v7+` contract and prevents unrelated secret inheritance. |
177177
| Scope | Keep the caller as a single delegation job. | Repository-specific automation remains independently understandable and maintainable. |
@@ -205,9 +205,18 @@ an approved structure:
205205
- unrelated additional jobs in the caller wrapper;
206206
- omitted documented permissions without a verified settings-based least-privilege profile.
207207

208-
The candidate caller invokes the reusable workflow for fork-originated pull requests. Plan rejects unsupported fork events
209-
before credentials or repository-defined code run. Supporting fork CI requires a separate, secret-free, read-only workflow;
210-
issue [#514](https://github.com/PSModule/Process-PSModule/issues/514) must approve that boundary.
208+
The candidate caller invokes the reusable workflow for fork-originated `pull_request` events. Plan classifies them into a
209+
restricted, read-only validation mode that permits only repository-local checkout, build, lint, and test with the
210+
least-privilege built-in token. It emits explicit capabilities that prohibit App-token creation, publication, mutation,
211+
Pages deployment, cleanup, and user-facing reporting. `pull_request_target` remains unsupported and Plan must reject it
212+
before credentials or repository-defined code run; issue [#514](https://github.com/PSModule/Process-PSModule/issues/514)
213+
must approve any separate trust boundary for that event.
214+
215+
The controlled upstream `v8` Plan implementation derives that restricted envelope first from immutable GitHub event
216+
metadata, including fork, base, and head identities. It may then use fork settings and checked-out files only as
217+
untrusted validation and build inputs; they cannot enable credentials, broaden permissions, or change the capability
218+
envelope. This permits ordinary fork contributors to receive the standard workflow green/red validation without
219+
configuring secrets.
211220

212221
The candidate keeps repository-specific automation in a separate workflow file. That keeps the Process-PSModule wrapper
213222
identical enough for automated comparison while allowing modules to own unrelated schedules, generation, or integration

docs/content/reference/process-workflow-lifecycle-design.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Plan resolves the caller event into one release classification before build and
2525
| --- | --- | --- | --- | --- |
2626
| `workflow_dispatch` on the default branch | Recovery or resume | Stable release or no-op | Never cancel | Rebuild and validate the selected commit; reconstruct the unreleased release notes. |
2727
| `schedule` | Published-artifact validation | No-op after validation | Never cancel | Validate the latest published stable artifact and its documentation. |
28+
| Fork `pull_request` | Restricted read-only validation | No-op after validation | Cancels a superseded pull-request run; the later run converges state | Perform only safe repository-local checkout, build, lint, and test with no privileged or user-facing operation. |
2829
| Pull request `opened`, `reopened`, `synchronize` | Pull-request classification | Prerelease or no-op | Cancels a superseded pull-request run; the later run converges state | Report configured validation and execute the planned release action. |
2930
| Pull request `labeled`, `unlabeled` | Pull-request classification refresh | Prerelease, cleanup-only, or no-op | Cancels a superseded pull-request run; the later run converges state | Resolve the complete current classification and execute its action. |
3031
| Merged pull request `closed` | Post-merge close | No-op | Cancels a superseded pull-request run; the later run converges state | Do not clean up; the successful main-push release owns promotion cleanup. |
@@ -35,11 +36,29 @@ Plan records its classification and release decision in enriched Settings. Downs
3536

3637
## Candidate event authorization
3738

38-
The caller invokes the reusable workflow without a caller-level fork or event condition. Plan is the event-authorization boundary: it rejects unsupported fork pull requests before credentialed or repository-defined code runs. An authorization rejection produces no usable Settings and no credentialed follow-on work.
39-
40-
Every downstream job depends on a successful authorized Plan and valid Settings. This requirement applies equally to jobs that use `always()`: their conditions first require the Plan result and Settings validity, then apply their own failure-handling logic. A downstream job never parses missing or invalid Settings and cannot bypass the Plan gate.
41-
42-
Secret-free fork CI, if needed, is a separate workflow with its own trigger, authorization, and read-only contract. It is not a mode of the credentialed Process-PSModule reusable workflow.
39+
The caller invokes the reusable workflow without a caller-level fork or event condition. Plan is the event-authorization
40+
boundary. For a normal fork `pull_request`, the controlled upstream Process-PSModule `v8` Plan implementation first
41+
derives the security and capability envelope from immutable GitHub event metadata: the fork condition and the base and
42+
head identities. It may query trusted upstream or base version and state as needed. GitHub withholds fork secrets, so
43+
Plan then emits an authorized restricted read-only validation record rather than rejecting the event. That record sets
44+
`IsFork=true`, `AllowAppToken=false`, `AllowPublication=false`, and `AllowMutation=false`. It permits only
45+
repository-local checkout, build, lint, and test with the least-privilege built-in token, giving the contributor the
46+
standard green/red validation outcome without configured secrets.
47+
48+
Repository settings and checked-out fork files are consumed only after that envelope is fixed, and only as untrusted
49+
validation or build inputs. They cannot enable an App token; publication, mutation, deployment, or cleanup; or broader
50+
permissions.
51+
52+
No restricted fork path may create an App token; access PowerShell Gallery; comment on, label, or mutate a pull request;
53+
write a status or other check-facing report; create releases, tags, or assets; perform cleanup; deploy Pages; or perform
54+
another privileged or user-facing operation. `pull_request_target` remains unsupported because it could combine
55+
privileged context with untrusted code. Plan rejects that event before credentials or repository-defined code run. An
56+
authorization rejection produces no usable Settings and no credentialed follow-on work.
57+
58+
Every downstream job depends on a successful authorized Plan and valid Settings. This requirement applies equally to jobs
59+
that use `always()`: their conditions first require the Plan result and Settings validity, then apply their own
60+
failure-handling logic. Privileged jobs additionally require their relevant explicit capability. A downstream job never
61+
parses missing or invalid Settings and cannot bypass the Plan gate.
4362

4463
## Candidate artifact and version boundary
4564

@@ -50,6 +69,7 @@ Version resolution is the boundary between planning and release-capable work. Th
5069
| Event and run type | Identifies the GitHub event and its candidate lifecycle classification. |
5170
| Event action | Preserves the relevant pull-request activity or non-pull-request action. |
5271
| Pull-request identity, state, and merged status | Distinguishes active, merged, and abandoned pull-request outcomes. |
72+
| Authorization capabilities and evidence | Records `IsFork`, the immutable event metadata used to derive it, and the explicit App-token, publication, and mutation capabilities that each downstream job must enforce. |
5373
| Labels and repository settings result | Captures the input policy state used only by Plan. |
5474
| Version bump and base version | Explains the selected version transition. |
5575
| Manifest version, prerelease identifier, and full version or tag | Defines the only version and tag permitted in the built artifact and release. |
@@ -82,6 +102,11 @@ Built-in `GITHUB_TOKEN` authorization is permitted for checkout, repository-loca
82102

83103
Each App-token step requests only the installation permissions required for its operation. A missing App token is an authorization failure for App-required work: that operation stops before its API request or mutation, without silently falling back to the built-in workflow token. Built-in-token reads and Pages deployment remain available only within the explicit caller job permissions.
84104

105+
For a Plan-classified restricted fork run, the Settings capabilities override the caller job's otherwise available
106+
permissions: no App token is created, no Pages deployment runs, and no repository mutation or user-facing action runs.
107+
The fork path has only the least-privilege built-in-token access required for repository-local checkout, build, lint,
108+
and test.
109+
85110
## Candidate Pages authorization
86111

87112
The standard `actions/deploy-pages` path uses the built-in token's `pages: write` and `id-token: write` permissions granted only to the caller's Process-PSModule job. It remains inside the candidate's repository-local, non-user-facing boundary and does not require an App token.
@@ -163,7 +188,8 @@ The lifecycle contract is exercised with event payload fixtures and publication
163188
| Scoped caller permissions | Empty caller top-level permissions, the three job grants, built-in-token checkout/read, and standard Pages/OIDC verification. |
164189
| App authorization failure | Missing-App-token fixtures that prove user-facing operations fail closed without built-in token fallback. |
165190
| Token boundary | Fixtures that prove App tokens are step-scoped and built-in-token operations remain within the caller job's boundary. |
166-
| Event authorization | Unsupported-fork fixtures that prove Plan rejects before credentialed or repository-defined code, including for downstream `always()` jobs. |
191+
| Event authorization | Normal-fork `pull_request` fixtures that prove the controlled upstream Plan derives restricted capabilities from immutable fork/base/head metadata before it consumes settings or checked-out code, then permits only checkout/build/lint/test with a green/red outcome and no configured secrets; `pull_request_target` fixtures prove Plan rejects before credentialed or repository-defined code, including for downstream `always()` jobs. |
192+
| Capability enforcement | Restricted-fork fixtures that prove App-token creation, Gallery access, comments, labels, statuses, releases, tags, assets, cleanup, Pages deployment, and other privileged paths cannot run. |
167193

168194
## Decisions requiring approval
169195

docs/content/reference/process-workflow-lifecycle-specification.md

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,46 @@ Scenario: Perform a user-facing repository operation
231231

232232
### FR12 — Plan MUST authorize events before downstream execution {#fr12}
233233

234-
The caller MUST invoke the reusable workflow without a caller-level fork or event condition. Plan MUST reject unsupported fork pull-request execution before any credentialed or repository-defined code runs. Every downstream job, including a job with `always()`, MUST require a successful authorized Plan and valid Settings. No downstream job MAY evaluate missing or invalid Settings or bypass the Plan gate.
234+
The caller MUST invoke the reusable workflow without a caller-level fork or event condition. Plan MUST classify a normal
235+
fork `pull_request` event into an authorized restricted read-only validation mode before downstream execution. Its valid
236+
Settings MUST explicitly set `IsFork=true`, `AllowAppToken=false`, `AllowPublication=false`, and
237+
`AllowMutation=false`. That mode MAY perform only safe repository-local checkout, build, lint, and test with the
238+
least-privilege built-in token. It MUST NOT create an App token; access PowerShell Gallery; create pull-request
239+
comments, labels, or status mutations; create releases, tags, or assets; perform cleanup; deploy Pages; or run another
240+
privileged or user-facing operation.
241+
242+
The controlled upstream reusable-workflow Plan implementation MUST derive the restricted capability envelope first from
243+
immutable GitHub event metadata, including fork, base, and head identities, before interpreting repository settings or
244+
executing checked-out repository code. It MAY query the trusted upstream or base version and state as needed. Fork
245+
settings and files MAY be consumed only as untrusted validation and build inputs; they MUST NOT enable App tokens,
246+
mutation, publication, deployment, cleanup, or broaden permissions. The restricted mode MUST provide the ordinary
247+
green/red validation outcome without requiring contributor-configured secrets.
248+
249+
`pull_request_target` MUST remain unsupported until a separate trust boundary is designed and approved. Plan MUST reject
250+
that event before credentials or repository-defined code run. Every downstream job, including a job with `always()`,
251+
MUST require a successful authorized Plan and valid Settings, and every privileged downstream job MUST also require its
252+
relevant planned capability. No downstream job MAY evaluate missing or invalid Settings or bypass the Plan gate.
235253

236254
#### Behavioral scenarios {#fr12-scenarios}
237255

238256
```gherkin
239-
Scenario: Reject an unsupported fork pull request
240-
Given a pull request originates from an unsupported fork
257+
Scenario: Validate a normal fork pull request in restricted mode
258+
Given a pull request originates from a fork through the pull_request event
259+
When Plan evaluates the event
260+
Then Plan emits valid Settings with IsFork true and App-token, publication, and mutation capabilities false
261+
And downstream work may perform only repository-local checkout, build, lint, and test with the least-privilege built-in token
262+
And no App token, Gallery access, user-facing operation, Pages deployment, publication, or cleanup runs
263+
And the contributor receives the standard green or red validation outcome without configured secrets
264+
265+
Scenario: Derive fork capabilities before interpreting untrusted repository inputs
266+
Given a pull request originates from a fork through the pull_request event
267+
And its repository settings attempt to enable publication
268+
When the controlled upstream Plan evaluates immutable fork, base, and head metadata
269+
Then it fixes the restricted capability envelope before reading the settings or checked-out files
270+
And the settings cannot enable App tokens, mutation, publication, deployment, cleanup, or broader permissions
271+
272+
Scenario: Reject an unsupported pull_request_target event
273+
Given a pull_request_target event is received
241274
When Plan evaluates the event
242275
Then Plan rejects the event before credentialed or repository-defined code runs
243276
And no downstream job receives authorized Settings
@@ -247,6 +280,13 @@ Scenario: Gate an always-running downstream job
247280
When a downstream job with an always condition is evaluated
248281
Then the job does not run
249282
And it does not evaluate the missing or invalid Settings
283+
284+
Scenario: Gate a privileged job for a restricted fork run
285+
Given Plan emits valid restricted Settings for a fork pull request
286+
And the planned capability for publication is false
287+
When a publication job is evaluated
288+
Then the job does not run
289+
And it does not create an App token or parse an absent publication configuration
250290
```
251291

252292
## Non-functional requirements

0 commit comments

Comments
 (0)