From d830abf1745839867270d0d429e0d50b7798ffe6 Mon Sep 17 00:00:00 2001 From: Ayobami Haastrup <47716486+AyobamiH@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:32:04 +0100 Subject: [PATCH] fix: refuse release redirects with Workers-compatible fetch mode --- docs/CURRENT-STATUS.md | 24 +++++++ ...ase-workers-redirect-failure-20261018.json | 63 +++++++++++++++++++ services/release-provider/src/policy.mjs | 2 +- .../release-provider/test/provider.test.mjs | 28 ++++++++- src/release-v1-github.ts | 4 +- 5 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 docs/evidence/release-workers-redirect-failure-20261018.json diff --git a/docs/CURRENT-STATUS.md b/docs/CURRENT-STATUS.md index 612b8cb..607c4bf 100644 --- a/docs/CURRENT-STATUS.md +++ b/docs/CURRENT-STATUS.md @@ -139,3 +139,27 @@ and its digest remain unchanged. Regression coverage exercises a lost settlement a later-attempt readback, tampered stored grants and read-only recovery after grant expiry, with the publication count fixed at one. This remains source validation, not a real publication or DoneState terminal outcome. + + +## Workers redirect compatibility repair — 18 October 2026 + +Protected release canary [39122966755](https://github.com/AyobamiH/agentproof-release-canary/actions/runs/39122966755) +passed authorisation but stopped at preparation with `release_policy_rejected`. +Its publication artifact SHA-256 is +`de151fc8a32090fb6916803de230d37b56a2e99aa2657c3193ad15471f49fdca`. +The existing exact release remained a draft; no reservation or publication evidence +was returned and the verifier job did not run. + +A GET-only reproduction using the Workers runtime identified the cause: +`redirect: "error"` is rejected before the GitHub request is sent. Node-only +fixtures had incorrectly required this mode. Both the release provider and separate +receipt signer's metadata reader now use `redirect: "manual"`; their existing +exact HTTP 200 checks continue to refuse redirects. Regression cases cover all +five redirect statuses, zero durable reservation and no redirected follow-up. + +This is a source correction awaiting exact-source deployment and live acceptance. +Existing target fences, signing identities, source/OIDC pins and the single-PATCH +policy are unchanged. The independently verified repository-patch run +[39121692291](https://github.com/AyobamiH/agentproof/actions/runs/39121692291) +remains evidence for `d89f6d54709830ff49373ab7c2797c927ced8ac9`. +RC6 npm publication is unchanged; independent-installation credit remains zero. diff --git a/docs/evidence/release-workers-redirect-failure-20261018.json b/docs/evidence/release-workers-redirect-failure-20261018.json new file mode 100644 index 0000000..37c3e69 --- /dev/null +++ b/docs/evidence/release-workers-redirect-failure-20261018.json @@ -0,0 +1,63 @@ +{ + "schema": "agentproof.release-runtime-compatibility-evidence.v1", + "recordedAt": "2026-10-18T11:32:02.594441+00:00", + "baseSource": "d89f6d54709830ff49373ab7c2797c927ced8ac9", + "failedWorkflow": "https://github.com/AyobamiH/agentproof-release-canary/actions/runs/39122966755", + "failedWorkflowCommit": "cc1dfb79c1a63a62e69049473b6170674667d8d1", + "publicationArtifactSha256": "de151fc8a32090fb6916803de230d37b56a2e99aa2657c3193ad15471f49fdca", + "before": { + "at": "2026-10-18T11:28:49.979Z", + "readOnly": true, + "result": { + "error": "release_v1:github_provider_unavailable", + "calls": [ + { + "path": "/repos/AyobamiH/agentproof-release-canary", + "failure": "TypeError: Invalid redirect value, must be one of \"follow\" or \"manual\" (\"error\" won't be implemented since it does not make sense at the edge; use \"manual\" and check the response status code)." + } + ] + } + }, + "after": { + "at": "2026-10-18T11:31:13.740Z", + "readOnly": true, + "result": { + "observation": { + "schema": "agentproof.release.observation.v1", + "ownerId": 47716486, + "repositoryId": 1423718326, + "releaseId": 416342082, + "tagName": "v0.0.0-rc.2026101801", + "tagCommit": "1231076c12485a02fae3c38b0b850a25274985d5", + "packageAssetId": 653036082, + "packageAssetName": "agentproof-disposable-canary.tgz", + "packageDigest": "sha256:064f989c25b86269004401959b1e2aa68cc0b3acbf310ba647b892aa5e28b186", + "provenanceAssetId": 653046879, + "provenanceDigest": "sha256:fa76d7de87ba327ccd81943563af7daed1c87775e049e52a047df59c5b1475b4", + "assetCount": 2, + "draft": true, + "prerelease": true, + "publishedAt": null + }, + "calls": [ + { + "path": "/repos/AyobamiH/agentproof-release-canary", + "status": 200, + "type": "application/json; charset=utf-8" + }, + { + "path": "/repos/AyobamiH/agentproof-release-canary/git/ref/tags/v0.0.0-rc.2026101801", + "status": 200, + "type": "application/json; charset=utf-8" + }, + { + "path": "/repos/AyobamiH/agentproof-release-canary/releases/416342082", + "status": 200, + "type": "application/json; charset=utf-8" + } + ] + } + }, + "scope": "Local Workers-runtime GET-only probe with existing maintainer authentication. No production private signing keys, no mutation, no durable reservation. Not a live consequence acceptance.", + "independentAdoptionCredit": 0 +} diff --git a/services/release-provider/src/policy.mjs b/services/release-provider/src/policy.mjs index f179622..ed6a557 100644 --- a/services/release-provider/src/policy.mjs +++ b/services/release-provider/src/policy.mjs @@ -167,7 +167,7 @@ export function releaseSigner(env) { } async function anonymousGitHubJson(path) { const response = await fetch("https://api.github.com/repos/AyobamiH/agentproof-release-canary" + path, { - method: "GET", redirect: "error", cache: "no-store", + method: "GET", redirect: "manual", cache: "no-store", headers: { accept: "application/vnd.github+json", "user-agent": "agentproof-separate-release-signer/1.0" }, signal: AbortSignal.timeout(12_000), }); diff --git a/services/release-provider/test/provider.test.mjs b/services/release-provider/test/provider.test.mjs index 518340d..b905fea 100644 --- a/services/release-provider/test/provider.test.mjs +++ b/services/release-provider/test/provider.test.mjs @@ -5,6 +5,7 @@ import { digest, stableJson } from "../../../dist/hash.js"; import { publicKeyFingerprint } from "../../../dist/signer.js"; import { verifyReleaseReceiptV2 } from "../../../dist/release-v1-receipt.js"; import { validateClaims, SUBJECT, CANARY_REPOSITORY, WORKFLOW } from "../src/identity.mjs"; +import { publicReleaseReadback } from "../src/policy.mjs"; import { ReleaseFence, ReleaseReceiptLedger, handle } from "../src/worker.mjs"; const HEAD = "1".repeat(40); @@ -76,7 +77,7 @@ function githubFixture(t, options={}) { t.mock.method(globalThis,"fetch",async (input,init={})=>{ const url = new URL(String(input)); assert.equal(url.origin,"https://api.github.com"); - assert.equal(init.redirect,"error"); + assert.equal(init.redirect,"manual"); assert.match(init.headers["user-agent"], /^agentproof-/); if (init.headers.authorization !== undefined) assert.equal(init.headers.authorization,"Bearer test-ephemeral-gh-token"); @@ -306,3 +307,28 @@ test("recovery reservation cannot execute until a distinct signed committed-auth assert.equal(api.mutationCount,1); }); + + +test("GitHub redirects cannot reserve a release or satisfy independent signer readback",async t=>{ + let status=301,calls=[]; + t.mock.method(globalThis,"fetch",async(input,init)=>{ + assert.equal(new URL(String(input)).origin,"https://api.github.com"); + assert.equal(init.method,"GET"); + assert.equal(init.redirect,"manual"); + calls.push({url:String(input),method:init.method}); + return new Response("untrusted redirect body",{ + status,headers:{location:"https://untrusted.invalid/steal","content-type":"text/plain"} + }); + }); + for(status of [301,302,303,307,308]){ + const authority=env("approval");calls=[]; + const rejected=await call("approval",authority,"/v1/prepare",{seed:seed()}); + assert.equal(rejected.status,403); + assert.equal(rejected.json.error,"release_policy_rejected"); + assert.equal(calls.length,1); + assert.equal(authority.ledger.size,0); + calls=[]; + await assert.rejects(publicReleaseReadback(subject),/public_readback_unavailable/); + assert.equal(calls.length,1); + } +}); diff --git a/src/release-v1-github.ts b/src/release-v1-github.ts index d516562..7bc78d9 100644 --- a/src/release-v1-github.ts +++ b/src/release-v1-github.ts @@ -46,8 +46,10 @@ export class GitHubReleaseV1Provider implements ReleaseV1Provider { releaseAssert(typeof token === "string" && token.length >= 8 && token.length <= 4096 && /^[\x21-\x7e]+$/.test(token) && !controller.signal.aborted, "github_token_unavailable"); + // Workers reject redirect: error. Manual mode plus exact 200 validation + // refuses redirects without forwarding the short-lived token. const response = await fetch(this.base + suffix, { - method, redirect: "error", cache: "no-store", signal: controller.signal, + method, redirect: "manual", cache: "no-store", signal: controller.signal, headers: { "accept": "application/vnd.github+json", "content-type": "application/json", "authorization": "Bearer " + token, "x-github-api-version": "2022-11-28", "user-agent": "agentproof-release-provider/1.0" },