diff --git a/.github/workflows/client-v1-conformance.yml b/.github/workflows/client-v1-conformance.yml index 1bfcccfe..2204f8f5 100644 --- a/.github/workflows/client-v1-conformance.yml +++ b/.github/workflows/client-v1-conformance.yml @@ -5,12 +5,65 @@ on: validator_revision: required: true type: string + producer_revision: + description: >- + Exact merged Chat commit to validate. Defaults to the dispatch ref + tip. It must already be an ancestor of that tip: an unmerged or + unrelated revision is refused, so this decouples a protected run from + whatever happens to be at the tip without widening what may be + validated. + required: false + type: string permissions: contents: read jobs: + producer-revision: + name: resolve-producer-revision + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + outputs: + revision: ${{ steps['resolve'].outputs.revision }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.sha }} + - id: resolve + name: Resolve and verify the producer revision + env: + OPENCOVEN_PRODUCER_REVISION_INPUT: ${{ inputs.producer_revision }} + OPENCOVEN_DISPATCH_SHA: ${{ github.sha }} + run: | + set -euo pipefail + requested="${OPENCOVEN_PRODUCER_REVISION_INPUT:-}" + if [ -z "$requested" ]; then + requested="$OPENCOVEN_DISPATCH_SHA" + fi + case "$requested" in + *[!0-9a-f]* | "" ) echo 'Producer revision must be an exact lowercase 40-hex commit.' >&2; exit 1 ;; + esac + if [ "${#requested}" -ne 40 ]; then + echo 'Producer revision must be an exact lowercase 40-hex commit.' >&2 + exit 1 + fi + if ! git cat-file -e "$requested^{commit}" 2>/dev/null; then + echo 'Producer revision is not a commit in this repository.' >&2 + exit 1 + fi + if ! git merge-base --is-ancestor "$requested" "$OPENCOVEN_DISPATCH_SHA"; then + echo 'Producer revision is not an ancestor of the dispatch ref; only merged revisions may be validated.' >&2 + exit 1 + fi + printf 'revision=%s\n' "$requested" >> "$GITHUB_OUTPUT" + echo "Validating producer revision $requested" windows-supervisor: name: build-windows-supervisor if: github.ref == 'refs/heads/main' + needs: producer-revision runs-on: macos-latest timeout-minutes: 30 permissions: @@ -22,7 +75,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - ref: ${{ github.sha }} + ref: ${{ needs['producer-revision'].outputs.revision }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24.18.1 @@ -42,7 +95,7 @@ jobs: platform-conformance: name: platform-conformance (${{ matrix.platform }}) if: github.ref == 'refs/heads/main' - needs: windows-supervisor + needs: [producer-revision, windows-supervisor] timeout-minutes: 60 strategy: fail-fast: false @@ -71,7 +124,7 @@ jobs: OPENCOVEN_VALIDATOR_REVISION_INPUT: ${{ inputs.validator_revision }} OPENCOVEN_PROTECTED_VALIDATOR_REVISION: ${{ vars.CLIENT_V1_CONFORMANCE_VALIDATOR_REVISION }} OPENCOVEN_CHAT_REPOSITORY: ${{ github.repository }} - OPENCOVEN_CHAT_SHA: ${{ github.sha }} + OPENCOVEN_CHAT_SHA: ${{ needs['producer-revision'].outputs.revision }} OPENCOVEN_WINDOWS_IMAGE_OS: 'win25-vs2026' OPENCOVEN_WINDOWS_IMAGE_VERSION: '20260907.229.1' OPENCOVEN_WINDOWS_PREVIOUS_IMAGE_VERSION: '20260824.214.3' @@ -1819,7 +1872,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - ref: ${{ github.sha }} + ref: ${{ needs['producer-revision'].outputs.revision }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 if: matrix.platform != 'win32-x64' with: diff --git a/docs/phase1-conformance.md b/docs/phase1-conformance.md index fbf668e9..db962baf 100644 --- a/docs/phase1-conformance.md +++ b/docs/phase1-conformance.md @@ -1,38 +1,5 @@ # Phase 1 real-authority conformance -## Bounded quota retry categories - -Protected [run 35146928092](https://github.com/OpenCoven/chat/actions/runs/35146928092) -used Chat #311 at `157fb3206b9b90f24049aa2043bae534d2b9a709` and SDK #293 at -`3459dcaad0877bbef2a25da24fbd521879ef020e`. Windows failed with `access-denied`, -root `harness-execution-aggregate`, scope `checkouts`, operation -`directory-enumeration-depth-3-plus`, and repeat `persistent`. Linux and macOS -records passed identity, timing, privacy, schema, and all 197 ordered assertions. -Windows emitted no record, and no accepted aggregate exists. This earlier quota -failure does not establish that the Cave build home repair passed isolation. - -The legacy `persistent` label means the repeat threw a non-missing exception. -It does not prove a second access denial. The bounded diagnostic now preserves -the initial category and reports the repeat as `persistent-`, using -only the existing fixed exception categories. For example, first-attempt access -denial followed by an I/O failure reports `access-denied` with `persistent-io`; -two access denials report `access-denied` with `persistent-access-denied`. -Legacy `persistent` remains accepted by the context normalizer. - -Run the portable classification and non-recovery checks with: - -```sh -pwsh -NoLogo -NoProfile -NonInteractive -File scripts/windows-quota-diagnostics.test.ps1 -``` - -The regression matrix covers every fixed repeat category, rejects private text, -preserves the first failure, and requires one whole-pass attempt for each -non-recoverable result. The change adds no retries and changes no quotas, -permissions, or recovery predicates. Native Windows fixtures require the refined -labels; their execution remains a delivery gate. Reviewed producer binding, -actual-merge SDK rebinding, both scope rotations, and fresh protected validation -remain required before claiming acceptance or identifying the failing checkout. - ## Cave build home isolation checkpoint Protected run `35138402347` failed on Windows with @@ -247,8 +214,8 @@ assignment, RPC decoder and primary/secondary exception pipeline. This fixture correction does not establish the protected installation failure's cause or relax the round-trip assertions. -The lock now selects reviewed source `0a35e571de69c3f17b2f490974caec36b34205a5`, -tree `4f98b5cff7ac65d7229c136490265ea60d2cd764`, including all 25 governed files +The lock now selects reviewed source `28821d4d035d7815df396b756c2e30ecb0d54f5e`, +tree `f893c71be4fd0eec23c8739716441bd2f533d764`, including all 25 governed files and ten production deltas. The checkout regression exercises all five labels from that immutable revision. SDK rebinding, both scope rotations and fresh protected validation remain required; this binding alone is not acceptance. @@ -2053,7 +2020,7 @@ revision authorities can therefore have different workflow hashes: | File | Bytes | SHA-256 | | --- | ---: | --- | -| `.github/workflows/client-v1-conformance.yml` | 178,086 | `3c2f0d5423533b7b2c6d601e9f91a250064704bceeb4e53090ebecc935cacb09` | +| `.github/workflows/client-v1-conformance.yml` | 180,386 | `be7f36a7ce1b3dd6834b565e33825c5af410c49ed0e193ffaffda91ef79125b3` | | `scripts/contract-canary.mjs` | 40,618 | `a4c2fe0a5eb6a5ff4653de5374c34c0fb46907c6806a5d23b86d8b37206ef958` | | `scripts/executable-resolution.mjs` | 9,154 | `31e3c412ff8c835f14522f36a59e91f4a4ba82913210ae8e3b4455217503f430` | | `scripts/owned-temp-directory.mjs` | 7,762 | `95f546ef9ed614f2a0f55d356ddfc54c943fc53b595b4eebebfcbd4db68e5c0b` | @@ -2821,31 +2788,26 @@ Fresh reviewed source binding, SDK rebinding, and protected validation are required before attributing the Windows failure or claiming a repaired run. -### Unsupported custody installation - -`secure_store_unavailable` covered two unrelated causes on the installation -preflight. `KeyringError::Unavailable` is returned both by a secure store that -is genuinely unavailable and by `CredentialCustody::installation_id`'s default -trait body, which a custody implementation reaches only by never overriding it. -`InstallationStage::classify` already rewrites `Unavailable` into -`installation_lock_unavailable`, `installation_entry_unavailable`, -`installation_read_unavailable`, `installation_write_unavailable` and -`installation_persistence_unavailable`, so those five stages were already -distinguishable; the default trait body was the remaining unclassified path. - -It now returns the fixed code `installation_custody_unsupported`, published as -`phase1.native-scenarios.native-preflight-installation-custody-unsupported`. -The outer launcher derives its native-stage allowlist from the producer -registry, so the identifier survives extraction without a second edit. Only the -fixed identifier is published: no message, stack, path, credential or -subprocess output is added. - -Protected run -[35100084575](https://github.com/OpenCoven/chat/actions/runs/35100084575) -reported `native-preflight-installation-secure-store-unavailable` on Windows -while Linux and macOS passed. This change does not repair that failure and does -not establish its cause; it separates the two causes so the next protected run -attributes it. +### Dispatching a protected run against a specific merged revision + +`workflow_dispatch` previously validated whatever `main` pointed at when the +run started. The cross-repository contract requires the evidence producer to be +a merge whose tree equals its reviewed second parent's tree, so a binding names +one exact merge; any later commit to `main` — conformance-related or not — +leaves that binding unable to describe the tip. Protected runs were therefore +only usable inside the window between a binding landing and the next merge. + +The optional `producer_revision` input names the commit to validate. The +`resolve-producer-revision` job requires an exact lowercase 40-hex commit that +exists in this repository and is an **ancestor of the dispatch ref**, then +publishes it for the supervisor build, the Windows bootstrap and the Unix +workspace checkout. Omitting it keeps the previous behaviour of validating the +dispatch ref tip. + +The ancestry requirement is what keeps this from widening the trust boundary: +an unmerged branch, an unrelated commit, or a revision from a fork is refused, +so a protected run still only ever validates reviewed history that reached +`main`. What changes is that it no longer has to be the newest such history. ### Unexpected installation RPC failures @@ -2947,7 +2909,7 @@ validation, attestation, and aggregation were skipped. `ReadBoundedDirectorySnapshot` materializes a bounded snapshot. On access denial, `ReadDirectorySnapshotOperation` performs one fresh, complete snapshot read under the existing quota-reader identity. A successful repeat supplies the measurement; a missing -directory is classified separately. In that historical producer, any other repeat exception produced `persistent`, +directory is classified separately. Any other repeat exception produces `persistent`, while the initial access-denied category is preserved. An injected access denial followed by an I/O exception now exercises that distinction through the production snapshot seam. Thus the log does not prove two identical ACL failures, a particular checkout, a denied diff --git a/phase1-conformance.lock.json b/phase1-conformance.lock.json index 531feea5..18f9551b 100644 --- a/phase1-conformance.lock.json +++ b/phase1-conformance.lock.json @@ -18,11 +18,11 @@ }, "harness": { "repository": "OpenCoven/chat", - "revision": "0a35e571de69c3f17b2f490974caec36b34205a5" + "revision": "28821d4d035d7815df396b756c2e30ecb0d54f5e" }, "harnessAuthority": { - "revision": "0a35e571de69c3f17b2f490974caec36b34205a5", - "tree": "4f98b5cff7ac65d7229c136490265ea60d2cd764", + "revision": "28821d4d035d7815df396b756c2e30ecb0d54f5e", + "tree": "f893c71be4fd0eec23c8739716441bd2f533d764", "files": [ { "path": "scripts/phase1-conformance.mjs", @@ -146,8 +146,8 @@ }, { "path": ".github/workflows/client-v1-conformance.yml", - "blob": "1bfcccfe5bbbc864182bcc8d8a5e3aa2ba7189ee", - "sha256": "3c2f0d5423533b7b2c6d601e9f91a250064704bceeb4e53090ebecc935cacb09" + "blob": "2204f8f5f4e9bc9b787e23e624038c376d518f7f", + "sha256": "be7f36a7ce1b3dd6834b565e33825c5af410c49ed0e193ffaffda91ef79125b3" } ], "productionDeltas": [ diff --git a/src/client-v1-conformance-workflow.test.ts b/src/client-v1-conformance-workflow.test.ts index 5a88c923..e38fce92 100644 --- a/src/client-v1-conformance-workflow.test.ts +++ b/src/client-v1-conformance-workflow.test.ts @@ -1,11 +1,12 @@ import { execFileSync, spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; import { dirname, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import * as ts from 'typescript'; -import { describe, expect, test } from 'vitest'; +import { afterAll, describe, expect, test } from 'vitest'; import { decodeWindowsSupervisorSource } from '../scripts/windows-supervisor-source.mjs'; const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -35,6 +36,8 @@ const validatorInputExpression = '${' + '{ inputs.validator_revision }}'; const protectedValidatorExpression = '${' + '{ vars.CLIENT_V1_CONFORMANCE_VALIDATOR_REVISION }}'; const githubRepositoryExpression = '${' + '{ github.repository }}'; const githubShaExpression = '${' + '{ github.sha }}'; +const resolvedProducerExpression = '${' + "{ steps['resolve'].outputs.revision }}"; +const producerRevisionExpression = '${' + "{ needs['producer-revision'].outputs.revision }}"; const expressionOpening = '${' + '{'; const uploadedSupervisorArtifactIdExpression = '${' + "{ steps['upload-supervisor'].outputs['artifact-id'] }}"; @@ -247,7 +250,25 @@ function verifyExactMainRefConstraint(label: string, job: string): void { } } +function verifyResolvedProducerRevision(job: string, workflow: string): void { + if ( + !job.includes('git merge-base --is-ancestor "$requested" "$OPENCOVEN_DISPATCH_SHA"') || + !job.includes('OPENCOVEN_DISPATCH_SHA: ' + githubShaExpression) || + !job.includes('revision: ' + resolvedProducerExpression) + ) { + throw new Error('producer revision job does not verify ancestry of the dispatch ref'); + } + if ( + workflow.includes( + 'ref: ' + githubShaExpression.replace('github.sha', 'inputs.producer_revision'), + ) + ) { + throw new Error('producer revision input is checked out without verification'); + } +} + function verifyHardenedWorkflowGraph(workflow: string): void { + const producerRevision = workflowJob(workflow, 'producer-revision'); const windowsSupervisor = workflowJob(workflow, 'windows-supervisor'); const producer = workflowJob(workflow, 'platform-conformance'); const validation = workflowJob(workflow, 'validate-conformance-artifacts'); @@ -255,6 +276,7 @@ function verifyHardenedWorkflowGraph(workflow: string): void { const aggregate = workflowJob(workflow, 'aggregate-conformance'); for (const [label, job] of [ + ['producer revision', producerRevision], ['windows supervisor', windowsSupervisor], ['producer', producer], ['validator', validation], @@ -264,7 +286,10 @@ function verifyHardenedWorkflowGraph(workflow: string): void { verifyExactMainRefConstraint(label, job); } + verifyResolvedProducerRevision(producerRevision, workflow); + for (const [label, job] of [ + ['producer revision', producerRevision], ['windows supervisor', windowsSupervisor], ['producer', producer], ['validator', validation], @@ -466,7 +491,9 @@ describe('client-v1 conformance workflow bootstrap', () => { const workflow = readFileSync(workflowPath, 'utf8'); const unixProducerCommand = readFileSync(unixProducerCommandPath, 'utf8'); - expect(workflow.match(/ {10}fetch-depth: 0/gu)).toHaveLength(3); + // four full checkouts: producer-revision resolution, the supervisor build, + // the Windows bootstrap workspace and the Unix workspace. + expect(workflow.match(/ {10}fetch-depth: 0/gu)).toHaveLength(4); expect(workflow).toContain('scripts/executable-resolution.mjs'); expect(workflow).toContain('resolveExecutableInvocation'); expect(workflow).toContain(" GIT_CONFIG_COUNT: '1'"); @@ -530,10 +557,10 @@ describe('client-v1 conformance workflow bootstrap', () => { expect(workflow).toContain(' windows-supervisor:'); expect(workflow).toContain( - " windows-supervisor:\n name: build-windows-supervisor\n if: github.ref == 'refs/heads/main'", + " windows-supervisor:\n name: build-windows-supervisor\n if: github.ref == 'refs/heads/main'\n needs: producer-revision", ); expect(workflow).toContain(' runs-on: macos-latest'); - expect(workflow).toContain(' needs: windows-supervisor'); + expect(workflow).toContain(' needs: [producer-revision, windows-supervisor]'); expect(workflow).toContain(' run: bash scripts/phase1-windows-supervisor-build.sh'); expect(workflow).toContain(' name: phase1-process-supervisor-win32-x64'); expect(workflow).toContain(`artifact_id: ${uploadedSupervisorArtifactIdExpression}`); @@ -654,6 +681,136 @@ ${childBootstrap.slice(validationStart, validationEnd)} ); }); +function producerRevisionScript(workflow: string): string { + const job = workflowJob(workflow, 'producer-revision'); + const marker = ' run: |\n'; + const start = job.indexOf(marker); + if (start < 0) { + throw new Error('producer-revision job does not define a resolve script'); + } + const lines: string[] = []; + for (const line of job.slice(start + marker.length).split('\n')) { + if (line.trim() !== '' && !line.startsWith(' ')) break; + lines.push(line.slice(10)); + } + return lines.join('\n'); +} + +type ResolveOutcome = { status: number; revision: string; stderr: string }; + +function runProducerResolve( + script: string, + repository: string, + dispatchSha: string, + requested: string | undefined, +): ResolveOutcome { + const outputPath = resolve(repository, 'github-output'); + writeFileSync(outputPath, ''); + const result = spawnSync('bash', ['-c', script], { + cwd: repository, + encoding: 'utf8', + timeout: 20_000, + env: { + ...process.env, + GITHUB_OUTPUT: outputPath, + OPENCOVEN_DISPATCH_SHA: dispatchSha, + ...(requested === undefined ? {} : { OPENCOVEN_PRODUCER_REVISION_INPUT: requested }), + }, + }); + const emitted = readFileSync(outputPath, 'utf8'); + const match = /^revision=([0-9a-f]{40})$/mu.exec(emitted); + return { + status: result.status ?? -1, + revision: match?.[1] ?? '', + stderr: result.stderr ?? '', + }; +} + +describe('producer revision ancestry gate', () => { + const script = producerRevisionScript(readFileSync(workflowPath, 'utf8')); + const repository = mkdtempSync(resolve(tmpdir(), 'opencoven-producer-ref-')); + const git = (...args: string[]) => + execFileSync('git', args, { + cwd: repository, + encoding: 'utf8', + timeout: 20_000, + env: { + ...process.env, + GIT_AUTHOR_NAME: 'Conformance', + GIT_AUTHOR_EMAIL: 'conformance@example.invalid', + GIT_COMMITTER_NAME: 'Conformance', + GIT_COMMITTER_EMAIL: 'conformance@example.invalid', + }, + }).trim(); + + git('init', '--quiet', '--initial-branch=main', '.'); + git('-c', 'commit.gpgsign=false', 'commit', '--quiet', '--allow-empty', '-m', 'base'); + const merged = git('rev-parse', 'HEAD'); + git('-c', 'commit.gpgsign=false', 'commit', '--quiet', '--allow-empty', '-m', 'tip'); + const tip = git('rev-parse', 'HEAD'); + git('checkout', '--quiet', '-b', 'unmerged', merged); + git('-c', 'commit.gpgsign=false', 'commit', '--quiet', '--allow-empty', '-m', 'unmerged'); + const unmerged = git('rev-parse', 'HEAD'); + git('checkout', '--quiet', 'main'); + + afterAll(() => { + rmSync(repository, { force: true, recursive: true }); + }); + + test('defaults to the dispatch tip when no revision is requested', () => { + const outcome = runProducerResolve(script, repository, tip, undefined); + expect(outcome.status).toBe(0); + expect(outcome.revision).toBe(tip); + }); + + test('accepts an exact merged ancestor of the dispatch tip', () => { + const outcome = runProducerResolve(script, repository, tip, merged); + expect(outcome.status).toBe(0); + expect(outcome.revision).toBe(merged); + }); + + test('accepts the dispatch tip named explicitly', () => { + const outcome = runProducerResolve(script, repository, tip, tip); + expect(outcome.status).toBe(0); + expect(outcome.revision).toBe(tip); + }); + + test('refuses a commit that is not merged into the dispatch tip', () => { + const outcome = runProducerResolve(script, repository, tip, unmerged); + expect(outcome.status).toBe(1); + expect(outcome.revision).toBe(''); + expect(outcome.stderr).toContain('only merged revisions may be validated'); + }); + + test('refuses a descendant of the dispatch tip', () => { + const outcome = runProducerResolve(script, repository, merged, tip); + expect(outcome.status).toBe(1); + expect(outcome.revision).toBe(''); + expect(outcome.stderr).toContain('only merged revisions may be validated'); + }); + + test.each([ + ['an abbreviated revision', (value: string) => value.slice(0, 12)], + // Fixed rather than derived: an all-digit fixture SHA would upper-case to + // itself and silently stop exercising the lowercase rule. + ['an uppercase revision', () => 'ABCDEF' + '0'.repeat(34)], + ['a ref name rather than a commit', () => 'main'], + ['an empty revision', () => ' '], + ])('refuses %s', (_label, transform) => { + const outcome = runProducerResolve(script, repository, tip, transform(merged)); + expect(outcome.status).toBe(1); + expect(outcome.revision).toBe(''); + expect(outcome.stderr).toContain('exact lowercase 40-hex commit'); + }); + + test('refuses a well-formed revision that is absent from the repository', () => { + const outcome = runProducerResolve(script, repository, tip, 'b'.repeat(40)); + expect(outcome.status).toBe(1); + expect(outcome.revision).toBe(''); + expect(outcome.stderr).toContain('not a commit in this repository'); + }); +}); + describe.skipIf(!validatorAvailable)('protected client-v1 conformance workflow', () => { test('accepts the committed protected workflow with the current SDK validator', async () => { const fixture = await workflowFixture(); @@ -1025,7 +1182,7 @@ ${source.slice(start, end)} `OPENCOVEN_VALIDATOR_REVISION_INPUT: ${validatorInputExpression}`, ); expect(environment).toContain(`OPENCOVEN_CHAT_REPOSITORY: ${githubRepositoryExpression}`); - expect(environment).toContain(`OPENCOVEN_CHAT_SHA: ${githubShaExpression}`); + expect(environment).toContain(`OPENCOVEN_CHAT_SHA: ${producerRevisionExpression}`); expect(runBody).not.toContain(validatorInputExpression); expect(runBody).not.toContain(expressionOpening); expect(runBody).not.toMatch(/\$\{\{\s*inputs\./u); diff --git a/src/phase1-conformance-lock.test.ts b/src/phase1-conformance-lock.test.ts index 9c84b7e2..28ad40b2 100644 --- a/src/phase1-conformance-lock.test.ts +++ b/src/phase1-conformance-lock.test.ts @@ -80,8 +80,8 @@ const committedHarnessAuthority = JSON.parse( readFileSync(resolve(projectRoot, 'phase1-conformance.lock.json'), 'utf8'), ).harnessAuthority; const expectedBehaviorAuthority = { - revision: '0a35e571de69c3f17b2f490974caec36b34205a5', - tree: '4f98b5cff7ac65d7229c136490265ea60d2cd764', + revision: '28821d4d035d7815df396b756c2e30ecb0d54f5e', + tree: 'f893c71be4fd0eec23c8739716441bd2f533d764', files: [ { path: 'scripts/owned-temp-directory.mjs', @@ -135,8 +135,8 @@ const expectedBehaviorAuthority = { }, { path: '.github/workflows/client-v1-conformance.yml', - blob: '1bfcccfe5bbbc864182bcc8d8a5e3aa2ba7189ee', - sha256: '3c2f0d5423533b7b2c6d601e9f91a250064704bceeb4e53090ebecc935cacb09', + blob: '2204f8f5f4e9bc9b787e23e624038c376d518f7f', + sha256: 'be7f36a7ce1b3dd6834b565e33825c5af410c49ed0e193ffaffda91ef79125b3', }, { path: 'scripts/process-owned-artifact-root.mjs',