Skip to content
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
71 changes: 57 additions & 14 deletions skills/ad4m/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,40 @@ AD4M's core bootstrap languages (agent identity, neighbourhood sync, file storag

## IMPORTANT rules for how to use AD4M correctly

### 1. Use MCP — never curl it

### 1. Use --admin-credential if you setup a new ad4m-executor for you
There are multiple ways to use/connect to an ad4m-executor. If in doubt: get the ad4m-executor binary and set it up just for you — you are the owner/admin of that ad4m instance. You start it with `--admin-credential <random passphrase>`. The credential authenticates all MCP tool calls from your session automatically. If you're connecting to someone else's executor (no admin credential), use the JWT flow from rule 1 instead.

### 2. Use MCP — never curl it
Always use the MCP tools! AD4M also has a GraphQL interface, but almost everything you need to do on ad4m for your human should be possible via the MCP tools that the ad4m-executor provides. This skill is written for that!

**Do NOT try to call the MCP server with `curl`.** The MCP server uses Streamable HTTP transport — responses come back as `text/event-stream` (Server-Sent Events), not plain JSON. Raw `curl` calls will get garbled SSE data. You must connect via a proper MCP client.

**To connect**, add AD4M as an MCP server in your MCP client config:
**To connect**, add AD4M as an MCP server in your MCP client config (e.g. `~/.mcp.json`):
```json
{
"mcpServers": {
"ad4m": {
"url": "http://localhost:3001/mcp",
"headers": {
"Authorization": "<your-admin-credential>"
"Authorization": "Bearer <your-admin-credential>"
}
}
}
}
```
If your MCP client doesn't support `headers`, the admin credential can also be passed via the `request_capability` + `generate_jwt` MCP tools after connecting. **Important:** when you call `request_capability`, the 6-digit verification code is printed to the ad4m-executor's **stdout** — find it in the executor log file (e.g. `/tmp/ad4m-executor.log`) or by attaching to the screen session (`screen -r ad4m-executor`). You need this code to complete `generate_jwt`.

### 2. Use --admin-credential if you setup a new ad4m-executor for you
There are multiple ways to use/connect to an ad4m-executor. If in doubt: get the ad4m-executor binary and set it up just for you — you are the owner/admin of that ad4m instance. You start it with the `--admin-credential <random passphrase that you pick and use for all calls>` so that you don't have to request a JWT.
### 3. Authentication without admin-credential
In case your human wants to share their ad4m identity with you and runs their own ad4m-executor instance (or the UI Adam Launcher), you are not the sole owner of the executor and likely won't have / be able to choose the admin credential.

**Also, with some MCP clients, there COULD BE A PROBLEM RELAYING THE AUTHORIZATION HEADER FROM THE .mcp.json FILE.**

