From 25c404ea84f97be6ae7a57562865e20f23123e57 Mon Sep 17 00:00:00 2001 From: Kris Kowal Date: Mon, 25 Mar 2024 23:39:53 -0700 Subject: [PATCH] refactor(daemon,cli): A handle for every agent --- packages/cli/demo/README.md | 12 +- packages/cli/src/commands/mkguest.js | 12 +- packages/cli/src/commands/mkhost.js | 12 +- packages/cli/src/endo.js | 12 +- packages/daemon/src/daemon.js | 229 +++++++++++++++++---------- packages/daemon/src/guest.js | 18 ++- packages/daemon/src/host.js | 59 +++++-- packages/daemon/src/pet-name.js | 5 + packages/daemon/src/pet-sitter.js | 8 + packages/daemon/src/types.d.ts | 40 +++-- packages/daemon/test/service.js | 4 +- packages/daemon/test/test-endo.js | 56 +++---- 12 files changed, 308 insertions(+), 159 deletions(-) diff --git a/packages/cli/demo/README.md b/packages/cli/demo/README.md index 3884f7af2b..dcdc4d7951 100644 --- a/packages/cli/demo/README.md +++ b/packages/cli/demo/README.md @@ -136,11 +136,15 @@ through which it obtains all of its authority. In this example, the doubler requests another counter from the user. We make a doubler mostly the same way we made the counter. -However, we must give a name to the agent running the doubler, which we will -later use to recognize requests coming from the doubler. +However, we must create a guest profile for the doubler. +The guest has two facets: its handle and its agent powers. +The handle appears in the "to" and "from" fields of messages exchanged with the +guest and provides no other capabilities. +The agent is a permission management broker that the doubler +can use to request other capabilities, like the counter. ``` -> endo mkguest doubler-agent +> endo mkguest doubler-handle doubler-agent > endo make doubler.js --name doubler --powers doubler-agent ``` @@ -149,7 +153,7 @@ resolve its request for a counter. ``` > endo inbox -0. "doubler-agent" requested "please give me a counter" +0. "doubler-handle" requested "please give me a counter" > endo resolve 0 counter ``` diff --git a/packages/cli/src/commands/mkguest.js b/packages/cli/src/commands/mkguest.js index 0793a08db2..781e2efcda 100644 --- a/packages/cli/src/commands/mkguest.js +++ b/packages/cli/src/commands/mkguest.js @@ -3,8 +3,16 @@ import os from 'os'; import { E } from '@endo/far'; import { withEndoAgent } from '../context.js'; -export const mkguest = async ({ name, agentNames, introducedNames }) => +export const mkguest = async ({ + handleName, + agentName, + agentNames, + introducedNames, +}) => withEndoAgent(agentNames, { os, process }, async ({ agent }) => { - const newGuest = await E(agent).provideGuest(name, { introducedNames }); + const newGuest = await E(agent).provideGuest(handleName, { + introducedNames, + agentName, + }); console.log(newGuest); }); diff --git a/packages/cli/src/commands/mkhost.js b/packages/cli/src/commands/mkhost.js index 781d3bf3c1..16013477c9 100644 --- a/packages/cli/src/commands/mkhost.js +++ b/packages/cli/src/commands/mkhost.js @@ -3,8 +3,16 @@ import os from 'os'; import { E } from '@endo/far'; import { withEndoAgent } from '../context.js'; -export const mkhost = async ({ name, agentNames, introducedNames }) => +export const mkhost = async ({ + handleName, + agentName, + agentNames, + introducedNames, +}) => withEndoAgent(agentNames, { os, process }, async ({ agent }) => { - const newHost = await E(agent).provideHost(name, { introducedNames }); + const newHost = await E(agent).provideHost(handleName, { + introducedNames, + agentName, + }); console.log(newHost); }); diff --git a/packages/cli/src/endo.js b/packages/cli/src/endo.js index e81d82feff..3ab6c50b05 100644 --- a/packages/cli/src/endo.js +++ b/packages/cli/src/endo.js @@ -415,7 +415,7 @@ export const main = async rawArgs => { }); program - .command('mkhost ') + .command('mkhost [agent-name]') .option(...commonOptions.as) .option( '--introduce ', @@ -424,14 +424,14 @@ export const main = async rawArgs => { {}, ) .description('makes a separate mailbox and storage for you') - .action(async (name, cmd) => { + .action(async (handleName, agentName, cmd) => { const { as: agentNames, introduce: introducedNames } = cmd.opts(); const { mkhost } = await import('./commands/mkhost.js'); - return mkhost({ name, agentNames, introducedNames }); + return mkhost({ handleName, agentName, agentNames, introducedNames }); }); program - .command('mkguest ') + .command('mkguest [agent-name]') .option(...commonOptions.as) .option( '--introduce ', @@ -440,10 +440,10 @@ export const main = async rawArgs => { {}, ) .description('makes a mailbox and storage for a guest (peer or program)') - .action(async (name, cmd) => { + .action(async (handleName, agentName, cmd) => { const { as: agentNames, introduce: introducedNames } = cmd.opts(); const { mkguest } = await import('./commands/mkguest.js'); - return mkguest({ name, agentNames, introducedNames }); + return mkguest({ agentName, handleName, agentNames, introducedNames }); }); program diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index bfb660e226..b0b2aedadc 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -214,6 +214,9 @@ const makeDaemonCore = async ( */ const formulaForId = new Map(); + /** @type {WeakMap<{}, string>} */ + const agentIdForHandle = new WeakMap(); + /** @param {string} id */ const getFormulaForId = async id => { await null; @@ -412,18 +415,18 @@ const makeDaemonCore = async ( /** * @param {string} workerId - * @param {string} guestId + * @param {string} powersId * @param {string} specifier * @param {import('./types.js').Context} context */ const makeControllerForUnconfinedPlugin = async ( workerId, - guestId, + powersId, specifier, context, ) => { context.thisDiesIfThatDies(workerId); - context.thisDiesIfThatDies(guestId); + context.thisDiesIfThatDies(powersId); const workerController = /** @type {import('./types.js').Controller} */ ( @@ -436,15 +439,13 @@ const makeDaemonCore = async ( workerDaemonFacet, `panic: No internal bootstrap for worker ${workerId}`, ); - const guestP = /** @type {Promise} */ ( - // Behold, recursion: - // eslint-disable-next-line no-use-before-define - provide(guestId) - ); + // Behold, recursion: + // eslint-disable-next-line no-use-before-define + const powersP = provide(powersId); const external = E(workerDaemonFacet).makeUnconfined( specifier, - guestP, // TODO fix type + /** @type {any} */ (powersP), /** @type {any} */ (makeFarContext(context)), ); return { external, internal: undefined }; @@ -452,18 +453,18 @@ const makeDaemonCore = async ( /** * @param {string} workerId - * @param {string} guestId + * @param {string} powersId * @param {string} bundleId * @param {import('./types.js').Context} context */ const makeControllerForSafeBundle = async ( workerId, - guestId, + powersId, bundleId, context, ) => { context.thisDiesIfThatDies(workerId); - context.thisDiesIfThatDies(guestId); + context.thisDiesIfThatDies(powersId); const workerController = /** @type {import('./types.js').Controller} */ ( @@ -476,23 +477,19 @@ const makeDaemonCore = async ( workerDaemonFacet, `panic: No internal bootstrap for worker ${workerId}`, ); - // Behold, recursion: - // eslint-disable-next-line no-use-before-define const readableBundleP = /** @type {Promise} */ ( // Behold, recursion: // eslint-disable-next-line no-use-before-define provide(bundleId) ); - const guestP = /** @type {Promise} */ ( - // Behold, recursion: - // eslint-disable-next-line no-use-before-define - provide(guestId) - ); + // Behold, recursion: + // eslint-disable-next-line no-use-before-define + const powersP = provide(powersId); const external = E(workerDaemonFacet).makeBundle( readableBundleP, - guestP, // TODO fix type + /** @type {any} */ (powersP), /** @type {any} */ (makeFarContext(context)), ); return { external, internal: undefined }; @@ -504,7 +501,12 @@ const makeDaemonCore = async ( * @param {import('./types.js').Formula} formula * @param {import('./types.js').Context} context */ - const makeControllerForFormula = (id, formulaNumber, formula, context) => { + const makeControllerForFormula = async ( + id, + formulaNumber, + formula, + context, + ) => { if (formula.type === 'eval') { return makeControllerForEval( formula.worker, @@ -536,8 +538,9 @@ const makeDaemonCore = async ( } else if (formula.type === 'host') { // Behold, recursion: // eslint-disable-next-line no-use-before-define - return makeIdentifiedHost( + const controller = await makeIdentifiedHost( id, + formula.handle, formula.petStore, formula.inspector, formula.worker, @@ -547,23 +550,37 @@ const makeDaemonCore = async ( platformNames, context, ); + const { external: agent } = controller; + const handle = agent.handle(); + agentIdForHandle.set(handle, id); + return controller; } else if (formula.type === 'guest') { // Behold, recursion: // eslint-disable-next-line no-use-before-define - return makeIdentifiedGuestController( + const controller = await makeIdentifiedGuestController( id, - formula.host, + formula.handle, + formula.hostAgent, + formula.hostHandle, formula.petStore, formula.worker, context, ); + const { external: agent } = controller; + const handle = agent.handle(); + agentIdForHandle.set(handle, id); + return controller; } else if (formula.type === 'handle') { - context.thisDiesIfThatDies(formula.agent); + const agent = /** @type {import('./types.js').EndoAgent} */ ( + // Behold, recursion: + // eslint-disable-next-line no-use-before-define + await provide(formula.agent) + ); + const handle = agent.handle(); + agentIdForHandle.set(handle, formula.agent); return { - external: {}, - internal: { - agentId: formula.agent, - }, + external: handle, + internal: undefined, }; } else if (formula.type === 'endo') { // Gateway is equivalent to E's "nonce locator". It provides a value for @@ -590,15 +607,15 @@ const makeDaemonCore = async ( cancel(new Error('Termination requested')); }, host: () => { - // Behold, recursion: return /** @type {Promise} */ ( + // Behold, recursion: // eslint-disable-next-line no-use-before-define provide(formula.host) ); }, leastAuthority: () => { - // Behold, recursion: return /** @type {Promise} */ ( + // Behold, recursion: // eslint-disable-next-line no-use-before-define provide(leastAuthorityId) ); @@ -759,7 +776,7 @@ const makeDaemonCore = async ( formulaForId.set(id, formula); // Memoize for lookup. - console.log(`Making ${id}`); + console.log(`Making ${formula.type} ${id}`); const { promise: partial, resolve: resolvePartial } = /** @type {import('@endo/promise-kit').PromiseKit>} */ ( makePromiseKit() @@ -875,24 +892,12 @@ const makeDaemonCore = async ( /** @type {import('./types.js').DaemonCore['provideControllerAndResolveHandle']} */ const provideControllerAndResolveHandle = async id => { - let currentId = id; - // eslint-disable-next-line no-constant-condition - while (true) { - const controller = provideController(currentId); - // eslint-disable-next-line no-await-in-loop - const internalFacet = await controller.internal; - if (internalFacet === undefined || internalFacet === null) { - return controller; - } - // @ts-expect-error We can't know the type of the internal facet. - if (internalFacet.agentId === undefined) { - return controller; - } - const handle = /** @type {import('./types.js').InternalHandle} */ ( - internalFacet - ); - currentId = handle.agentId; + const handle = /** @type {{}} */ (await provide(id)); + const agentId = agentIdForHandle.get(handle); + if (agentId === undefined) { + throw assert.error(assert.details`No agent for handle ${id}`); } + return provideController(agentId); }; /** @type {import('./types.js').DaemonCore['formulateReadableBlob']} */ @@ -927,22 +932,31 @@ const makeDaemonCore = async ( }; /** - * Formulates a `handle` formula and synchronously adds it to the formula graph. - * The returned promise is resolved after the formula is persisted. + * Unlike other formulate functions, formulateNumberedHandle *only* writes a + * formula to the formula graph and does not attempt to incarnate it. + * This is to break an incarnation cycle between agents and their handles. + * The agent must be incarnated first, contains its own handle object, and + * produces a agentIdForHandle entry as a side-effect. + * Explicitly incarnating the handle formula later simply looks up the handle + * reference on the already-incarnated agent. * * @param {string} formulaNumber - The formula number of the handle to formulate. * @param {string} agentId - The formula identifier of the handle's agent. - * @returns {import('./types.js').FormulateResult} The formulated handle. + * @returns {Promise} */ - const formulateNumberedHandle = (formulaNumber, agentId) => { + const formulateNumberedHandle = async (formulaNumber, agentId) => { /** @type {import('./types.js').HandleFormula} */ const formula = { type: 'handle', agent: agentId, }; - return /** @type {import('./types').FormulateResult} */ ( - formulate(formulaNumber, formula) - ); + await persistencePowers.writeFormula(formulaNumber, formula); + const id = formatId({ + number: formulaNumber, + node: ownNodeIdentifier, + }); + formulaForId.set(id, formula); + return id; }; /** @@ -1027,9 +1041,22 @@ const makeDaemonCore = async ( const storeId = (await formulateNumberedPetStore(await randomHex512())).id; + const hostFormulaNumber = await randomHex512(); + const hostId = formatId({ + number: hostFormulaNumber, + node: ownNodeIdentifier, + }); + + const handleId = await formulateNumberedHandle( + await randomHex512(), + hostId, + ); + return harden({ ...remainingSpecifiedIdentifiers, - hostFormulaNumber: await randomHex512(), + hostFormulaNumber, + hostId, + handleId, storeId, /* eslint-disable no-use-before-define */ inspectorId: ( @@ -1045,6 +1072,7 @@ const makeDaemonCore = async ( /** @type {import('./types.js').HostFormula} */ const formula = { type: 'host', + handle: identifiers.handleId, petStore: identifiers.storeId, inspector: identifiers.inspectorId, worker: identifiers.workerId, @@ -1073,10 +1101,8 @@ const makeDaemonCore = async ( }); await deferredTasks.execute({ - agentId: formatId({ - number: identifiers.hostFormulaNumber, - node: ownNodeIdentifier, - }), + agentId: identifiers.hostId, + handleId: identifiers.handleId, }); return identifiers; @@ -1085,22 +1111,35 @@ const makeDaemonCore = async ( }; /** @type {import('./types.js').DaemonCore['formulateGuestDependencies']} */ - const formulateGuestDependencies = async hostId => - harden({ - guestFormulaNumber: await randomHex512(), - hostHandleId: ( - await formulateNumberedHandle(await randomHex512(), hostId) - ).id, + const formulateGuestDependencies = async (hostAgentId, hostHandleId) => { + const guestFormulaNumber = await randomHex512(); + const guestId = formatId({ + number: guestFormulaNumber, + node: ownNodeIdentifier, + }); + const handleId = await formulateNumberedHandle( + await randomHex512(), + guestId, + ); + return harden({ + guestFormulaNumber, + guestId, + handleId, + hostAgentId, + hostHandleId, storeId: (await formulateNumberedPetStore(await randomHex512())).id, workerId: (await formulateNumberedWorker(await randomHex512())).id, }); + }; /** @type {import('./types.js').DaemonCore['formulateNumberedGuest']} */ const formulateNumberedGuest = identifiers => { /** @type {import('./types.js').GuestFormula} */ const formula = { type: 'guest', - host: identifiers.hostHandleId, + handle: identifiers.handleId, + hostHandle: identifiers.hostHandleId, + hostAgent: identifiers.hostAgentId, petStore: identifiers.storeId, worker: identifiers.workerId, }; @@ -1111,16 +1150,17 @@ const makeDaemonCore = async ( }; /** @type {import('./types.js').DaemonCore['formulateGuest']} */ - const formulateGuest = async (hostId, deferredTasks) => { + const formulateGuest = async (hostAgentId, hostHandleId, deferredTasks) => { return formulateNumberedGuest( await formulaGraphJobs.enqueue(async () => { - const identifiers = await formulateGuestDependencies(hostId); + const identifiers = await formulateGuestDependencies( + hostAgentId, + hostHandleId, + ); await deferredTasks.execute({ - agentId: formatId({ - number: identifiers.guestFormulaNumber, - node: ownNodeIdentifier, - }), + agentId: identifiers.guestId, + handleId: identifiers.handleId, }); return identifiers; @@ -1226,36 +1266,50 @@ const makeDaemonCore = async ( }; /** - * @param {string} hostId + * @param {string} hostAgentId + * @param {string} hostHandleId * @param {string} [specifiedPowersId] */ - const providePowersId = async (hostId, specifiedPowersId) => { + const providePowersId = async ( + hostAgentId, + hostHandleId, + specifiedPowersId, + ) => { await null; if (typeof specifiedPowersId === 'string') { return specifiedPowersId; } - const guestFormulationData = await formulateGuestDependencies(hostId); + const guestFormulationData = await formulateGuestDependencies( + hostAgentId, + hostHandleId, + ); const guestFormulation = await formulateNumberedGuest(guestFormulationData); return guestFormulation.id; }; /** * Helper for `formulateUnconfined` and `formulateBundle`. - * @param {string} hostId + * @param {string} hostAgentId + * @param {string} hostHandleId * @param {import('./types.js').DeferredTasks} deferredTasks * @param {string} [specifiedWorkerId] * @param {string} [specifiedPowersId] */ const formulateCapletDependencies = async ( - hostId, + hostAgentId, + hostHandleId, deferredTasks, specifiedWorkerId, specifiedPowersId, ) => { const ownFormulaNumber = await randomHex512(); const identifiers = harden({ - powersId: await providePowersId(hostId, specifiedPowersId), + powersId: await providePowersId( + hostAgentId, + hostHandleId, + specifiedPowersId, + ), capletId: formatId({ number: ownFormulaNumber, node: ownNodeIdentifier, @@ -1269,7 +1323,8 @@ const makeDaemonCore = async ( /** @type {import('./types.js').DaemonCore['formulateUnconfined']} */ const formulateUnconfined = async ( - hostId, + hostAgentId, + hostHandleId, specifier, deferredTasks, specifiedWorkerId, @@ -1278,7 +1333,8 @@ const makeDaemonCore = async ( const { powersId, capletFormulaNumber, workerId } = await formulaGraphJobs.enqueue(() => formulateCapletDependencies( - hostId, + hostAgentId, + hostHandleId, deferredTasks, specifiedWorkerId, specifiedPowersId, @@ -1297,7 +1353,8 @@ const makeDaemonCore = async ( /** @type {import('./types.js').DaemonCore['formulateBundle']} */ const formulateBundle = async ( - hostId, + hostAgentId, + hostHandleId, bundleId, deferredTasks, specifiedWorkerId, @@ -1306,7 +1363,8 @@ const makeDaemonCore = async ( const { powersId, capletFormulaNumber, workerId } = await formulaGraphJobs.enqueue(() => formulateCapletDependencies( - hostId, + hostAgentId, + hostHandleId, deferredTasks, specifiedWorkerId, specifiedPowersId, @@ -1610,7 +1668,8 @@ const makeDaemonCore = async ( formula.type, formulaNumber, harden({ - host: provide(formula.host), + hostAgent: provide(formula.hostAgent), + hostHandle: provide(formula.hostHandle), }), ); } else if (formula.type === 'make-bundle') { diff --git a/packages/daemon/src/guest.js b/packages/daemon/src/guest.js index 0cc9b91f40..433da27448 100644 --- a/packages/daemon/src/guest.js +++ b/packages/daemon/src/guest.js @@ -20,6 +20,8 @@ export const makeGuestMaker = ({ }) => { /** * @param {string} guestId + * @param {string} handleId + * @param {string} hostAgentId * @param {string} hostHandleId * @param {string} petStoreId * @param {string} mainWorkerId @@ -27,12 +29,15 @@ export const makeGuestMaker = ({ */ const makeIdentifiedGuestController = async ( guestId, + handleId, + hostAgentId, hostHandleId, petStoreId, mainWorkerId, context, ) => { context.thisDiesIfThatDies(hostHandleId); + context.thisDiesIfThatDies(hostAgentId); context.thisDiesIfThatDies(petStoreId); context.thisDiesIfThatDies(mainWorkerId); @@ -40,7 +45,8 @@ export const makeGuestMaker = ({ await provide(petStoreId) ); const specialStore = makePetSitter(basePetStore, { - SELF: guestId, + AGENT: guestId, + SELF: handleId, HOST: hostHandleId, }); const hostController = @@ -56,7 +62,7 @@ export const makeGuestMaker = ({ const mailbox = makeMailbox({ petStore: specialStore, - selfId: guestId, + selfId: handleId, context, }); const { petStore } = mailbox; @@ -90,8 +96,16 @@ export const makeGuestMaker = ({ respond, } = mailbox; + const handle = makeExo( + 'EndoGuestHandle', + M.interface('EndoGuestHandle', {}), + {}, + ); + /** @type {import('./types.js').EndoGuest} */ const guest = { + // Agent + handle: () => handle, // Directory has, identify, diff --git a/packages/daemon/src/host.js b/packages/daemon/src/host.js index 800e755427..0a216876a7 100644 --- a/packages/daemon/src/host.js +++ b/packages/daemon/src/host.js @@ -12,7 +12,7 @@ const { quote: q } = assert; /** @param {string} name */ const assertPowersName = name => { - ['NONE', 'SELF', 'ENDO'].includes(name) || assertPetName(name); + ['NONE', 'AGENT', 'ENDO'].includes(name) || assertPetName(name); }; /** @@ -50,6 +50,7 @@ export const makeHostMaker = ({ }) => { /** * @param {string} hostId + * @param {string} handleId * @param {string} storeId * @param {string} inspectorId * @param {string} mainWorkerId @@ -61,6 +62,7 @@ export const makeHostMaker = ({ */ const makeIdentifiedHost = async ( hostId, + handleId, storeId, inspectorId, mainWorkerId, @@ -80,7 +82,8 @@ export const makeHostMaker = ({ ); const specialStore = makePetSitter(basePetStore, { ...platformNames, - SELF: hostId, + AGENT: hostId, + SELF: handleId, ENDO: endoId, NETS: networksDirectoryId, INFO: inspectorId, @@ -89,7 +92,7 @@ export const makeHostMaker = ({ const mailbox = makeMailbox({ petStore: specialStore, - selfId: hostId, + selfId: handleId, context, }); const { petStore } = mailbox; @@ -273,6 +276,7 @@ export const makeHostMaker = ({ // eslint-disable-next-line no-use-before-define const { value } = await formulateUnconfined( hostId, + handleId, specifier, tasks, workerId, @@ -308,6 +312,7 @@ export const makeHostMaker = ({ // eslint-disable-next-line no-use-before-define const { value } = await formulateBundle( hostId, + handleId, bundleId, tasks, workerId, @@ -348,7 +353,7 @@ export const makeHostMaker = ({ if (id !== undefined) { return { id, - value: /** @type {Promise} */ (provideController(id).external), + value: /** @type {Promise} */ (provide(id)), }; } } @@ -356,13 +361,21 @@ export const makeHostMaker = ({ }; /** - * @param {string} [petName] - The pet name of the agent. + * @param {string} [handleName] - The pet name of the handle. + * @param {string} [agentName] - The pet name of the agent. */ - const getDeferredTasksForAgent = petName => { + const getDeferredTasksForAgent = (handleName, agentName) => { /** @type {import('./types.js').DeferredTasks} */ const tasks = makeDeferredTasks(); - if (petName !== undefined) { - tasks.push(identifiers => petStore.write(petName, identifiers.agentId)); + if (handleName !== undefined) { + tasks.push(identifiers => + petStore.write(handleName, identifiers.handleId), + ); + } + if (agentName !== undefined) { + tasks.push(identifiers => + petStore.write(agentName, identifiers.agentId), + ); } return tasks; }; @@ -372,7 +385,10 @@ export const makeHostMaker = ({ * @param {import('./types.js').MakeHostOrGuestOptions} [opts] * @returns {Promise<{id: string, value: Promise}>} */ - const makeHost = async (petName, { introducedNames = {} } = {}) => { + const makeHost = async ( + petName, + { introducedNames = {}, agentName = undefined } = {}, + ) => { let host = getNamedAgent(petName); if (host === undefined) { const { value, id } = @@ -380,7 +396,7 @@ export const makeHostMaker = ({ await formulateHost( endoId, networksDirectoryId, - getDeferredTasksForAgent(petName), + getDeferredTasksForAgent(petName, agentName), ); host = { value: Promise.resolve(value), id }; } @@ -398,16 +414,23 @@ export const makeHostMaker = ({ }; /** - * @param {string} [petName] + * @param {string} [handleName] * @param {import('./types.js').MakeHostOrGuestOptions} [opts] * @returns {Promise<{id: string, value: Promise}>} */ - const makeGuest = async (petName, { introducedNames = {} } = {}) => { - let guest = getNamedAgent(petName); + const makeGuest = async ( + handleName, + { introducedNames = {}, agentName = undefined } = {}, + ) => { + let guest = getNamedAgent(handleName); if (guest === undefined) { const { value, id } = // Behold, recursion: - await formulateGuest(hostId, getDeferredTasksForAgent(petName)); + await formulateGuest( + hostId, + handleId, + getDeferredTasksForAgent(handleName, agentName), + ); guest = { value: Promise.resolve(value), id }; } @@ -481,8 +504,16 @@ export const makeHostMaker = ({ respond, } = mailbox; + const handle = makeExo( + 'EndoHostHandle', + M.interface('EndoHostHandle', {}), + {}, + ); + /** @type {import('./types.js').EndoHost} */ const host = { + // Agent + handle: () => handle, // Directory has, identify, diff --git a/packages/daemon/src/pet-name.js b/packages/daemon/src/pet-name.js index 81e3c87574..2a40065765 100644 --- a/packages/daemon/src/pet-name.js +++ b/packages/daemon/src/pet-name.js @@ -4,6 +4,11 @@ const { quote: q } = assert; const validNamePattern = /^[a-z][a-z0-9-]{0,127}$/; +/** + * @param {string} petName + */ +export const isPetName = petName => validNamePattern.test(petName); + /** * @param {string} petName */ diff --git a/packages/daemon/src/pet-sitter.js b/packages/daemon/src/pet-sitter.js index c5e93e58b6..44ddd69592 100644 --- a/packages/daemon/src/pet-sitter.js +++ b/packages/daemon/src/pet-sitter.js @@ -1,5 +1,6 @@ // @ts-check +import { isPetName } from './pet-name.js'; import { parseId } from './formula-identifier.js'; const { quote: q } = assert; @@ -20,6 +21,13 @@ export const makePetSitter = (petStore, specialNames) => { if (Object.hasOwn(specialNames, petName)) { return specialNames[petName]; } + if (!isPetName(petName)) { + throw new Error( + `Invalid pet name ${q(petName)} and not one of ${Object.keys( + specialNames, + ).join(', ')}`, + ); + } return petStore.identifyLocal(petName); }; diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index 56df30c203..7000817a48 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -81,10 +81,12 @@ export type WorkerDeferredTaskParams = { */ export type AgentDeferredTaskParams = { agentId: string; + handleId: string; }; type HostFormula = { type: 'host'; + handle: string; worker: string; inspector: string; petStore: string; @@ -94,7 +96,9 @@ type HostFormula = { type GuestFormula = { type: 'guest'; - host: string; + handle: string; + hostHandle: string; + hostAgent: string; petStore: string; worker: string; }; @@ -484,6 +488,7 @@ export interface EndoWorker { } export type MakeHostOrGuestOptions = { + agentName?: string; introducedNames?: Record; }; @@ -508,7 +513,8 @@ export interface EndoNetwork { connect: (address: string, farContext: FarContext) => EndoGateway; } -export interface EndoGuest extends EndoDirectory { +export interface EndoAgent extends EndoDirectory { + handle: () => {}; listMessages: Mail['listMessages']; followMessages: Mail['followMessages']; resolve: Mail['resolve']; @@ -518,17 +524,12 @@ export interface EndoGuest extends EndoDirectory { request: Mail['request']; send: Mail['send']; } + +export interface EndoGuest extends EndoAgent {} + export type FarEndoGuest = FarRef; -export interface EndoHost extends EndoDirectory { - listMessages: Mail['listMessages']; - followMessages: Mail['followMessages']; - resolve: Mail['resolve']; - reject: Mail['reject']; - adopt: Mail['adopt']; - dismiss: Mail['dismiss']; - request: Mail['request']; - send: Mail['send']; +export interface EndoHost extends EndoAgent { store( readerRef: ERef>, petName: string, @@ -725,6 +726,9 @@ export type DeferredTasks> = { type FormulateNumberedGuestParams = { guestFormulaNumber: string; + handleId: string; + guestId: string; + hostAgentId: string; hostHandleId: string; storeId: string; workerId: string; @@ -738,6 +742,8 @@ type FormulateHostDependenciesParams = { type FormulateNumberedHostParams = { hostFormulaNumber: string; + hostId: string; + handleId: string; workerId: string; storeId: string; inspectorId: string; @@ -757,7 +763,8 @@ export interface DaemonCore { }>; formulateBundle: ( - hostId: string, + hostAgentId: string, + hostHandleId: string, bundleId: string, deferredTasks: DeferredTasks, specifiedWorkerId?: string, @@ -771,7 +778,7 @@ export interface DaemonCore { ) => FormulateResult; formulateEval: ( - hostId: string, + nameHubId: string, source: string, codeNames: string[], endowmentIdsOrPaths: (string | string[])[], @@ -781,6 +788,7 @@ export interface DaemonCore { formulateGuest: ( hostId: string, + hostHandleId: string, deferredTasks: DeferredTasks, ) => FormulateResult; @@ -790,7 +798,8 @@ export interface DaemonCore { * @returns The formula identifiers for the guest formulation's dependencies. */ formulateGuestDependencies: ( - hostId: string, + hostAgentId: string, + hostHandleId: string, ) => Promise>; formulateHost: ( @@ -832,7 +841,8 @@ export interface DaemonCore { ) => FormulateResult; formulateUnconfined: ( - hostId: string, + hostAgentId: string, + hostHandleId: string, specifier: string, deferredTasks: DeferredTasks, specifiedWorkerId?: string, diff --git a/packages/daemon/test/service.js b/packages/daemon/test/service.js index fea3d1e17d..5bb719e9d0 100644 --- a/packages/daemon/test/service.js +++ b/packages/daemon/test/service.js @@ -2,13 +2,13 @@ import { E } from '@endo/far'; import { makeExo } from '@endo/exo'; import { M } from '@endo/patterns'; -export const make = powers => { +export const make = agent => { return makeExo( 'Service', M.interface('Service', {}, { defaultGuards: 'passable' }), { async ask() { - return E(powers).request( + return E(agent).request( 'HOST', 'the meaning of life, the universe, everything', 'answer', diff --git a/packages/daemon/test/test-endo.js b/packages/daemon/test/test-endo.js index 8f00d079cb..1c8dfcdecb 100644 --- a/packages/daemon/test/test-endo.js +++ b/packages/daemon/test/test-endo.js @@ -73,7 +73,7 @@ const makeHostWithTestNetwork = async (locator, cancelled) => { const network = E(host).makeUnconfined( 'MAIN', serviceLocation, - 'SELF', + 'AGENT', 'test-network', ); @@ -418,18 +418,20 @@ test('persist unconfined services and their requests', async t => { const iteratorRef = E(host).followMessages(); const { value: message } = await E(iteratorRef).next(); const { number, who } = E.get(message); - t.is(await who, 'o1'); + t.is(await who, 'h1'); await E(host).resolve(await number, 'grant'); })(); const requesterFinished = (async () => { const { host } = await makeHost(locator, cancelled); await E(host).provideWorker('w1'); - await E(host).provideGuest('o1'); + await E(host).provideGuest('h1', { + agentName: 'a1', + }); const servicePath = path.join(dirname, 'test', 'service.js'); const serviceLocation = url.pathToFileURL(servicePath).href; - await E(host).makeUnconfined('w1', serviceLocation, 'o1', 's1'); + await E(host).makeUnconfined('w1', serviceLocation, 'a1', 's1'); await E(host).provideWorker('w2'); const answer = await E(host).evaluate( @@ -479,18 +481,18 @@ test('persist confined services and their requests', async t => { const iteratorRef = E(host).followMessages(); const { value: message } = await E(iteratorRef).next(); const { number, who } = E.get(message); - t.is(await who, 'o1'); + t.is(await who, 'h1'); await E(host).resolve(await number, 'grant'); })(); const requesterFinished = (async () => { const { host } = await makeHost(locator, cancelled); await E(host).provideWorker('w1'); - await E(host).provideGuest('o1'); + await E(host).provideGuest('h1', { agentName: 'a1' }); const servicePath = path.join(dirname, 'test', 'service.js'); await doMakeBundle(host, servicePath, bundleName => - E(host).makeBundle('w1', bundleName, 'o1', 's1'), + E(host).makeBundle('w1', bundleName, 'a1', 's1'), ); await E(host).provideWorker('w2'); @@ -636,7 +638,7 @@ test('indirect cancellation via worker', async t => { const counterPath = path.join(dirname, 'test', 'counter.js'); const counterLocation = url.pathToFileURL(counterPath).href; - await E(host).makeUnconfined('worker', counterLocation, 'SELF', 'counter'); + await E(host).makeUnconfined('worker', counterLocation, 'AGENT', 'counter'); t.is( 1, await E(host).evaluate( @@ -704,13 +706,13 @@ test.failing('indirect cancellation via caplet', async t => { await E(host).provideWorker('w1'); const counterPath = path.join(dirname, 'test', 'counter.js'); const counterLocation = url.pathToFileURL(counterPath).href; - await E(host).makeUnconfined('w1', counterLocation, 'SELF', 'counter'); + await E(host).makeUnconfined('w1', counterLocation, 'AGENT', 'counter'); await E(host).provideWorker('w2'); - await E(host).provideGuest('guest'); + await E(host).provideGuest('guest', { agentName: 'guest-agent' }); const doublerPath = path.join(dirname, 'test', 'doubler.js'); const doublerLocation = url.pathToFileURL(doublerPath).href; - await E(host).makeUnconfined('w2', doublerLocation, 'guest', 'doubler'); + await E(host).makeUnconfined('w2', doublerLocation, 'guest-agent', 'doubler'); E(host).resolve(0, 'counter'); t.is( @@ -743,13 +745,13 @@ test('cancel because of requested capability', async t => { const { host } = await makeHost(locator, cancelled); await E(host).provideWorker('worker'); - await E(host).provideGuest('guest'); + await E(host).provideGuest('guest', { agentName: 'guest-agent' }); const messages = E(host).followMessages(); const counterPath = path.join(dirname, 'test', 'counter-agent.js'); const counterLocation = url.pathToFileURL(counterPath).href; - E(host).makeUnconfined('worker', counterLocation, 'guest', 'counter'); + E(host).makeUnconfined('worker', counterLocation, 'guest-agent', 'counter'); await E(host).evaluate('worker', '0', [], [], 'zero'); await E(messages).next(); @@ -783,7 +785,7 @@ test('cancel because of requested capability', async t => { ), ); - await E(host).cancel('guest'); + await E(host).cancel('guest-agent'); t.is( 1, @@ -950,9 +952,9 @@ test('lookup with single petname', async t => { const resolvedValue = await E(host).evaluate( 'MAIN', - 'E(SELF).lookup("ten")', - ['SELF'], - ['SELF'], + 'E(AGENT).lookup("ten")', + ['AGENT'], + ['AGENT'], ); t.is(resolvedValue, 10); }); @@ -965,9 +967,9 @@ test('lookup with petname path (inspector)', async t => { const resolvedValue = await E(host).evaluate( 'MAIN', - 'E(SELF).lookup("INFO", "ten", "source")', - ['SELF'], - ['SELF'], + 'E(AGENT).lookup("INFO", "ten", "source")', + ['AGENT'], + ['AGENT'], ); t.is(resolvedValue, '10'); }); @@ -981,9 +983,9 @@ test('lookup with petname path (caplet with lookup method)', async t => { const resolvedValue = await E(host).evaluate( 'MAIN', - 'E(SELF).lookup("lookup", "name")', - ['SELF'], - ['SELF'], + 'E(AGENT).lookup("lookup", "name")', + ['AGENT'], + ['AGENT'], ); t.is(resolvedValue, 'Looked up: name'); }); @@ -996,9 +998,9 @@ test('lookup with petname path (value has no lookup method)', async t => { await t.throwsAsync( E(host).evaluate( 'MAIN', - 'E(SELF).lookup("ten", "someName")', - ['SELF'], - ['SELF'], + 'E(AGENT).lookup("ten", "someName")', + ['AGENT'], + ['AGENT'], ), { message: 'target has no method "lookup", has []' }, ); @@ -1045,7 +1047,7 @@ test('guest cannot access host methods', async t => { const guest = E(host).provideGuest('guest'); const guestsHost = E(guest).lookup('HOST'); - await t.throwsAsync(() => E(guestsHost).lookup('SELF'), { + await t.throwsAsync(() => E(guestsHost).lookup('ANY'), { message: /target has no method "lookup"/u, }); const revealedTarget = await E.get(guestsHost).targetId;