diff --git a/packages/cli/src/commands/eval.js b/packages/cli/src/commands/eval.js index b4341fb37c..a62adaa688 100644 --- a/packages/cli/src/commands/eval.js +++ b/packages/cli/src/commands/eval.js @@ -2,6 +2,7 @@ import os from 'os'; import { E } from '@endo/far'; import { withEndoParty } from '../context.js'; +import { parsePetNamePath } from '../pet-name.js'; export const evalCommand = async ({ source, @@ -27,13 +28,13 @@ export const evalCommand = async ({ return pair; }); const codeNames = pairs.map(pair => pair[0]); - const endowmentNames = pairs.map(pair => pair[1]); + const petNames = pairs.map(pair => parsePetNamePath(pair[1])); const result = await E(party).evaluate( workerName, source, codeNames, - endowmentNames, + petNames, resultName, ); console.log(result); diff --git a/packages/cli/src/pet-name.js b/packages/cli/src/pet-name.js new file mode 100644 index 0000000000..fdea2a8214 --- /dev/null +++ b/packages/cli/src/pet-name.js @@ -0,0 +1,18 @@ +/** + * Splits a dot-delimited pet name path into an array of pet names. + * Throws if any of the path segments are empty. + * + * @param {string} petNamePath - A dot-delimited pet name path. + * @returns {string[]} - The pet name path, as an array of pet names. + */ +export const parsePetNamePath = petNamePath => { + const petNames = petNamePath.split('.'); + for (const petName of petNames) { + if (petName === '') { + throw new Error( + `Pet name path "${petNamePath}" contains an empty segment.`, + ); + } + } + return petNames; +}; diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index cb63a4ba62..ba4b40f956 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -227,6 +227,26 @@ const makeEndoBootstrap = ( return { external, internal: undefined }; }; + /** + * @param {string} agentFormulaIdentifier + * @param {string[]} path + * @param {import('./types.js').Terminator} terminator + */ + const makeControllerForLookup = async ( + agentFormulaIdentifier, + path, + terminator, + ) => { + terminator.thisDiesIfThatDies(agentFormulaIdentifier); + + // Behold, recursion: + // eslint-disable-next-line no-use-before-define + const agent = provideValueForFormulaIdentifier(agentFormulaIdentifier); + + const external = E(agent).lookup(...path); + return { external, internal: undefined }; + }; + /** * @param {string} workerFormulaIdentifier * @param {string} guestFormulaIdentifier @@ -325,6 +345,8 @@ const makeEndoBootstrap = ( formula.values, terminator, ); + } else if (formula.type === 'lookup') { + return makeControllerForLookup(formula.agent, formula.path, terminator); } else if (formula.type === 'make-unconfined') { return makeControllerForUnconfinedPlugin( formula.worker, @@ -491,6 +513,12 @@ const makeEndoBootstrap = ( // controllerForFormulaIdentifier and formulaIdentifierForRef, since the // former bypasses the latter in order to avoid a round trip with disk. + /** + * @param {string} formulaType - The type of the formula. + * @param {string} formulaNumber - The number of the formula. + * @param {import('./types').Formula} formula - The formula. + * @returns {Promise} The value of the formula. + */ const provideValueForNumberedFormula = async ( formulaType, formulaNumber, @@ -508,7 +536,7 @@ const makeEndoBootstrap = ( // Behold, recursion: // eslint-disable-next-line no-use-before-define const terminator = makeTerminator(formulaIdentifier); - partial.catch(error => terminator.terminate()); + partial.catch(() => terminator.terminate()); const controller = harden({ terminator, external: E.get(partial).external.then(value => { @@ -647,6 +675,7 @@ const makeEndoBootstrap = ( if ( ![ 'eval-id512', + 'lookup', 'make-unconfined-id512', 'make-bundle-id512', 'guest-id512', @@ -676,6 +705,15 @@ const makeEndoBootstrap = ( worker: provideValueForFormulaIdentifier(formula.worker), }), ); + } else if (formula.type === 'lookup') { + return makeInspector( + formula.type, + formulaNumber, + harden({ + agent: provideValueForFormulaIdentifier(formula.agent), + path: formula.path, + }), + ); } else if (formula.type === 'guest') { return makeInspector( formula.type, diff --git a/packages/daemon/src/host.js b/packages/daemon/src/host.js index c6d3aa4601..6fe23bf1e0 100644 --- a/packages/daemon/src/host.js +++ b/packages/daemon/src/host.js @@ -1,7 +1,7 @@ // @ts-check import { Far } from '@endo/far'; -import { assertPetName } from './pet-name.js'; +import { assertPetName, petNamePathFrom } from './pet-name.js'; const { quote: q } = assert; @@ -185,15 +185,15 @@ export const makeHostMaker = ({ /** * @param {string | 'MAIN' | 'NEW'} workerName * @param {string} source - * @param {Array} codeNames - * @param {Array} petNames + * @param {string[]} codeNames + * @param {(string | string[])[]} petNamePaths * @param {string} resultName */ const evaluate = async ( workerName, source, codeNames, - petNames, + petNamePaths, resultName, ) => { const workerFormulaIdentifier = await provideWorkerFormulaIdentifier( @@ -203,24 +203,52 @@ export const makeHostMaker = ({ if (resultName !== undefined) { assertPetName(resultName); } - if (petNames.length !== codeNames.length) { + if (petNamePaths.length !== codeNames.length) { throw new Error('Evaluator requires one pet name for each code name'); } const formulaIdentifiers = await Promise.all( - petNames.map(async (petName, index) => { + petNamePaths.map(async (petNameOrPath, index) => { if (typeof codeNames[index] !== 'string') { throw new Error(`Invalid endowment name: ${q(codeNames[index])}`); } - const formulaIdentifier = lookupFormulaIdentifierForName(petName); - if (formulaIdentifier === undefined) { - throw new Error(`Unknown pet name ${q(petName)}`); + + const petNamePath = petNamePathFrom(petNameOrPath); + if (petNamePath.length === 1) { + const formulaIdentifier = lookupFormulaIdentifierForName( + petNamePath[0], + ); + if (formulaIdentifier === undefined) { + throw new Error(`Unknown pet name ${q(petNamePath[0])}`); + } + return formulaIdentifier; } - return formulaIdentifier; + + const lookupAgent = lookupFormulaIdentifierForName('SELF'); + const digester = makeSha512(); + digester.updateText(`${lookupAgent},${petNamePath.join(',')}`); + const lookupFormulaNumber = digester.digestHex().slice(32, 64); + + // TODO:lookup Check if the lookup formula already exists in the store + + const lookupFormula = { + /** @type {'lookup'} */ + type: 'lookup', + agent: lookupAgent, + path: petNamePath, + }; + + const { formulaIdentifier: lookupFormulaIdentifier } = + await provideValueForNumberedFormula( + 'lookup', + lookupFormulaNumber, + lookupFormula, + ); + return lookupFormulaIdentifier; }), ); - const formula = { + const evalFormula = { /** @type {'eval'} */ type: 'eval', worker: workerFormulaIdentifier, @@ -232,7 +260,7 @@ export const makeHostMaker = ({ // Behold, recursion: // eslint-disable-next-line no-use-before-define const { formulaIdentifier, value } = await provideValueForFormula( - formula, + evalFormula, 'eval-id512', ); if (resultName !== undefined) { diff --git a/packages/daemon/src/pet-name.js b/packages/daemon/src/pet-name.js index 337a836135..81e3c87574 100644 --- a/packages/daemon/src/pet-name.js +++ b/packages/daemon/src/pet-name.js @@ -12,3 +12,11 @@ export const assertPetName = petName => { throw new Error(`Invalid pet name ${q(petName)}`); } }; + +/** + * @param {string | string[]} petNameOrPetNamePath + */ +export const petNamePathFrom = petNameOrPetNamePath => + typeof petNameOrPetNamePath === 'string' + ? [petNameOrPetNamePath] + : petNameOrPetNamePath; diff --git a/packages/daemon/src/pet-store.js b/packages/daemon/src/pet-store.js index ac8509e2cc..3b51332425 100644 --- a/packages/daemon/src/pet-store.js +++ b/packages/daemon/src/pet-store.js @@ -9,7 +9,7 @@ const { quote: q } = assert; const validIdPattern = /^[0-9a-f]{128}$/; const validFormulaPattern = - /^(?:host|pet-store|pet-inspector|(?:readable-blob-sha512|worker-id512|pet-store-id512|eval-id512|make-unconfined-id512|make-bundle-id512|host-id512|guest-id512):[0-9a-f]{128}|web-bundle:[0-9a-f]{32})$/; + /^(?:host|pet-store|pet-inspector|(?:readable-blob-sha512|worker-id512|pet-store-id512|eval-id512|make-unconfined-id512|make-bundle-id512|host-id512|guest-id512):[0-9a-f]{128}|(?:lookup|web-bundle):[0-9a-f]{32})$/; /** * @param {import('./types.js').FilePowers} filePowers diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index 77f1b0eb83..5404fa51e6 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -72,6 +72,20 @@ type EvalFormula = { // TODO formula slots }; +type LookupFormula = { + type: 'lookup'; + + /** + * The formula identifier of the guest or host to call lookup on. + */ + agent: string; + + /** + * The pet name path. + */ + path: Array; +}; + type MakeUnconfinedFormula = { type: 'make-unconfined'; worker: string; @@ -97,6 +111,7 @@ type WebBundleFormula = { export type Formula = | GuestFormula | EvalFormula + | LookupFormula | MakeUnconfinedFormula | MakeBundleFormula | WebBundleFormula; diff --git a/packages/daemon/test/test-endo.js b/packages/daemon/test/test-endo.js index a2082b2501..683aafc99e 100644 --- a/packages/daemon/test/test-endo.js +++ b/packages/daemon/test/test-endo.js @@ -1041,3 +1041,33 @@ test('lookup with petname path (value has no lookup method)', async t => { await stop(locator); }); + +test('evaluate name resolved by lookup path', async t => { + const { promise: cancelled, reject: cancel } = makePromiseKit(); + t.teardown(() => cancel(Error('teardown'))); + const locator = makeLocator('tmp', 'name-resolved-by-lookup-path'); + + await stop(locator).catch(() => {}); + await reset(locator); + await start(locator); + + const { getBootstrap } = await makeEndoClient( + 'client', + locator.sockPath, + cancelled, + ); + const bootstrap = getBootstrap(); + const host = E(bootstrap).host(); + + await E(host).evaluate('MAIN', '10', [], [], 'ten'); + + const resolvedValue = await E(host).evaluate( + 'MAIN', + 'foo', + ['foo'], + [['INFO', 'ten', 'source']], + ); + t.is(resolvedValue, '10'); + + await stop(locator); +});