fix(plugins): keep imports on the verified package pin (#1873)
This commit was merged in pull request #1924.
This commit is contained in:
1 parent
b44b14c7df
commit
99ce579e3c
8 files changed
+166
-10
No files matched your search
@@ -8,7 +8,8 @@ const group = (codes, rail, nextStep) => {
|
||||
entries.push([code, guidance]);
|
||||
}
|
||||
};
|
||||
group("plugin-disabled", "R3/R6/R10: installed, pinned and enabled plugin capabilities", "Ask the instance owner to verify the installed plugin and the organization owner to enable it; then relaunch the agent.");
|
||||
group("plugin-disabled", "R3/R6/R10: installed, pinned and enabled plugin capabilities", "Ask the instance owner to verify the installed plugin and the organization owner to enable it. A replacement of an already loaded package needs a host restart; then relaunch the agent.");
|
||||
group("plugin-restart-required", "R3/R10: one imported package pin per host process", "Ask the instance owner to restart the host after replacing an already loaded package, then retry through the supported path.");
|
||||
group(
|
||||
"access-refused access-revoked share-exists share-not-covered share-not-found tester-dev-only tester-off tester-refused tester-required tester-target-not-running thread-not-allowed thread-not-owned",
|
||||
"R7: caller access and scoped session boundary",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { loadRole } from "./role.mjs";
|
||||
import { END_PROTECTED } from "./vocabulary.mjs";
|
||||
import { checkPlugins, checkUsageSettings } from "../../plugins/src/kinds.mjs";
|
||||
import { hasModule } from "../../plugins/src/loader.mjs";
|
||||
import { readPluginLock } from "../../plugins/src/pinned.mjs";
|
||||
import { checkMapping } from "../../library/src/mapping.mjs";
|
||||
import { forkProblem, giteaName, parseGiteaUrl, HOST_SERVICE, CREATIVE_DEFINITIONS } from "./vocabulary.mjs";
|
||||
import { checkAppearance } from "./appearance.mjs";
|
||||
@@ -333,8 +334,13 @@ export function validateOrganizationDocument(document, file, { rolesDir, dataRoo
|
||||
// here; whether each module loads is the loader's check, at load time.
|
||||
const plugins = document.plugins === undefined ? {}
|
||||
: structuredClone(checkPlugins(document.plugins, `${file} plugins`, { fail: (_code, message) => refuse(message) }));
|
||||
let installedNames = new Set();
|
||||
if (dataRoot !== null && plugins["system-type"]?.some(id=>!hasModule("system-type",id))) {
|
||||
try { installedNames = new Set(Object.keys(readPluginLock(dataRoot).plugins)); }
|
||||
catch { installedNames = null; } // the loader reports an unreadable lock; core still loads
|
||||
}
|
||||
for (const id of plugins["system-type"] ?? []) {
|
||||
if (dataRoot === null && !hasModule("system-type", id)) refuse(`${file} plugins.system-type names unknown module ${id}`);
|
||||
if (!hasModule("system-type", id) && installedNames !== null && !installedNames.has(id)) refuse(`${file} plugins.system-type names unknown module ${id}`);
|
||||
}
|
||||
const usage = document.usage === undefined ? undefined : checkUsageSettings(document.usage);
|
||||
const appearance = checkAppearance(document.appearance, `${file} appearance`);
|
||||
|
||||
@@ -578,6 +578,19 @@ or network. They receive JSON on stdin and return JSON on stdout.
|
||||
The loader checks the complete package against the lock before every installed
|
||||
module import. A refusal logs static codes and the validated package name;
|
||||
core modules continue. A bad pin evicts previously loaded installed modules.
|
||||
Once a package path has been imported, the process latches its verified pin before
|
||||
the first import attempt (including failed attempts). Removing and reinstalling a
|
||||
different pin at that path refuses with `plugin-restart-required` until the host
|
||||
restarts. The latch is shared by loaders and host tools, so cached helper modules
|
||||
cannot run under a replacement pin. A fresh sandbox tool process has no old cache.
|
||||
|
||||
Executable dependencies must be bundled in the package and imported by relative
|
||||
path (or an in-package file URL). A shared resolver guard refuses bare third-party
|
||||
lookups, executable data URLs and paths outside that package; Node built-ins remain
|
||||
available. This applies to static imports, dynamic imports and `createRequire`,
|
||||
including sandbox tools. It constrains module resolution, not R10 host authority:
|
||||
trusted host code can still use filesystem and other host APIs.
|
||||
|
||||
A role referencing a missing or disabled plugin refuses at launch with
|
||||
`plugin-disabled` and the package name. Disabling never fires or rewrites an
|
||||
agent; its next plugin tool call refuses with that same code.
|
||||
@@ -0,0 +1,35 @@
|
||||
// PL1: executable dependencies must be bundled inside the pinned package.
|
||||
// This constrains module resolution, not the host authority granted by R10.
|
||||
// Kept standalone so a sandbox tool can use this same guard in its fresh process.
|
||||
import { isBuiltin, registerHooks } from 'node:module';
|
||||
import { isAbsolute, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const roots = new Set();
|
||||
let registered = false;
|
||||
const inside = (root, path) => {
|
||||
const rel = relative(root, path);
|
||||
return rel !== '..' && !rel.startsWith('../') && !isAbsolute(rel);
|
||||
};
|
||||
const refused = () => { throw new Error('plugin import refused'); };
|
||||
export function guardPluginImports(directory) {
|
||||
roots.add(resolve(directory));
|
||||
if (registered) return;
|
||||
registerHooks({resolve(specifier, context, nextResolve) {
|
||||
let parent;
|
||||
try { if (context.parentURL?.startsWith('file:')) parent = fileURLToPath(context.parentURL); }
|
||||
catch { return refused(); }
|
||||
const root = parent && [...roots].find(dir => inside(dir, parent));
|
||||
if (!root || isBuiltin(specifier)) return nextResolve(specifier, context);
|
||||
// Refuse bare lookups before Node searches unpinned ancestor node_modules.
|
||||
if (!specifier.startsWith('./') && !specifier.startsWith('../') && !specifier.startsWith('/') && !specifier.startsWith('file:')) return refused();
|
||||
try {
|
||||
const candidate = new URL(specifier, context.parentURL);
|
||||
if (candidate.protocol !== 'file:' || !inside(root, fileURLToPath(candidate))) return refused();
|
||||
const result = nextResolve(specifier, context);
|
||||
if (!result.url.startsWith('file:') || !inside(root, fileURLToPath(result.url))) return refused();
|
||||
return result;
|
||||
} catch { return refused(); }
|
||||
}});
|
||||
registered = true;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { lstatSync, readdirSync, realpathSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { KINDS, NAME, PluginError, namesFor } from "./kinds.mjs";
|
||||
import { verifiedPlugin, readPluginLock } from "./pinned.mjs";
|
||||
import { verifiedPlugin, readPluginLock, claimPluginImport } from "./pinned.mjs";
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
@@ -247,6 +247,7 @@ async function loadPinnedModule(spec, name, barrier, dataRoot) {
|
||||
barrier("checked", {kind:spec.kind,name});
|
||||
checked = verifiedPlugin(dataRoot,name,spec.kind);
|
||||
const entry = join(checked.dir,spec.kind,"index.mjs");
|
||||
claimPluginImport(checked);
|
||||
let module;
|
||||
try { module=await import(pathToFileURL(entry).href + '?pin=' + checked.manifest.sha256); }
|
||||
catch { refuse("load-failed","installed plugin load refused",name); }
|
||||
|
||||
@@ -4,6 +4,18 @@ import { constants, openSync, closeSync, fstatSync, lstatSync, readdirSync, read
|
||||
import { isAbsolute, join, resolve } from 'node:path';
|
||||
import { NAME } from './kinds.mjs';
|
||||
import { bad, checkManifest, HASH } from './manifest.mjs';
|
||||
import { guardPluginImports } from './imports.mjs';
|
||||
// Node caches transitive modules by canonical path. Never attribute old cached
|
||||
// code to a replacement pin, even after a failed import or a new loader instance.
|
||||
const importedPins = new Map();
|
||||
function checkImportedPin(dir, hash, name) {
|
||||
if (importedPins.has(dir) && importedPins.get(dir) !== hash) bad('plugin-restart-required',name);
|
||||
}
|
||||
export function claimPluginImport({dir,manifest}) {
|
||||
checkImportedPin(dir,manifest.sha256,manifest.name);
|
||||
guardPluginImports(dir);
|
||||
importedPins.set(dir,manifest.sha256); // before the first import attempt
|
||||
}
|
||||
export function pluginPaths(dataRoot) {
|
||||
if (typeof dataRoot !== 'string' || !isAbsolute(dataRoot) || resolve(dataRoot) !== dataRoot) bad('plugin-root');
|
||||
return {root:join(dataRoot,'plugins'),lock:join(dataRoot,'plugins.lock.json'),mutex:join(dataRoot,'plugins.install.lock')};
|
||||
@@ -79,16 +91,17 @@ export function readPluginLock(dataRoot) {
|
||||
try { const st=lstatSync(lock); if (st.isSymbolicLink() || !st.isFile() || st.uid!==process.getuid() || st.mode&0o022) bad('invalid-lock'); doc=JSON.parse(readRegular(lock,st)); }
|
||||
catch (e) { if(e.code==='ENOENT') return {version:1,plugins:{}}; bad('invalid-lock'); }
|
||||
if (!doc || doc.version!==1 || Object.keys(doc).some(k=>!['version','plugins'].includes(k)) || !doc.plugins || Array.isArray(doc.plugins) || typeof doc.plugins!=='object') bad('invalid-lock');
|
||||
for(const [n,p] of Object.entries(doc.plugins)) if(!NAME.test(n) || !p || Object.keys(p).sort().join(',')!=='sha256,source,version' || !HASH.test(p.sha256) || typeof p.version!=='string') bad('invalid-lock');
|
||||
for(const [n,p] of Object.entries(doc.plugins)) if(n.length>32 || !NAME.test(n) || !p || Object.keys(p).sort().join(',')!=='sha256,source,version' || !HASH.test(p.sha256) || typeof p.version!=='string') bad('invalid-lock');
|
||||
return doc;
|
||||
}
|
||||
export function verifiedPlugin(dataRoot,name,kind=null) {
|
||||
if(typeof name!=='string' || !NAME.test(name)) bad('bad-name');
|
||||
if(typeof name!=='string' || name.length>32 || !NAME.test(name)) bad('bad-name');
|
||||
try {
|
||||
const pin=readPluginLock(dataRoot).plugins[name]; if(!pin) bad('missing-lock',name);
|
||||
const {root}=pluginPaths(dataRoot); const dir=join(root,name); const result=readPlugin(dir);
|
||||
if(result.manifest.name!==name || result.manifest.sha256!==pin.sha256 || result.manifest.version!==pin.version) bad('hash-mismatch',name);
|
||||
if(kind && !result.manifest.kinds.includes(kind)) bad('unknown-kind',name);
|
||||
checkImportedPin(dir,result.manifest.sha256,name);
|
||||
return {...result,dir};
|
||||
} catch(e) { bad(['missing-lock','hash-mismatch','unknown-kind','unsafe-path','invalid-lock'].includes(e.code)?e.code:'plugin-unavailable',name); }
|
||||
} catch(e) { bad(['missing-lock','hash-mismatch','unknown-kind','unsafe-path','invalid-lock','plugin-restart-required'].includes(e.code)?e.code:'plugin-unavailable',name); }
|
||||
}
|
||||
@@ -3,11 +3,12 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, realpathSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
import { organizationPlugins, assertPluginCapabilities, pluginName } from './organization.mjs';
|
||||
import { verifiedPlugin } from './pinned.mjs';
|
||||
import { verifiedPlugin, claimPluginImport } from './pinned.mjs';
|
||||
import { bad, validArguments } from './manifest.mjs';
|
||||
const SCRIPT = `let input=''; for await(const b of process.stdin)input+=b;
|
||||
const {guardPluginImports}=await import('file:///plugin-guard.mjs');guardPluginImports('/plugin');
|
||||
const {entry,args}=JSON.parse(input); const m=await import(entry);
|
||||
process.stdout.write(JSON.stringify(await m.execute(args)));`;
|
||||
function sandboxTool(dir,entry,args) {
|
||||
@@ -15,7 +16,7 @@ function sandboxTool(dir,entry,args) {
|
||||
// No home, config, workspace, sockets or inherited environment is exposed.
|
||||
const binds=['/usr','/lib','/lib64','/bin'].filter(existsSync).flatMap(p=>['--ro-bind',realpathSync(p),p]);
|
||||
return new Promise((resolve,reject)=>{
|
||||
const child=spawn('bwrap',['--die-with-parent','--unshare-all','--new-session','--proc','/proc','--dev','/dev','--tmpfs','/tmp',...binds,'--ro-bind',realpathSync(process.execPath),'/node','--ro-bind',dir,'/plugin','--chdir','/plugin','--clearenv','/node','--input-type=module','-e',SCRIPT],{env:{PATH:'/usr/bin:/bin'},stdio:['pipe','pipe','pipe']});
|
||||
const child=spawn('bwrap',['--die-with-parent','--unshare-all','--new-session','--proc','/proc','--dev','/dev','--tmpfs','/tmp',...binds,'--ro-bind',realpathSync(process.execPath),'/node','--ro-bind',fileURLToPath(new URL('./imports.mjs',import.meta.url)),'/plugin-guard.mjs','--ro-bind',dir,'/plugin','--chdir','/plugin','--clearenv','/node','--input-type=module','-e',SCRIPT],{env:{PATH:'/usr/bin:/bin'},stdio:['pipe','pipe','pipe']});
|
||||
let out='',size=0;const timer=setTimeout(()=>child.kill('SIGKILL'),30000);
|
||||
child.stdout.on('data',b=>{size+=b.length;if(size>1024*1024)child.kill('SIGKILL');else out+=b;});
|
||||
child.stderr.resume();child.on('error',()=>{clearTimeout(timer);reject(new Error('sandbox refused'));});
|
||||
@@ -40,11 +41,12 @@ export function pluginService({dataRoot,read}) {
|
||||
if(!role.tools?.includes(name))bad('not-authorized');
|
||||
const tool=view.manifests.flatMap(m=>m.tools).find(t=>t.name===name);if(!tool)bad('plugin-disabled',prefix);
|
||||
if(!validArguments(args,tool.arguments))bad('invalid-request');
|
||||
const {dir,manifest}=verifiedPlugin(dataRoot,prefix);
|
||||
const checked=verifiedPlugin(dataRoot,prefix),{dir,manifest}=checked;
|
||||
if(tool.runs==='sandbox') {
|
||||
try {return await sandboxTool(dir,tool.entry,structuredClone(args));}
|
||||
catch {bad('plugin-tool-failed',prefix);}
|
||||
}
|
||||
claimPluginImport(checked);
|
||||
let module;
|
||||
try {module=await import(pathToFileURL(join(dir,tool.entry)).href+`?pin=${manifest.sha256}`);}
|
||||
catch {bad('plugin-tool-failed',prefix);}
|
||||
|
||||
@@ -210,4 +210,89 @@ test('PL1 A3 organization copy readers preserve the derived installed-plugin roo
|
||||
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'));
|
||||
});
|
||||
Reference in new issue
Block a user