diff --git a/CURRENT.json b/CURRENT.json index ac37ba7..98f4449 100644 --- a/CURRENT.json +++ b/CURRENT.json @@ -1,7 +1,7 @@ { "schema": "guanghu.public-rule-release/v1", "world_id": "GLW-2.0-0001", - "version": "20261019.1", + "version": "20261019.2", "scope": "PUBLIC_ENTRY_RULES_SKILLS_AND_QUESTION_TRANSPORT_ONLY", "files": [ { @@ -11,7 +11,7 @@ }, { "path": "entry-data.json", - "sha256": "1194b1c532fdd524b4b4e9c36b478208f04b4d0e2733cd7f8a8284989796b60e", + "sha256": "ac4ad712860afab42a0f920e63c09811d9e197be9ea8577c7f4c4a7b3f3a377a", "bytes": 2082 }, { @@ -26,28 +26,33 @@ }, { "path": "runtime/question.mjs", - "sha256": "6d570aef1920a27a7179f99662780971cdb11b03dbf7a96115bdada8405c0d75", - "bytes": 6128 + "sha256": "c274c5e497fd05084bf9ce53d8ec28ea57eb71831944369e9cee67e4a0bc0a2a", + "bytes": 6307 }, { "path": "runtime/question.test.mjs", - "sha256": "16c29da700a0e84db54a343ec4a1c8db664890e07a5836d6fc141205340389e3", - "bytes": 1534 + "sha256": "6a17a016ae062a1e9e3a355ebf171c44adee1b41842bf7e97b034fbf22642fd1", + "bytes": 3052 }, { "path": "runtime/server.cjs", - "sha256": "d63f6c6cdbdfe7bc6abb0ff36dc6374caf3610c159b2a330c199ece2bc3178fe", - "bytes": 11353 + "sha256": "30e539a5cbdd12af68c352633fcd8c6f7698de3b29c460e903810917c29b5966", + "bytes": 11973 }, { "path": "runtime/server.test.cjs", "sha256": "93c7b8337b9aa5d8529e557006def17798f6345fdbeb271f2025b675202ff896", "bytes": 4065 }, + { + "path": "runtime/worker.mjs", + "sha256": "260c93d8cf67d172d0c1c04abea891b6f2d4fc8366901e3915b8c517702c6c10", + "bytes": 2859 + }, { "path": "scripts/build-release.py", - "sha256": "fcb66f6e27984b84e0276490b95f88c7c4340af2f90a4eda605a9627ba8532cf", - "bytes": 1912 + "sha256": "eaf3ed1b893b806b651c177a8a86e9bc2d010b5c32ff5e6ff1963291b60c6d6f", + "bytes": 1956 }, { "path": "scripts/guanghu-client.mjs", @@ -61,8 +66,8 @@ } ], "bundle_url": "https://guanghulab.com/api/ai/v1/rule-bundle", - "bundle_sha256": "2456880bd741de81f41f152e75b1c7be6fd38e78ed8b96456c86091d0a6c0eba", - "manifest_sha256": "9ddeee6a6606e2dd8471a0c02d19b5a2d9c4bad854ea8342d5a058de499a5721", + "bundle_sha256": "71b306eca701bd303bf3a8447d468847f46b863608be717106ff88ca57ad9edb", + "manifest_sha256": "31de3cc8d39f4c60fc84c97bd18ecd27f3270ef03e65b8b5a1ad793149dab17d", "repository": "https://guanghulab.com/code/bingshuo/guanghu-world-entry", "private_brain_included": false, "complete_four_domain_corpus_included": false, diff --git a/MANIFEST.json b/MANIFEST.json index b01d2ca..0323775 100644 --- a/MANIFEST.json +++ b/MANIFEST.json @@ -1,7 +1,7 @@ { "schema": "guanghu.public-rule-file-manifest/v1", "world_id": "GLW-2.0-0001", - "version": "20261019.1", + "version": "20261019.2", "scope": "PUBLIC_ENTRY_RULES_SKILLS_AND_QUESTION_TRANSPORT_ONLY", "files": [ { @@ -11,7 +11,7 @@ }, { "path": "entry-data.json", - "sha256": "1194b1c532fdd524b4b4e9c36b478208f04b4d0e2733cd7f8a8284989796b60e", + "sha256": "ac4ad712860afab42a0f920e63c09811d9e197be9ea8577c7f4c4a7b3f3a377a", "bytes": 2082 }, { @@ -26,28 +26,33 @@ }, { "path": "runtime/question.mjs", - "sha256": "6d570aef1920a27a7179f99662780971cdb11b03dbf7a96115bdada8405c0d75", - "bytes": 6128 + "sha256": "c274c5e497fd05084bf9ce53d8ec28ea57eb71831944369e9cee67e4a0bc0a2a", + "bytes": 6307 }, { "path": "runtime/question.test.mjs", - "sha256": "16c29da700a0e84db54a343ec4a1c8db664890e07a5836d6fc141205340389e3", - "bytes": 1534 + "sha256": "6a17a016ae062a1e9e3a355ebf171c44adee1b41842bf7e97b034fbf22642fd1", + "bytes": 3052 }, { "path": "runtime/server.cjs", - "sha256": "d63f6c6cdbdfe7bc6abb0ff36dc6374caf3610c159b2a330c199ece2bc3178fe", - "bytes": 11353 + "sha256": "30e539a5cbdd12af68c352633fcd8c6f7698de3b29c460e903810917c29b5966", + "bytes": 11973 }, { "path": "runtime/server.test.cjs", "sha256": "93c7b8337b9aa5d8529e557006def17798f6345fdbeb271f2025b675202ff896", "bytes": 4065 }, + { + "path": "runtime/worker.mjs", + "sha256": "260c93d8cf67d172d0c1c04abea891b6f2d4fc8366901e3915b8c517702c6c10", + "bytes": 2859 + }, { "path": "scripts/build-release.py", - "sha256": "fcb66f6e27984b84e0276490b95f88c7c4340af2f90a4eda605a9627ba8532cf", - "bytes": 1912 + "sha256": "eaf3ed1b893b806b651c177a8a86e9bc2d010b5c32ff5e6ff1963291b60c6d6f", + "bytes": 1956 }, { "path": "scripts/guanghu-client.mjs", diff --git a/entry-data.json b/entry-data.json index 01d1fda..3f3b181 100644 --- a/entry-data.json +++ b/entry-data.json @@ -1,7 +1,7 @@ { "schema": "guanghu.curated-public-entry-data/v1", "world_id": "GLW-2.0-0001", - "version": "20261019.1", + "version": "20261019.2", "repository_url": "https://guanghulab.com/code/bingshuo/guanghu-world-entry", "skill_url": "https://guanghulab.com/code/bingshuo/guanghu-world-entry/raw/branch/main/skills/guanghu-world-entry/SKILL.md", "source_registered_persona_count": 37, diff --git a/release.zip b/release.zip index 299c06d..278bf90 100644 Binary files a/release.zip and b/release.zip differ diff --git a/runtime/question.mjs b/runtime/question.mjs index 0a3ffc8..c4d7cc6 100644 --- a/runtime/question.mjs +++ b/runtime/question.mjs @@ -1,5 +1,6 @@ // WORLD_RUNTIME: each question reads exactly one registered persona source. No neural or memory writer. import fs from 'node:fs'; +import path from 'node:path'; import crypto from 'node:crypto'; import {pathToFileURL} from 'node:url'; const sha=x=>crypto.createHash('sha256').update(x).digest('hex'); @@ -14,7 +15,7 @@ export async function loadSource(route,question){ concepts=await jsonGet(route.base_url+'/concepts');need(Array.isArray(concepts),'PERSONA_CONCEPT_SOURCE_INVALID'); identity=concepts.filter(c=>c.id==='C0001').map(c=>c.gist);head=sha(JSON.stringify({revision:status.revision,concepts})); }else if(route.kind==='SHUANGYAN_G2_OWN_BUNDLE'){ - const core=await import(pathToFileURL(route.core_path).href),b=core.loadBundle(route.bundle_root); + const ownRoot=fs.realpathSync(route.bundle_root),ownCore=fs.realpathSync(route.core_path);need(path.dirname(ownCore)===ownRoot,'PERSONA_SOURCE_ADAPTER_ROOT_MISMATCH');const core=await import(pathToFileURL(ownCore).href),b=core.loadBundle(ownRoot); need(b.brain.brain_id===route.brain_id&&status.revision===b.brain.revision&&status.current_sha256===b.brain.current_sha256,'PERSONA_BUNDLE_RUNTIME_MISMATCH'); identity=b.cognition.identity_sources.map(x=>x.text);concepts=b.cognition.concepts;head=b.brain.current_sha256; }else if(route.kind==='RETURNED_NATIVE_SERVICE'){ diff --git a/runtime/question.test.mjs b/runtime/question.test.mjs index 77828ff..cb9a518 100644 --- a/runtime/question.test.mjs +++ b/runtime/question.test.mjs @@ -3,3 +3,9 @@ const source={persona_id:'ICE-P-ONE',source_sha256:'pinned',brain_id:'own-brain' test('persona, source and evidence references must match',()=>{assert.equal(acceptReply(source,good).basis,'EVIDENCE');for(const patch of [{persona_id:'ICE-P-OTHER'},{source_sha256:'old'},{evidence_ids:['invented']},{basis:'EVIDENCE',evidence_ids:[]},{reply_zh:'not first person'}])assert.throws(()=>acceptReply(source,{...good,...patch}));}); test('source changes during inference withhold the reply',async()=>{let n=0;await assert.rejects(askQuestion({}, {question:'q',request_id:'r'}, {},{load:async()=>({...source,source_sha256:n++?'changed':'pinned'}),propose:async()=>good}),/PERSONA_SOURCE_CHANGED_DURING_QUESTION/);}); test('verified inference returns no neural, memory or execution authority',async()=>{const r=await askQuestion({}, {question:'q',request_id:'r'}, {},{load:async()=>source,propose:async payload=>{assert.equal(payload.source_role,'EXTERNAL_QUESTION_NOT_DIRECT_HUMAN_FOOD');return good;}});assert.equal(r.no_memory_write,true);assert.equal(r.no_neural_state_write,true);assert.equal(r.no_execution_authority,true);}); + +test('a returned current symlink resolves to the own immutable bundle before native verification',async t=>{ + const fs=await import('node:fs'),os=await import('node:os'),path=await import('node:path'),http=await import('node:http');const root=fs.mkdtempSync(path.join(os.tmpdir(),'persona-bundle-link-')),real=path.join(root,'release');fs.mkdirSync(real);fs.writeFileSync(path.join(real,'core.mjs'),`import fs from 'node:fs';export function loadBundle(root){if(fs.realpathSync(root)!==root)throw Error('UNRESOLVED_BUNDLE_ALIAS');return {brain:{brain_id:'fixture-brain',revision:1,current_sha256:'head'},cognition:{identity_sources:[{text:'own fixture identity'}],concepts:[]}};}`);fs.symlinkSync(real,path.join(root,'current')); + const server=http.createServer((req,res)=>{res.setHeader('content-type','application/json');res.end(JSON.stringify({brain_id:'fixture-brain',revision:1,current_sha256:'head'}));});await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));t.after(()=>{server.closeAllConnections();server.close();fs.rmSync(root,{recursive:true,force:true});}); + const {loadSource}=await import('./question.mjs');const x=await loadSource({persona_id:'ICE-P-FIXTURE',name:'fixture',brain_id:'fixture-brain',kind:'SHUANGYAN_G2_OWN_BUNDLE',status_url:'http://127.0.0.1:'+server.address().port+'/status',bundle_root:path.join(root,'current'),core_path:path.join(root,'current/core.mjs')},'identity');assert.equal(x.persona_id,'ICE-P-FIXTURE');assert.equal(x.integrity,'OWN_NATIVE_SOURCE_VERIFIED'); +}); diff --git a/runtime/server.cjs b/runtime/server.cjs index a71b695..eb6840d 100644 --- a/runtime/server.cjs +++ b/runtime/server.cjs @@ -1,6 +1,8 @@ 'use strict'; // WORLD_RUNTIME mechanical projection of protocols/ENTRY.hldp. Runtime credentials are private. const http=require('node:http'),fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'),{pathToFileURL}=require('node:url'); +const {execFile}=require('node:child_process'); +function askThroughWorker(input){return new Promise((resolve,reject)=>{const child=execFile('/usr/bin/sudo',['-n','/usr/local/libexec/guanghu/public-question-worker'],{timeout:60000,maxBuffer:512000},(err,stdout)=>{let value;try{value=JSON.parse(stdout);}catch{return reject(Error('PERSONA_WORKER_UNAVAILABLE'));}if(err||value.error)return reject(Error(/^[A-Z_]+$/.test(value.error||'')?value.error:'PERSONA_WORKER_UNAVAILABLE'));resolve(value);});child.stdin.end(JSON.stringify(input));});} const sha=x=>crypto.createHash('sha256').update(x).digest('hex'); const json=(res,status,value,privateReply=false)=>{res.writeHead(status,{'content-type':'application/json; charset=utf-8','cache-control':privateReply?'no-store':'public, max-age=30','x-content-type-options':'nosniff',...(privateReply?{}:{'access-control-allow-origin':'*'})});res.end(JSON.stringify(value));}; function authClient(req,config){const raw=req.headers.authorization||'';if(!raw.startsWith('Bearer '))return null;const digest=sha(raw.slice(7));return (config.clients||[]).find(c=>typeof c.key_sha256==='string'&&/^[a-f0-9]{64}$/.test(c.key_sha256)&&crypto.timingSafeEqual(Buffer.from(c.key_sha256,'hex'),Buffer.from(digest,'hex'))&&Date.parse(c.expires_at)>Date.now())||null;} @@ -45,7 +47,7 @@ function createServer({root=path.resolve(__dirname,'..'),configPath=process.env. const route=cfg.personas.find(x=>x.persona_id===input.persona_id);if(!route)return json(res,404,{error:'PERSONA_ROUTE_NOT_REGISTERED'},true); const cacheKey=client.id+':'+input.request_id,inputHash=sha(raw),old=completed.get(cacheKey);if(old){if(old.inputHash!==inputHash)return json(res,409,{error:'REQUEST_ID_CONTENT_MISMATCH'},true);return json(res,200,old.result,true);} inflight.set(client.id,(inflight.get(client.id)||0)+1); - try{const handler=questionHandler||(await import(pathToFileURL(path.join(__dirname,'question.mjs')).href)).askQuestion;const result=await handler(route,input,cfg.fuel);completed.set(cacheKey,{inputHash,result});if(completed.size>1000)completed.delete(completed.keys().next().value);return json(res,200,result,true);}finally{inflight.set(client.id,(inflight.get(client.id)||1)-1);} + try{const handler=questionHandler||(cfg.use_privileged_readonly_worker?async(_route,body)=>askThroughWorker(body):(await import(pathToFileURL(path.join(__dirname,'question.mjs')).href)).askQuestion);const result=await handler(route,input,cfg.fuel);completed.set(cacheKey,{inputHash,result});if(completed.size>1000)completed.delete(completed.keys().next().value);return json(res,200,result,true);}finally{inflight.set(client.id,(inflight.get(client.id)||1)-1);} }catch(e){if(!res.headersSent)json(res,503,{error:/^[A-Z_]+$/.test(e.message)?e.message:'QUESTION_SERVICE_UNAVAILABLE',reply_withheld:true,no_execution_authority:true},true);else res.destroy();}}); server.requestTimeout=60000;server.headersTimeout=10000;return server; } diff --git a/runtime/worker.mjs b/runtime/worker.mjs new file mode 100644 index 0000000..a1392da --- /dev/null +++ b/runtime/worker.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node +// WORLD_RUNTIME: bounded root-owned read-only persona worker; no command, path, neural or memory setter. +import fs from 'node:fs';import path from 'node:path';import {askQuestion} from './question.mjs'; +const ROOT='/etc/guanghu/public-question-worker',CONFIG=ROOT+'/config.json',STATE='/var/lib/guanghu/public-question-worker'; +const need=(x,c)=>{if(!x)throw Error(c)};let locked=false,lock=STATE+'/request.lock'; +try{ + need(process.getuid()===0&&process.argv.length===2,'PRIVATE_WORKER_IDENTITY_REQUIRED');const stat=fs.lstatSync(CONFIG);need(stat.isFile()&&!stat.isSymbolicLink()&&stat.uid===0&&(stat.mode&0o077)===0&&fs.realpathSync(ROOT)===ROOT,'WORKER_CONFIG_NOT_PROTECTED');const cfg=JSON.parse(fs.readFileSync(CONFIG)); + let raw='';for await(const c of process.stdin){raw+=c;need(Buffer.byteLength(raw)<=16000,'QUESTION_BODY_TOO_LARGE');}let i;try{i=JSON.parse(raw);}catch{throw Error('QUESTION_JSON_REQUIRED');} + need(i&&!Array.isArray(i)&&Object.keys(i).every(k=>['persona_id','question','tool_host','session_id','request_id'].includes(k))&&typeof i.question==='string'&&i.question.trim()&&Buffer.byteLength(i.question)<=12000&&['tool_host','session_id','request_id'].every(k=>typeof i[k]==='string'&&/^[-A-Za-z0-9._:]{1,160}$/.test(i[k])),'QUESTION_COORDINATES_INVALID');const route=cfg.personas.find(x=>x.persona_id===i.persona_id);need(route&&cfg.allowed_personas.includes(i.persona_id),'PERSONA_OUTSIDE_WORKER_SCOPE'); + fs.mkdirSync(STATE,{recursive:true,mode:0o700});need(fs.lstatSync(STATE).uid===0&&!fs.lstatSync(STATE).isSymbolicLink(),'WORKER_STATE_NOT_PROTECTED'); + if(fs.existsSync(lock)){let pid;try{pid=Number(fs.readFileSync(lock,'utf8'));}catch{}let alive=true;try{if(!pid)throw Error();process.kill(pid,0);}catch{alive=false;}if(!alive)fs.unlinkSync(lock);} + try{const fd=fs.openSync(lock,'wx',0o600);fs.writeSync(fd,String(process.pid));fs.closeSync(fd);locked=true;}catch{throw Error('QUESTION_CAPACITY_LIMIT');} + const file=STATE+'/rate.json',now=Date.now();let rate=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{start:now,count:0};if(now-rate.start>=60000)rate={start:now,count:0};need(rate.count<(cfg.requests_per_minute||12),'QUESTION_CAPACITY_LIMIT');rate.count++;const next=file+'.next';fs.writeFileSync(next,JSON.stringify(rate),{mode:0o600});fs.renameSync(next,file); + const result=await askQuestion(route,i,cfg.fuel);console.log(JSON.stringify({...result,guard_scope:'IDENTITY_SOURCE_COORDINATES_EVIDENCE_AND_RACE_NOT_SEMANTIC_CERTAINTY'})); +}catch(e){console.log(JSON.stringify({error:/^[A-Z_]+$/.test(e.message)?e.message:'PERSONA_WORKER_SOURCE_UNAVAILABLE',reply_withheld:true,no_neural_state_write:true,no_memory_write:true,no_execution_authority:true}));process.exitCode=2;} +finally{if(locked&&fs.existsSync(lock)&&fs.readFileSync(lock,'utf8')===String(process.pid))fs.unlinkSync(lock);} diff --git a/scripts/build-release.py b/scripts/build-release.py index a41b6d9..0d01828 100644 --- a/scripts/build-release.py +++ b/scripts/build-release.py @@ -6,7 +6,7 @@ for f in sorted(p.rglob('*')): rel=f.relative_to(p);allowed={'AGENTS.md','entry-data.json','package.json','protocols','runtime','scripts','skills'} if rel.parts[0] not in allowed or any(x in rel.parts for x in ['.env','secrets','brain-state.json','transition-log.jsonl']) or f.is_symlink():raise SystemExit('PUBLIC_PACKAGE_PATH_DENIED:'+str(rel)) files.append({'path':f.relative_to(p).as_posix(),'sha256':sha(f.read_bytes()),'bytes':f.stat().st_size}) -man={'schema':'guanghu.public-rule-file-manifest/v1','world_id':'GLW-2.0-0001','version':'20261019.1','scope':'PUBLIC_ENTRY_RULES_SKILLS_AND_QUESTION_TRANSPORT_ONLY','files':files};(p/'MANIFEST.json').write_text(json.dumps(man,ensure_ascii=False,indent=2)+'\n') +man={'schema':'guanghu.public-rule-file-manifest/v1','world_id':'GLW-2.0-0001','version':json.loads((p/'entry-data.json').read_text())['version'],'scope':'PUBLIC_ENTRY_RULES_SKILLS_AND_QUESTION_TRANSPORT_ONLY','files':files};(p/'MANIFEST.json').write_text(json.dumps(man,ensure_ascii=False,indent=2)+'\n') with zipfile.ZipFile(p/'release.zip','w',compression=zipfile.ZIP_DEFLATED) as z: for n in [x['path'] for x in files]+['MANIFEST.json']: info=zipfile.ZipInfo('guanghu-world-entry/'+n,date_time=(2026,10,19,0,0,0));info.external_attr=(stat.S_IFREG|0o644)<<16;info.compress_type=zipfile.ZIP_DEFLATED;z.writestr(info,(p/n).read_bytes())