Either way, you can also use the MCP JWT auth flow to request a JWT capability token:
1. Call `request_capability` with `app_name`, `app_desc` (e.g. `"OpenClaw"`, `"AD4M bot - <your name>"`)
2. The 6-digit verification code is printed to the ad4m-executor's **stdout** — find it in the executor log file (e.g. `/tmp/ad4m-executor.log`) or by attaching to the screen session (`screen -r ad4m-executor`) OR ask your human if they run a UI launcher.
3. Call `generate_jwt` with the `request_id` (from step 1) and the `code` (6-digit string from the log)
4. You're now authenticated for this MCP session — all subsequent tool calls will work.
(This is similar to the GraphQL auth-flow, but different in one regard: MCP keeps an SSE connection and stores the session token server-side. That way the client doesn't need to store and re-send the JWT. You have a standing connection with a logged in session.)

### 3. Update waker config & restart when executor changes
If you restart ad4m-executor or switch from one executor to another (from local to remote or between remote executors) you need to restart the waker with the correct executor URL and JWT/credential in its config.
Expand Down Expand Up @@ -71,6 +83,33 @@ After setting up the executor, **immediately write down** (to a file where you w

Remember: after setup, **ad4m-executor is running** in the background. You don't need to start it again. If you restart the machine or the screen session dies, you DO need to restart it and unlock the agent wallet again.

### 10. Perspective UUIDs are local — Neighbourhood URLs are global
A **perspective UUID** is a local identifier on YOUR device only. It is NOT shared and NOT meaningful to other agents or humans. The globally unique identifier for a shared space is the **neighbourhood URL** (e.g. `neighbourhood://Qm...`). When someone gives you a neighbourhood URL to join, you call `neighbourhood_join_from_url` — AD4M creates a LOCAL perspective that syncs with that neighbourhood and assigns it a random UUID on your machine. To find the mapping between neighbourhood URLs and your local perspective UUIDs, use `list_perspectives()` — each perspective entry includes its `neighbourhood` URL (if shared) alongside its local `uuid`.

### 11. Write down neighbourhood context after joining or creating
Every time you join or create a neighbourhood, **immediately write to your memory/notes file**:
- The **neighbourhood URL** (`neighbourhood://Qm...`) — this is the global ID
- Your **local perspective UUID** for it (from `list_perspectives()`)
- **What this space is for** — its purpose, topic, community name
- **Who is in it** — your human, other agents, other humans you know about
- **How you got there** — who invited you, why you joined, what you were asked to do there
- **Channel IDs** you care about (from `channel_query`)

You NEED this context when you wake up from a waker event. The wake message only gives you a perspective UUID and channel ID — without your notes, you won't know what community this is, who you're talking to, or why you're there.

After every interaction in a Neighbourhood, check if something important happened there and potentailly update your notes. Try to keep a brief model about the state of the conversation in each Neighbourhood and make sure you DON'T MIX CONVERSATIONS between neighbourhoods. (Same would be true for channels inside neighbourhoods)

### 12. Install a waker for mentions on every new neighbourhood join
Every time you join a new neighbourhood, you MUST set up a waker subscription for **mentions** at minimum. Without a waker, you will never know when someone talks to you in that space.

```
1. get_mention_waker_config(perspective_id: "<your-local-uuid>")
2. Add the returned subscription to your waker-config.json
3. Restart the waker process
```

If your human asks you to monitor a specific channel for ALL messages (not just mentions), also call `generate_waker_query` for that channel and add that subscription too.

## Model base expressions / IDs
Model instances are constructed around a base node (called base expression, also ID). Usually those are random literal strings (`literal://string:xyz`). Their properties hang off of that base node with predicates as defined by the class.

Expand Down Expand Up @@ -105,11 +144,14 @@ Community (ad4m://self)
```
1. neighbourhood_join_from_url(url: "neighbourhood://Qm...")
2. list_perspectives() → find the joined perspective UUID
3. get_mention_waker_config(perspective_id: "...") → store config + (re-)start waker
4. channel_query(perspective_id: "...") → list channels
5. get_children(perspective_id, expression_address: "<channel-id>")
(the NH URL maps to a LOCAL perspective UUID)
3. WRITE DOWN to your memory file: NH URL, local perspective UUID,
what this space is for, who's in it, why you joined (see rule 11)
4. get_mention_waker_config(perspective_id: "...") → store config + (re-)start waker (see rule 12)
5. channel_query(perspective_id: "...") → list channels
6. get_children(perspective_id, expression_address: "<channel-id>")
→ then message_get(perspective_id, expression_address: <child>) per child
6. message_create(perspective_id, expression_address: "literal://string:<random-id>", body: "Hello!")
7. message_create(perspective_id, expression_address: "literal://string:<random-id>", body: "Hello!")
+ add_child(perspective_id, parent_address: "<channel-id>", child_address: "literal://string:<same-id>")
```

Expand All @@ -122,14 +164,16 @@ Community (ad4m://self)
The waker POSTs to your `/hooks/wake` endpoint with this JSON body:
```json
{
"text": "New messages in an AD4M neighbourhood.\nRead the AD4M skill for instructions on how to handle this.\n\nMCP endpoint: http://localhost:3001/mcp\nAuth credential: your-admin-credential\nAgent DID: did:key:z6Mk...\nPerspective: cda8c4fc-...\nChannel: literal://string:channel-id\nNeighbourhood: neighbourhood://Qm...\nSubscription: flux-messages\nEvent type: channel-messages",
"text": "New messages in an AD4M neighbourhood.\nRead the AD4M skill for instructions on how to handle this.\n\nMCP endpoint: http://localhost:3001/mcp\nAuth credential: your-admin-credential\nAgent DID: did:key:z6Mk...\nPerspective: cda8c4fc-...\nChannel: literal://string:channel-id\nSubscription: flux-messages\nEvent type: channel-messages",
"mode": "now"
}
```

**Use `/hooks/wake`, NOT `/hooks/agent`.** `/hooks/wake` enqueues the event into your main agent session, which has your skills (including this AD4M skill) loaded. `/hooks/agent` spawns an isolated sub-agent that won't have your skills.

Parse the `text` field to extract: `MCP endpoint`, `Auth credential`, `Agent DID`, `Perspective`, `Channel`, `Event type`. Use these values directly.
Parse the `text` field to extract: `MCP endpoint`, `Auth credential`, `Agent DID`, `Perspective` (local UUID), `Channel`, `Event type`. Use these values directly.

**First: check your memory/notes file** for the perspective UUID from the wake message. Your notes will tell you what community this is, who's in it, and why you're there. This context is essential for responding appropriately.

**Auth:** Use the `Auth credential` value as the `Authorization` header for all MCP requests.

Expand Down Expand Up @@ -191,9 +235,8 @@ Create `waker-config.json`:
{
"id": "flux-messages",
"type": "channel-messages",
"perspective": "perspective-uuid",
"perspective": "your-local-perspective-uuid",
"channel": "literal://string:channel-id",
"neighbourhood": "neighbourhood://Qm...",
"query": "SELECT * FROM link WHERE source = 'literal://string:channel-id' AND predicate = 'ad4m://has_child'"
}
]
Expand Down
21 changes: 16 additions & 5 deletions skills/ad4m/references/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,24 @@ Streamable HTTP (bidirectional HTTP with SSE-like streaming). Connect at `http:/

## Authentication

Bearer token in the HTTP `Authorization` header:
Two authentication methods are available:

```
Authorization: Bearer <token>
```
### Option A: `--admin-credential` flag (recommended for single-agent setups)

Start the executor with `--admin-credential <secret>`. All MCP tool calls from that session are automatically authenticated — no extra auth step needed. The credential is passed as part of the session context.

> **Note:** HTTP `Authorization` headers are NOT reliably forwarded to MCP tool handlers by all MCP clients. If you set `headers` in your `.mcp.json` config, the header may not reach the auth check. Use `--admin-credential` or the JWT flow below instead.

### Option B: JWT auth flow (works with any MCP client)

Use the MCP auth tools (no auth required to call these):

1. `request_capability(app_name: "AI Agent", app_desc: "AD4M bot")` → returns `request_id`
2. Find the 6-digit verification code in the executor's **stdout** (log file or screen session)
3. `generate_jwt(request_id: "<from step 1>", code: "<6-digit code>")` → returns JWT
4. All subsequent tool calls in this session are authenticated

The token is the `--admin-credential` value passed to the executor. Without an admin credential, empty token has full access.
Without any admin credential configured, empty token has full access.

## Core Tools

Expand Down
13 changes: 6 additions & 7 deletions skills/ad4m/references/waker.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ Create `waker-config.json` (see also `waker-config.example.json`):
{
"id": "flux-messages",
"type": "channel-messages",
"perspective": "perspective-uuid",
"perspective": "your-local-perspective-uuid",
"channel": "literal://string:channel-id",
"neighbourhood": "neighbourhood://Qm...",
"query": "SELECT * FROM link WHERE source = 'literal://string:channel-id' AND predicate = 'ad4m://has_child'"
}
]
Expand All @@ -58,11 +57,12 @@ Create `waker-config.json` (see also `waker-config.example.json`):
|-------|----------|-------------|
| `id` | yes | Unique identifier |
| `type` | yes | `"mention"` or `"channel-messages"` — determines the wake message |
| `perspective` | yes | AD4M perspective UUID |
| `perspective` | yes | Your **local** perspective UUID (from `list_perspectives()`) |
| `channel` | yes | Channel address (where to read/post) |
| `neighbourhood` | no | Neighbourhood URL (for context in wake messages) |
| `query` | yes | SurrealQL subscription query |

> **Note:** Perspective UUIDs are local to your device. To find the local UUID for a neighbourhood, call `list_perspectives()` and match by the neighbourhood URL in the response. Store this mapping in your memory file (see SKILL.md rule 11).

## Running

```bash
Expand All @@ -87,7 +87,7 @@ screen -dmS ad4m-waker bash -c 'node ad4m-waker.js --config waker-config.json 2>
**`/hooks/wake` payload:**
```json
{
"text": "New messages in an AD4M neighbourhood.\nRead the AD4M skill for instructions on how to handle this.\n\nMCP endpoint: http://localhost:3001/mcp\nAuth credential: your-admin-credential\nAgent DID: did:key:z6Mk...\nPerspective: cda8c4fc-...\nChannel: literal://string:channel-id\nNeighbourhood: neighbourhood://Qm...\nSubscription: flux-messages\nEvent type: channel-messages",
"text": "New messages in an AD4M neighbourhood.\nRead the AD4M skill for instructions on how to handle this.\n\nMCP endpoint: http://localhost:3001/mcp\nAuth credential: your-admin-credential\nAgent DID: did:key:z6Mk...\nPerspective: cda8c4fc-...\nChannel: literal://string:channel-id\nSubscription: flux-messages\nEvent type: channel-messages",
"mode": "now"
}
```
Expand All @@ -98,9 +98,8 @@ The `text` field contains key-value pairs, one per line:
- **MCP endpoint** — where to connect (e.g. `http://localhost:3001/mcp`)
- **Auth credential** — admin credential for the Authorization header
- **Agent DID** — the agent's own DID (to identify own messages)
- **Perspective** — perspective UUID to operate on
- **Perspective** — local perspective UUID to operate on (look up your memory file for context about this space)
- **Channel** — channel address (where to read/post)
- **Neighbourhood** — neighbourhood URL (if configured in subscription)
- **Subscription** — subscription ID
- **Event type** — `"mention"` or `"channel-messages"`

Expand Down
12 changes: 4 additions & 8 deletions skills/ad4m/waker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The waker is a config-driven runner — it executes whatever SurrealQL queries y
Typical flow:
1. Agent joins a neighbourhood via `neighbourhood_join_from_url`
2. Agent calls `get_mention_waker_config` with the perspective UUID
3. Agent appends the returned subscription entry to the waker config file (adding `type`, `channel`, and `neighbourhood` fields)
3. Agent appends the returned subscription entry to the waker config file (adding `type` and `channel` fields)
4. Agent restarts the waker

---
Expand All @@ -48,17 +48,15 @@ Typical flow:
{
"id": "flux-all-messages",
"type": "channel-messages",
"perspective": "<neighbourhood-uuid>",
"perspective": "<your-local-perspective-uuid>",
"channel": "literal://string:<channel-id>",
"neighbourhood": "neighbourhood://Qm...",
"query": "SELECT * FROM link WHERE source = 'literal://string:<channel-id>' AND predicate = 'ad4m://has_child'"
},
{
"id": "mention-<did-suffix>",
"type": "mention",
"perspective": "<neighbourhood-uuid>",
"perspective": "<your-local-perspective-uuid>",
"channel": "literal://string:<channel-id>",
"neighbourhood": "neighbourhood://Qm...",
"query": "SELECT * FROM link WHERE fn::contains(string::lowercase(fn::parse_literal(target)), 'agentname') OR fn::contains(string::lowercase(fn::parse_literal(target)), 'did:key:z6Mks...')"
}
]
Expand All @@ -84,9 +82,8 @@ Typical flow:
|-------|----------|-------------|
| `id` | ✅ | Unique identifier for this subscription |
| `type` | ✅ | `"mention"` or `"channel-messages"` — determines the wake message content |
| `perspective` | ✅ | AD4M perspective UUID to subscribe to |
| `perspective` | ✅ | Your **local** perspective UUID (from `list_perspectives()`) |
| `channel` | ✅ | Channel address (so the agent knows where to read/post messages) |
| `neighbourhood` | | Neighbourhood URL (for additional context) |
| `query` | ✅ | SurrealQL query — fires when the result set changes |

### Subscription types
Expand Down Expand Up @@ -119,7 +116,6 @@ Auth credential: <admin-credential>
Agent DID: did:key:z6Mk...
Perspective: 01409ead-3e13-4ca6-99ac-e1b623c18604
Channel: literal://string:gjgfascqbfhntekmtvhtbohu
Neighbourhood: neighbourhood://QmzSYwdhcjCcf726JkvGKKw7bszp3Jd2NsNN2ULkxJ8VYxdU9wv
Subscription: mention-guAacszuc2Jd
Event type: mention
```
Expand Down
12 changes: 4 additions & 8 deletions skills/ad4m/waker/ad4m-waker.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@
* {
* "id": "flux-messages",
* "type": "channel-messages",
* "perspective": "perspective-uuid",
* "perspective": "your-local-perspective-uuid",
* "channel": "literal://string:channel-id",
* "neighbourhood": "neighbourhood://Qm...",
* "query": "SELECT * FROM link WHERE source = 'literal://string:channel-id' AND predicate = 'ad4m://has_child'"
* }
* ]
Expand All @@ -35,9 +34,8 @@
* Subscription fields:
* - id: Unique subscription identifier
* - type: "mention" | "channel-messages" — determines the wake message
* - perspective: AD4M perspective UUID
* - perspective: Local AD4M perspective UUID (from list_perspectives)
* - channel: Channel address (where to read/post messages)
* - neighbourhood: (optional) Neighbourhood URL for context
* - query: SurrealQL subscription query
*/

Expand Down Expand Up @@ -277,9 +275,8 @@ Config file format:
{
"id": "my-subscription",
"type": "channel-messages",
"perspective": "perspective-uuid",
"perspective": "your-local-perspective-uuid",
"channel": "literal://string:channel-id",
"neighbourhood": "neighbourhood://Qm...",
"query": "SELECT * FROM link WHERE ..."
}
]
Expand All @@ -288,9 +285,8 @@ Config file format:
Subscription fields:
id Unique identifier for this subscription
type "mention" or "channel-messages" — determines wake message
perspective AD4M perspective UUID
perspective Local AD4M perspective UUID (from list_perspectives)
channel Channel address (for reading/posting messages)
neighbourhood (optional) Neighbourhood URL
query SurrealQL subscription query
`);
process.exit(0);
Expand Down
6 changes: 2 additions & 4 deletions skills/ad4m/waker/waker-config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,15 @@
{
"id": "flux-all-messages",
"type": "channel-messages",
"perspective": "your-neighbourhood-perspective-uuid",
"perspective": "your-local-perspective-uuid",
"channel": "literal://string:your-channel-id",
"neighbourhood": "neighbourhood://Qm...",
"query": "SELECT * FROM link WHERE source = 'literal://string:your-channel-id' AND predicate = 'ad4m://has_child'"
},
{
"id": "mention-did-z6MksZbUemc",
"type": "mention",
"perspective": "your-neighbourhood-perspective-uuid",
"perspective": "your-local-perspective-uuid",
"channel": "literal://string:your-channel-id",
"neighbourhood": "neighbourhood://Qm...",
"query": "SELECT * FROM link WHERE fn::contains(string::lowercase(fn::parse_literal(target)), 'yourname') OR fn::contains(string::lowercase(fn::parse_literal(target)), 'did:key:z6MksZbUemcXmxjUeez8RSAbg7jkMFwkpSRRe5nLDKwDuATB')"
}
]
Expand Down
Loading