Before we get into subscription mechanics, there's three things we need to touch on that are very commonly used in Gall agents. The first is defining an agent's types in a
/sur structure file, the second is
mark files, and the third is permissions. Note the example code presented in this lesson will not yet build a fully functioning Gall agent, we'll get to that in the next lesson.
In the previous lesson on pokes, we used a very simple union in the
vase for incoming pokes:
=/ action !<(?(%inc %dec) vase)
A real Gall agent is likely to have a more complicated API. The most common approach is to define a head-tagged union of all possible poke types the agent will accept, and another for all possible updates it might send out to subscribers. Rather than defining these types in the agent itself, you would typically define them in a separate core saved in the
/sur directory of the desk. The
/sur directory is the canonical location for userspace type definitions.
With this approach, your agent can simply import the structures file and make use of its types. Additionally, if someone else wants to write an agent that interfaces with yours, they can include your structure file in their own desk to interact with your agent's API in a type-safe way.
Let's look at a practical example. If we were creating a simple To-Do app, our agent might accept a few possible
actions as pokes: Adding a new task, deleting a task, toggling a task's "done" status, and renaming an existing task. It might also be able to send
updates out to subscribers when these events occur. If our agent were named
%todo, it might have the following structure in
Click to expand
|%+$ id @+$ name @t+$ task [=name done=?]+$ tasks (map id task)+$ action$% [%add =name][%del =id][%toggle =id][%rename =id =name]==+$ update$% [%add =id =name][%del =id][%toggle =id][%rename =id =name][%initial =tasks]==--
%todo agent could then import this structure file with a fashep ford rune (
/-) at the beginning of the agent like so:
The agent's state could be defined like:
|%+$ versioned-state$% state-0==+$ state-0 [%0 =tasks:todo]+$ card card:agent:gall--
Then, in its
on-poke arm, it could handle these actions in the following manner:
++ on-poke|= [=mark =vase]^- (quip card _this)|^?> =(src.bowl our.bowl)?+ mark (on-poke:def mark vase)%todo-action=^ cards state(handle-poke !<(action:todo vase))[cards this]==::++ handle-poke|= =action:todo^- (quip card _state)?- -.action%add:_ state(tasks (~(put by tasks) now.bowl [name.action %.n])):~ :* %give %fact ~[/updates] %todo-update!>(`update:todo`[%add now.bowl name.action])====::%del:_ state(tasks (~(del by tasks) id.action)):~ :* %give %fact ~[/updates] %todo-update!>(`update:todo`action)====::%toggle:_ %= statetasks %+ ~(jab by tasks)id.action|=(=task:todo task(done !done.task))==:~ :* %give %fact ~[/updates] %todo-update!>(`update:todo`action)====::%rename:_ %= statetasks %+ ~(jab by tasks)id.action|=(=task:todo task(name name.action))==:~ :* %give %fact ~[/updates] %todo-update!>(`update:todo`action)====::%allow`state(friends (~(put in friends) who.action))::%kick:_ state(friends (~(del in friends) who.action)):~ [%give %kick ~[/updates] `who.action]====--
Let's break this down a bit. Firstly, our
on-poke arm includes a barket (
|^) rune. Barket creates a core with a
$ arm that's computed immediately. We extract the
vase to the
action:todo type and immediately pass it to the
handle-poke arm of the core created with the barket. This
handle-poke arm tests what kind of
action it's received by checking its head. It then updates the state, and also sends an update to subscribers, as appropriate. Don't worry too much about the
card for now - we'll cover subscriptions in the next lesson.
Notice that the
handle-poke arm produces a
(quip card _state) rather than
(quip card _this). The call to
handle-poke is also part of the following expression:
=^ cards state(handle-poke !<(action:todo vase))[cards this]
The tisket (
=^) expression takes two arguments: A new named noun to pin to the subject (
cards in this case), and an existing wing of the subject to modify (
state in this case). Since
(quip card _state), we're saving the
cards it produces to
cards and replacing the existing
state with its new one. Finally, we produce
[cards this], where
this will now contain the modified
[cards this] is a
(quip card _this), which our
on-poke arm is expected to produce.
This might seem a little convoluted, but it's a common pattern we do for two reasons. Firstly, it's not ideal to be passing around the entire
this agent core - it's much tidier just passing around the
state, until you actually want to return it to Gall. Secondly, It's much easier to read when the poke handling logic is separated into its own arm. This is a fairly simple example but if your agent is more complex, handling multiple marks and containing additional logic before it gets to the actual contents of the
vase, structuring things this way can be useful.
You can of course structure your
on-poke arm differently than we've done here - we're just demonstrating a typical pattern.
So far we've just used a
%noun mark for pokes - we haven't really delved into what such
marks represent, or considered writing custom ones.
Formally, marks are file types in the Clay filesystem. They correspond to mark files in the
/mar directory of a desk. The
%noun mark, for example, corresponds to the
/mar/noun.hoon file. Mark files define the actual hoon data type for the file (e.g. a
* noun for the
%noun mark), but they also specify some extra things:
- Methods for converting between the mark in question and other marks.
- Revision control functions like patching, diffing, merging, etc.
Aside from their use by Clay for storing files in the filesystem, they're also used extensively for exchanging data with the outside world, and for exchanging data between Gall agents. When data comes in from a remote ship, destined for a particular Gall agent, it will be validated by the file in
/mar that corresponds to its mark before being delivered to the agent. If the remote data has no corresponding mark file in
/mar or it fails validation, it will crash before it touches the agent.
A mark file is a door with exactly three arms. The door's sample is the data type the mark will handle. For example, the sample of the
%noun mark is just
non=*, since it handles any noun. The three arms are as follows:
grab: Methods for converting to our mark from other marks.
grow: Methods for converting from our mark to other marks.
grad: Revision control functions.
In the context of Gall agents, you'll likely just use marks for sending and receiving data, and not for actually storing files in Clay. Therefore, it's unlikely you'll need to write custom revision control functions in the
grad arm. Instead, you can simply delegate
grad functions to another mark - typically
%noun. If you want to learn more about writing such
grad functions, you can refer to the Marks Guide in the Clay vane documentation, which is much more comprehensive, but it's not necessary for our purposes here.
Here's a very simple mark file for the
action structure we created in the previous section:
Click to expand
/- todo|_ =action:todo++ grab|%++ noun action:todo--++ grow|%++ noun action--++ grad %noun--
We've imported the
/sur/todo.hoon structure library from the previous section, and we've defined the sample of the door as
=action:todo, since that's what it will handle. Now let's consider the arms:
grab: This handles conversion methods to our mark. It contains a core with arm names corresponding to other marks. In this case, it can only convert from a
nounmark, so that's the core's only arm. The
nounarm simply calls the
actionstructure from our structure library. This is called "clamming" or "molding" - when some noun comes in, it gets called like
(action:todo [some-noun])- producing data of the
actiontype if it nests, and crashing otherwise.
grow: This handles conversion methods from our mark. Like
grab, it contains a core with arm names corresponding to other marks. Here we've also only added an arm for a
%nounmark. In this case,
actiondata will come in as the sample of our door, and the
nounarm simply returns it, since it's already a noun (as everything is in Hoon).
grad: This is the revision control arm, and as you can see we've simply delegated it to the
This mark file could be saved as
/mar/todo/action.hoon, and then the
on-poke arm in the previous example could test for it instead of
%noun like so:
++ on-poke|= [=mark =vase]|^ ^- (quip card _this)?+ mark (on-poke:def mark vase)%todo-action...
%todo-action will be resolved to
/mar/todo/action.hoon - the hyphen will be interpreted as
/ if there's not already a
This simple mark file isn't all that useful. Typically, you'd add
json arms to
grab, which allow your data to be converted to and from JSON, and therefore allow your agent to communicate with a web front-end. Front-ends, JSON, and Eyre's APIs which facilitate such communications will be covered in the separate Full-Stack Walkthrough, which you might like to work through after completing this guide. For now though, it's still useful to use marks and understand how they work.
One further note on marks - while data from remote ships must have a matching mark file in
/mar, it's possible to exchange data between local agents with "fake" marks - ones that don't exist in
on-poke arm could, for example, use a made-up mark like
%foobar for actions initiated locally. This is because marks come into play only at validation boundaries, none of which are crossed when doing local agent-to-agent communications.
In example agents so far, we haven't bothered to check where events such as pokes are actually coming from - our example agents would accept data from anywhere, including random foreign ships. We'll now have a look at how to handle such permission checks.
Back in lesson 2 we discussed the bowl. The
bowl includes a couple of useful fields:
our field just contains the
@p of the local ship. The
src field contains the
@p of the ship from which the event originated, and is updated for every new event.
When messages come in over Ames from other ships on the network, they're encrypted with our ship's public keys and signed by the ship which sent them. The Ames vane decrypts and verifies the messages using keys in the Jael vane, which are obtained from the Azimuth Ethereum contract and Layer 2 data where Urbit ID ownership and keys are recorded. This means the originating
@p of all messages are cryptographically validated before being passed on to Gall, so the
@p specified in the
src field of the
bowl can be trusted to be correct, which makes checking permissions very simple.
You're free to use whatever logic you want for this, but the most common way is to use wutgar (
?>) and wutgal (
?<) runes, which are respectively True and False assertions that crash if they don't evaluate to the expected truth value. To only allow messages from the local ship, you can just do the following in the relevant agent arm:
?> =(src.bowl our.bowl)
A common permission is to allow messages from the local ship, as well as all of its moons, which can be done with the
team:title standard library function:
?> (team:title our.bowl src.bowl)
If we want to only allow messages from a particular set of ships, we could, for example, have a
(set @p) in our agent's state called
allowed. Then, we can use the
has:in set function to check:
?> (~(has in allowed) src.bowl)
If we wanted to check a ship was allowed in a particular group in the Groups app, we could scry our ship's
%group-store agent and compare:
?> .^(? %gx /(scot %p our.bowl)/group-store/(scot %da now.bowl)/groups/ship/~bitbet-bolbel/urbit-community/join/(scot %p src.bowl)/noun)
There are many ways to handle permissions, it all depends on your particular use case.
- An agent's type definitions live in the
/surdirectory of a desk.
/surfile is a core, typically containing a number of lusbuc (
/surfiles are imported with the fashep (
/-) Ford rune at the beginning of a file.
- Agent API types, for pokes and updates to subscribers, are commonly defined as head-tagged unions such as
- Mark files live in the
/mardirectory of a desk.
- A mark like
%foocorresponds to a file in
- Marks are file types in Clay, but are also used for passing data between agents as well as for external data generally.
- A mark file is a door with a sample of the data type it handles and exactly three arms:
groweach contain a core with arm names corresponding to other marks.
growdefine functions for converting to and from our mark, respectively.
graddefines revision control functions for Clay, but you'd typically just delegate such functions to the
- Incoming data from remote ships will have their marks validated by the corresponding mark file in
- Messages passed between agents on a local ship don't necessarily need mark files in
- Mark files are most commonly used for converting an agent's native types to JSON, in order to interact with a web front-end.
- The source of incoming messages from remote ships are cryptographically validated by Ames and provided to Gall, which then populates the
srcfield of the
- Permissions are most commonly enforced with wutgar (
?>) and wutgal (
?<) assertions in the relevant agent arms.
- Messages can be restricted to the local ship with
?> =(src.bowl our.bowl)or to its moons as well with
?> (team:title our.bowl src.bowl).
- There are many other ways to handle permissions, it just depends on the needs of the particular agent.
- Have a quick look at the tisket documentation.
- Try writing a mark file for the
update:todotype, in a similar fashion to the
action:todoone in the mark file section. You can compare yours to the one we'll use in the next lesson.