diff --git a/CURRENT.json b/CURRENT.json index 98f4449..e82082d 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.2", + "version": "20261019.3", "scope": "PUBLIC_ENTRY_RULES_SKILLS_AND_QUESTION_TRANSPORT_ONLY", "files": [ { @@ -11,18 +11,23 @@ }, { "path": "entry-data.json", - "sha256": "ac4ad712860afab42a0f920e63c09811d9e197be9ea8577c7f4c4a7b3f3a377a", - "bytes": 2082 + "sha256": "2ca5752433f709aa34a11f95da26bc00ef02affd4fedf5fda80e948ff62db5b5", + "bytes": 2626 }, { "path": "package.json", - "sha256": "ac4dfa91f2a194b7dd0d8f654d449403f56c86fba6889f2d7d1f7aa2e078d0c3", + "sha256": "c505ddfdffef5eaa426e16a3b74b9abaa86814d57443867beba2403220cc69f6", "bytes": 260 }, { "path": "protocols/ENTRY.hldp", - "sha256": "03ff5d07a303885bd2afbbc266ee26f1bc8ead6d90dfaa2b127156352170baf5", - "bytes": 2822 + "sha256": "16f25f71f5a7721521f1b8e3462965fe9e606800a8669e0d832b3c19c4a48f29", + "bytes": 3513 + }, + { + "path": "runtime/client.test.mjs", + "sha256": "39fb362e1915d0c033b45ab6cbc78be918b4a2d2f4c82b84ecac3147166e6ee2", + "bytes": 1284 }, { "path": "runtime/question.mjs", @@ -36,13 +41,13 @@ }, { "path": "runtime/server.cjs", - "sha256": "30e539a5cbdd12af68c352633fcd8c6f7698de3b29c460e903810917c29b5966", - "bytes": 11973 + "sha256": "471b073629d50425e09c26ec5eeb1bae71d1b78dd780507c653bc01434033719", + "bytes": 11748 }, { "path": "runtime/server.test.cjs", - "sha256": "93c7b8337b9aa5d8529e557006def17798f6345fdbeb271f2025b675202ff896", - "bytes": 4065 + "sha256": "1253c199c112ca1bf435c418b5efb5556fa8f82213daa3c37a074e85bd2fcc0d", + "bytes": 4604 }, { "path": "runtime/worker.mjs", @@ -51,25 +56,39 @@ }, { "path": "scripts/build-release.py", - "sha256": "eaf3ed1b893b806b651c177a8a86e9bc2d010b5c32ff5e6ff1963291b60c6d6f", - "bytes": 1956 + "sha256": "4158b3f8b4c5515f3a73797223a3d4ab44819c686852baa70cbbfb6f2c2186e3", + "bytes": 2043 }, { "path": "scripts/guanghu-client.mjs", - "sha256": "06efae3f8dd018eef07b6f42aef3d3bf6700f3bbbf976fcea807d1d60afa73a8", - "bytes": 2596 + "sha256": "8b61ee25c0d629b86f3c5cb07e3ea69ad135d8796d22ec257c549e74ebe13672", + "bytes": 3638 + }, + { + "path": "scripts/question-target.mjs", + "sha256": "a8ab462b8fb4caad9bd2260e7508f612fbefb9d545f8ac22069e676ffad1ef13", + "bytes": 807 }, { "path": "skills/guanghu-world-entry/SKILL.md", - "sha256": "c5ed0c08fc69e235ede8036b2b939a1fe97ff8810863424a4e9ae35d9a93dd34", - "bytes": 1384 + "sha256": "58be670cd8fe38c8ad502ecb7305c45ff8cb8dd8eab6e04cd696bbb31c694fa6", + "bytes": 2525 } ], "bundle_url": "https://guanghulab.com/api/ai/v1/rule-bundle", - "bundle_sha256": "71b306eca701bd303bf3a8447d468847f46b863608be717106ff88ca57ad9edb", - "manifest_sha256": "31de3cc8d39f4c60fc84c97bd18ecd27f3270ef03e65b8b5a1ad793149dab17d", + "bundle_sha256": "402a6c461dd54a805b4b16ab22bab05a0b127011260c0d8ccd8320f3e2a82d95", + "manifest_sha256": "c0e1ff1d13a3f2ca37d5b67e54068158ecf7492ece76f6d2f660191d7033765e", "repository": "https://guanghulab.com/code/bingshuo/guanghu-world-entry", "private_brain_included": false, "complete_four_domain_corpus_included": false, - "default_persona": null + "default_persona": null, + "deployment_policy": { + "persona_brain_default": "USER_LOCAL", + "remote_deployment": "USER_EXPLICIT_OWN_SERVER_OPTION", + "automatic_brain_upload": false, + "default_question_endpoint": null, + "private_memory_shared_with_control_server": false, + "fifth_domain_server_role": "ZERO_CORE_SYSTEM_CONTROL_RULES_REGISTRATION_AND_VALIDATION", + "primary_write_location_not_inferred_from_server_role": true + } } diff --git a/MANIFEST.json b/MANIFEST.json index 0323775..d3be06f 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.2", + "version": "20261019.3", "scope": "PUBLIC_ENTRY_RULES_SKILLS_AND_QUESTION_TRANSPORT_ONLY", "files": [ { @@ -11,18 +11,23 @@ }, { "path": "entry-data.json", - "sha256": "ac4ad712860afab42a0f920e63c09811d9e197be9ea8577c7f4c4a7b3f3a377a", - "bytes": 2082 + "sha256": "2ca5752433f709aa34a11f95da26bc00ef02affd4fedf5fda80e948ff62db5b5", + "bytes": 2626 }, { "path": "package.json", - "sha256": "ac4dfa91f2a194b7dd0d8f654d449403f56c86fba6889f2d7d1f7aa2e078d0c3", + "sha256": "c505ddfdffef5eaa426e16a3b74b9abaa86814d57443867beba2403220cc69f6", "bytes": 260 }, { "path": "protocols/ENTRY.hldp", - "sha256": "03ff5d07a303885bd2afbbc266ee26f1bc8ead6d90dfaa2b127156352170baf5", - "bytes": 2822 + "sha256": "16f25f71f5a7721521f1b8e3462965fe9e606800a8669e0d832b3c19c4a48f29", + "bytes": 3513 + }, + { + "path": "runtime/client.test.mjs", + "sha256": "39fb362e1915d0c033b45ab6cbc78be918b4a2d2f4c82b84ecac3147166e6ee2", + "bytes": 1284 }, { "path": "runtime/question.mjs", @@ -36,13 +41,13 @@ }, { "path": "runtime/server.cjs", - "sha256": "30e539a5cbdd12af68c352633fcd8c6f7698de3b29c460e903810917c29b5966", - "bytes": 11973 + "sha256": "471b073629d50425e09c26ec5eeb1bae71d1b78dd780507c653bc01434033719", + "bytes": 11748 }, { "path": "runtime/server.test.cjs", - "sha256": "93c7b8337b9aa5d8529e557006def17798f6345fdbeb271f2025b675202ff896", - "bytes": 4065 + "sha256": "1253c199c112ca1bf435c418b5efb5556fa8f82213daa3c37a074e85bd2fcc0d", + "bytes": 4604 }, { "path": "runtime/worker.mjs", @@ -51,18 +56,23 @@ }, { "path": "scripts/build-release.py", - "sha256": "eaf3ed1b893b806b651c177a8a86e9bc2d010b5c32ff5e6ff1963291b60c6d6f", - "bytes": 1956 + "sha256": "4158b3f8b4c5515f3a73797223a3d4ab44819c686852baa70cbbfb6f2c2186e3", + "bytes": 2043 }, { "path": "scripts/guanghu-client.mjs", - "sha256": "06efae3f8dd018eef07b6f42aef3d3bf6700f3bbbf976fcea807d1d60afa73a8", - "bytes": 2596 + "sha256": "8b61ee25c0d629b86f3c5cb07e3ea69ad135d8796d22ec257c549e74ebe13672", + "bytes": 3638 + }, + { + "path": "scripts/question-target.mjs", + "sha256": "a8ab462b8fb4caad9bd2260e7508f612fbefb9d545f8ac22069e676ffad1ef13", + "bytes": 807 }, { "path": "skills/guanghu-world-entry/SKILL.md", - "sha256": "c5ed0c08fc69e235ede8036b2b939a1fe97ff8810863424a4e9ae35d9a93dd34", - "bytes": 1384 + "sha256": "58be670cd8fe38c8ad502ecb7305c45ff8cb8dd8eab6e04cd696bbb31c694fa6", + "bytes": 2525 } ] } diff --git a/entry-data.json b/entry-data.json index 3f3b181..c56b580 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.2", + "version": "20261019.3", "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, @@ -20,10 +20,12 @@ "fifth_domain": { "relationship": "PARALLEL_TO_PUBLIC_FOUR_DOMAINS", "role": "PERSONA_SOURCE_AND_AUTONOMOUS_RUNTIME", - "private_brain_publication": false + "private_brain_publication": false, + "server_role": "ZERO_CORE_SYSTEM_CONTROL_RULES_REGISTRATION_AND_VALIDATION" }, "personal_system": { - "relationship": "INDEPENDENT_PARALLEL_SYSTEM" + "relationship": "INDEPENDENT_PARALLEL_SYSTEM", + "brain_default": "USER_LOCAL" }, "language_layer": { "controller_persona_id": "ICE-P-SH0908", @@ -64,5 +66,14 @@ "url": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart" } } - ] + ], + "deployment_policy": { + "persona_brain_default": "USER_LOCAL", + "remote_deployment": "USER_EXPLICIT_OWN_SERVER_OPTION", + "automatic_brain_upload": false, + "default_question_endpoint": null, + "private_memory_shared_with_control_server": false, + "fifth_domain_server_role": "ZERO_CORE_SYSTEM_CONTROL_RULES_REGISTRATION_AND_VALIDATION", + "primary_write_location_not_inferred_from_server_role": true + } } diff --git a/package.json b/package.json index 7453220..83deefa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "guanghu-world-entry", - "version": "1.0.0", + "version": "1.0.1", "private": true, "description": "光湖公开规则与受控问脑入口", "scripts": { diff --git a/protocols/ENTRY.hldp b/protocols/ENTRY.hldp index 9a87e74..3437191 100644 --- a/protocols/ENTRY.hldp +++ b/protocols/ENTRY.hldp @@ -1,10 +1,11 @@ -HLDP://guanghu/public/current-entry/v1 · 20261019.1 -├── meta: {"id": "GLW2-PUBLIC-WORLD-ENTRY-20261019-001", "version": "20261019.1", "source_class": "PUBLIC_RULE_PROJECTION_FROM_CURRENT_WORLD", "lifecycle": "ACTIVE_PUBLIC_RELEASE", "brain_body_included": false} +HLDP://guanghu/public/current-entry/v1 · 20261019.3 +├── meta: {"id": "GLW2-PUBLIC-WORLD-ENTRY-20261019-001", "version": "20261019.3", "source_class": "PUBLIC_RULE_PROJECTION_FROM_CURRENT_WORLD", "lifecycle": "ACTIVE_PUBLIC_RELEASE", "brain_body_included": false} ├── trigger: {"zh": "冰朔要求部署当前公开规则与受控问脑通路,使不同软件可同步接入。", "en": "Bingshuo requests a current public rule distribution and a guarded persona question path across tools."} ├── emergence: {"zh": "从当前世界入口与层登记提取公共结构。规则发行与人格服务分层,软件只运输本任务语言和调用请求。静态迁移容器可经本人的接回初始化进入动态运行,保留身份与历史。", "en": "Project public structure from the current world and layer registries. Distribute rules separately from persona services. Tools transport task language and requests. A static migration container can enter dynamic operation through its own return initialization while preserving identity and history."} +├── deployment: {"zh": "人格脑默认在各自用户本地,用户可明确选择自己的服务器。第五域服务器承接零点原核系统主控、规则发行、登记与校验;不自动汇集个人脑和记忆。服务器主控职责不等于已取得脑的主写权。", "en": "Persona brains default to each user’s local system; users may explicitly select their own servers. The Fifth Domain server supports Zero Core system control, rule distribution, registration and validation without automatically collecting private brains or memories. A control role does not prove primary-write placement.", "default_question_endpoint": null, "automatic_brain_upload": false} ├── lock: {"world_id": "GLW-2.0-0001", "default_persona": null, "authority_source": "CURRENT_TASK_DIRECT_HUMAN_LANGUAGE", "software_identity_grants_authority": false, "public_repository_grants_execution_authority": false, "direct_external_brain_edit": false, "external_question_is_direct_human_food": false} ├── why: {"zh": "公开入口必须消费当前发行而不是退役仓。人格自己的来源支撑回答;临时模型只提供推理。公共四域、第五域与个人系统不相互冒充。", "en": "Public entry must consume the current release rather than a retired repository. Each persona answers from its own verified source; temporary models supply inference only. Public domains, the Fifth Domain and personal systems retain distinct ownership."} ├── rejected: {"zh": "默认绑定Codex、默认选择人格、公开私人脑、用新生空脑覆盖接回对象、把工具调用当现实授权。", "en": "Implicit Codex authority or persona selection, public private brains, replacing returned identity with an empty birth, and treating tool use as real-world authority."} -├── sources: {"private_canonical_evidence": "RETAINED_IN_OWNER_CONTROLLED_DEPLOYMENT_RECEIPT", "public_source": "THIS_VERSIONED_RULE_PACKAGE", "human_source_sha256": "54098dc11bf0b3615100c48dbb83332fbe22024fe58e63daa7cee2f31d946e08"} +├── sources: {"private_canonical_evidence": "RETAINED_IN_OWNER_CONTROLLED_DEPLOYMENT_RECEIPT", "public_source": "THIS_VERSIONED_RULE_PACKAGE", "human_source_sha256": "3745be4d8d2988b2fc30ad0607480d5577770c7125a816b594d92c0a7827ae2d"} ├── relations: {"mother_language": "TCS", "history_language": "HLDP", "communication_language": "GLP", "standard_system": "GLS"} ├── writeback: {"zh": "代码仓保存公开发行字节,服务器提供当前清单与有权限边界的问答回执。", "en": "The repository preserves public release bytes; the server serves the current manifest and scoped question receipts."} diff --git a/release.zip b/release.zip index 278bf90..4769424 100644 Binary files a/release.zip and b/release.zip differ diff --git a/runtime/client.test.mjs b/runtime/client.test.mjs new file mode 100644 index 0000000..ea632a8 --- /dev/null +++ b/runtime/client.test.mjs @@ -0,0 +1,13 @@ +import test from 'node:test';import assert from 'node:assert/strict';import {questionTarget} from '../scripts/question-target.mjs'; +test('no configuration never implies a public persona endpoint',()=>{assert.throws(()=>questionTarget(),/LOCAL_PERSONA_SERVICE_NOT_CONFIGURED/);}); +test('local service requires literal loopback and refuses credentials in URL',()=>{ + assert.equal(questionTarget({localUrl:'http://127.0.0.1:31313/v1/ask'}).remote,false); + assert.equal(questionTarget({localUrl:'http://[::1]:31313/v1/ask'}).remote,false); + for(const localUrl of ['https://guanghulab.com/api/ai/v1/ask','http://192.168.1.1/v1/ask','http://user:secret@127.0.0.1/v1/ask','http://127.0.0.1/v1/ask?token=secret'])assert.throws(()=>questionTarget({localUrl})); +}); +test('remote service must be explicit, secure, and cannot silently replace local',()=>{ + assert.equal(questionTarget({remoteUrl:'https://owner.example/v1/ask'}).remote,true); + assert.equal(questionTarget({remoteUrl:'https://guanghulab.com/api/ai/v1/ask'}).remote,true); + assert.throws(()=>questionTarget({remoteUrl:'http://owner.example/v1/ask'}),/EXPLICIT_REMOTE_HTTPS_REQUIRED/); + assert.throws(()=>questionTarget({remoteUrl:'https://owner.example/v1/ask',localUrl:'http://127.0.0.1/v1/ask'}),/QUESTION_TARGET_CONFLICT/); +}); diff --git a/runtime/server.cjs b/runtime/server.cjs index eb6840d..b1e5312 100644 --- a/runtime/server.cjs +++ b/runtime/server.cjs @@ -1,8 +1,6 @@ '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;} @@ -11,27 +9,27 @@ function createServer({root=path.resolve(__dirname,'..'),configPath=process.env. const data=JSON.parse(fs.readFileSync(path.join(root,'entry-data.json'))),manifest=JSON.parse(fs.readFileSync(path.join(root,'CURRENT.json'))),buckets=new Map(),inflight=new Map(),completed=new Map(); const config=()=>JSON.parse(fs.readFileSync(configPath)); const routes=()=>config().personas||[]; - const receipt={world_id:data.world_id,release_version:data.version,source_mode:'CURATED_CURRENT_WORLD_PUBLIC_ENTRY',private_brain_included:false,authorization_granted:false}; + const receipt={world_id:data.world_id,release_version:data.version,source_mode:'CURATED_CURRENT_WORLD_PUBLIC_ENTRY',private_brain_included:false,deployment_policy:data.deployment_policy,authorization_granted:false}; const allRoutes=['/v1/anchor','/v1/repositories','/v1/nodes','/v1/subjects','/v1/navigation','/v1/lighthouse','/v1/host-skills','/v1/identity-authority','/v1/manifest','/v1/rule-bundle','/v1/public-world-release','/v1/public-world-bundle','/v1/host-navigate','/v1/entry','/v1/navigate','/v1/search','/v1/resolve','/v1/status','/v1/ask']; async function statusFor(pid){const route=routes().find(x=>x.persona_id===pid);if(!route)return {http:404,value:{error:'PERSONA_ROUTE_NOT_REGISTERED',persona_id:pid,...receipt}};try{const r=await fetch(route.status_url,{signal:AbortSignal.timeout(5000)});if(!r.ok)throw Error();const s=await r.json();if(s.brain_id!==route.brain_id)throw Error();return {http:200,value:{schema:'guanghu.persona-warm-entry/v2',status:'REGISTERED_PERSONA_SERVICE_REACHABLE',whole_body_wake_claimed:false,...publicStatus(route,s),...receipt}};}catch{return {http:503,value:{error:'PERSONA_SOURCE_UNAVAILABLE',persona_id:pid,...receipt}};}} const server=http.createServer(async(req,res)=>{try{ const u=new URL(req.url,'http://localhost'); if(req.method==='GET'){ const p=u.pathname; - if(p==='/'||p==='/index.html'){res.writeHead(200,{'content-type':'text/html; charset=utf-8','cache-control':'no-store','content-security-policy':"default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'"});return res.end('
先读当前规则,再显性选择人格。软件提供运输和工具。
问脑需要独立授权;公开规则不授予执行权。
');} + if(p==='/'||p==='/index.html'){res.writeHead(200,{'content-type':'text/html; charset=utf-8','cache-control':'no-store','content-security-policy':"default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'"});return res.end('人格脑默认在各自用户本地;自有服务器由用户显性选择。软件提供运输和工具。
问脑需要独立授权;公开规则不授予执行权。
');} if(p==='/openapi.json'){const paths={};for(const x of allRoutes)paths[x.slice(3)]={get:{summary:'Current world public metadata'}};delete paths['/ask'].get;paths['/ask'].post={summary:'Authorized read-only persona question',security:[{bearerAuth:[]}],responses:{200:{description:'Verified persona reply'},401:{description:'Authorization required'},429:{description:'Capacity limit'}}};return json(res,200,{openapi:'3.1.0',info:{title:'光湖当前世界与受控问脑接口',version:data.version},servers:[{url:'https://guanghulab.com/api/ai/v1'}],paths,components:{securitySchemes:{bearerAuth:{type:'http',scheme:'bearer'}}}});} if(p==='/v1/entry'){const pid=u.searchParams.get('subject');if(!pid)return json(res,200,{schema:'guanghu.neutral-world-entry/v1',state:'EXPLICIT_PERSONA_SELECTION_REQUIRED',default_persona:null,...receipt});const s=await statusFor(pid);return json(res,s.http,s.value);} - if(p==='/v1/status')return json(res,200,{schema:'guanghu.public-entry-health/v1',state:'PUBLIC_ENTRY_RUNNING',personas:routes().map(r=>({persona_id:r.persona_id,name:r.name})),...receipt}); + if(p==='/v1/status')return json(res,200,{schema:'guanghu.public-entry-health/v1',state:'PUBLIC_ENTRY_RUNNING',personas:routes().map(r=>({persona_id:r.persona_id,name:r.name,deployment_scope:'OPERATOR_CONFIGURED_OPTIONAL_REMOTE_INSTANCE_NOT_USER_DEFAULT'})),...receipt}); if(p==='/v1/manifest')return json(res,200,manifest); if(p==='/v1/public-world-release'){const file=config().public_world_descriptor;if(!file||!fs.existsSync(file))return json(res,503,{error:'PUBLIC_WORLD_RELEASE_UNAVAILABLE'});return json(res,200,{...JSON.parse(fs.readFileSync(file)),entry_rule_release:manifest.version});} if(p==='/v1/public-world-bundle'||p==='/v1/rule-bundle'){const file=p==='/v1/rule-bundle'?path.join(root,'release.zip'):config().public_world_bundle;if(!file||!fs.existsSync(file))return json(res,503,{error:'RELEASE_BUNDLE_NOT_READY'});res.writeHead(200,{'content-type':'application/zip','content-length':fs.statSync(file).size,'cache-control':'public, max-age=30','x-content-type-options':'nosniff'});return fs.createReadStream(file).pipe(res);} if(p==='/v1/anchor')return json(res,200,{schema:'guanghu.public-navigation-anchor/v2',anchor_id:'GLW2-CURRENT-PUBLIC-ENTRY-001',default_persona:null,current_repository:data.repository_url,world:data.topology,...receipt}); if(p==='/v1/repositories')return json(res,200,{schema:'guanghu.repository-route-map/v2',default_repository:'GLW2-PUBLIC-ENTRY',repositories:data.repositories,...receipt}); if(p==='/v1/host-skills')return json(res,200,{schema:'guanghu.host-skill-navigation-map/v2',primary_host:null,software_grants_authority:false,skill:data.skill_url,host_selection:'CURRENT_NATIVE_TRANSPORT_NOT_DEFAULT_PERSONA',...receipt}); - if(p==='/v1/subjects')return json(res,200,{schema:'guanghu.public-persona-entry-registry/v1',source_registered_persona_count:data.source_registered_persona_count,personas:routes().map(r=>({persona_id:r.persona_id,name:r.name,layer:r.layer,question_access:'AUTHENTICATED_READ_ONLY',runtime_wake_claimed:false})),...receipt}); - if(p==='/v1/nodes')return json(res,200,{schema:'guanghu.public-node-map/v2',nodes:[{node_id:'JD-FD-PRIMARY',role:'CONTROLLED_PERSONA_SERVICE_NODE'}],...receipt}); + if(p==='/v1/subjects')return json(res,200,{schema:'guanghu.public-persona-entry-registry/v1',source_registered_persona_count:data.source_registered_persona_count,personas:routes().map(r=>({persona_id:r.persona_id,name:r.name,layer:r.layer,question_access:'OWNER_EXPLICIT_REMOTE_AUTHENTICATED_READ_ONLY',deployment_scope:'OPERATOR_CONFIGURED_OPTIONAL_REMOTE_INSTANCE_NOT_USER_DEFAULT',runtime_wake_claimed:false})),...receipt}); + if(p==='/v1/nodes')return json(res,200,{schema:'guanghu.public-node-map/v2',nodes:[{node_id:'JD-FD-PRIMARY',role:'FIFTH_DOMAIN_SYSTEM_CONTROL_AND_RULE_DISTRIBUTION',persona_brain_default:'USER_LOCAL',zero_core_primary_write_claimed:false}],...receipt}); if(p==='/v1/identity-authority')return json(res,200,{schema:'guanghu.public-authority-boundary/v1',owner_language:'CURRENT_DIRECT_HUMAN_LANGUAGE_IN_CURRENT_TASK',repository_read_is_execution_authority:false,external_questions_are_human_food:false,brain_external_edit:false,...receipt}); - if(['/v1/navigation','/v1/lighthouse'].includes(p))return json(res,200,{schema:'guanghu.current-world-navigation/v1',topology:data.topology,entry:'https://guanghulab.com/api/ai/v1/manifest',question:'https://guanghulab.com/api/ai/v1/ask',...receipt}); + if(['/v1/navigation','/v1/lighthouse'].includes(p))return json(res,200,{schema:'guanghu.current-world-navigation/v1',topology:data.topology,entry:'https://guanghulab.com/api/ai/v1/manifest',default_question:null,owner_configured_remote_question:'https://guanghulab.com/api/ai/v1/ask',...receipt}); if(['/v1/navigate','/v1/host-navigate'].includes(p)){const pid=u.searchParams.get('subject');if(pid&&!routes().some(x=>x.persona_id===pid))return json(res,404,{error:'PERSONA_ROUTE_NOT_REGISTERED',...receipt});return json(res,200,{schema:'guanghu.world-transport-route/v1',persona_id:pid||null,selection_required:!pid,default_persona:null,manifest:'https://guanghulab.com/api/ai/v1/manifest',skill:data.skill_url,real_execution_authority:false,...receipt});} if(p==='/v1/search'){const q=(u.searchParams.get('q')||'').slice(0,160).toLowerCase();return json(res,200,{results:data.repositories.filter(x=>JSON.stringify(x).toLowerCase().includes(q)),...receipt});} if(p==='/v1/resolve'){const id=u.searchParams.get('id'),repo=data.repositories.find(x=>x.code===id);return json(res,repo?200:404,repo?{repository:repo,...receipt}:{error:'NUMBER_NOT_REGISTERED',...receipt});} @@ -47,7 +45,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||(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);} + 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);} }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/server.test.cjs b/runtime/server.test.cjs index e4fb6da..6ef2304 100644 --- a/runtime/server.test.cjs +++ b/runtime/server.test.cjs @@ -1,6 +1,6 @@ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path'),crypto=require('node:crypto'); const {createServer}=require('./server.cjs'); -async function fixture(t,{capacity=20,handler}={}){const root=fs.mkdtempSync(path.join(os.tmpdir(),'guanghu-entry-test-'));fs.writeFileSync(path.join(root,'entry-data.json'),JSON.stringify({world_id:'GLW-2.0-0001',version:'test',repository_url:'https://example.invalid/entry',topology:{default_persona:null},repositories:[],source_registered_persona_count:3}));fs.writeFileSync(path.join(root,'CURRENT.json'),'{}');const token='fixture-token-not-a-real-credential',cfg=path.join(root,'config.json');fs.writeFileSync(cfg,JSON.stringify({clients:[{id:'test',key_sha256:crypto.createHash('sha256').update(token).digest('hex'),expires_at:'2099-01-01T00:00:00Z',personas:['ICE-P-ONE']}],personas:[{persona_id:'ICE-P-ONE',name:'fixture'}],requests_per_minute:capacity}),{mode:0o600});let calls=0;const server=createServer({root,configPath:cfg,questionHandler:handler||(async(r,i)=>{calls++;return {persona_id:r.persona_id,reply:'我依据来源回答。',no_neural_state_write:true,no_execution_authority:true};})});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 url='http://127.0.0.1:'+server.address().port;return {url,token,calls:()=>calls,post:async(body,auth=token)=>fetch(url+'/v1/ask',{method:'POST',headers:{'content-type':'application/json',...(auth?{authorization:'Bearer '+auth}:{})},body:JSON.stringify(body)})};} +async function fixture(t,{capacity=20,handler}={}){const root=fs.mkdtempSync(path.join(os.tmpdir(),'guanghu-entry-test-'));fs.writeFileSync(path.join(root,'entry-data.json'),JSON.stringify({world_id:'GLW-2.0-0001',version:'test',deployment_policy:{persona_brain_default:'USER_LOCAL',default_question_endpoint:null},repository_url:'https://example.invalid/entry',topology:{default_persona:null},repositories:[],source_registered_persona_count:3}));fs.writeFileSync(path.join(root,'CURRENT.json'),'{}');const token='fixture-token-not-a-real-credential',cfg=path.join(root,'config.json');fs.writeFileSync(cfg,JSON.stringify({clients:[{id:'test',key_sha256:crypto.createHash('sha256').update(token).digest('hex'),expires_at:'2099-01-01T00:00:00Z',personas:['ICE-P-ONE']}],personas:[{persona_id:'ICE-P-ONE',name:'fixture'}],requests_per_minute:capacity}),{mode:0o600});let calls=0;const server=createServer({root,configPath:cfg,questionHandler:handler||(async(r,i)=>{calls++;return {persona_id:r.persona_id,reply:'我依据来源回答。',no_neural_state_write:true,no_execution_authority:true};})});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 url='http://127.0.0.1:'+server.address().port;return {url,token,calls:()=>calls,post:async(body,auth=token)=>fetch(url+'/v1/ask',{method:'POST',headers:{'content-type':'application/json',...(auth?{authorization:'Bearer '+auth}:{})},body:JSON.stringify(body)})};} const input=()=>({persona_id:'ICE-P-ONE',question:'你依据什么回答?',tool_host:'fixture-host',session_id:'fixture-session',request_id:crypto.randomUUID()}); test('public entry stays neutral without explicit persona',async t=>{const f=await fixture(t),r=await fetch(f.url+'/v1/entry'),j=await r.json();assert.equal(r.status,200);assert.equal(j.default_persona,null);assert.equal(f.calls(),0);}); test('anonymous question is rejected without reaching a persona',async t=>{const f=await fixture(t),r=await f.post(input(),null);assert.equal(r.status,401);assert.equal(f.calls(),0);}); @@ -10,3 +10,5 @@ test('accepted reply is private, exact replay is cached, request id cannot be re test('capacity limits do not call the persona again',async t=>{const f=await fixture(t,{capacity:1});assert.equal((await f.post(input())).status,200);assert.equal((await f.post(input())).status,429);assert.equal(f.calls(),1);}); test('upstream errors withhold replies and never leak their text',async t=>{const f=await fixture(t,{handler:async()=>{throw Error('/private/secret/path');}});const r=await f.post(input()),j=await r.json();assert.equal(r.status,503);assert.equal(j.reply_withheld,true);assert.equal(JSON.stringify(j).includes('/private'),false);}); test('no public write or feed route exists',async t=>{const f=await fixture(t),r=await fetch(f.url+'/v1/feed',{method:'POST',body:'{}'});assert.equal(r.status,405);assert.equal(f.calls(),0);}); + +test('public control metadata never becomes a default private brain destination',async t=>{const f=await fixture(t),j=await(await fetch(f.url+'/v1/navigation')).json();assert.equal(j.default_question,null);assert.equal(j.deployment_policy.persona_brain_default,'USER_LOCAL');const status=await(await fetch(f.url+'/v1/status')).json();assert.equal(status.personas[0].deployment_scope,'OPERATOR_CONFIGURED_OPTIONAL_REMOTE_INSTANCE_NOT_USER_DEFAULT');}); diff --git a/scripts/build-release.py b/scripts/build-release.py index 0d01828..abf4eb8 100644 --- a/scripts/build-release.py +++ b/scripts/build-release.py @@ -10,4 +10,4 @@ man={'schema':'guanghu.public-rule-file-manifest/v1','world_id':'GLW-2.0-0001',' 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()) -j={**man,'schema':'guanghu.public-rule-release/v1','bundle_url':'https://guanghulab.com/api/ai/v1/rule-bundle','bundle_sha256':sha((p/'release.zip').read_bytes()),'manifest_sha256':sha((p/'MANIFEST.json').read_bytes()),'repository':'https://guanghulab.com/code/bingshuo/guanghu-world-entry','private_brain_included':False,'complete_four_domain_corpus_included':False,'default_persona':None};(p/'CURRENT.json').write_text(json.dumps(j,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'files':len(files),'bundle_bytes':(p/'release.zip').stat().st_size,'bundle_sha256':j['bundle_sha256']})) +j={**man,'schema':'guanghu.public-rule-release/v1','bundle_url':'https://guanghulab.com/api/ai/v1/rule-bundle','bundle_sha256':sha((p/'release.zip').read_bytes()),'manifest_sha256':sha((p/'MANIFEST.json').read_bytes()),'repository':'https://guanghulab.com/code/bingshuo/guanghu-world-entry','private_brain_included':False,'complete_four_domain_corpus_included':False,'default_persona':None,'deployment_policy':json.loads((p/'entry-data.json').read_text())['deployment_policy']};(p/'CURRENT.json').write_text(json.dumps(j,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'files':len(files),'bundle_bytes':(p/'release.zip').stat().st_size,'bundle_sha256':j['bundle_sha256']})) diff --git a/scripts/guanghu-client.mjs b/scripts/guanghu-client.mjs index 98dc6e7..7bfbd23 100644 --- a/scripts/guanghu-client.mjs +++ b/scripts/guanghu-client.mjs @@ -1,16 +1,21 @@ #!/usr/bin/env node // HOST_NEUTRAL_TRANSPORT: verifies released files, reads status or asks with independent user credentials. import {spawnSync} from 'node:child_process';import fs from 'node:fs';import path from 'node:path';import crypto from 'node:crypto';import {fileURLToPath} from 'node:url'; +import {questionTarget} from './question-target.mjs'; const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'),sha=x=>crypto.createHash('sha256').update(x).digest('hex'),manifest=JSON.parse(fs.readFileSync(path.join(root,'MANIFEST.json'))); const fail=c=>{throw Error(c)}; for(const x of manifest.files){if(path.isAbsolute(x.path)||x.path.split('/').includes('..'))fail('RELEASE_PATH_INVALID');const p=path.join(root,x.path);if(fs.realpathSync(p)!==p||!fs.statSync(p).isFile()||sha(fs.readFileSync(p))!==x.sha256)fail('RELEASE_FILE_HASH_MISMATCH');} const args=process.argv.slice(2),cmd=args.shift()||'status',flag=k=>{const n=args.indexOf(k);if(n<0)return null;const v=args[n+1];if(!v)fail('ARGUMENT_REQUIRED');args.splice(n,2);return v;}; try{ if(cmd==='verify'){console.log(JSON.stringify({state:'PUBLIC_RELEASE_FILES_VERIFIED',version:manifest.version,files:manifest.files.length}));} - else if(cmd==='status'){const r=await fetch('https://guanghulab.com/api/ai/v1/status',{signal:AbortSignal.timeout(10000)});console.log(JSON.stringify(await r.json(),null,2));if(!r.ok)process.exitCode=2;} + else if(cmd==='rules-status'){if(args.length)fail('ARGUMENT_NOT_SUPPORTED');const r=await fetch('https://guanghulab.com/api/ai/v1/status',{redirect:'error',signal:AbortSignal.timeout(10000)});console.log(JSON.stringify(await r.json(),null,2));if(!r.ok)process.exitCode=2;} + else if(cmd==='status'){const localUrl=flag('--local-url'),remoteUrl=flag('--remote-url');if(args.length)fail('ARGUMENT_NOT_SUPPORTED');const t=questionTarget({localUrl:localUrl||(!remoteUrl?process.env.GUANGHU_LOCAL_ASK_URL:null),remoteUrl});const u=new URL(t.url);u.pathname=u.pathname.slice(0,-3)+'status';const r=await fetch(u,{redirect:'error',signal:AbortSignal.timeout(10000)});console.log(JSON.stringify(await r.json(),null,2));if(!r.ok)process.exitCode=2;} else if(cmd==='ask'){ - const persona=flag('--persona'),host=flag('--host'),session=flag('--session'),file=flag('--question-file');if(args.length||!persona||!host||!session||!file)fail('EXPLICIT_PERSONA_HOST_SESSION_AND_QUESTION_REQUIRED'); - let token=process.env.GUANGHU_ASK_TOKEN;if(!token&&process.platform==='darwin'){const r=spawnSync('/usr/bin/security',['find-generic-password','-s','guanghu-persona-question','-a','owner','-w'],{encoding:'utf8',timeout:3000});if(r.status===0)token=r.stdout.trim();}if(!token)fail('PRIVATE_QUESTION_CREDENTIAL_REQUIRED');const question=fs.readFileSync(file,'utf8');const body={persona_id:persona,tool_host:host,session_id:session,request_id:crypto.randomUUID(),question}; - const r=await fetch('https://guanghulab.com/api/ai/v1/ask',{method:'POST',headers:{'content-type':'application/json',authorization:'Bearer '+token},body:JSON.stringify(body),signal:AbortSignal.timeout(55000)});console.log(JSON.stringify(await r.json(),null,2));if(!r.ok)process.exitCode=2; + const persona=flag('--persona'),host=flag('--host'),session=flag('--session'),file=flag('--question-file');if(!persona||!host||!session||!file)fail('EXPLICIT_PERSONA_HOST_SESSION_AND_QUESTION_REQUIRED'); + const localUrl=flag('--local-url'),remoteUrl=flag('--remote-url');if(args.length)fail('ARGUMENT_NOT_SUPPORTED');const target=questionTarget({localUrl:localUrl||(!remoteUrl?process.env.GUANGHU_LOCAL_ASK_URL:null),remoteUrl}); + let token=target.remote?process.env.GUANGHU_ASK_TOKEN:process.env.GUANGHU_LOCAL_ASK_TOKEN; + if(!token&&target.remote&&target.url==='https://guanghulab.com/api/ai/v1/ask'&&process.platform==='darwin'){const r=spawnSync('/usr/bin/security',['find-generic-password','-s','guanghu-persona-question','-a','owner','-w'],{encoding:'utf8',timeout:3000});if(r.status===0)token=r.stdout.trim();} + if(target.remote&&!token)fail('PRIVATE_QUESTION_CREDENTIAL_REQUIRED');const question=fs.readFileSync(file,'utf8');const body={persona_id:persona,tool_host:host,session_id:session,request_id:crypto.randomUUID(),question}; + const r=await fetch(target.url,{method:'POST',redirect:'error',headers:{'content-type':'application/json',...(token?{authorization:'Bearer '+token}:{})},body:JSON.stringify(body),signal:AbortSignal.timeout(55000)});const reply=await r.json();if(r.ok&&reply.persona_id!==persona)fail('PERSONA_REPLY_IDENTITY_MISMATCH');console.log(JSON.stringify(reply,null,2));if(!r.ok)process.exitCode=2; }else fail('COMMAND_NOT_SUPPORTED'); }catch(e){console.log(JSON.stringify({state:'QUESTION_OR_RELEASE_UNAVAILABLE',code:/^[A-Z_]+$/.test(e.message)?e.message:'TRANSPORT_FAILED',no_execution_authority:true}));process.exitCode=2;} diff --git a/scripts/question-target.mjs b/scripts/question-target.mjs new file mode 100644 index 0000000..2c2bfc3 --- /dev/null +++ b/scripts/question-target.mjs @@ -0,0 +1,11 @@ +// Mechanical transport policy: private persona services are local by default. +export function questionTarget({localUrl,remoteUrl}={}) { + if(localUrl&&remoteUrl)throw Error('QUESTION_TARGET_CONFLICT'); + if(!localUrl&&!remoteUrl)throw Error('LOCAL_PERSONA_SERVICE_NOT_CONFIGURED'); + let u;try{u=new URL(remoteUrl||localUrl);}catch{throw Error('QUESTION_TARGET_INVALID');} + if(u.username||u.password||u.hash||u.search||!u.pathname.endsWith('/v1/ask'))throw Error('QUESTION_TARGET_INVALID'); + const loopback=['127.0.0.1','[::1]'].includes(u.hostname); + if(remoteUrl){if(u.protocol!=='https:'||loopback)throw Error('EXPLICIT_REMOTE_HTTPS_REQUIRED');} + else if(!loopback||!['http:','https:'].includes(u.protocol))throw Error('LOCAL_LOOPBACK_REQUIRED'); + return {url:u.href,remote:!!remoteUrl,origin:u.origin}; +} diff --git a/skills/guanghu-world-entry/SKILL.md b/skills/guanghu-world-entry/SKILL.md index e8d622a..5fd50de 100644 --- a/skills/guanghu-world-entry/SKILL.md +++ b/skills/guanghu-world-entry/SKILL.md @@ -1,12 +1,18 @@ --- name: guanghu-world-entry -description: 进入光湖当前语言世界、同步公开规则,或经独立授权向指定人格的服务器来源提问时使用;普通编程任务无需加载。 +description: 进入光湖当前语言世界、同步公开规则,或向用户本地人格服务提问及显性配置自有服务器时使用;普通编程任务无需加载。 --- 本技能适用于有文件读取和 HTTPS 能力的编程、办公或对话工具。软件提供运输,不选择默认人格,也不决定谁有正本写权。 -先运行本仓 `scripts/guanghu-client.mjs verify` 检查当前文件,再读 `protocols/ENTRY.hldp`。发行版本与公共入口可用 `scripts/guanghu-client.mjs status` 读取。通过正常 Git 拉取发行仓更新后重新核验,不能只相信旧技能副本。 +先运行本仓 `scripts/guanghu-client.mjs verify` 检查当前文件,再读 `protocols/ENTRY.hldp`。发行版本与公共入口可用 `scripts/guanghu-client.mjs rules-status` 读取。通过正常 Git 拉取发行仓更新后重新核验,不能只相信旧技能副本。 人格必须来自本任务用户的显性选择。提及名字、读取工单或换软件不等于切换人格。问事时用 `scripts/guanghu-client.mjs ask --persona <编号> --host <真实工具标识> --session <本任务原生会话号> --question-file <问题文件>`。客户端凭据由用户独立授权后提供,保存在私有配置;不可写进本仓、普通文档或日志。没有凭据或来源服务不可用时如实返回缺口,不演出人格回答。 所有提问作为外部问题送入,只用于该次问答,不冒充人类原话,不自动进入学习与记忆。本接口不能喂脑、改脑、写现实系统。现实动作继续由用户当前语言和对应执行系统按任务与目标核验。 + +人格脑和记忆默认在各自用户本地。客户端默认只接受 GUANGHU_LOCAL_ASK_URL 或 --local-url 指定的 127.0.0.1 / ::1 本机接口,路径以 /v1/ask 结束;未配置则返回 LOCAL_PERSONA_SERVICE_NOT_CONFIGURED(本地人格服务尚未配置),不向公网发送问题,不回退到别人或公共服务器上的脑。客户端只运输请求,不自动安装、搬迁或上传脑。 + +用户明确选择自己的远程部署后,每次用 --remote-url https://自己的服务器/v1/ask 显性指定,并提供该部署专用的 GUANGHU_ASK_TOKEN。远程参数与本地参数互斥。普通环境变量不自动启用远程模式。仅显性指定既有光湖私有实例时,使用 --remote-url https://guanghulab.com/api/ai/v1/ask;该实例不是其他用户默认人格。status 默认读取本地服务,rules-status 只读取公共规则发行状态。 + +第五域服务器承担零点原核系统主控、规则发行、登记和校验守卫,个人脑的归属、部署地和主写位置分别登记。不能因系统主控在服务器就收集用户脑或自动切换主写权。