Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/agent-task-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- "contracts/agent-task-workflow-request.fixture.json"
- "contracts/run-agent-task-reusable-workflow-interface.v1.json"
- "docs/agent-task-reusable-workflow.md"
- "fixtures/agent-task-reusable-workflow-consumer*.yml"
- "package-lock.json"
- "package.json"
- "tests/agent-task-*.test.ts"
Expand All @@ -23,6 +24,7 @@ on:
- "contracts/agent-task-workflow-request.fixture.json"
- "contracts/run-agent-task-reusable-workflow-interface.v1.json"
- "docs/agent-task-reusable-workflow.md"
- "fixtures/agent-task-reusable-workflow-consumer*.yml"
- "package-lock.json"
- "package.json"
- "tests/agent-task-*.test.ts"
Expand Down
45 changes: 37 additions & 8 deletions .github/workflows/run-agent-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: Run Agent Task (reusable)
'on':
workflow_call:
inputs:
wp_codebox_release_ref:
description: Exact immutable WP Codebox release tag used for this workflow invocation, in vX.Y.Z form.
type: string
required: true
external_package_source:
description: "JSON descriptor for exactly one standalone .agent.json file: repository, immutable revision, file path, and sha256-bytes-v1 digest."
type: string
Expand Down Expand Up @@ -146,8 +150,7 @@ jobs:
run-agent-task:
runs-on: ubuntu-latest
env:
# Advance this only in a subsequent workflow commit after helper changes land.
WP_CODEBOX_HELPER_REVISION: d9e329e7c3bb852b857870a26d8f3853feef751d
WP_CODEBOX_RELEASE_REF: ${{ inputs.wp_codebox_release_ref }}
permissions:
contents: write
pull-requests: write
Expand All @@ -160,22 +163,48 @@ jobs:
credential_mode: ${{ steps.execute.outputs.credential_mode }}
declared_artifacts_json: ${{ steps.execute.outputs.declared_artifacts_json }}
steps:
- name: Validate pinned WP Codebox helper revision
- name: Validate coherent WP Codebox release tag
env:
WORKFLOW_REF: ${{ github.workflow_ref }}
run: |
if ! [[ "${WP_CODEBOX_HELPER_REVISION}" =~ ^[0-9a-f]{40}$ ]]; then
echo "WP_CODEBOX_HELPER_REVISION must be a full lowercase Git commit SHA" >&2
if ! [[ "${WP_CODEBOX_RELEASE_REF}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "wp_codebox_release_ref must be an immutable vX.Y.Z release tag" >&2
exit 1
fi
expected_workflow_ref="Automattic/wp-codebox/.github/workflows/run-agent-task.yml@${WP_CODEBOX_RELEASE_REF}"
if [[ "${WORKFLOW_REF}" != "${expected_workflow_ref}" ]]; then
echo "The reusable workflow tag and wp_codebox_release_ref must match exactly" >&2
exit 1
fi

- name: Checkout WP Codebox workflow helpers
uses: actions/checkout@v4
with:
repository: Automattic/wp-codebox
# This checkout fetches the exact immutable helper source and verifies
# its reachability independently of the caller's workflow revision.
ref: ${{ env.WP_CODEBOX_HELPER_REVISION }}
ref: ${{ inputs.wp_codebox_release_ref }}
path: .wp-codebox-workflow

- name: Verify WP Codebox workflow helper release
working-directory: .wp-codebox-workflow
run: |
git ls-remote --exit-code --refs origin "refs/tags/${WP_CODEBOX_RELEASE_REF}" >/dev/null
remote_tag_object="$(git ls-remote --refs origin "refs/tags/${WP_CODEBOX_RELEASE_REF}" | cut -f1)"
remote_tag_commit="$(git ls-remote origin "refs/tags/${WP_CODEBOX_RELEASE_REF}^{}" | cut -f1)"
remote_tag_commit="${remote_tag_commit:-${remote_tag_object}}"
checked_out_commit="$(git rev-parse HEAD)"
if [[ "${checked_out_commit}" != "${remote_tag_commit}" ]]; then
echo "Checked-out WP Codebox helpers do not match the remote release tag commit" >&2
exit 1
fi
EXPECTED_VERSION="${WP_CODEBOX_RELEASE_REF#v}" node --input-type=module -e '
import { readFileSync } from "node:fs";
const expected = process.env.EXPECTED_VERSION;
const actual = JSON.parse(readFileSync("package.json", "utf8")).version;
if (actual !== expected) {
throw new Error("WP Codebox helper package version " + actual + " does not match release tag v" + expected);
}
'

- name: Set up Node.js
uses: actions/setup-node@v4
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"schema": "wp-codebox/reusable-workflow-interface/v1",
"workflow": ".github/workflows/run-agent-task.yml",
"inputs": {
"wp_codebox_release_ref": { "required": true, "type": "string" },
"external_package_source": { "required": true, "type": "string" },
"workload_id": { "required": false, "type": "string", "default": "agent-task" },
"workload_label": { "required": false, "type": "string", "default": "Run Agent Task" },
Expand Down
34 changes: 21 additions & 13 deletions docs/agent-task-reusable-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ WP Codebox publishes a reusable GitHub Actions workflow for generic agent tasks:
```yaml
jobs:
run-agent-task:
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@main
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@v0.12.3
with:
wp_codebox_release_ref: v0.12.3
external_package_source: '{"repository":"OWNER/agent-packages","revision":"0123456789abcdef0123456789abcdef01234567","path":"agents/example.agent.json","digest":"sha256-bytes-v1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
target_repo: Automattic/example-target
prompt: Refresh the configured surface from source evidence.
Expand All @@ -29,24 +30,27 @@ public native package, invokes the package-declared agent through the native cha
in a credential-free verification environment, and returns actual runtime and
publication data.

## Helper Source Revision
## Helper Release Tag

The workflow checks out its helper scripts from one immutable WP Codebox commit
configured as `WP_CODEBOX_HELPER_REVISION`. This source revision is independent
of the caller repository, caller commit, and future revisions of
`run-agent-task.yml`; no GitHub workflow SHA context selects helper source.
Callers must invoke the reusable workflow from an exact WP Codebox release tag
and pass that same tag as `wp_codebox_release_ref`. For example, callers use
`Automattic/wp-codebox/.github/workflows/run-agent-task.yml@v0.12.3` together
with `wp_codebox_release_ref: v0.12.3`. The accepted format is exactly
`vX.Y.Z`; branches, commit SHAs, moving major tags, prereleases, and arbitrary
refs are rejected.

Helper changes land first. A subsequent commit that updates this reusable
workflow advances the helper revision to the full SHA containing those merged
files. The workflow validates the full-SHA shape and `actions/checkout` fetches
that exact source before running helpers. The regression test reads the required
helper files at the pinned revision and checks their SHA-256 digests.
The workflow validates that its own `uses:` reference and
`wp_codebox_release_ref` match exactly before checkout. It always checks helpers
out from `Automattic/wp-codebox`, verifies the checked-out commit equals the
remote release tag commit, and verifies the checked-out `package.json` version
equals the requested tag without its `v` prefix. The caller cannot select a
different helper repository.

This pin fixes [#1755](https://github.com/Automattic/wp-codebox/issues/1755) and the failed Build callers [29281470179](https://github.com/Automattic/build-with-wordpress/actions/runs/29281470179)
and [29281470159](https://github.com/Automattic/build-with-wordpress/actions/runs/29281470159), where a foreign caller SHA was incorrectly used as a WP Codebox checkout ref.
This release-coherence contract fixes [#1759](https://github.com/Automattic/wp-codebox/issues/1759).

## Inputs

- `wp_codebox_release_ref`: required exact immutable WP Codebox release tag. It must match the `@vX.Y.Z` tag in the caller's `uses:` declaration exactly.
- `external_package_source`: immutable descriptor with `repository`, full commit `revision`, one package-relative `.agent.json` `path`, and `digest`. Packages are supported only from publicly accessible GitHub repositories, fetched from canonical `https://github.com/OWNER/REPOSITORY.git` without credentials. `digest` is exactly `sha256-bytes-v1:<lowercase-sha256>` over the raw file bytes; filenames and JSON content are UTF-8-safe and are not normalized before hashing.
- `EXTERNAL_PACKAGE_SOURCE_POLICY`: required reusable-workflow secret, supplied by the caller's operator-controlled secret configuration. Its strict version 1 JSON shape is `{"version":1,"repositories":{"owner/repository":["agents/example.agent.json"]}}`. Every entry is an exact standalone `.agent.json` path. The policy is validated in runner memory, is never part of task input, and is not uploaded.
- `target_repo`: `OWNER/REPO` target repository.
Expand Down Expand Up @@ -167,6 +171,10 @@ must migrate checks to executable `validation_dependencies`,
`verification_commands`, or `drift_checks`; context and artifact preparation
remain caller-workflow responsibilities. This is an intentional exposed-workflow breaking change.

`wp_codebox_release_ref` is a required v1 input. Existing callers must switch
their `uses:` reference from a branch or other ref to an exact release tag and
pass that identical tag through this input.

## Upload safety limits

The reusable workflow stages every uploaded file through a fail-closed policy. Only regular UTF-8 files of 4 MiB or less are uploaded, after configured secret values are redacted. Symlinks, special files, binary files, and files larger than 4 MiB are excluded from the upload staging directory. This applies to task artifacts and workflow request, input, and result files.
Expand Down
12 changes: 12 additions & 0 deletions fixtures/agent-task-reusable-workflow-consumer-mismatched.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Mismatched WP Codebox release consumer

on: workflow_dispatch

jobs:
run-agent-task:
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@v0.12.3
with:
wp_codebox_release_ref: v0.12.4
external_package_source: '{"repository":"Automattic/example-agent-packages","revision":"0123456789abcdef0123456789abcdef01234567","path":"agents/example.agent.json","digest":"sha256-bytes-v1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
target_repo: Automattic/example-target
prompt: Refresh the configured surface.
12 changes: 12 additions & 0 deletions fixtures/agent-task-reusable-workflow-consumer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Coherent WP Codebox release consumer

on: workflow_dispatch

jobs:
run-agent-task:
uses: Automattic/wp-codebox/.github/workflows/run-agent-task.yml@v0.12.3
with:
wp_codebox_release_ref: v0.12.3
external_package_source: '{"repository":"Automattic/example-agent-packages","revision":"0123456789abcdef0123456789abcdef01234567","path":"agents/example.agent.json","digest":"sha256-bytes-v1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
target_repo: Automattic/example-target
prompt: Refresh the configured surface.
47 changes: 29 additions & 18 deletions tests/agent-task-reusable-workflow.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import assert from "node:assert/strict"
import { execFile } from "node:child_process"
import { createHash } from "node:crypto"
import { mkdir, mkdtemp, readFile, readdir, symlink, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
Expand All @@ -14,6 +13,7 @@ const publicWorkflowSurface = workflow.slice(0, workflow.indexOf("jobs:"))

assert.match(workflow, /^name: Run Agent Task \(reusable\)$/m)
assert.match(workflow, /workflow_call:/)
assert.match(workflow, /wp_codebox_release_ref:/)
assert.match(workflow, /external_package_source:/)
assert.match(workflow, /EXTERNAL_PACKAGE_SOURCE_POLICY:/)
assert.doesNotMatch(publicWorkflowSurface, /external_package_allowed_repositories:|external_package_allowed_paths:/)
Expand All @@ -40,25 +40,34 @@ assert.match(workflow, /agent-task-artifacts/)
assert.match(workflow, /prepare-agent-task-upload\.mjs/)
assert.match(workflow, /agent-task-upload/)
assert.match(workflow, /if: always\(\)/)
const helperRevision = workflow.match(/WP_CODEBOX_HELPER_REVISION:\s*([0-9a-f]{40})/)?.[1]
assert.ok(helperRevision, "The workflow must declare one full immutable WP Codebox helper revision")
assert.match(workflow, /ref: \$\{\{ env\.WP_CODEBOX_HELPER_REVISION \}\}/)
assert.doesNotMatch(workflow, /github\.(?:workflow_sha|workflow_ref)/)
assert.match(workflow, /WP_CODEBOX_RELEASE_REF: \$\{\{ inputs\.wp_codebox_release_ref \}\}/)
assert.match(workflow, /\^v\[0-9\]\+\\\.\[0-9\]\+\\\.\[0-9\]\+\$/)
assert.match(workflow, /WORKFLOW_REF: \$\{\{ github\.workflow_ref \}\}/)
assert.match(workflow, /The reusable workflow tag and wp_codebox_release_ref must match exactly/)
assert.match(workflow, /repository: Automattic\/wp-codebox/)
assert.match(workflow, /ref: \$\{\{ inputs\.wp_codebox_release_ref \}\}/)
assert.match(workflow, /Verify WP Codebox workflow helper release/)
assert.match(workflow, /git ls-remote --exit-code --refs origin "refs\/tags\/\$\{WP_CODEBOX_RELEASE_REF\}"/)
assert.match(workflow, /checked_out_commit.*remote_tag_commit/)
assert.match(workflow, /JSON\.parse\(readFileSync\("package\.json", "utf8"\)\)\.version/)
assert.doesNotMatch(workflow, /steps\.[^.]+\.outputs\.ref/)
const foreignCallerSha = "ba2df8c2215407b1a58edbff29e3ddcb5efa2249"
assert.notEqual(helperRevision, foreignCallerSha, "A caller repository SHA must not select WP Codebox helpers")
assert.doesNotMatch(workflow, new RegExp(foreignCallerSha))
assert.match(workflow, /Validate pinned WP Codebox helper revision/)
assert.match(workflow, /Validate coherent WP Codebox release tag/)

const requiredHelperFiles = {
".github/scripts/run-agent-task/build-codebox-task-request.mjs": "ed1fbc144428bfbf222810a8a467390e8cd5b45daa0f3e64e03162949d6c84df",
".github/scripts/run-agent-task/execute-native-agent-task.mjs": "324c6d12dd01880bc9e4cd880ea6c713cb1bfd630b4925c53d78af7283e4a459",
".github/scripts/run-agent-task/prepare-agent-task-upload.mjs": "93511e4174e705f55ff014ba68ac52fdaca771ce3f19f3921cceb661e69569da",
const parseConsumerReleaseTags = (consumer: string) => ({
workflow: consumer.match(/uses: Automattic\/wp-codebox\/\.github\/workflows\/run-agent-task\.yml@([^\s]+)/)?.[1],
helpers: consumer.match(/wp_codebox_release_ref: ([^\s]+)/)?.[1],
})
const isExactReleaseTag = (value: string | undefined) => /^v\d+\.\d+\.\d+$/.test(value ?? "")
const isCoherentConsumer = (consumer: string) => {
const { workflow: workflowTag, helpers: helperTag } = parseConsumerReleaseTags(consumer)
return isExactReleaseTag(workflowTag) && workflowTag === helperTag
}
for (const [path, digest] of Object.entries(requiredHelperFiles)) {
const { stdout } = await execFileAsync("git", ["show", `${helperRevision}:${path}`], { encoding: "buffer" })
const actualDigest = createHash("sha256").update(stdout).digest("hex")
assert.equal(actualDigest, digest, `Pinned helper source changed unexpectedly: ${path}`)
const coherentConsumer = await readFile(new URL("../fixtures/agent-task-reusable-workflow-consumer.yml", import.meta.url), "utf8")
const mismatchedConsumer = await readFile(new URL("../fixtures/agent-task-reusable-workflow-consumer-mismatched.yml", import.meta.url), "utf8")
assert.equal(isCoherentConsumer(coherentConsumer), true, "An exact matching workflow and helper release tag must succeed")
assert.equal(isCoherentConsumer(mismatchedConsumer), false, "Mismatched workflow and helper release tags must fail")
for (const invalidRef of ["main", "v0", "v0.12", "v0.12.3-rc.1", "0123456789abcdef0123456789abcdef01234567"]) {
assert.equal(isExactReleaseTag(invalidRef), false, `Non-release ref must fail: ${invalidRef}`)
}
assert.doesNotMatch(publicWorkflowSurface, /step_budget:|tool_results_key:/)
assert.doesNotMatch(workflow, /steps\.plan\.outputs/)
Expand All @@ -67,7 +76,9 @@ assert.doesNotMatch(publicWorkflowSurface, /mount|component path|ability id|prov

const docs = await readFile(new URL("../docs/agent-task-reusable-workflow.md", import.meta.url), "utf8")
assert.match(docs, /^# Agent Task Reusable Workflow/m)
assert.match(docs, /Automattic\/wp-codebox\/.github\/workflows\/run-agent-task.yml@main/)
assert.match(docs, /Automattic\/wp-codebox\/.github\/workflows\/run-agent-task.yml@v0\.12\.3/)
assert.match(docs, /wp_codebox_release_ref: v0\.12\.3/)
assert.match(docs, /branches, commit SHAs, moving major tags, prereleases, and arbitrary\nrefs are rejected/)
assert.match(docs, /external_package_source/)
assert.match(docs, /runner_workspace/)
assert.match(docs, /access_token_repos/)
Expand Down
Loading