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
46 changes: 46 additions & 0 deletions .github/workflows/governance-read-preflight.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Governance Read-only Credential Preflight

on:
pull_request:
paths:
- .github/workflows/governance-read-preflight.yml
- scripts/release/governance-read-preflight.cjs
- tests/release/governance-read-preflight.test.js
push:
branches: [main]
paths:
- .github/workflows/governance-read-preflight.yml
workflow_dispatch:

permissions:
contents: read

jobs:
contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: false
- uses: actions/setup-node@v4
with:
node-version: '24.19.0'
- run: node --test tests/release/governance-read-preflight.test.js

verify-private-read:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
needs: contract-tests
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
with:
submodules: false
- uses: actions/setup-node@v4
with:
node-version: '24.19.0'
- name: Probe private governance read access without displaying credential
env:
CORTEX_GOVERNANCE_READ_TOKEN: ${{ secrets.CORTEX_GOVERNANCE_READ_TOKEN }}
CORTEX_AUTO_RELEASE_ENABLED: ${{ vars.CORTEX_AUTO_RELEASE_ENABLED }}
run: node scripts/release/governance-read-preflight.cjs
46 changes: 46 additions & 0 deletions scripts/release/governance-read-preflight.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use strict";
const records = [
["decisions/D-release-1.15.0-eca99a7c.json","decision_id","D-release-1.15.0-eca99a7c"],
["waitpoints/WP-release-1.15.0-eca99a7c.json","waitpoint_id","WP-release-1.15.0-eca99a7c"]
];
class PreflightError extends Error {
constructor(code){ super("GOVERNANCE_READ_PREFLIGHT_BLOCKED: "+code); this.code=code; }
}
async function probe(token, requester=fetch) {
if (!token || typeof token !== "string") throw new PreflightError("SECRET_MISSING");
for (const [file,field,expected] of records) {
let response;
try {
response = await requester("https://api.github.com/repos/Kucell/cortex-agent-agent/contents/"+file+"?ref=main",{
headers:{
Authorization:"Bearer "+token,
Accept:"application/vnd.github+json",
"X-GitHub-Api-Version":"2022-11-28"
},
redirect:"error",
signal:AbortSignal.timeout(10000)
});
} catch (_) { throw new PreflightError("PRIVATE_API_UNAVAILABLE"); }
if (!response || !response.ok) throw new PreflightError("PRIVATE_CONTENT_READ_DENIED");
let data;
try {
const payload=await response.json();
if (payload.type!=="file" || payload.encoding!=="base64" || typeof payload.content!=="string") throw Error();
data=JSON.parse(Buffer.from(payload.content,"base64").toString("utf8"));
} catch (_) { throw new PreflightError("PRIVATE_CONTENT_UNREADABLE"); }
if (!data || data[field]!==expected) throw new PreflightError("RECORD_ID_MISMATCH");
}
return {read_only_access_verified:true,record_count:records.length};
}
async function main() {
if (process.env.GITHUB_REPOSITORY!=="Kucell/cortex-agent" ||
process.env.GITHUB_REF!=="refs/heads/main") throw new PreflightError("MAIN_REQUIRED");
if (process.env.CORTEX_AUTO_RELEASE_ENABLED!=="false") throw new PreflightError("AUTO_PUBLISH_MUST_REMAIN_DISABLED");
const result=await probe(process.env.CORTEX_GOVERNANCE_READ_TOKEN);
console.log("Private governance read access verified for "+result.record_count+" records; auto publish remains disabled.");
}
if(require.main===module) main().catch(e=>{
console.error(e instanceof PreflightError?e.message:"GOVERNANCE_READ_PREFLIGHT_BLOCKED: UNKNOWN");
process.exitCode=1;
});
module.exports={probe,PreflightError};
39 changes: 39 additions & 0 deletions tests/release/governance-read-preflight.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use strict";
const test=require("node:test");
const assert=require("node:assert/strict");
const {probe}=require("../../scripts/release/governance-read-preflight.cjs");
const docs=[
{decision_id:"D-release-1.15.0-eca99a7c"},
{waitpoint_id:"WP-release-1.15.0-eca99a7c"}
];
const response=(record)=>({ok:true,async json(){return {type:"file",encoding:"base64",content:Buffer.from(JSON.stringify(record)).toString("base64")}}});
test("reads expected immutable historical records without leaking content",async()=>{
let index=0;
const result=await probe("FAKE_TOKEN",async(url,opts)=>{
assert.match(url,/https:\/\/api\.github\.com\/repos\/Kucell\/cortex-agent-agent\/contents\//);
assert.equal(opts.redirect,"error");
assert.equal(opts.headers.Authorization,"Bearer FAKE_TOKEN");
return response(docs[index++]);
});
assert.equal(index,2);
assert.deepEqual(result,{read_only_access_verified:true,record_count:2});
assert.doesNotMatch(JSON.stringify(result),/FAKE_TOKEN|D-release-/);
});
test("missing secret fails closed",async()=>{
await assert.rejects(()=>probe(""),/SECRET_MISSING/);
});
test("missing authorization fails closed",async()=>{
for(const status of [401,403,404]){
await assert.rejects(()=>probe("FAKE",async()=>({ok:false,status})),/PRIVATE_CONTENT_READ_DENIED/);
}
});
test("invalid or mismatched record fails closed",async()=>{
await assert.rejects(()=>probe("FAKE",async()=>response({decision_id:"WRONG"})),/RECORD_ID_MISMATCH/);
await assert.rejects(()=>probe("FAKE",async()=>({ok:true,async json(){return {type:"dir"}}})),/PRIVATE_CONTENT_UNREADABLE/);
});
test("network error cannot reveal token",async()=>{
let error;
try{await probe("SECRET_HIDDEN",async()=>{throw Error("SECRET_HIDDEN");});}catch(e){error=e;}
assert.equal(error.code,"PRIVATE_API_UNAVAILABLE");
assert.doesNotMatch(error.message,/SECRET_HIDDEN/);
});
Loading