mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 05:56:37 +01:00
v1.91.123.0 fix(cso): comprehensive verify can patch read-only snapshots and accepts runtime-plan's profile (#3240)
* fix(cso): comprehensive verify can patch read-only snapshots and accepts runtime-plan's profile * docs(changelog): unreleased entry for the /cso verify repair path * v1.91.123.0 stamp
This commit is contained in:
1 parent
521fd42489
commit
56f25ef82b
8 files changed
+31
-7
No files matched your search
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.123.0] - 2026-10-17
|
||||
|
||||
**/cso comprehensive repairs run again.** In the first paid runtime qualification, the agent recorded 148 repair reviews across 42 comprehensive runs, but not one verification attempt started. Every runtime scored 0/24 on held-out repairs. Two helper bugs stacked: the patch write failed on the snapshot's read-only files, and `runtime-plan` handed the agent a profile name that `verify` refused.
|
||||
|
||||
### Fixed
|
||||
- **`verify` can patch read-only snapshot files.** Snapshot copies keep their 0444 modes, so writing the patched file failed with EACCES for any non-root user, which is how real users and the evaluator run. The error surfaced as a generic "unexpected or unsafe input". The helper now makes the file owner-writable for the write and restores its mode afterwards.
|
||||
- **`runtime-plan` returns the qualified runtime ID** (for example `node-24.4.0-amd64`) instead of the stack alias, and the skill copies that value into the verification request. `verify` rejects any other profile with an `INVALID_ARGUMENT` that names the expected ID, before an attempt is counted.
|
||||
|
||||
### If you…
|
||||
- **…build a verification request by hand:** use the `runtimeProfile` that `runtime-plan` printed. The stack alias is refused.
|
||||
|
||||
## [1.91.122.0] - 2026-10-17
|
||||
|
||||
**Main no longer goes red on 2026-11-16.** All seven `.osv-scanner.toml` suppressions expired on 2026-11-30, and `test/osv-ignore-expiry.test.ts` fails 14 days early. Six are gone and the seventh is re-dated with an upstream link. Dependabot's grouped dev bump (#3228) is replaced by hand, and Version Gate stops failing every dependabot package.json PR.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.122.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.123.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+12
-3
@@ -2056,7 +2056,7 @@ function testPlan(args: string[]) {
|
||||
...publicTestPlan(manifest, canonicalTestPlan(join(dir, 'snapshot'), stack)),
|
||||
};
|
||||
}
|
||||
function runtimePlan(args: string[]) {
|
||||
function runtimePlan(args: string[], dependencies: CsoCliDependencies) {
|
||||
const { dir } = run(args);
|
||||
if (!args.length || !['node', 'bun', 'python', 'rails'].includes(args[0]))
|
||||
throw new CsoError('INVALID_ARGUMENT', 'runtime-plan requires one supported stack and --port PORT');
|
||||
@@ -2075,9 +2075,13 @@ function runtimePlan(args: string[]) {
|
||||
preparation.prerequisites.map((item) => item.message).join('; ') ||
|
||||
`${stack} preparation metadata is incomplete`,
|
||||
);
|
||||
let runtimeProfile = preparation.runtimeProfile;
|
||||
try {
|
||||
runtimeProfile = selectRuntime(runtimeProfile, platform(), dependencies.runtimeCatalog).id;
|
||||
} catch {}
|
||||
return {
|
||||
stack,
|
||||
runtimeProfile: preparation.runtimeProfile,
|
||||
runtimeProfile,
|
||||
start: publicStartPlan(manifest, canonicalStartPlan(join(dir, 'snapshot'), stack, port)),
|
||||
tests: publicTestPlan(manifest, canonicalTestPlan(join(dir, 'snapshot'), stack)),
|
||||
};
|
||||
@@ -2339,6 +2343,11 @@ async function verify(args: string[], dependencies: CsoCliDependencies) {
|
||||
} catch (error: any) {
|
||||
throw new CsoError('PREREQUISITE', error?.message || 'Qualified runtime is unavailable');
|
||||
}
|
||||
if (request.runtimeProfile !== runtime.id)
|
||||
throw new CsoError(
|
||||
'INVALID_ARGUMENT',
|
||||
`runtimeProfile must be the qualified runtime ID ${runtime.id} returned by runtime-plan`,
|
||||
);
|
||||
const verifier = runtime;
|
||||
if (!['node', 'bun', 'python', 'rails'].includes(runtime.stack))
|
||||
throw new CsoError(
|
||||
@@ -2848,7 +2857,7 @@ export async function dispatchCsoCommand(
|
||||
result = testPlan(args);
|
||||
break;
|
||||
case 'runtime-plan':
|
||||
result = runtimePlan(args);
|
||||
result = runtimePlan(args, dependencies);
|
||||
break;
|
||||
case 'recheck':
|
||||
result = await recheck(args, dependencies);
|
||||
|
||||
@@ -1035,7 +1035,9 @@ export function preparePatchedSource(snapshot: string, target: string, request:
|
||||
);
|
||||
const mode = exists ? fs.statSync(file).mode & 0o777 : 0o600;
|
||||
secureDirectory(dirname(file));
|
||||
if (exists) fs.chmodSync(file, mode | 0o200);
|
||||
fs.writeFileSync(file, change.after, { mode });
|
||||
fs.chmodSync(file, mode);
|
||||
}
|
||||
}
|
||||
export function certify(params: {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.122",
|
||||
"version": "1.91.123",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -189,6 +189,7 @@ describe('tested repair certificate gate',()=>{
|
||||
test('review is a separate content-bound but explicitly self-attested artifact',()=>{const raw:any=request('vulnerable\n','fixed\n');raw.review.reviewedPatchHash=patchHash(raw);const req=validateVerificationRequest(raw),artifact=makeReviewArtifact('run',req,'producer-session');raw.review.artifactId=artifact.id;const bound=validateVerificationRequest(raw);expect(validateReviewArtifact(artifact,'run',bound)).toMatchObject({producer:'producer-session',assurance:'self_attested'});expect(()=>makeReviewArtifact('run',req,req.review.reviewer)).toThrow('distinct');expect(()=>validateReviewArtifact({...artifact,rationale:'changed'},'run',bound)).toThrow('does not bind');});
|
||||
test('executor observations have a strict helper-owned shape',()=>{expect(()=>validateVerificationObservation({booted:true,legitimate:true,security:'pass',existingTests:true,output:'ok',inputHash:'',tested:true})).toThrow('Unexpected');expect(()=>validateVerificationObservation({booted:'yes',legitimate:true,security:'pass',existingTests:true,output:'ok',inputHash:''})).toThrow('booleans');});
|
||||
test('secret-bearing fixtures cannot enter a replayable verification bundle',async()=>{const runDir=tmp(),snapshot=path.join(runDir,'snapshot');fs.mkdirSync(snapshot);fs.writeFileSync(path.join(snapshot,'app.js'),'vulnerable\n');const raw:any=request('vulnerable\n','fixed\n');raw.review.reviewedPatchHash=patchHash(raw);raw.fixtures={'secret.txt':CREDENTIAL_CANARY};const manifest:any={entries:[{path:'app.js',transformation:undefined}]};const runtime:any={id:'node',image:'runtime@sha256:'+'a'.repeat(64),platform:'linux/amd64'};expect(verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:{observe:async()=>{throw new Error('must not execute');}},persist:false})).rejects.toMatchObject({code:'REDACTION_FAILED'});});
|
||||
test('a non-root producer patches a read-only snapshot file and keeps its mode',()=>{const root=tmp(),before=path.join(root,'before'),after=path.join(root,'after');fs.mkdirSync(before);fs.writeFileSync(path.join(before,'app.js'),'vulnerable\n');fs.chmodSync(path.join(before,'app.js'),0o444);const raw:any=request('vulnerable\n','fixed\n');raw.review.reviewedPatchHash=patchHash(raw);preparePatchedSource(before,after,validateVerificationRequest(raw));expect(fs.readFileSync(path.join(after,'app.js'),'utf8')).toBe('fixed\n');expect(fs.statSync(path.join(after,'app.js')).mode&0o777).toBe(0o444);});
|
||||
test('declared patches require exact pristine preimages',()=>{const root=tmp(),before=path.join(root,'before'),after=path.join(root,'after');fs.mkdirSync(before);fs.writeFileSync(path.join(before,'app.js'),'changed\n');const raw:any=request('vulnerable\n','fixed\n');raw.review.reviewedPatchHash=patchHash(raw);expect(()=>preparePatchedSource(before,after,validateVerificationRequest(raw))).toThrow('preimage');});
|
||||
test('helper derives dependency and configuration patch effects',()=>{expect(fileEffect('package.json')).toBe('dependency');expect(fileEffect('apps/x/pyproject.toml')).toBe('dependency');expect(fileEffect('config/environments/test.rb')).toBe('configuration');const root=tmp(),before=path.join(root,'before'),after=path.join(root,'after');fs.mkdirSync(before);fs.writeFileSync(path.join(before,'package.json'),'{}');const raw:any=request('vulnerable\n','fixed\n');raw.changes=[{path:'package.json',beforeSha256:sha256('{}'),after:'{"dependencies":{}}',effect:'configuration'}];raw.boundaryFiles=['package.json'];raw.review.reviewedPatchHash=patchHash(raw);expect(()=>preparePatchedSource(before,after,validateVerificationRequest(raw))).toThrow('must be declared as dependency');});
|
||||
test('verification evidence stays outside both source trees and temporary copies are deleted on errors',async()=>{const runDir=tmp(),snapshot=path.join(runDir,'snapshot');fs.mkdirSync(snapshot);fs.writeFileSync(path.join(snapshot,'app.js'),'vulnerable\n',{mode:0o755});const testBody='test("ok",()=>{})\n';fs.writeFileSync(path.join(snapshot,'app.test.js'),testBody);const packageJson='{"scripts":{"test":"node --test"}}';fs.writeFileSync(path.join(snapshot,'package.json'),packageJson);const raw:any=request('vulnerable\n','fixed\n');raw.findingId='1abcdefabcdefabcdefabcdefabcdefa';raw.existingTests=[{executable:'/usr/local/bin/node',args:['--test','--test-reporter=tap','./app.test.js']}];raw.testFiles=['app.test.js'];raw.changes.push({path:'security/policy.js',beforeSha256:null,after:'export const enforced = true\n',effect:'source'});raw.review.reviewedPatchHash=patchHash(raw);const manifest:any={version:3,createdAt:'2026-01-01T00:00:00Z',expiresAt:'2026-01-08T00:00:00Z',root:'/repo',headCommit:'a'.repeat(40),originalHash:'b'.repeat(64),executionHash:treeHash(snapshot),entries:[{path:'app.js',originalHash:sha256('vulnerable\n'),executionHash:sha256('vulnerable\n'),bytes:11,mode:0o755},{path:'app.test.js',originalHash:sha256(testBody),executionHash:sha256(testBody),bytes:Buffer.byteLength(testBody),mode:0o600},{path:'package.json',originalHash:sha256(packageJson),executionHash:sha256(packageJson),bytes:packageJson.length,mode:0o600}]};const runtime:any={id:'node',stack:'node',image:'runtime@sha256:'+'a'.repeat(64),platform:'linux/amd64'};const executor:any={observe:async(source:string,phase:string,_request:any,_runtime:any,_verifier:any,work:string)=>{expect(work.startsWith(source+path.sep)).toBe(false);fs.mkdirSync(path.join(work,phase),{recursive:true});fs.writeFileSync(path.join(work,phase,'policy.json'),'evidence');return{booted:true,legitimate:true,security:phase==='before'?'intended_failure':'pass',existingTests:true,output:'ok',inputHash:''};}};let unattested:any;try{await verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor,persist:false});}catch(error){unattested=error;}expect(unattested).toMatchObject({code:'PREREQUISITE',attempt:{artifactKind:'repair_candidate',findingId:raw.findingId,reproduction:'reproduced',repair:'proposed',bundleIssued:false,testCompletionAssurance:'self_reported'}});expect(unattested.message).toContain('out-of-process external assertion witness');expect(fs.existsSync(path.join(runDir,'bundles'))).toBe(false);expect(fs.readdirSync(path.join(runDir,'verification'))).toEqual([]);expect(fs.statSync(path.join(snapshot,'app.js')).mode&0o777).toBe(0o755);const failing:any={observe:async()=>{throw new Error('stop');}};expect(verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:failing,persist:false})).rejects.toThrow('stop');expect(fs.readdirSync(path.join(runDir,'verification'))).toEqual([]);const afterFailure:any={observe:async(_source:string,phase:string)=>{if(phase==='after')throw new CsoError('TOOL_FAILED','after setup failed');return{booted:true,legitimate:true,security:'intended_failure',existingTests:true,output:'reproduced safely',inputHash:''};}};let failed:any;try{await verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:afterFailure});}catch(error){failed=error;}expect(failed).toMatchObject({code:'TOOL_FAILED',attempt:{findingId:raw.findingId,reproduction:'reproduced',repair:'failed',bundleIssued:false}});const attempt=failed.attempt,artifact=JSON.parse(fs.readFileSync(path.join(runDir,'verification-attempts',`${attempt.id}.json`),'utf8'));expect(artifact).toMatchObject({id:attempt.id,findingId:raw.findingId,reproduction:'reproduced',repair:'failed',bundleIssued:false});expect(fs.existsSync(path.join(runDir,'bundles'))).toBe(false);expect(fs.readdirSync(path.join(runDir,'verification'))).toEqual([]);const assertionFailure:any={observe:async(_source:string,phase:string)=>({booted:true,legitimate:true,security:'intended_failure',existingTests:true,output:phase==='before'?'reproduced safely':'repair remained vulnerable',inputHash:''})};let notRepaired:any;try{await verifyRepair({runId:'run',runDir,manifest,rawRequest:raw,runtime,verifier:runtime,policyHash:'c'.repeat(64),archives:[],executor:assertionFailure});}catch(error){notRepaired=error;}expect(notRepaired).toMatchObject({code:'ASSERTION_FAILED',attempt:{reproduction:'reproduced',repair:'failed',bundleIssued:false,after:{security:'intended_failure'}}});expect(fs.existsSync(path.join(runDir,'bundles'))).toBe(false);expect(fs.readdirSync(path.join(runDir,'verification-attempts'))).toHaveLength(2);const brokenStartup:any={observe:async()=>({booted:false,legitimate:false,security:'pass',existingTests:false,output:'application never started',inputHash:''})};let blocked:any;try{await verifyRepair({runLine truncated
|
||||
|
||||
@@ -191,11 +191,12 @@ suite('CSO Node producer-visible repair lifecycle fixture', () => {
|
||||
const submittedFinding = submitted.findings[0];
|
||||
expect(submittedFinding).toMatchObject({ reproduction: 'not_attempted', repair: 'not_attempted', closure: 'open' });
|
||||
const plan = await cli.command<any>(['runtime-plan', original.runId, 'node', '--port', '34569']);
|
||||
expect(plan.runtimeProfile).toBe(cli.runtime.id);
|
||||
const vulnerable = fs.readFileSync(path.join(originalRunDirectory, 'snapshot', 'policy.js'), 'utf8');
|
||||
const repaired = "module.exports={status:403,body:'DENIED'};\n";
|
||||
const request: VerificationRequest = {
|
||||
findingId: submittedFinding.id,
|
||||
runtimeProfile: cli.runtime.id,
|
||||
runtimeProfile: plan.runtimeProfile,
|
||||
port: 34569,
|
||||
start: plan.start.command,
|
||||
legitimate: [{ name: 'legitimate control', path: '/control', method: 'GET', expected: { status: 200, includes: 'CONTROL_OK' } }],
|
||||
|
||||
Reference in new issue
Block a user