Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 30 additions & 63 deletions .github/workflows/desktop-installed-gate.yml

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Explicit security review remains required

This change affects a GitHub Actions security boundary targeting privileged self-hosted runners. Repository policy requires explicit security review before merge.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,26 @@ name: desktop installed-artifact gate
# publication wiring into release.yml is a separate change.

on:
workflow_dispatch:
inputs:
version:
description: Release version whose desktop artifacts the gate installs
required: true
type: string
from-version:
description: Older release used for the staged npm runtime and the Linux update phases
required: true
type: string
# Hook inputs are FILE NAMES, never command text. The runner's operator installs
# audited executables in a hooks directory (vars.OPENCODEX_GATE_HOOKS_DIR) and a
# dispatch picks among them by name; the gate executes the file directly, so this
# workflow can never become an arbitrary-shell surface on a persistent runner.
consent-hook:
description: Name of the runner hook that answers the takeover consent prompt
required: false
type: string
tray-click-hook:
description: Name of the runner hook that left-clicks the tray icon
required: false
type: string
tray-quit-hook:
description: Name of the runner hook that opens the tray menu and chooses Quit
required: false
type: string
tray-check-hook:
description: Name of the runner hook that chooses Check for Updates in the tray
required: false
type: string
tray-install-hook:
description: Name of the runner hook that chooses Install update in the tray
required: false
type: string
elevate-accept-hook:
description: Name of the runner hook that answers the deb update's elevation prompt (drives the accept path)
required: false
type: string
# repository_dispatch always loads this workflow from the default branch. Unlike
# workflow_dispatch, a caller cannot select a branch-owned workflow definition and
# thereby route arbitrary steps onto these privileged self-hosted runners.
repository_dispatch:
types: [desktop-installed-gate]
Comment on lines +13 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟨 Unvalidated dispatch payloads reach privileged self-hosted runners

Repository dispatches no longer validate required versions before scheduling. Malformed events can occupy every privileged gate runner and trigger failing artifact operations after approval.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +13 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Land the dispatcher on the default branch before switching

When this commit lands on dev, the default branch (main) still does not contain .github/workflows/desktop-installed-gate.yml; GitHub states that repository_dispatch triggers a run only when the workflow file exists on the default branch. Consequently, every attempted dispatch is ignored until a later promotion to main, disabling the installed-artifact gate during the period in which releases are prepared from dev. Install a trusted dispatcher definition on main before removing the existing trigger, or use an already-default-branch workflow as the dispatch trampoline. GitHub’s repository_dispatch documentation documents this restriction.

AGENTS.md reference: AGENTS.md:L339-L341

Useful? React with 👍 / 👎.


permissions:
contents: read

concurrency:
group: desktop-installed-gate-${{ inputs.version }}
group: desktop-installed-gate-${{ github.event.client_payload.version }}
cancel-in-progress: false

jobs:
macos:
runs-on: [self-hosted, opencodex-gate-macos]
timeout-minutes: 60
# Required-review environment: no run reaches the GUI runner without a maintainer
# approval, and the checkout below pins the driver to the protected dev branch, so a
# dispatched ref cannot smuggle modified gate code onto the machine.
# approval. repository_dispatch loads the workflow from the default branch, and the
# checkout below pins the driver itself to the protected dev branch.
environment: opencodex-desktop-gate
defaults:
run:
Expand All @@ -77,7 +44,7 @@ jobs:
- name: Download the release artifact
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_VERSION: ${{ github.event.client_payload['version'] }}
GATE_ARTIFACTS: ${{ runner.temp }}/gate-artifacts
run: |
mkdir -p "$GATE_ARTIFACTS"
Expand All @@ -88,11 +55,11 @@ jobs:

- name: Run the installed-artifact gate
env:
RELEASE_VERSION: ${{ inputs.version }}
FROM_VERSION: ${{ inputs.from-version }}
CONSENT_HOOK: ${{ inputs.consent-hook }}
TRAY_CLICK_HOOK: ${{ inputs.tray-click-hook }}
TRAY_QUIT_HOOK: ${{ inputs.tray-quit-hook }}
RELEASE_VERSION: ${{ github.event.client_payload['version'] }}
FROM_VERSION: ${{ github.event.client_payload['from-version'] }}
CONSENT_HOOK: ${{ github.event.client_payload['consent-hook'] }}
TRAY_CLICK_HOOK: ${{ github.event.client_payload['tray-click-hook'] }}
TRAY_QUIT_HOOK: ${{ github.event.client_payload['tray-quit-hook'] }}
GATE_HOOKS_DIR: ${{ vars.OPENCODEX_GATE_HOOKS_DIR }}
GATE_ARTIFACTS: ${{ runner.temp }}/gate-artifacts
GATE_WORK: ${{ runner.temp }}/installed-gate
Expand Down Expand Up @@ -143,7 +110,7 @@ jobs:
- name: Download the release artifact
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
RELEASE_VERSION: ${{ github.event.client_payload['version'] }}
GATE_ARTIFACTS: ${{ runner.temp }}/gate-artifacts
run: |
mkdir -p "$GATE_ARTIFACTS"
Expand All @@ -154,11 +121,11 @@ jobs:

- name: Run the installed-artifact gate
env:
RELEASE_VERSION: ${{ inputs.version }}
FROM_VERSION: ${{ inputs.from-version }}
CONSENT_HOOK: ${{ inputs.consent-hook }}
TRAY_CLICK_HOOK: ${{ inputs.tray-click-hook }}
TRAY_QUIT_HOOK: ${{ inputs.tray-quit-hook }}
RELEASE_VERSION: ${{ github.event.client_payload['version'] }}
FROM_VERSION: ${{ github.event.client_payload['from-version'] }}
CONSENT_HOOK: ${{ github.event.client_payload['consent-hook'] }}
TRAY_CLICK_HOOK: ${{ github.event.client_payload['tray-click-hook'] }}
TRAY_QUIT_HOOK: ${{ github.event.client_payload['tray-quit-hook'] }}
GATE_HOOKS_DIR: ${{ vars.OPENCODEX_GATE_HOOKS_DIR }}
GATE_ARTIFACTS: ${{ runner.temp }}/gate-artifacts
GATE_WORK: ${{ runner.temp }}/installed-gate
Expand Down Expand Up @@ -218,8 +185,8 @@ jobs:
- name: Download the release artifacts
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
FROM_VERSION: ${{ inputs.from-version }}
RELEASE_VERSION: ${{ github.event.client_payload['version'] }}
FROM_VERSION: ${{ github.event.client_payload['from-version'] }}
ARTIFACT_SUFFIX: ${{ matrix.suffix }}
GATE_ARTIFACTS: ${{ runner.temp }}/gate-artifacts
run: |
Expand All @@ -235,16 +202,16 @@ jobs:

- name: Run the installed-artifact gate
env:
RELEASE_VERSION: ${{ inputs.version }}
FROM_VERSION: ${{ inputs.from-version }}
RELEASE_VERSION: ${{ github.event.client_payload['version'] }}
FROM_VERSION: ${{ github.event.client_payload['from-version'] }}
GATE_FORMAT: ${{ matrix.format }}
ARTIFACT_SUFFIX: ${{ matrix.suffix }}
CONSENT_HOOK: ${{ inputs.consent-hook }}
TRAY_CLICK_HOOK: ${{ inputs.tray-click-hook }}
TRAY_QUIT_HOOK: ${{ inputs.tray-quit-hook }}
TRAY_CHECK_HOOK: ${{ inputs.tray-check-hook }}
TRAY_INSTALL_HOOK: ${{ inputs.tray-install-hook }}
ELEVATE_ACCEPT_HOOK: ${{ inputs.elevate-accept-hook }}
CONSENT_HOOK: ${{ github.event.client_payload['consent-hook'] }}
TRAY_CLICK_HOOK: ${{ github.event.client_payload['tray-click-hook'] }}
TRAY_QUIT_HOOK: ${{ github.event.client_payload['tray-quit-hook'] }}
TRAY_CHECK_HOOK: ${{ github.event.client_payload['tray-check-hook'] }}
TRAY_INSTALL_HOOK: ${{ github.event.client_payload['tray-install-hook'] }}
ELEVATE_ACCEPT_HOOK: ${{ github.event.client_payload['elevate-accept-hook'] }}
GATE_HOOKS_DIR: ${{ vars.OPENCODEX_GATE_HOOKS_DIR }}
GATE_ARTIFACTS: ${{ runner.temp }}/gate-artifacts
GATE_WORK: ${{ runner.temp }}/installed-gate
Expand Down
8 changes: 5 additions & 3 deletions tests/ci-workflows/installed-gate-drivers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,10 @@ describe("installed-artifact gate workflow", () => {

test("it is dispatch-only: a stateful GUI machine must never run because a push happened", () => {
const triggers = Array.isArray(workflow.on) ? workflow.on : Object.keys(workflow.on ?? {});
expect(triggers).toEqual(["workflow_dispatch"]);
expect(triggers).toEqual(["repository_dispatch"]);
expect((workflow.on as Record<string, { types?: string[] }>).repository_dispatch?.types).toEqual([
"desktop-installed-gate",
]);
});

test("least privilege: read-only contents and nothing else", () => {
Expand Down Expand Up @@ -508,8 +511,7 @@ describe("installed-artifact gate workflow", () => {
});

test("GUI automation inputs are hook names, never command text", () => {
const dispatch = (workflow.on as Record<string, { inputs?: Record<string, unknown> }>).workflow_dispatch;
const inputNames = Object.keys(dispatch?.inputs ?? {});
const inputNames = [...text.matchAll(/client_payload\[['"]([^'"]+)['"]\]/g)].map(match => match[1]);
expect(inputNames).toContain("consent-hook");
expect(inputNames).toContain("tray-quit-hook");
expect(inputNames).toContain("elevate-accept-hook");
Comment on lines +514 to 517

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Payload-key test no longer verifies the dispatch contract

The regex collects duplicated payload references from the workflow body, not declared inputs. It cannot detect missing required fields or distinguish accepted keys from incidental references.

(Refers to this code)

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Expand Down
Loading