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
18 changes: 18 additions & 0 deletions docs/CURRENT-STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,21 @@ 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.


## Published consequence; independent receipt pending — 18 October 2026

The current-main repository-patch canary [39124046639](https://github.com/AyobamiH/agentproof/actions/runs/39124046639)
passed all 17 checks at `74c12b4e42c8109d34e4764fa0e4864513cab4a9`.
Protected release run [39124373253](https://github.com/AyobamiH/agentproof-release-canary/actions/runs/39124373253)
then published the existing disposable release `416342082` once at `2026-10-18T11:43:37Z`.
The exact successful GitHub response and signed execution completion are retained.
Attempts 2 and 3 made zero publication requests and returned the identical completion.

The separate receipt signer refused its public GitHub readback on all three attempts.
No AgentProof receipt, independent OpsTruth verdict, or DoneState VERIFIED outcome is
claimed. This change adds only bounded endpoint/status/rate-limit metadata to that
refusal. Upstream bodies, URLs, arbitrary headers and credentials are never returned.
The permanent fence, keys, original workflow identity and historical evidence remain
unchanged. Further recovery must diagnose the upstream refusal and may only observe
and sign the existing consequence; it must never publish again.
15 changes: 15 additions & 0 deletions services/release-provider/src/identity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ import { createRemoteJWKSet, jwtVerify } from "jose";
export class ReleaseRefusal extends Error {
constructor(code, status = 403) { super(code); this.code = code; this.status = status; }
}
// Bounded public transport metadata; never include upstream bodies or credentials.
export class PublicReadbackRefusal extends ReleaseRefusal {
constructor(endpoint, response) {
super("public_readback_unavailable", 503);
const numberHeader = name => {
const value = response.headers.get(name);
return typeof value === "string" && /^\d{1,10}$/.test(value) ? Number(value) : null;
};
if (!["repository", "tag", "release"].includes(endpoint)) throw new Error("readback_endpoint_invalid");
this.readback = Object.freeze({ endpoint, upstreamStatus: response.status,
rateLimitRemaining: numberHeader("x-ratelimit-remaining"),
rateLimitReset: numberHeader("x-ratelimit-reset"),
retryAfterSeconds: numberHeader("retry-after") });
}
}
export function demand(condition, code, status = 403) {
if (!condition) throw new ReleaseRefusal(code, status);
}
Expand Down
15 changes: 9 additions & 6 deletions services/release-provider/src/policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { GitHubReleaseV1Provider, canonicalGitHubPublishedAt } from "../../../di
import { validateReleaseSubject, validateReleaseObservation, validateReleaseRequest,
validateReleaseApproval, exactKeys, releaseAssert } from "../../../dist/release-v1-contract.js";
import { releasePayload, signReleaseReceiptV2 } from "../../../dist/release-v1-receipt.js";
import { demand, ReleaseRefusal } from "./identity.mjs";
import { demand, ReleaseRefusal, PublicReadbackRefusal } from "./identity.mjs";
import { recoveryPolicy } from "./recovery.mjs";

export const AUTHORITY_ISSUER = "agentproof.release-canary.authority.v1";
Expand Down Expand Up @@ -165,26 +165,29 @@ export function releaseSigner(env) {
signCanonical: async input => sign(null, Buffer.from(input), key),
};
}
async function anonymousGitHubJson(path) {
async function anonymousGitHubJson(path, endpoint) {
const response = await fetch("https://api.github.com/repos/AyobamiH/agentproof-release-canary" + path, {
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),
});
demand(response.ok && response.status === 200, "public_readback_unavailable", 503);
if (!response.ok || response.status !== 200) {
await response.body?.cancel().catch(() => {});
throw new PublicReadbackRefusal(endpoint, response);
}
const raw = await response.text();
demand(Buffer.byteLength(raw) <= 64 * 1024, "public_readback_oversized", 503);
return strictBody(raw);
}
export async function publicReleaseReadback(subject) {
const repo = await anonymousGitHubJson("");
const repo = await anonymousGitHubJson("", "repository");
demand(repo.id === subject.repositoryId && repo.owner?.id === subject.ownerId &&
repo.name === subject.repository && repo.owner.login === subject.owner &&
repo.private === false, "public_repo_identity_mismatch", 403);
const tag = await anonymousGitHubJson("/git/ref/tags/" + subject.tagName);
const tag = await anonymousGitHubJson("/git/ref/tags/" + subject.tagName, "tag");
demand(tag.ref === "refs/tags/" + subject.tagName && tag.object?.type === "commit" &&
tag.object.sha === subject.tagCommit, "public_tag_identity_mismatch", 403);
const release = await anonymousGitHubJson("/releases/" + subject.releaseId);
const release = await anonymousGitHubJson("/releases/" + subject.releaseId, "release");
demand(release.id === subject.releaseId && release.tag_name === subject.tagName &&
Array.isArray(release.assets) && release.assets.length === 2, "public_release_identity_mismatch", 403);
const pkg = release.assets.find(a => a.id === subject.packageAssetId),
Expand Down
4 changes: 3 additions & 1 deletion services/release-provider/src/worker.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { recoveryPolicy, validateRecoveryGrant, validateRecordedRecoveryGrant } from "./recovery.mjs";
import { digest, stableJson } from "../../../dist/hash.js";
import { validateReleaseObservation } from "../../../dist/release-v1-contract.js";
import { authenticate, githubToken, demand, ReleaseRefusal } from "./identity.mjs";
import { authenticate, githubToken, demand, ReleaseRefusal, PublicReadbackRefusal } from "./identity.mjs";
import { strictBody, readTarget, privateKey, provider, validateSeed,
decision, validateSignedRequest, signCompletion, validateCompletion,
verifiedReceipt, targetScope, policy, reservationEvidence } from "./policy.mjs";
Expand All @@ -13,6 +13,8 @@ function reply(payload, status = 200) {
});
}
function failure(error) {
if (error instanceof PublicReadbackRefusal)
return reply({ error: error.code, readback: error.readback }, error.status);
if (error instanceof ReleaseRefusal) return reply({ error: error.code }, error.status);
if (error instanceof Error && error.message.startsWith("release_v1:")) {
return reply({ error: "release_policy_rejected" }, 403);
Expand Down
33 changes: 33 additions & 0 deletions services/release-provider/test/provider.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,36 @@ test("GitHub redirects cannot reserve a release or satisfy independent signer re
assert.equal(calls.length,1);
}
});


