Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(daemon): Undeniable host INFO tool #1916

Merged
merged 6 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions packages/daemon/src/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ const delay = async (ms, cancelled) => {
});
};

/**
* Creates an inspector object for a formula.
*
* @param {string} type - The formula type.
* @param {string} number - The formula number.
* @param {Record<string, unknown>} record - A mapping from special names to formula values.
* @returns {import('./types.js').EndoInspector} The inspector for the given formula.
*/
const makeInspector = (type, number, record) =>
Far(`Inspector (${type} ${number})`, {
lookup: async petName => {
if (!Object.hasOwn(record, petName)) {
return undefined;
}
return record[petName];
},
list: () => Object.keys(record),
});

/**
* @param {import('./types.js').DaemonicPowers} powers
* @param {Promise<number>} webletPortP
Expand Down Expand Up @@ -371,14 +390,21 @@ const makeEndoBootstrap = (
assertPetName,
);
return { external, internal: undefined };
} else if (formulaIdentifier === 'pet-inspector') {
// Behold, unavoidable forward-reference:
// eslint-disable-next-line no-use-before-define
const external = makePetStoreInspector('pet-store');
return { external, internal: undefined };
} else if (formulaIdentifier === 'host') {
const storeFormulaIdentifier = 'pet-store';
const inspectorFormulaIdentifier = 'pet-inspector';
const workerFormulaIdentifier = `worker-id512:${zero512}`;
// Behold, recursion:
// eslint-disable-next-line no-use-before-define
return makeIdentifiedHost(
formulaIdentifier,
storeFormulaIdentifier,
inspectorFormulaIdentifier,
workerFormulaIdentifier,
terminator,
);
Expand Down Expand Up @@ -407,6 +433,13 @@ const makeEndoBootstrap = (
return { external, internal: undefined };
} else if (formulaType === 'worker-id512') {
return makeIdentifiedWorkerController(formulaNumber, terminator);
} else if (formulaType === 'pet-inspector-id512') {
// Behold, unavoidable forward-reference:
// eslint-disable-next-line no-use-before-define
const external = makePetStoreInspector(
`pet-store-id512:${formulaNumber}`,
);
return { external, internal: undefined };
} else if (formulaType === 'pet-store-id512') {
const external = petStorePowers.makeIdentifiedPetStore(
formulaNumber,
Expand All @@ -415,12 +448,14 @@ const makeEndoBootstrap = (
return { external, internal: undefined };
} else if (formulaType === 'host-id512') {
const storeFormulaIdentifier = `pet-store-id512:${formulaNumber}`;
const inspectorFormulaIdentifier = `pet-inspector-id512:${formulaNumber}`;
const workerFormulaIdentifier = `worker-id512:${formulaNumber}`;
// Behold, recursion:
// eslint-disable-next-line no-use-before-define
return makeIdentifiedHost(
formulaIdentifier,
storeFormulaIdentifier,
inspectorFormulaIdentifier,
workerFormulaIdentifier,
terminator,
);
Expand Down Expand Up @@ -583,6 +618,117 @@ const makeEndoBootstrap = (
makeMailbox,
});

/**
* Creates an inspector for the current party's pet store, used to create
* inspectors for values therein. Notably, can provide references to otherwise
* un-nameable values such as the `MAIN` worker. See `KnownEndoInspectors` for
* more details.
*
* @param {string} petStoreFormulaIdentifier
* @returns {Promise<import('./types').EndoInspector>}
*/
const makePetStoreInspector = async petStoreFormulaIdentifier => {
const petStore = await provideValueForFormulaIdentifier(
petStoreFormulaIdentifier,
);

/**
* @param {string} petName - The pet name to inspect.
* @returns {Promise<import('./types').KnownEndoInspectors[string]>} An
* inspector for the value of the given pet name.
*/
const lookup = async petName => {
Comment on lines +635 to +640
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have types for the individual known inspectors (see types.d.ts), but due to the indirection between the petName argument and the formulaType, it is difficult (impossible?) to map the petName to the "correct" return type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. We can’t be more specific than a union of all known inspector types. Lookup in general has this limitation. For some Endo APIs, we could overload a formula type argument and provide guarantees of specific types, to make some casts unnecessary.

const formulaIdentifier = petStore.lookup(petName);
if (formulaIdentifier === undefined) {
throw new Error(`Unknown pet name ${petName}`);
}
const { type: formulaType, number: formulaNumber } =
parseFormulaIdentifier(formulaIdentifier);
if (
![
'eval-id512',
'make-unconfined-id512',
'make-bundle-id512',
'guest-id512',
'web-bundle',
].includes(formulaType)
) {
return makeInspector(formulaType, formulaNumber, harden({}));
}
const formula = await persistencePowers.readFormula(
formulaType,
formulaNumber,
);
if (formula.type === 'eval') {
return makeInspector(
formula.type,
formulaNumber,
harden({
endowments: Object.fromEntries(
formula.names.map((name, index) => {
return [
name,
provideValueForFormulaIdentifier(formula.values[index]),
];
}),
),
source: formula.source,
worker: provideValueForFormulaIdentifier(formula.worker),
}),
);
} else if (formula.type === 'guest') {
return makeInspector(
formula.type,
formulaNumber,
harden({
host: provideValueForFormulaIdentifier(formula.host),
}),
);
} else if (formula.type === 'make-bundle') {
return makeInspector(
formula.type,
formulaNumber,
harden({
bundle: provideValueForFormulaIdentifier(formula.bundle),
powers: provideValueForFormulaIdentifier(formula.powers),
worker: provideValueForFormulaIdentifier(formula.worker),
}),
);
} else if (formula.type === 'make-unconfined') {
return makeInspector(
formula.type,
formulaNumber,
harden({
powers: provideValueForFormulaIdentifier(formula.powers),
specifier: formula.type,
worker: provideValueForFormulaIdentifier(formula.worker),
}),
);
} else if (formula.type === 'web-bundle') {
return makeInspector(
formula.type,
formulaNumber,
harden({
bundle: provideValueForFormulaIdentifier(formula.bundle),
powers: provideValueForFormulaIdentifier(formula.powers),
}),
);
}
// @ts-expect-error this should never occur
return makeInspector(formula.type, formulaNumber, harden({}));
};

/** @returns {string[]} The list of all names in the pet store. */
const list = () => petStore.list();

const info = Far('Endo inspector facet', {
lookup,
list,
});

return info;
};

