From 0837bb5127d2762ea1603c42c08133ff14c25556 Mon Sep 17 00:00:00 2001 From: grishencorp Date: Mon, 21 Sep 2026 15:57:48 -0300 Subject: [PATCH 1/3] Allow standing owner approval for owner-initiated npm releases --- docs/security/npm-release-security.md | 35 ++++++++++++++++++-- scripts/approve-owner-npm-release.mjs | 25 ++++++++++++++ scripts/npm-owner-review-exception.mjs | 19 +++++++++-- tests/npm-owner-review-exception.test.mjs | 8 +++++ tests/owner-release-approval.test.mjs | 9 +++++ tests/verify-npm-release-governance.test.mjs | 9 +++++ 6 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 scripts/approve-owner-npm-release.mjs create mode 100644 tests/owner-release-approval.test.mjs diff --git a/docs/security/npm-release-security.md b/docs/security/npm-release-security.md index e2f3d81b..1286beec 100644 --- a/docs/security/npm-release-security.md +++ b/docs/security/npm-release-security.md @@ -9,8 +9,8 @@ external configuration. No CI job may make an AgentPlat package publicly installable. CI may only place the exact reviewed tarballs into npm staged publishing. A maintainer who did not initiate the release must normally review those staged bytes and approve them -with 2FA. The version-bound owner-approved exceptions below are the only exceptions to -independent npm deployment review; it does not waive byte review or 2FA. +with 2FA. The standing owner exception below and historical version-bound exceptions +permit owner approval without another reviewer. Neither waives byte review or 2FA. The release uses one artifact set: @@ -179,3 +179,34 @@ Beta 8 closure on 2026-09-14 (UTC): all 65 packages were approved with npm were removed. Governance verification passed with no findings. The standing owner PR-review exception remains. See the [distribution record](../releases/beta8-distribution-20260914.md). + + +## Standing owner release exception — 2026-09-21 + +The owner explicitly authorized no independent review for changes and releases +he initiates. This supersedes the release-version limit for future owner runs. +The existing owner-only PR exception remains unchanged; required checks still run. + +For npm, keep `npm-production` protected-main-only, disable administrator bypass, +and retain exactly `douglas-grishen` (user ID `207043696`) as required reviewer. +Set `prevent_self_review=false`. The owner approval can be performed automatically +on his behalf, without asking him to name another reviewer, only after preparation +has passed and both the original actor and rerun actor are that exact account. +Other initiators still require the owner's separate review. Do not automatically +approve another contributor's run or broaden this to other administrators. + +Set `AGENTPLAT_NPM_OWNER_REVIEW_VERSION=owner-initiated` and +`AGENTPLAT_NPM_OWNER_REVIEW_LOGIN=douglas-grishen` after the updated guard is on main. +These standing settings remain after publication; the Beta 7/8 restoration steps +above describe those historical temporary exceptions, not this standing policy. +For the already-prepared Beta 9 run using the old guard, retain unset exception +flags until staging completes; its owner-only GitHub approval is authorized by +this instruction and does not require rebuilding the reviewed package bytes. + +Use `node scripts/approve-owner-npm-release.mjs RUN_ID` for automatic owner +approval. It checks authenticated user ID, original/rerun actors, workflow, +protected branch, successful artifact preparation and the exact environment +reviewer configuration before approving the pending staging deployment. +A task authorized to publish an owner-initiated release should perform this step +without requesting another independent reviewer. npm staged-byte approval and +2FA remain a separate human-controlled publication boundary. diff --git a/scripts/approve-owner-npm-release.mjs b/scripts/approve-owner-npm-release.mjs new file mode 100644 index 00000000..9ea03328 --- /dev/null +++ b/scripts/approve-owner-npm-release.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import {execFileSync} from 'node:child_process'; +import {canOwnerApproveRelease} from './npm-owner-review-exception.mjs'; +const runId=process.argv[2];assert.match(runId??'',/^\d+$/,'supply a release run ID'); +const repo='repos/Agentplat/agentplat'; +const get=p=>JSON.parse(execFileSync('gh',['api',p],{encoding:'utf8'})); +const run=get(`${repo}/actions/runs/${runId}`),viewer=get('user'); +assert(canOwnerApproveRelease({actor:run.actor,triggeringActor:run.triggering_actor,viewer}),'Automatic review is limited to owner-initiated and owner-rerun releases'); +assert.equal(run.head_branch,'main');assert.equal(run.path,'.github/workflows/release.yml'); +assert.equal(run.event,'workflow_dispatch'); +const jobs=get(`${repo}/actions/runs/${runId}/jobs`).jobs; +assert(jobs.some(j=>j.name==='prepare'&&j.conclusion==='success'),'Exact artifacts must pass preparation first'); +const env=get(`${repo}/environments/npm-production`); +assert.equal(env.can_admins_bypass,false); +assert.equal(env.deployment_branch_policy.protected_branches,true); +assert.equal(env.deployment_branch_policy.custom_branch_policies,false); +const review=env.protection_rules.find(r=>r.type==='required_reviewers'); +assert.equal(review.prevent_self_review,false); +assert.deepEqual(review.reviewers.map(r=>({type:r.type,id:r.reviewer.id})),[{type:'User',id:viewer.id}]); +const pending=get(`${repo}/actions/runs/${runId}/pending_deployments`); +const target=pending.find(p=>p.environment.name==='npm-production'); +assert(target?.current_user_can_approve,'Owner approval must be available for this pending environment'); +execFileSync('gh',['api','--method','POST',`${repo}/actions/runs/${runId}/pending_deployments`,'--input','-'],{ + input:JSON.stringify({environment_ids:[target.environment.id],state:'approved',comment:'Standing owner authorization: owner-initiated release; independently prepared exact artifacts. npm staged byte review and 2FA remain required.'}),stdio:['pipe','pipe','pipe']}); +console.log('Owner-initiated staging approved; npm publication still requires staged-byte approval and 2FA.'); diff --git a/scripts/npm-owner-review-exception.mjs b/scripts/npm-owner-review-exception.mjs index 93bc4bfd..a413d835 100644 --- a/scripts/npm-owner-review-exception.mjs +++ b/scripts/npm-owner-review-exception.mjs @@ -1,10 +1,12 @@ import assert from "node:assert/strict"; -// Explicit owner authorization for Beta 8 on 2026-09-12. A future release -// must restore independent review and remove the environment exception flags. +// Historical Beta 8 exception plus standing owner authorization (2026-09-21). +// Only owner-initiated runs may be approved automatically; other runs retain review. export const NPM_OWNER_REVIEW_EXCEPTION = Object.freeze({ releaseVersion: "0.3.0-beta.8", ownerLogin: "douglas-grishen", + ownerId: 207043696, + standingMode: "owner-initiated", scope: "all", distTag: "next", }); @@ -17,6 +19,10 @@ export function matchesOwnerReviewException({ ownerReviewLogin, }) { const expected = NPM_OWNER_REVIEW_EXCEPTION; + if (ownerReviewVersion === expected.standingMode) + return ownerReviewLogin === expected.ownerLogin && + typeof releaseVersion === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(releaseVersion) && + ["all", "public-consumer"].includes(scope) && ["next", "latest"].includes(distTag); return ( releaseVersion === expected.releaseVersion && scope === expected.scope && @@ -41,8 +47,9 @@ export function assertStageReviewPolicy({ manifest, environment }) { ownerReviewVersion, ownerReviewLogin, }), - "Owner review exception applies only to the approved Beta 8 all/next release", + "Owner review exception must match the standing owner policy or historical Beta 8 approval", ); + if (ownerReviewVersion === NPM_OWNER_REVIEW_EXCEPTION.standingMode) return; assert.equal( environment.GITHUB_ACTOR, NPM_OWNER_REVIEW_EXCEPTION.ownerLogin, @@ -56,3 +63,9 @@ export function assertStageReviewPolicy({ manifest, environment }) { ); } } + +export function canOwnerApproveRelease({ actor, triggeringActor, viewer }) { + return [actor, triggeringActor, viewer].every(person => + person?.login === NPM_OWNER_REVIEW_EXCEPTION.ownerLogin && + person?.id === NPM_OWNER_REVIEW_EXCEPTION.ownerId); +} diff --git a/tests/npm-owner-review-exception.test.mjs b/tests/npm-owner-review-exception.test.mjs index d67b1ca6..c912dc42 100644 --- a/tests/npm-owner-review-exception.test.mjs +++ b/tests/npm-owner-review-exception.test.mjs @@ -48,3 +48,11 @@ test("empty GitHub variable expansions preserve ordinary review", () => { }), ); }); + +test('standing exception permits future releases and preserves historical limits',()=>{ + const x=fixture();x.manifest.releaseVersion='0.3.0-beta.9'; + x.environment.AGENTPLAT_NPM_OWNER_REVIEW_VERSION='owner-initiated'; + assert.doesNotThrow(()=>assertStageReviewPolicy(x)); + x.environment.AGENTPLAT_NPM_OWNER_REVIEW_LOGIN='other'; + assert.throws(()=>assertStageReviewPolicy(x)); +}); diff --git a/tests/owner-release-approval.test.mjs b/tests/owner-release-approval.test.mjs new file mode 100644 index 00000000..8d58f7fa --- /dev/null +++ b/tests/owner-release-approval.test.mjs @@ -0,0 +1,9 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {canOwnerApproveRelease} from '../scripts/npm-owner-review-exception.mjs'; +const owner={login:'douglas-grishen',id:207043696}; +test('only the owner can automatically approve his own original and rerun release',()=>{ + assert(canOwnerApproveRelease({actor:owner,triggeringActor:owner,viewer:owner})); + for(const field of ['actor','triggeringActor','viewer'])for(const person of [undefined,{...owner,id:1},{...owner,login:'other'}]) + assert.equal(canOwnerApproveRelease({...{actor:owner,triggeringActor:owner,viewer:owner},[field]:person}),false); +}); diff --git a/tests/verify-npm-release-governance.test.mjs b/tests/verify-npm-release-governance.test.mjs index 31c07c59..74673b9c 100644 --- a/tests/verify-npm-release-governance.test.mjs +++ b/tests/verify-npm-release-governance.test.mjs @@ -134,3 +134,12 @@ test("owner PR exception rejects other actors, rulesets and always bypass", () = assert.equal(analyzeNpmReleaseGovernance(state).status, "failed"); } }); + +test('standing npm owner exception preserves the sole-owner reviewer boundary',()=>{ + const s=secureState();s.releaseVersion='0.3.0-beta.9'; + s.environment.protection_rules[0]={type:'required_reviewers',prevent_self_review:false,reviewers:[{type:'User',reviewer:{login:'douglas-grishen'}}]}; + s.environmentVariables.variables.push({name:'AGENTPLAT_NPM_OWNER_REVIEW_VERSION',value:'owner-initiated'},{name:'AGENTPLAT_NPM_OWNER_REVIEW_LOGIN',value:'douglas-grishen'}); + assert.equal(analyzeNpmReleaseGovernance(s).status,'passed'); + s.environment.protection_rules[0].reviewers.push({type:'User',reviewer:{login:'other'}}); + assert.equal(analyzeNpmReleaseGovernance(s).status,'failed'); +}); From f5621a04b6a9794c1cc63c6473c0505912e5318e Mon Sep 17 00:00:00 2001 From: grishencorp Date: Mon, 21 Sep 2026 16:01:41 -0300 Subject: [PATCH 2/3] Record standing owner review policy for future coding tasks --- AGENTS.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ede8158d..f773354b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,3 +28,14 @@ Use canonical AgentPlat terms. Prefer existing contracts and adapters over new p ## Canonical names AgentPlat Agent Room, AgentPlat Collaboration Protocol, AgentPlat Planner, AgentPlat Handoff, AgentPlat Collective Runtime, AgentPlat Agent Morphogenesis, AgentPlat Agent Mesh, AgentPlat Inference Control and AgentPlat Evidence Boundary. + +## Owner-initiated changes and releases + +The owner authorized a standing exception to independent review on 2026-09-21. +For `douglas-grishen` (GitHub ID `207043696`), use the existing owner-only PR +review exception after required checks pass. For an npm release initiated and +rerun by that same owner, approve its prepared staging deployment on his behalf +with `node scripts/approve-owner-npm-release.mjs RUN_ID`; do not ask him to name +another reviewer. Other initiators still require separate owner review. Preserve +required CI, protected main, stage-only publishing, staged-byte review and npm +2FA. See `docs/security/npm-release-security.md` for the exact boundary. From e203151b579a6097b7d148b37a661c842bb3540a Mon Sep 17 00:00:00 2001 From: grishencorp Date: Mon, 21 Sep 2026 16:10:09 -0300 Subject: [PATCH 3/3] Bind standing review configuration to immutable owner identity --- scripts/verify-npm-release-governance.mjs | 4 +++- tests/verify-npm-release-governance.test.mjs | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/verify-npm-release-governance.mjs b/scripts/verify-npm-release-governance.mjs index cb30f531..b8f087c9 100644 --- a/scripts/verify-npm-release-governance.mjs +++ b/scripts/verify-npm-release-governance.mjs @@ -56,7 +56,9 @@ export function analyzeNpmReleaseGovernance({ reviewerRule?.reviewers?.length === 1 && reviewerRule.reviewers[0].type === "User" && reviewerRule.reviewers[0].reviewer?.login === - NPM_OWNER_REVIEW_EXCEPTION.ownerLogin; + NPM_OWNER_REVIEW_EXCEPTION.ownerLogin && + (ownerReviewVersion !== NPM_OWNER_REVIEW_EXCEPTION.standingMode || + reviewerRule.reviewers[0].reviewer?.id === NPM_OWNER_REVIEW_EXCEPTION.ownerId); if ( !reviewerRule || (reviewerRule.prevent_self_review !== true && !ownerReviewer) diff --git a/tests/verify-npm-release-governance.test.mjs b/tests/verify-npm-release-governance.test.mjs index 74673b9c..5d2f2564 100644 --- a/tests/verify-npm-release-governance.test.mjs +++ b/tests/verify-npm-release-governance.test.mjs @@ -137,9 +137,12 @@ test("owner PR exception rejects other actors, rulesets and always bypass", () = test('standing npm owner exception preserves the sole-owner reviewer boundary',()=>{ const s=secureState();s.releaseVersion='0.3.0-beta.9'; - s.environment.protection_rules[0]={type:'required_reviewers',prevent_self_review:false,reviewers:[{type:'User',reviewer:{login:'douglas-grishen'}}]}; + s.environment.protection_rules[0]={type:'required_reviewers',prevent_self_review:false,reviewers:[{type:'User',reviewer:{login:'douglas-grishen',id:207043696}}]}; s.environmentVariables.variables.push({name:'AGENTPLAT_NPM_OWNER_REVIEW_VERSION',value:'owner-initiated'},{name:'AGENTPLAT_NPM_OWNER_REVIEW_LOGIN',value:'douglas-grishen'}); assert.equal(analyzeNpmReleaseGovernance(s).status,'passed'); + s.environment.protection_rules[0].reviewers[0].reviewer.id=1; + assert.equal(analyzeNpmReleaseGovernance(s).status,'failed'); + s.environment.protection_rules[0].reviewers[0].reviewer.id=207043696; s.environment.protection_rules[0].reviewers.push({type:'User',reviewer:{login:'other'}}); assert.equal(analyzeNpmReleaseGovernance(s).status,'failed'); });