Skip to content

Commit

Permalink
chore: Merge Pet Dæmon and CLI (merge #2082)
Browse files Browse the repository at this point in the history
  • Loading branch information
kriskowal authored Feb 15, 2024
2 parents 75bd2d2 + ff58c06 commit 3656123
Show file tree
Hide file tree
Showing 88 changed files with 8,905 additions and 1,320 deletions.
5 changes: 2 additions & 3 deletions packages/bundle-source/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,8 @@ export const makeBundleCache = (wr, cwd, readPowers, opts) => {
const todo = makePromiseKit();
loaded.set(targetName, { rootPath, bundle: todo.promise });
const bundle = await validateOrAdd(rootPath, targetName, log)
.then(
({ bundleFileName }) =>
import(`${wr.readOnly().neighbor(bundleFileName)}`),
.then(({ bundleFileName }) =>
import(`${wr.readOnly().neighbor(bundleFileName)}`),
)
.then(m => harden(m.default));
assert.equal(bundle.moduleFormat, 'endoZipBase64');
Expand Down
3 changes: 2 additions & 1 deletion packages/bundle-source/src/zip-base64.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function bundleZipBase64(
options = {},
grantedPowers = {},
) {
const { dev = false, cacheSourceMaps = false } = options;
const { dev = false, cacheSourceMaps = false, commonDependencies } = options;
const powers = { ...readPowers, ...grantedPowers };
const {
computeSha512,
Expand Down Expand Up @@ -174,6 +174,7 @@ export async function bundleZipBase64(
sourceMapHook(sourceMap, sourceDescriptor) {
sourceMapJobs.add(writeSourceMap(sourceMap, sourceDescriptor));
},
commonDependencies,
});
assert(sha512);
await Promise.all(sourceMapJobs);
Expand Down
8 changes: 4 additions & 4 deletions packages/captp/src/ts-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import type { ESingleMethod, Unpromise } from '@endo/eventual-send';
export type TrapHandler<T> = T extends (...args: infer P) => infer R
? (...args: P) => Unpromise<R>
: T extends Record<string | number | symbol, Function>
? {
[K in keyof T]: Unpromise<T[K]>;
}
: T;
? {
[K in keyof T]: Unpromise<T[K]>;
}
: T;

/* Types for Trap proxy calls. */
type TrapSingleMethod<T> = {
Expand Down
318 changes: 318 additions & 0 deletions packages/cli/demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@

# Install Endo

One day, this will be as simple as:

> npm install -g @endo/cli
Until that day, you will need to clone the Endo repository, install
dependencies using Yarn, and arrange for the Endo command to be available as
`endo` in whatever shell you favor.

```
> git clone https://github.com/endojs/endo.git
> yarn
> alias endo=$PWD/packages/cli/bin/endo
```

# A counter example

This is a worker caplet, or worklet, that counts numbers.

```js
import { Far } from '@endo/far';

export const make = () => {
let counter = 0;
return Far('Counter', {
incr() {
counter += 1;
return counter;
},
});
};
```

We can create an instance of the counter and give it a name.

```
> endo make counter.js --name counter
```

Then, we can send messages to the counter and see their responses.
These `endo eval` commands are executing a tightly confined JavaScript program
and reporting the program's completion value.
Because of the confinement to a private Hardened JavaScript Compartment,
the program must be endowed with all of its dependencies,
in this case, a `counter`.

```
> endo eval 'E(counter).incr()' counter
1
> endo eval 'E(counter).incr()' counter
2
> endo eval 'E(counter).incr()' counter
3
```

> Aside: in all the above cases, we use `counter` both as the property name
> that will appear in the compartment's global object (and global scope) as
> well as the name of formula that produced the value.
> These may be different.
>
> ```
> > endo eval 'E(c).incr()' c:counter
> 4
> ```
Endo preserves the commands that led to the creation of the `counter` value,
which form a directed acyclic graph of "formulas".
If we kill the Endo Pet Daemon and all its workers (or "vats"), the memory
of how these values were made remains, but their memory is otherwise lost.
```
> endo restart
> endo eval 'E(counter).incr()' counter
1
> endo eval 'E(counter).incr()' counter
2
> endo eval 'E(counter).incr()' counter
3
```
> Aside, since Eventual Send, the machinery under the `E` operator, abstracts
> the counter reference in both space and time, it does not matter much which
> process these evaluations occur in.
> The default is a worker called `MAIN`, whose formula number is 0.
> Use the `-w` or `--worker` flag to specify a different worker.
>
> ```
> > endo spawn greeter
> > endo eval --worker greeter '"Hello, World!"' --name greeting
> Hello, World!
> > endo show greeting
> Hello, World!
> ```
# Doubler Agent
The counter example requires no additional authority.
It provides a simple service and depends only on the ability to compute.
The doubler worklet depends upon a counter, which it doubles.
We can use the doubler to demonstrate how caplets can run as guests
and request additional capabilities from the user.
```js
import { E, Far } from '@endo/far';
export const make = powers => {
const counter = E(powers).request(
'HOST',
'a counter, suitable for doubling',
'my-counter'
);
return Far('Doubler', {
async incr() {
const n = await E(counter).incr();
return n * 2;
},
});
};
```

The doubler receives a `powers` object: an interface granted by the host user
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.

```
> endo mkguest doubler-agent
> endo make doubler.js --name doubler --powers doubler-agent
```

This creates a doubler, but the doubler cannot respond until we
resolve its request for a counter.

```
> endo inbox
0. "doubler-agent" requested "please give me a counter"
> endo resolve 0 counter
```

> Aside, `endo reject 0` would have rejected the request,
> leaving the `doubler-agent` permanently broken.
Now we can get a response from the doubler.

```
> endo eval 'E(doubler).incr()' doubler
8
> endo eval 'E(doubler).incr()' doubler
10
> endo eval 'E(doubler).incr()' doubler
12
```

Also, in the optional second argument to `request`, `doubler.js` names the
request `my-counter`.
Any subsequent time a worklet running with the powers of `doubler-agent` asks
for a power using the name `my-counter`, it will get a reference to the same
eventual response.
The daemon preserves the formulas needed to recreate `my-counter` across
restarts and reboots.

```
> endo restart
> endo eval 'E(doubler).incr()' doubler
2
> endo eval 'E(doubler).incr()' doubler
4
> endo eval 'E(doubler).incr()' doubler
6
```

# Sending Messages

So far, we have run guest programs like the doubler.
Guests and hosts can exchange messages and those messages can convey powerful
objects.

In this example, we create a fake guest named "alice" and we send them our
"doubler".
Then, assuming the guise of "alice", we find the message in our inbox
and adopt the "doubler" object into our own store.

```
> endo mkguest alice
> endo send alice 'Please enjoy this @doubler.'
> endo inbox --as alice
0. "HOST" sent "Please enjoy this @doubler."
> endo adopt --as alice 0 doubler
> endo list --as alice
doubler
> endo dismiss --as alice 0
```

# Names in transit are no-one's names

Sending a message with the `@name` notation means that the recipient
will see the name we chose for the object.
This is convenient in the common case, but not necessary.
The sender can choose a different name for the capability they send with the
notation `@name-they-see:name-we-have`.
Then, the receiver may choose to adopt the capability with a different name of
their own.

In this example, we send alice our "doubler" but let it appear as merely
"counter" in the message body.
Then, alice adopts "counter", giving it their own name, "redoubler".

```
> endo send alice 'Please enjoy this @counter:doubler.'
> endo inbox --as alice
1. "HOST" sent "Please enjoy this @counter."
> endo adopt --as alice 1 counter --name redoubler
> endo list --as alice
redoubler
> endo dismiss --as alice 1
```

# Mailboxes are symmetric

Guests can also send their host messages.
In this example, "alice" send the doubler back to us, their host.

```
> endo send HOST --as alice 'This is the @doubler you sent me.'
> endo inbox
0. "alice" sent "This is the @doubler you sent me."
> endo adopt 0 doubler doubler-from-alice
> endo dismiss 0
```

For a guest, the reserved name HOST refers to their host.
For both hosts and guests, SELF is the name of their own powers object.

# Familiar Chat

The pet daemon (or familiar, if you will) maintains a petstore and mailbox for
each agent, like you (the host), and all your guests (like the doubler-agent).
The `endo list` command shows you the pet names in your pet store.
The `endo inbox` command (and `endo inbox --follow` command), shows
messages from your various guests.

Weblets are web page caplets.
These are programs, like the counter and doubler above, except that
they run in a web page.
The pet daemon can host any number of fully independent web applications on
subdomains of `endo.localhost:8920`.
Each of these applications is connected to the pet daemon and can make
the same kinds of requests for data and powers.

_Familiar Chat_ is an example application that provides a web app for
interacting with your pet daemon.

```
> endo install familiar-chat cat.js --powers SELF
```

This command creates a web page named familiar-chat and endows it with the
authority to maintain your petstore and mailbox.
You can then open that page.

```
> endo open familiar-chat
```

So, if you were to simulate a request from your cat:

```
> endo mkguest cat
> endo request 'pet me' --as cat
```

This will appear in your Familiar Chat web page, where you can resolve
or reject that request with any value you have a pet name for.
For example, in your web browser, you will see something like:

> ## Familiar Chat
> 1. *cat* requests "pet me" `[ ]` `[resolve]` `[reject]`
If you enter the name `counter` and press `[resolve]` and return to your
terminal, you will see that the `endo request 'pet me' --as cat` command has
received the counter and exited.

> ## Familiar Chat
> 1. *cat* requests "pet me" _fulfilled_
# Running a confined script

Beyond weblets and worklets, a runlet is more like a `node` script:
it runs in your shell and interacts with you directly, not in a supervised
worker process behind the scenes.
But, like other caplets, it is fully confined and only receives the powers
you have granted.
They can also receive additional command line arguments.

Instead of exporting a `make` function, a runlet exports a `main` function
with a slightly different signature.

```js
export const main = async (powers, ...args) => {
console.log('Hello, World!', args);
return 42;
};
```

If a runlet returns a promise for some value, it will print that value
before exiting gracefully.

```
> endo run runlet.js a b c
Hello, World! [ 'a', 'b', 'c' ]
42
```
Loading

0 comments on commit 3656123

Please sign in to comment.