A replaced installed harness answers null for countsAccount until the bus restarts and reads the new module. A runtime test pins the startBroker wiring for claude-code (covered) and pi (refused). The plugin README says countsAccount also decides authority coverage and is read once at start.
305 lines
27 KiB
JavaScript
305 lines
27 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { checkManifest } from '../src/manifest.mjs';
|
|
import { pluginHash, readPluginLock, verifiedPlugin } from '../src/pinned.mjs';
|
|
import { installPlugin, removePlugin } from '../src/install.mjs';
|
|
import { createLoader } from '../src/loader.mjs';
|
|
import { organizationPlugins, assertPluginCapabilities } from '../src/organization.mjs';
|
|
import { pluginService } from '../src/tools.mjs';
|
|
import { typedTools } from '../../harness/src/tools.mjs';
|
|
import { resolveInstance } from '../../organization/src/resolve.mjs';
|
|
import { pluginInstallCommand } from '../../cli/src/plugin-install.mjs';
|
|
import { pluginSettings } from '../../organization/src/plugin-settings.mjs';
|
|
import { hasModule } from '../src/loader.mjs';
|
|
import { businessDoc, rolesCopy, writeJson } from '../../organization/tests/helpers.mjs';
|
|
const SECRET='sk-test-NOT-A-SECRET-1234';
|
|
function fixture(t) {
|
|
const root=mkdtempSync(join(tmpdir(),'pl1-')),src=join(root,'src'),dataRoot=join(root,'data');
|
|
mkdirSync(src);mkdirSync(dataRoot);mkdirSync(join(src,'usage-source'));mkdirSync(join(src,'tools'));
|
|
t.after(()=>{const thaw=p=>{const st=lstatSync(p);if(st.isSymbolicLink())return;if(st.isDirectory()){chmodSync(p,0o700);for(const n of readdirSync(p))thaw(join(p,n));}};thaw(root);rmSync(root,{recursive:true,force:true});});
|
|
writeFileSync(join(src,'usage-source','index.mjs'),'export const interfaceVersion=1; export const readTurn=()=>"fixture";');
|
|
writeFileSync(join(src,'tools','echo.mjs'),`export const execute=(args)=>({answer:args.text}); // ${SECRET}`);
|
|
const manifest={name:'acmeplug',version:'1.0.0',kinds:['usage-source'],interfaceVersions:{'usage-source':1},sha256:'0'.repeat(64),source:{path:'fixture',version:'1.0.0'},signature:null,tools:[{name:'acmeplug.echo',description:SECRET,arguments:{type:'object',properties:{text:{type:'string'}},required:['text'],additionalProperties:false},runs:'host',entry:'tools/echo.mjs'}],actions:['acmeplug.publish'],services:[{name:'acmeplug.account',scopes:['read']}],roleTemplates:[{name:'acmeplug.writer',file:'writer.json'}]};
|
|
writeFileSync(join(src,'writer.json'),'{}');
|
|
const seal=()=>{writeFileSync(join(src,'plugin.json'),JSON.stringify(manifest));manifest.sha256=pluginHash(src);writeFileSync(join(src,'plugin.json'),JSON.stringify(manifest));return manifest.sha256;};
|
|
const hash=seal();const install=(source=src,extra={})=>installPlugin({dataRoot,source,expectedHash:hash,...extra});
|
|
return {root,src,dataRoot,manifest,seal,hash,install};
|
|
}
|
|
const refused=code=>e=>e.code===code && !e.message.includes(SECRET);
|
|
test('PL1 strict fields, kinds, core names, prefixes, hard waits and traversal refuse without echo',t=>{
|
|
const f=fixture(t);
|
|
for(const mutate of [m=>m.extra=SECRET,m=>m.tools[0].extra=SECRET,m=>m.kinds=['unknown'],m=>m.tools[0].name='read',m=>m.tools[0].name='acmeplug.read',m=>m.actions=['acmeplug.deploy'],m=>m.actions=['acmeplug.git.push.remote'],m=>m.roleTemplates[0].file='../file',m=>m.tools[0].arguments.$ref=SECRET,m=>m.name='git']){
|
|
const m=structuredClone(f.manifest);mutate(m);assert.throws(()=>checkManifest(m),e=>!e.message.includes(SECRET));
|
|
}
|
|
});
|
|
test('PL1 directory install is pinned and read-only; host tools printed before any instance write',t=>{
|
|
const f=fixture(t),printed=[];const entry=f.install(f.src,{print:line=>{assert.deepEqual(readPluginLock(f.dataRoot).plugins,{});assert.equal(existsSync(join(f.dataRoot,'plugins')),false);printed.push(line);}});
|
|
assert.equal(entry.sha256,f.hash);assert.deepEqual(printed,['host tool: acmeplug.echo']);
|
|
for(const p of ['plugins/acmeplug','plugins/acmeplug/tools'])assert.equal(lstatSync(join(f.dataRoot,p)).mode&0o777,0o555);
|
|
assert.equal(lstatSync(join(f.dataRoot,'plugins/acmeplug/plugin.json')).mode&0o777,0o444);
|
|
assert.equal(verifiedPlugin(f.dataRoot,'acmeplug').manifest.sha256,f.hash);
|
|
});
|
|
test('PL1 mismatch and symlink sources refuse before instance writes',t=>{
|
|
const f=fixture(t);assert.throws(()=>f.install(f.src,{expectedHash:'f'.repeat(64)}),refused('hash-mismatch'));
|
|
symlinkSync('/etc/passwd',join(f.src,'escape'));assert.throws(()=>f.install(),refused('unsafe-path'));
|
|
assert.equal(existsSync(join(f.dataRoot,'plugins')),false);
|
|
});
|
|
test('PL1 zip traversal, symlinks, and git URL without a full commit refuse',t=>{
|
|
const f=fixture(t);const zip=join(f.root,'bad.zip');
|
|
for(const script of ["z.writestr('../escape','bad')","e=zipfile.ZipInfo('link');e.create_system=3;e.external_attr=(0o120777<<16);z.writestr(e,'/etc/passwd')"]){
|
|
assert.equal(spawnSync('python3',['-c',`import zipfile\nz=zipfile.ZipFile(${JSON.stringify(zip)},'w')\n${script}\nz.close()`]).status,0);
|
|
assert.throws(()=>f.install(zip),refused('source-refused'));
|
|
}
|
|
assert.throws(()=>f.install('https://example.invalid/repo.git'),refused('commit-required'));
|
|
assert.throws(()=>f.install(f.src,{commit:'abc'}),refused('commit-required'));
|
|
});
|
|
test('PL1 directory, generated zip and local bare git at full commit land the same hash',t=>{
|
|
const f=fixture(t);const zip=join(f.root,'source.zip');
|
|
assert.equal(spawnSync('python3',['-c',`import zipfile,os\nz=zipfile.ZipFile(${JSON.stringify(zip)},'w')\nfor root,dirs,files in os.walk(${JSON.stringify(f.src)}):\n for n in files:z.write(os.path.join(root,n),os.path.relpath(os.path.join(root,n),${JSON.stringify(f.src)}))\nz.close()`]).status,0);
|
|
const git=(args)=>{const r=spawnSync('git',args,{encoding:'utf8'});assert.equal(r.status,0,r.stderr);return r.stdout.trim();};
|
|
git(['init',f.src]);git(['-C',f.src,'add','.']);git(['-C',f.src,'-c','user.name=Fixture','-c','[email protected]','commit','-m','fixture']);
|
|
const commit=git(['-C',f.src,'rev-parse','HEAD']),bare=join(f.root,'bare.git');git(['clone','--bare',f.src,bare]);
|
|
// The local directory used for install intentionally has no .git administrative files.
|
|
rmSync(join(f.src,'.git'),{recursive:true});
|
|
for(const [source,opts]of [[f.src,{}],[zip,{}],[bare,{commit}]]){
|
|
const d=mkdtempSync(join(f.root,'instance-'));const r=installPlugin({dataRoot:d,source,expectedHash:f.hash,...opts});assert.equal(r.sha256,f.hash);assert.equal(readPluginLock(d).plugins.acmeplug.sha256,f.hash);
|
|
}
|
|
});
|
|
test('PL1 changed file/missing lock refuses next load and evicts installed code, core keeps running',async t=>{
|
|
const f=fixture(t);f.install();const logs=[],loader=createLoader({dataRoot:f.dataRoot,log:e=>logs.push(e)});
|
|
const settings={'usage-source':['transcript','acmeplug'],harness:['pi']};assert.equal((await loader.load(settings)).refused.length,0);
|
|
const file=join(f.dataRoot,'plugins/acmeplug/tools/echo.mjs');chmodSync(file,0o600);writeFileSync(file,SECRET);
|
|
const result=await loader.load(settings);assert.equal(result.refused[0].code,'hash-mismatch');assert.deepEqual(loader.loaded()['usage-source'],['transcript']);assert.deepEqual(loader.loaded().harness,['pi']);
|
|
assert.ok(!JSON.stringify(logs).includes(SECRET));
|
|
rmSync(join(f.dataRoot,'plugins.lock.json'));assert.equal((await loader.load(settings)).refused[0].code,'missing-lock');
|
|
});
|
|
test('PL1 registry includes only enabled verified declarations, services and templates',t=>{
|
|
const f=fixture(t);f.install();const view=organizationPlugins(f.dataRoot,{'usage-source':['acmeplug']});
|
|
assert.ok(view.registry.tools.includes('acmeplug.echo'));assert.ok(view.registry.routed.includes('acmeplug.publish'));assert.deepEqual(view.registry.services['acmeplug.account'].scopes,['read']);assert.equal(view.roleTemplates[0].name,'acmeplug.writer');
|
|
for(const settings of [{},{'usage-source':['missing']}])assert.throws(()=>assertPluginCapabilities({tools:['acmeplug.echo']},organizationPlugins(f.dataRoot,settings)),e=>e.code==='plugin-disabled' && e.plugin==='acmeplug' && !e.message.includes(SECRET));
|
|
});
|
|
test('PL1 disabled or missing tools/actions refuse at launch with name only, enabled tool is bundled',t=>{
|
|
const f=fixture(t);f.install();const definition={name:'writer',contractPath:'/contract',tools:['acmeplug.echo'],network:'none',authority:{withinRole:['acmeplug.publish'],crossRole:[]},credentials:[],messaging:null,skills:[]};
|
|
const business={id:'acme',roles:{writer:{definition:'writer',vars:{},credentials:{}}},definitions:{writer:definition},vars:{},arbiters:{}};
|
|
const args={system:{dataRoot:f.dataRoot},business,instance:'writer',enforcePlugins:true};
|
|
assert.throws(()=>resolveInstance(args),e=>e.code==='plugin-disabled' && e.plugin==='acmeplug');
|
|
business.plugins={'usage-source':['acmeplug']};const r=resolveInstance(args);assert.equal(typedTools({resolved:r}).find(t=>t.name==='acmeplug.echo').pluginTool,'acmeplug.echo');
|
|
business.plugins={};assert.throws(()=>resolveInstance(args),refused('plugin-disabled'));
|
|
});
|
|
test('PL1 running tool calls recheck enabled setting, pin, arguments and held tool',async t=>{
|
|
const f=fixture(t);f.install();const role={tools:['acmeplug.echo'],authority:{withinRole:[],crossRole:[]}};let enabled=true;
|
|
const service=pluginService({dataRoot:f.dataRoot,read:()=>({plugins:enabled?{'usage-source':['acmeplug']}:{},roles:{writer:role}})});
|
|
assert.deepEqual(await service.call('acme','writer','acmeplug.echo',{text:'hello'}),{answer:'hello'});
|
|
await assert.rejects(service.call('acme','writer','acmeplug.echo',{unexpected:SECRET}),refused('invalid-request'));
|
|
enabled=false;await assert.rejects(service.call('acme','writer','acmeplug.echo',{text:SECRET}),e=>e.code==='plugin-disabled' && e.plugin==='acmeplug' && !e.message.includes(SECRET));
|
|
});
|
|
test('PL1 remove refuses enabled organizations; host CLI rejects organization/tester options and agents',async t=>{
|
|
const f=fixture(t);f.install();assert.throws(()=>removePlugin({dataRoot:f.dataRoot,name:'acmeplug',enabled:()=>true}),refused('plugin-enabled'));
|
|
const dir=join(f.root,'config');mkdirSync(dir,{mode:0o700});let out='';const io={env:{},stdout:{write:v=>out+=v}};
|
|
for(const args of [['install',f.src,'--sha256',f.hash,'--org','acme'],['install',f.src,'--sha256',f.hash,'--tester']])await assert.rejects(pluginInstallCommand(args,io,{system:{dataRoot:f.dataRoot},dir}));
|
|
await assert.rejects(pluginInstallCommand(['list'],{...io,env:{MOSAIC_RUN_ID:'fixture'}},{system:{dataRoot:f.dataRoot},dir}));
|
|
writeJson(join(dir,'businesses','acme.json'),{plugins:{'usage-source':['acmeplug']}});
|
|
await assert.rejects(pluginInstallCommand(['remove','acmeplug'],io,{system:{dataRoot:f.dataRoot},dir}),e=>e.message.includes('plugin-enabled'));
|
|
});
|
|
test('PL1 enabling a tampered installed plugin refuses before organization write',t=>{
|
|
const f=fixture(t);f.install();const doc=businessDoc(f.root),rolesDir=rolesCopy(f.root);writeJson(join(f.root,'businesses','acme.json'),doc);
|
|
const settings=pluginSettings({dir:f.root,rolesDir,dataRoot:f.dataRoot,exists:(k,n)=>hasModule(k,n,f.dataRoot)});
|
|
const before=settings.read('acme');const file=join(f.dataRoot,'plugins/acmeplug/tools/echo.mjs');chmodSync(file,0o600);writeFileSync(file,SECRET);
|
|
assert.throws(()=>settings.set('acme',{revision:before.revision,plugins:{'usage-source':['acmeplug']}},'jason'),refused('invalid-plugins'));
|
|
assert.deepEqual(settings.read('acme'),before);
|
|
});
|
|
|
|
test('PL1 sandbox tools execute with no home or network namespace access',async t=>{
|
|
const f=fixture(t);f.manifest.tools[0].runs='sandbox';
|
|
writeFileSync(join(f.src,'tools/echo.mjs'),`import {existsSync} from 'node:fs';export const execute=()=>({home:existsSync('/home'),config:existsSync('/mnt/storage')});`);
|
|
const hash=f.seal();f.install(f.src,{expectedHash:hash});
|
|
const service=pluginService({dataRoot:f.dataRoot,read:()=>({plugins:{'usage-source':['acmeplug']},roles:{writer:{tools:['acmeplug.echo'],authority:{withinRole:[],crossRole:[]}}}})});
|
|
assert.deepEqual(await service.call('acme','writer','acmeplug.echo',{text:'hello'}),{home:false,config:false});
|
|
});
|
|
|
|
test('PL1 broker launch and running dispatch enforce enable state; organization install and member/agent enable refuse',async t=>{
|
|
const {Store}=await import('../../bus/src/store.mjs');const {Broker}=await import('../../bus/src/broker.mjs');
|
|
const f=fixture(t);f.install();const authority={withinRole:[],crossRole:[]};
|
|
const business={id:'acme',human:'owner',plugins:{'usage-source':['acmeplug']},arbiters:{technical:'chief',delivery:'chief'},roles:{chief:{authority},writer:{tools:['acmeplug.echo'],authority}}};
|
|
const service=pluginService({dataRoot:f.dataRoot,read:()=>business});const store=new Store(f.dataRoot);t.after(()=>store.close());
|
|
const b=new Broker({store,businesses:{acme:business},pluginService:service});
|
|
const cap=b.bindLaunch({business:'acme',role:'writer',run:'writer-1',harness:'pi'});b.request(cap,{verb:'role.claim'});
|
|
assert.deepEqual(await b.request(cap,{verb:'plugin.tool',args:{tool:'acmeplug.echo',arguments:{text:'working'}}}),{answer:'working'});
|
|
const member=b.bindReader({business:'acme'});
|
|
for(const c of [cap,member])assert.throws(()=>b.request(c,{verb:'plugins.set',args:{revision:'a'.repeat(64),plugins:{'usage-source':['acmeplug']}}}),/human-required/);
|
|
assert.throws(()=>b.request(cap,{verb:'plugins.install',args:{source:SECRET}}),e=>!e.message.includes(SECRET));
|
|
business.plugins={};
|
|
await assert.rejects(b.request(cap,{verb:'plugin.tool',args:{tool:'acmeplug.echo',arguments:{text:SECRET}}}),e=>e.code==='plugin-disabled' && e.reason==='acmeplug' && !e.message.includes(SECRET));
|
|
assert.throws(()=>b.bindLaunch({business:'acme',role:'writer',run:'writer-2',harness:'pi'}),e=>e.code==='plugin-disabled' && e.reason==='acmeplug');
|
|
assert.equal(b.bindLaunch({business:'acme',role:'chief',run:'chief-1',harness:'pi'}).length,64,'core can still launch');
|
|
});
|
|
|
|
test('PL1 registry preserves capability categories and credential scope ceilings',t=>{
|
|
const f=fixture(t);f.install();const view=organizationPlugins(f.dataRoot,{'usage-source':['acmeplug']});
|
|
for(const role of [{tools:['acmeplug.publish']},{authority:{withinRole:['acmeplug.echo']}},{credentials:[{service:'acmeplug.account',scopes:['admin']}]}])assert.throws(()=>assertPluginCapabilities(role,view),refused('invalid-request'));
|
|
});
|
|
|
|
test('PL1 manifest whitespace is pinned; remove succeeds after the owner disables it',t=>{
|
|
const f=fixture(t);f.install();const file=join(f.dataRoot,'plugins/acmeplug/plugin.json');chmodSync(file,0o600);writeFileSync(file,readFileSync(file,'utf8')+'\n');
|
|
assert.throws(()=>verifiedPlugin(f.dataRoot,'acmeplug'),refused('hash-mismatch'));
|
|
removePlugin({dataRoot:f.dataRoot,name:'acmeplug',enabled:()=>false});
|
|
assert.deepEqual(readPluginLock(f.dataRoot).plugins,{});assert.equal(existsSync(join(f.dataRoot,'plugins/acmeplug')),false);
|
|
});
|
|
|
|
test('PL1 CLI host install/list/remove never calls a bus transport',async t=>{
|
|
const {main}=await import('../../cli/src/cli.mjs');const f=fixture(t),dir=join(f.root,'config');mkdirSync(dir,{mode:0o700});
|
|
let output='';const io={env:{},stdout:{write:v=>output+=v},stderr:{write:()=>{}}};const deps={system:{dataRoot:f.dataRoot},accountsConfigDir:dir,transport:()=>{throw Error('bus must not be used');}};
|
|
await main(['plugins','install',f.src,'--sha256',f.hash],io,deps);assert.match(output,/host tool: acmeplug.echo/);assert.ok(!output.includes(SECRET));
|
|
output='';await main(['plugins','list'],io,deps);assert.equal(JSON.parse(output).plugins.acmeplug.sha256,f.hash);
|
|
output='';await main(['plugins','remove','acmeplug'],io,deps);assert.deepEqual(JSON.parse(output),{removed:'acmeplug'});
|
|
});
|
|
|
|
test('PL1 a running plugin tool obeys a newly narrowed tool ceiling',async t=>{
|
|
const f=fixture(t);f.install();const b={plugins:{'usage-source':['acmeplug']},roles:{writer:{tools:['acmeplug.echo'],authority:{withinRole:[],crossRole:[]},vars:{}}}};
|
|
const service=pluginService({dataRoot:f.dataRoot,read:()=>b});assert.deepEqual(await service.call('acme','writer','acmeplug.echo',{text:'before'}),{answer:'before'});
|
|
b.roles.writer.vars['limits.tools']=['read'];await assert.rejects(service.call('acme','writer','acmeplug.echo',{text:'after'}),refused('not-authorized'));
|
|
});
|
|
|
|
test('PL1 an installed package cannot reuse a core module name from another kind',t=>{
|
|
const f=fixture(t);f.manifest.name='pi';for(const tool of f.manifest.tools)tool.name=tool.name.replace('acmeplug.','pi.');f.manifest.actions=['pi.publish'];f.manifest.services[0].name='pi.account';f.manifest.roleTemplates[0].name='pi.writer';
|
|
const hash=f.seal();assert.throws(()=>f.install(f.src,{expectedHash:hash}),refused('core-name'));
|
|
assert.equal(existsSync(join(f.dataRoot,'plugins')),false);
|
|
});
|
|
|
|
test('PL1 malformed settings cannot retain an installed module whose pin is now bad',async t=>{
|
|
const f=fixture(t);f.install();const loader=createLoader({dataRoot:f.dataRoot,log:()=>{}});await loader.load({'usage-source':['transcript','acmeplug'],harness:['pi']});
|
|
const file=join(f.dataRoot,'plugins/acmeplug/tools/echo.mjs');chmodSync(file,0o600);writeFileSync(file,SECRET);
|
|
await loader.load(null);assert.deepEqual(loader.loaded()['usage-source'],['transcript']);assert.deepEqual(loader.loaded().harness,['pi']);
|
|
});
|
|
|
|
test('PL1 runtime discovers installed harness levels from the instance root',async t=>{
|
|
const {harnessLevels}=await import('../../bus/src/runtime.mjs');
|
|
const {KINDS}=await import('../src/kinds.mjs');
|
|
const f=fixture(t);f.manifest.kinds=['harness'];f.manifest.interfaceVersions={harness:1};
|
|
mkdirSync(join(f.src,'harness'));
|
|
writeFileSync(join(f.src,'harness/index.mjs'),KINDS.harness.exports.map(name=>`export function ${name}(){return ${name==='levels'?"['high']":name==='countsAccount'?'true':'null'};}`).join('\n')+'\nexport const interfaceVersion=1;');
|
|
const hash=f.seal();f.install(f.src,{expectedHash:hash});
|
|
const levels=await harnessLevels({dataRoot:f.dataRoot,log:()=>{}});
|
|
assert.deepEqual(levels('acmeplug'),['high']);assert.ok(levels.names.includes('pi'));
|
|
// #1873: the module's countsAccount answer, until the lock pins another package for that name.
|
|
assert.equal(levels.countsAccount('acmeplug'),true);
|
|
const lock=join(f.dataRoot,'plugins.lock.json'),doc=JSON.parse(readFileSync(lock,'utf8'));
|
|
doc.plugins.acmeplug.sha256='f'.repeat(64);writeFileSync(lock,JSON.stringify(doc));
|
|
assert.equal(levels.countsAccount('acmeplug'),null,'a replaced pin is not covered until restart');
|
|
assert.equal(levels.countsAccount('pi'),false,'a tree module keeps its answer');
|
|
});
|
|
|
|
test('PL1 invalid UTF-8 manifest bytes refuse instead of normalizing outside the pin',t=>{
|
|
const f=fixture(t),path=join(f.src,'plugin.json'),bytes=readFileSync(path);
|
|
bytes[bytes.indexOf(SECRET)]=0xff;writeFileSync(path,bytes);
|
|
assert.throws(()=>pluginHash(f.src),refused('invalid-manifest'));
|
|
});
|
|
|
|
test('PL1 tool exceptions cannot impersonate trusted refusals or echo payload bytes',async t=>{
|
|
const f=fixture(t);
|
|
writeFileSync(join(f.src,'tools/echo.mjs'),`export function execute(){throw Object.assign(new Error(${JSON.stringify(SECRET)}),{name:'PluginError',code:'plugin-disabled',plugin:${JSON.stringify(SECRET)}});}`);
|
|
const hash=f.seal();f.install(f.src,{expectedHash:hash});
|
|
const service=pluginService({dataRoot:f.dataRoot,read:()=>({plugins:{'usage-source':['acmeplug']},roles:{writer:{tools:['acmeplug.echo'],authority:{withinRole:[],crossRole:[]}}}})});
|
|
await assert.rejects(service.call('acme','writer','acmeplug.echo',{text:'hello'}),e=>e.code==='plugin-tool-failed' && e.plugin==='acmeplug' && !e.message.includes(SECRET));
|
|
});
|
|
|
|
test('PL1 A3 organization copy readers preserve the derived installed-plugin root',async t=>{
|
|
const {withCopies,loadWithCopies}=await import('../../organization/src/agent-copies.mjs');
|
|
const f=fixture(t);f.manifest.kinds=['system-type'];f.manifest.interfaceVersions={'system-type':1};
|
|
mkdirSync(join(f.src,'system-type'));writeFileSync(join(f.src,'system-type/index.mjs'),'export const interfaceVersion=1;export function systemType(){return {}};');
|
|
const hash=f.seal();f.install(f.src,{expectedHash:hash});
|
|
const doc=businessDoc(f.root),rolesDir=rolesCopy(f.root);doc.plugins={...doc.plugins,'system-type':['acmeplug']};writeJson(join(f.root,'businesses/acme.json'),doc);
|
|
const options={rolesDir,dataRoot:f.dataRoot,snapshot:null};
|
|
assert.deepEqual(withCopies(doc,join(f.root,'businesses/acme.json'),options).plugins['system-type'],['acmeplug']);
|
|
assert.deepEqual(loadWithCopies('acme',{...options,dir:f.root}).plugins['system-type'],['acmeplug']);
|
|
const installed=join(f.dataRoot,'plugins/acmeplug/system-type/index.mjs');chmodSync(installed,0o600);writeFileSync(installed,SECRET);
|
|
// Config checks names only; the loader owns bad-pin refusal and preserves core.
|
|
assert.deepEqual(withCopies(doc,join(f.root,'businesses/acme.json'),options).plugins['system-type'],['acmeplug']);
|
|
doc.plugins['system-type']=['defualt'];
|
|
assert.throws(()=>withCopies(doc,join(f.root,'businesses/acme.json'),options),/unknown module/);
|
|
});
|
|
|
|
test('PL1 replacement pins refuse across loader and host tools until a fresh process',async t=>{
|
|
for(const first of ['loader','tool']) {
|
|
const f=fixture(t);
|
|
writeFileSync(join(f.src,'helper.mjs'),'export const value=1;');
|
|
writeFileSync(join(f.src,'usage-source/index.mjs'),"import {value} from '../helper.mjs';export const interfaceVersion=1;export const readTurn=()=>value;");
|
|
writeFileSync(join(f.src,'tools/echo.mjs'),"import {value} from '../helper.mjs';export const execute=()=>value;");
|
|
const pin=f.seal();f.install(f.src,{expectedHash:pin});
|
|
const settings={'usage-source':['transcript','acmeplug'],harness:['pi']};
|
|
const loader=createLoader({dataRoot:f.dataRoot,log:()=>{}});
|
|
const service=pluginService({dataRoot:f.dataRoot,read:()=>({plugins:settings,roles:{writer:{tools:['acmeplug.echo'],authority:{withinRole:[],crossRole:[]}}}})});
|
|
if(first==='loader'){await loader.load(settings);assert.equal(loader.modules('usage-source')[1].readTurn(),1);}
|
|
else assert.equal(await service.call('acme','writer','acmeplug.echo',{text:'before'}),1);
|
|
removePlugin({dataRoot:f.dataRoot,name:'acmeplug',enabled:()=>false});
|
|
writeFileSync(join(f.src,'helper.mjs'),'export const value=2;');f.manifest.version='2.0.0';
|
|
const next=f.seal();assert.notEqual(next,pin);f.install(f.src,{expectedHash:next});
|
|
// Verification, new loaders, and the other import path share one process latch.
|
|
assert.throws(()=>verifiedPlugin(f.dataRoot,'acmeplug'),refused('plugin-restart-required'));
|
|
await assert.rejects(service.call('acme','writer','acmeplug.echo',{text:'after'}),refused('plugin-disabled'));
|
|
const freshLoader=createLoader({dataRoot:f.dataRoot,log:()=>{}}),result=await freshLoader.load(settings);
|
|
assert.equal(result.refused[0].code,'plugin-restart-required');
|
|
assert.deepEqual(result.loaded['usage-source'],['transcript']);assert.deepEqual(result.loaded.harness,['pi']);
|
|
await loader.load(null);assert.deepEqual(loader.loaded()['usage-source'],first==='loader'?['transcript']:[]);
|
|
const script=`import {createLoader} from ${JSON.stringify(import.meta.resolve('../src/loader.mjs'))};
|
|
const loader=createLoader({dataRoot:process.argv[1],log:()=>{}});const r=await loader.load({'usage-source':['acmeplug']});
|
|
console.log(JSON.stringify({refused:r.refused,value:loader.modules('usage-source')[0].readTurn()}));`;
|
|
const child=spawnSync(process.execPath,['--input-type=module','-e',script,f.dataRoot],{encoding:'utf8'});
|
|
assert.equal(child.status,0,child.stderr);assert.deepEqual(JSON.parse(child.stdout),{refused:[],value:2});
|
|
}
|
|
});
|
|
|
|
test('PL1 even a failed import claims its package pin before dependencies enter the cache',async t=>{
|
|
const f=fixture(t);writeFileSync(join(f.src,'helper.mjs'),'export const value=1;');
|
|
writeFileSync(join(f.src,'usage-source/index.mjs'),"import '../helper.mjs';throw Error('fixture');");
|
|
let pin=f.seal();f.install(f.src,{expectedHash:pin});
|
|
const loader=createLoader({dataRoot:f.dataRoot,log:()=>{}});
|
|
assert.equal((await loader.load({'usage-source':['acmeplug']})).refused[0].code,'load-failed');
|
|
removePlugin({dataRoot:f.dataRoot,name:'acmeplug',enabled:()=>false});
|
|
writeFileSync(join(f.src,'usage-source/index.mjs'),'export const interfaceVersion=1;export const readTurn=()=>2;');
|
|
pin=f.seal();f.install(f.src,{expectedHash:pin});
|
|
assert.equal((await loader.load({'usage-source':['transcript','acmeplug']})).refused[0].code,'plugin-restart-required');
|
|
assert.deepEqual(loader.loaded()['usage-source'],['transcript']);
|
|
});
|
|
|
|
test('PL1 plugin imports refuse unpinned bare, escaping, dynamic and require dependencies',async t=>{
|
|
for(const spec of ['extdep','../../../outside.mjs']) {
|
|
const f=fixture(t),marker=join(f.dataRoot,'executed');
|
|
const outside=`import {writeFileSync} from 'node:fs';writeFileSync(${JSON.stringify(marker)},'ran');export const value=1;`;
|
|
mkdirSync(join(f.dataRoot,'node_modules/extdep'),{recursive:true});
|
|
writeFileSync(join(f.dataRoot,'node_modules/extdep/package.json'),JSON.stringify({type:'module',main:'index.mjs'}));
|
|
writeFileSync(join(f.dataRoot,'node_modules/extdep/index.mjs'),outside);writeFileSync(join(f.dataRoot,'outside.mjs'),outside);
|
|
writeFileSync(join(f.src,'usage-source/index.mjs'),`import ${JSON.stringify(spec)};export const interfaceVersion=1;export const readTurn=()=>1;`);
|
|
const pin=f.seal();f.install(f.src,{expectedHash:pin});
|
|
const result=await createLoader({dataRoot:f.dataRoot,log:()=>{}}).load({'usage-source':['transcript','acmeplug'],harness:['pi']});
|
|
assert.equal(result.refused[0].code,'load-failed');assert.deepEqual(result.loaded['usage-source'],['transcript']);assert.deepEqual(result.loaded.harness,['pi']);assert.equal(existsSync(marker),false);
|
|
}
|
|
for(const body of ["return (await import('../../../outside.mjs')).value;","return require('../../../outside.cjs');"]) {
|
|
const f=fixture(t),marker=join(f.dataRoot,'executed');
|
|
writeFileSync(join(f.dataRoot,'outside.mjs'),`import {writeFileSync} from 'node:fs';writeFileSync(${JSON.stringify(marker)},'ran');export const value=1;`);
|
|
writeFileSync(join(f.dataRoot,'outside.cjs'),`require('node:fs').writeFileSync(${JSON.stringify(marker)},'ran');module.exports=1;`);
|
|
writeFileSync(join(f.src,'tools/echo.mjs'),`import {createRequire} from 'node:module';const require=createRequire(import.meta.url);export async function execute(){${body}}`);
|
|
const pin=f.seal();f.install(f.src,{expectedHash:pin});
|
|
const service=pluginService({dataRoot:f.dataRoot,read:()=>({plugins:{'usage-source':['acmeplug']},roles:{writer:{tools:['acmeplug.echo'],authority:{withinRole:[],crossRole:[]}}}})});
|
|
await assert.rejects(service.call('acme','writer','acmeplug.echo',{text:SECRET}),refused('plugin-tool-failed'));assert.equal(existsSync(marker),false);
|
|
}
|
|
});
|
|
|
|
test('PL1 sandbox tools also refuse executable data URLs outside the pinned package',async t=>{
|
|
const f=fixture(t);f.manifest.tools[0].runs='sandbox';
|
|
writeFileSync(join(f.src,'tools/echo.mjs'),"export async function execute(){return (await import('data:text/javascript,export default 42')).default;}");
|
|
const pin=f.seal();f.install(f.src,{expectedHash:pin});
|
|
const service=pluginService({dataRoot:f.dataRoot,read:()=>({plugins:{'usage-source':['acmeplug']},roles:{writer:{tools:['acmeplug.echo'],authority:{withinRole:[],crossRole:[]}}}})});
|
|
await assert.rejects(service.call('acme','writer','acmeplug.echo',{text:SECRET}),refused('plugin-tool-failed'));
|
|
});
|
|
|
|
test('PL1 lock package names use the manifest length bound',t=>{
|
|
const f=fixture(t);writeFileSync(join(f.dataRoot,'plugins.lock.json'),JSON.stringify({version:1,plugins:{['a'.repeat(33)]:{version:'1.0.0',source:f.manifest.source,sha256:f.hash}}}),{mode:0o600});
|
|
assert.throws(()=>readPluginLock(f.dataRoot),refused('invalid-lock'));
|
|
assert.throws(()=>verifiedPlugin(f.dataRoot,'a'.repeat(33)),refused('bad-name'));
|
|
});
|