const endoBootstrap = Far('Endo private facet', {
// TODO for user named

Expand Down
5 changes: 3 additions & 2 deletions packages/daemon/src/formula-identifier.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const { quote: q } = assert;

const numberlessFormulasIdentifiers = new Set([
'pet-store',
'host',
'endo',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add a comment that these are sorted, to maintain that invariant.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll defer this to when I look into our lint configs.

'host',
'least-authority',
'pet-inspector',
'pet-store',
'web-page-js',
]);

Expand Down
3 changes: 3 additions & 0 deletions packages/daemon/src/host.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ export const makeHostMaker = ({
/**
* @param {string} hostFormulaIdentifier
* @param {string} storeFormulaIdentifier
* @param {string} inspectorFormulaIdentifier
* @param {string} mainWorkerFormulaIdentifier
* @param {import('./types.js').Terminator} terminator
*/
const makeIdentifiedHost = async (
hostFormulaIdentifier,
storeFormulaIdentifier,
inspectorFormulaIdentifier,
mainWorkerFormulaIdentifier,
terminator,
) => {
Expand Down Expand Up @@ -58,6 +60,7 @@ export const makeHostMaker = ({
selfFormulaIdentifier: hostFormulaIdentifier,
specialNames: {
SELF: hostFormulaIdentifier,
INFO: inspectorFormulaIdentifier,
NONE: 'least-authority',
ENDO: 'endo',
},
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon/src/pet-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { quote: q } = assert;

const validIdPattern = /^[0-9a-f]{128}$/;
const validFormulaPattern =
/^(?:host|pet-store|(?: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}|web-bundle:[0-9a-f]{32})$/;

/**
* @param {import('./types.js').FilePowers} filePowers
Expand Down
15 changes: 15 additions & 0 deletions packages/daemon/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,21 @@ export interface EndoHost {
): Promise<unknown>;
}

export type EndoInspector<Record = string> = {
lookup: (petName: Record) => Promise<unknown>;
list: () => Record[];
};

export type KnownEndoInspectors = {
'eval-id512': EndoInspector<'endowments' | 'source' | 'worker'>;
'make-unconfined-id512': EndoInspector<'host'>;
'make-bundle-id512': EndoInspector<'bundle' | 'powers' | 'worker'>;
'guest-id512': EndoInspector<'bundle' | 'powers'>;
'web-bundle': EndoInspector<'powers' | 'specifier' | 'worker'>;
// This is an "empty" inspector, in that there is nothing to `lookup()` or `list()`.
[formulaType: string]: EndoInspector<string>;
};

export type EndoWebBundle = {
url: string;
bundle: ERef<EndoReadable>;
Expand Down
90 changes: 90 additions & 0 deletions packages/daemon/test/test-endo.js
Original file line number Diff line number Diff line change
Expand Up @@ -694,3 +694,93 @@ test('make a host', async t => {

await stop(locator);
});

test('name and reuse inspector', async t => {
const { promise: cancelled, reject: cancel } = makePromiseKit();
t.teardown(() => cancel(Error('teardown')));
const locator = makeLocator('tmp', 'inspector-reuse');

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).provideWorker('worker');

const counterPath = path.join(dirname, 'test', 'counter.js');
await E(host).makeUnconfined('worker', counterPath, 'NONE', 'counter');

const inspector = await E(host).evaluate(
'worker',
'E(INFO).lookup("counter")',
['INFO'],
['INFO'],
'inspector',
);
t.regex(String(inspector), /Alleged: Inspector.+make-unconfined/u);

const worker = await E(host).evaluate(
'worker',
'E(inspector).lookup("worker")',
['inspector'],
['inspector'],
);
t.regex(String(worker), /Alleged: EndoWorker/u);

await stop(locator);
});

// TODO: This test verifies existing behavior when pet-naming workers.
// This behavior is undesirable. See: https://github.com/endojs/endo/issues/2021
test('eval-mediated worker name', async t => {
const { promise: cancelled, reject: cancel } = makePromiseKit();
t.teardown(() => cancel(Error('teardown')));
const locator = makeLocator('tmp', 'eval-worker-name');

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).provideWorker('worker');

const counterPath = path.join(dirname, 'test', 'counter.js');
await E(host).makeUnconfined('worker', counterPath, 'NONE', 'counter');

// We create a petname for the worker of `counter`.
// Note that while `worker === counter-worker`, it doesn't matter here.
const counterWorker = await E(host).evaluate(
'worker',
'E(E(INFO).lookup("counter")).lookup("worker")',
['INFO'],
['INFO'],
'counter-worker',
);
t.regex(String(counterWorker), /Alleged: EndoWorker/u);

try {
await E(host).evaluate(
'counter-worker', // Our worker pet name
'E(counter).incr()',
['counter'],
['counter'],
);
t.fail('should have thrown');
} catch (error) {
// This is the error that we don't want
t.regex(error.message, /typeof target is "undefined"/u);
await stop(locator);
}
});
Loading