接入受限原生只读助手并修复来源物理路径核验

This commit is contained in:
铸渊 ICE-P-ZY001 2026-10-19 21:27:02 +08:00
commit 67d90d90a4
9 changed files with 61 additions and 26 deletions

View file

@ -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'){

View file

@ -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');
});

View file

@ -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;
}

16
runtime/worker.mjs Normal file
View file

@ -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);}