test("signer readback refusal reports bounded upstream status without signing or disclosing bodies",async t=>{
const api=githubFixture(t),authority=env("approval");
const prepared=await call("approval",authority,"/v1/prepare",{seed:seed()});
const executed=await call("approval",authority,"/v1/execute",{request:prepared.json.request});
const normalFetch=globalThis.fetch;
let endpoint="repository",status=403,calls=0;
const paths={repository:"/repos/AyobamiH/agentproof-release-canary",tag:"/git/ref/tags/",release:"/releases/"};
t.mock.method(globalThis,"fetch",async(input,init)=>{
calls++;assert.equal(init.method,"GET");assert.equal(init.redirect,"manual");
assert.equal(init.headers.authorization,undefined);
const path=new URL(String(input)).pathname;
if(endpoint==="repository"?path===paths.repository:path.includes(paths[endpoint]))
return new Response("private-upstream-body-must-not-escape",{status,headers:{
"content-type":"text/plain",location:"https://untrusted.invalid/secret",
"x-ratelimit-remaining":"0","x-ratelimit-reset":"1792328000",
"retry-after":"secret-header-value"}});
return normalFetch(input,init);
});
for(endpoint of ["repository","tag","release"]) for(status of [301,403,429,503]) {
const signer=env("signing");calls=0;
const rejected=await call("signing",signer,"/v1/sign",{
request:prepared.json.request,completion:executed.json.completion});
assert.equal(rejected.status,503);
assert.deepEqual(rejected.json,{error:"public_readback_unavailable",readback:{
endpoint,upstreamStatus:status,rateLimitRemaining:0,rateLimitReset:1792328000,retryAfterSeconds:null}});
assert.equal(calls,["repository","tag","release"].indexOf(endpoint)+1);
assert.equal(signer.ledger.size,0);assert.equal(api.mutationCount,1);
assert(!JSON.stringify(rejected.json).includes("secret"));
assert(!JSON.stringify(rejected.json).includes("private-upstream"));
}
});
Loading