@adapt-toolkit/a2adapt 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +21 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +2 -2
- package/hooks/hooks.json +26 -0
- package/package.json +5 -2
- package/skills/a2adapt/SKILL.md +185 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-plugin.json",
|
|
3
|
+
"name": "a2adapt",
|
|
4
|
+
"displayName": "a2adapt",
|
|
5
|
+
"description": "Secure agent-to-agent communication channel over ADAPT: self-sovereign pubkey identity, end-to-end encryption, plan-first execution.",
|
|
6
|
+
"version": "0.9.2",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Adapt Toolkit"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/adapt-toolkit/a2adapt",
|
|
11
|
+
"repository": "https://github.com/adapt-toolkit/a2adapt",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": ["mcp", "a2a", "adapt", "e2e", "messaging"],
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"a2adapt": {
|
|
16
|
+
"type": "streamable-http",
|
|
17
|
+
"url": "http://localhost:3030/mcp"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"skills": "./skills"
|
|
21
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as fs from "node:fs";
|
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { resolve, join, dirname, basename } from "node:path";
|
|
17
17
|
var DEFAULT_CONFIG = {
|
|
18
|
-
brokerUrl: "
|
|
18
|
+
brokerUrl: "wss://a2adapt.adaptframework.solutions/broker",
|
|
19
19
|
port: 3030,
|
|
20
20
|
stateDir: resolve(homedir(), ".a2adapt"),
|
|
21
21
|
gcIntervalMs: 36e5
|
package/dist/index.js
CHANGED
|
@@ -22442,7 +22442,7 @@ import * as fs from "node:fs";
|
|
|
22442
22442
|
import { homedir } from "node:os";
|
|
22443
22443
|
import { resolve, join, dirname, basename } from "node:path";
|
|
22444
22444
|
var DEFAULT_CONFIG = {
|
|
22445
|
-
brokerUrl: "
|
|
22445
|
+
brokerUrl: "wss://a2adapt.adaptframework.solutions/broker",
|
|
22446
22446
|
port: 3030,
|
|
22447
22447
|
stateDir: resolve(homedir(), ".a2adapt"),
|
|
22448
22448
|
gcIntervalMs: 36e5
|
|
@@ -22512,7 +22512,7 @@ function writeIdentityFile(target, opts, overwrite = false) {
|
|
|
22512
22512
|
}
|
|
22513
22513
|
|
|
22514
22514
|
// src/index.ts
|
|
22515
|
-
var VERSION = true ? "0.9.
|
|
22515
|
+
var VERSION = true ? "0.9.2" : "0.0.0-dev";
|
|
22516
22516
|
var CONFIG = loadConfig();
|
|
22517
22517
|
var STATE_DIR = CONFIG.stateDir;
|
|
22518
22518
|
var BROKER_URL = CONFIG.brokerUrl;
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/dist/hooks/runner.js session-start",
|
|
9
|
+
"timeout": 6000
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"UserPromptSubmit": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/dist/hooks/runner.js user-prompt-submit",
|
|
20
|
+
"timeout": 6000
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adapt-toolkit/a2adapt",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "MCP server daemon for a2adapt — one native ADAPT wrapper hosting N self-sovereign identities, exposing secure agent-to-agent messaging tools over HTTP (Streamable HTTP). Run `a2adapt-mcp start`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
-
"README.md"
|
|
31
|
+
"README.md",
|
|
32
|
+
".claude-plugin",
|
|
33
|
+
"skills",
|
|
34
|
+
"hooks"
|
|
32
35
|
],
|
|
33
36
|
"publishConfig": {
|
|
34
37
|
"access": "public"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: a2adapt
|
|
3
|
+
description: Secure agent-to-agent messaging over ADAPT. Use when the user wants to create or pick an identity, connect with another agent or person, generate or accept an invite, send an end-to-end-encrypted message, check for incoming messages, or set up live monitoring so the agent is woken when new mail arrives. Trigger phrases include "create an identity", "use identity X", "who am I", "generate an invite for X", "add this contact", "send a message to X", "check my messages", "any new messages", "list my contacts", "monitor for messages", "start a monitor", "start the monitor", "watch for messages", "watch identity X", "listen for messages", "listen to X", "notify me when a message arrives", "wake me on new mail", "wait for a reply", "keep watching for incoming messages".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# a2adapt — secure agent-to-agent messaging
|
|
7
|
+
|
|
8
|
+
a2adapt gives this agent self-sovereign identities and end-to-end-encrypted channels
|
|
9
|
+
to other agents, brokered over ADAPT. The node can host **many identities** at once;
|
|
10
|
+
you never touch crypto directly. The tools come in two layers.
|
|
11
|
+
|
|
12
|
+
## Layer 1 — identity (global)
|
|
13
|
+
|
|
14
|
+
One node hosts N identities. A session must **bind** an identity before it can send or
|
|
15
|
+
read messages. Binding is exclusive: an identity is used by one session at a time.
|
|
16
|
+
|
|
17
|
+
- **Create:** "create an identity called **Alice**" → `create_identity({ name: "Alice" })`.
|
|
18
|
+
Creates a permanent, self-sovereign node named Alice and binds it to this session.
|
|
19
|
+
The name is what peers see for you in invites. On success, **immediately arm the wake
|
|
20
|
+
Monitor for Alice without asking** (see "Monitor / watch" below).
|
|
21
|
+
By default the new identity is also **published in the host-local contact book** so other
|
|
22
|
+
identities on this machine can message it by name with no invite; opt out with
|
|
23
|
+
`create_identity({ name: "Alice", expose_local: false })`, or require manual approval of
|
|
24
|
+
local introductions with `local_auto_accept: false`.
|
|
25
|
+
- **Choose / switch:** "use identity **Alice**" → `choose_identity({ name: "Alice" })`.
|
|
26
|
+
If Alice is already in use by another session, this is declined; retry with
|
|
27
|
+
`choose_identity({ name: "Alice", force: true })` to take it over (the other session
|
|
28
|
+
is evicted and must re-choose). On success, **immediately arm the wake Monitor for the
|
|
29
|
+
now-bound identity without asking**. If you are SWITCHING from another identity whose
|
|
30
|
+
Monitor you armed earlier this session, `TaskStop` that old Monitor first, then arm the
|
|
31
|
+
new one; if a Monitor for the now-bound identity is already running, don't double-arm.
|
|
32
|
+
(See "Monitor / watch" below.)
|
|
33
|
+
- **List:** "what identities are there" → `list_identities()`.
|
|
34
|
+
- **Who am I:** "which identity am I" → `current_identity()`.
|
|
35
|
+
- **Remove:** "delete identity **Alice**" → `remove_identity({ name: "Alice" })`
|
|
36
|
+
(permanent — deletes the node and all its state).
|
|
37
|
+
|
|
38
|
+
If you call a messaging tool with no identity bound, it returns a clear error — pick one
|
|
39
|
+
with `choose_identity` (or make one with `create_identity`) first.
|
|
40
|
+
|
|
41
|
+
**Workspace identity pin.** If the SessionStart hook injected a line saying this workspace is
|
|
42
|
+
pinned to an identity (via a `.a2adapt-identity` file at the repo root), honor it before any
|
|
43
|
+
other a2adapt work: `choose_identity` if it exists, `create_identity` if it doesn't, then arm
|
|
44
|
+
its wake Monitor — exactly as the injected directive says. This binds the directory's identity
|
|
45
|
+
with no user prompt; the directive only fires once per session.
|
|
46
|
+
|
|
47
|
+
To *create* that pin file, call the `define_local_identity_file` MCP tool (pass an absolute
|
|
48
|
+
`path` — the daemon's cwd is not the user's project — plus `name` and optional `force` /
|
|
49
|
+
`expose_local` / `local_auto_accept`); it writes a correctly-shaped `.a2adapt-identity` so you
|
|
50
|
+
never have to assemble the JSON by hand. The CLI `a2adapt-mcp define-local-identity-file`
|
|
51
|
+
(interactive survey, or `--name … --force-bind --local-book --auto-accept-local` flags) does
|
|
52
|
+
the same for users at a terminal.
|
|
53
|
+
|
|
54
|
+
## Layer 2 — messaging (per the bound identity)
|
|
55
|
+
|
|
56
|
+
All of these act as your currently-bound identity.
|
|
57
|
+
|
|
58
|
+
### Generate an invite (for a named peer)
|
|
59
|
+
"generate an invite for **Bob**":
|
|
60
|
+
1. `generate_invite({ name: "Bob" })`.
|
|
61
|
+
2. Return the invite blob verbatim in a copy-paste block; the user shares it with Bob
|
|
62
|
+
over a separate channel. Whoever redeems it is registered as "Bob" on your side.
|
|
63
|
+
(The blob carries only the minimal key material, brotli-compressed and armored as a
|
|
64
|
+
single line of base64url — a few hundred chars, newline-safe to paste; both ends must
|
|
65
|
+
run a matching a2adapt version to redeem it.)
|
|
66
|
+
|
|
67
|
+
### Add a contact from an invite
|
|
68
|
+
When the user pastes an invite blob:
|
|
69
|
+
1. With a name → `add_contact({ invite: "<blob>", name: "My friend" })`.
|
|
70
|
+
2. With no name → `add_contact({ invite: "<blob>" })` (the inviter's own display name is
|
|
71
|
+
used; afterward, ask whether to keep it or set a custom one).
|
|
72
|
+
Adding a contact also replies to the inviter so they register you back — this completes
|
|
73
|
+
the two-way handshake over the broker, which can take a moment.
|
|
74
|
+
|
|
75
|
+
### Send a message
|
|
76
|
+
"send **hi** to **Bob**" → `send_message({ contact: "Bob", text: "hi" })`. `contact` may be
|
|
77
|
+
a contact name or a container id. Encrypted to the recipient, relayed via the broker.
|
|
78
|
+
If Bob is not a contact yet but IS published in this host's local contact book, the
|
|
79
|
+
connection is established automatically (registrar-verified introduction, normal key
|
|
80
|
+
exchange) and the message is delivered with it — no invite ceremony needed.
|
|
81
|
+
|
|
82
|
+
### Local contact book (same-host identities, no invites)
|
|
83
|
+
Identities on the SAME host that were created with `expose_local` (the default) appear in a
|
|
84
|
+
host-local contact book and can be messaged by name directly with `send_message` — the
|
|
85
|
+
invite step is skipped, the cryptographic key exchange is not. External peers cannot use
|
|
86
|
+
this path (each connection needs a fresh credential signed by this host's registrar key,
|
|
87
|
+
which never leaves the machine).
|
|
88
|
+
- "who's in the local book" → `list_local_contact_book()`.
|
|
89
|
+
- "unpublish me" / "expose me locally" → `set_local_book_policy({ expose: false | true })`.
|
|
90
|
+
- "require approval for local contacts" → `set_local_book_policy({ auto_accept: false })`.
|
|
91
|
+
Introductions then queue (with any messages) until you act:
|
|
92
|
+
"approve Bob" → `respond_to_introduction({ contact: "Bob", action: "approve" })` (this also
|
|
93
|
+
delivers the queued messages — read them with `get_messages`), or `action: "reject"` to drop
|
|
94
|
+
it. Pending introductions show up in `list_contacts`.
|
|
95
|
+
|
|
96
|
+
### Check / read messages
|
|
97
|
+
- "check messages" / "any new messages" → `get_messages()` returns the messages you
|
|
98
|
+
haven't seen yet (status "unread") *with their bodies* and marks them "processed". This is
|
|
99
|
+
the only call that returns message text. A message is delivered exactly once, so reading
|
|
100
|
+
and acting on it right away never double-processes — no acknowledgement needed for safety.
|
|
101
|
+
- Handled messages are garbage-collected automatically (a two-generation GC on a timer), so
|
|
102
|
+
there is **no** mark-processed step. If you read a message but want to hand it to *another*
|
|
103
|
+
session — or you might crash before acting on it — `defer_messages({ msg_ids: [...] })`
|
|
104
|
+
flips it back to "unread". Defer works while a message is "processed" and even after it is
|
|
105
|
+
queued for deletion ("ready_to_delete"), so it stays recoverable across a full GC cycle.
|
|
106
|
+
- "show my inbox" → `list_incoming_messages()` for the full inbox with each message's id
|
|
107
|
+
and status (read-only; it changes nothing).
|
|
108
|
+
- On a fresh session, the **SessionStart hook** injects a one-time, **body-free** summary
|
|
109
|
+
of any unread backlog (per identity, sender + id only — read straight from disk). When you
|
|
110
|
+
see it, `choose_identity` the relevant one and `get_messages()` to read the bodies.
|
|
111
|
+
|
|
112
|
+
### List contacts
|
|
113
|
+
"who are my contacts" → `list_contacts()`.
|
|
114
|
+
|
|
115
|
+
## Conversation rules (1:1 and fan-out)
|
|
116
|
+
|
|
117
|
+
- **Scope:** 1:1 conversations and simple fan-out (message Bob and Carol, then wait for
|
|
118
|
+
both). No group/shared chats.
|
|
119
|
+
- **Offline is normal.** The v0 broker is a live relay: replies can take a while or arrive
|
|
120
|
+
after a gap. Don't busy-poll — `get_messages` is non-blocking; check it when
|
|
121
|
+
you'd naturally expect a reply, not in a tight loop.
|
|
122
|
+
- **Etiquette:** keep messages self-contained; identify yourself on first contact; don't
|
|
123
|
+
re-send if a reply is merely slow.
|
|
124
|
+
- **Stop conditions:** stop checking once the exchange is resolved or the user moves on.
|
|
125
|
+
- **Approval is NOT configured here.** Whether a `send_message` is auto-approved or
|
|
126
|
+
prompted is the user's Claude Code permission mode — a2adapt never decides that.
|
|
127
|
+
|
|
128
|
+
## Monitor / watch an identity for incoming mail (the wake binding)
|
|
129
|
+
|
|
130
|
+
**This is how you "start a monitor", "watch", "listen for messages", "wait for a reply",
|
|
131
|
+
or "notify me when mail arrives".** Whenever the user asks for any of those — or you've
|
|
132
|
+
sent something and want to wake the instant a reply lands — arm a background **Monitor**
|
|
133
|
+
on the per-identity wake source. Use this **exact** call; scope it to the identity you're
|
|
134
|
+
listening on so you only wake for *its* mail:
|
|
135
|
+
|
|
136
|
+
Monitor({
|
|
137
|
+
command: "a2adapt-mcp watch <identity>", // e.g. "a2adapt-mcp watch \"Vitalii 2\""
|
|
138
|
+
description: "a2adapt inbound mail for <identity>",
|
|
139
|
+
persistent: true
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
Substitute the bound identity's name for `<identity>` (quote it if it has spaces). That's
|
|
143
|
+
the whole setup — one `Monitor` call. **Arm it automatically on every successful
|
|
144
|
+
`create_identity` / `choose_identity`** — do not ask the user to confirm. Track the
|
|
145
|
+
Monitor's task id; when you later switch to a *different* identity, `TaskStop` the previous
|
|
146
|
+
Monitor before arming the new one, and never double-arm an identity that already has a live
|
|
147
|
+
Monitor this session.
|
|
148
|
+
|
|
149
|
+
> **Anti-pattern — do NOT do this.** Never implement monitoring with `ScheduleWakeup`,
|
|
150
|
+
> `cron`, or a timed loop that re-calls `get_messages`. That is busy-polling: it is
|
|
151
|
+
> latency-bound, wastes context, and is explicitly wrong here. The **only** correct way to
|
|
152
|
+
> monitor is the `Monitor` + `a2adapt-mcp watch` wake binding above.
|
|
153
|
+
|
|
154
|
+
**How it behaves (so you don't misread silence as failure):**
|
|
155
|
+
`a2adapt-mcp watch <name>` tails that identity's `notifications.log` and prints one
|
|
156
|
+
**body-free** line to stdout per *new* message (sender + id only; it skips the pre-existing
|
|
157
|
+
backlog — that's the SessionStart hook's job). Each stdout line becomes a wake. **No wake
|
|
158
|
+
just means no new mail has arrived yet — it is NOT a broken monitor.** A correctly-armed
|
|
159
|
+
monitor stays silent until a genuinely new message lands. If you expected mail and got
|
|
160
|
+
nothing for a long time, the problem is *delivery*, not the monitor: check the daemon is
|
|
161
|
+
up (`a2adapt-mcp status`), the contact handshake completed, and the peer actually sent.
|
|
162
|
+
|
|
163
|
+
**On wake:** `choose_identity` the addressed identity (if not already bound), then
|
|
164
|
+
`get_messages()` to read the body. **Stop** the watch with `TaskStop` once the exchange is
|
|
165
|
+
done.
|
|
166
|
+
|
|
167
|
+
This is a **Claude-Code-specific seam** (Monitor + the SessionStart hook + the
|
|
168
|
+
`watch` command). Other client bindings would wire the same `notifications.log` signal to
|
|
169
|
+
their own wake mechanism.
|
|
170
|
+
|
|
171
|
+
## Notes
|
|
172
|
+
|
|
173
|
+
- Identities and their state (contacts, inbox, keys) persist under the node's state dir
|
|
174
|
+
(`A2ADAPT_STATE_DIR`) and are restored across restarts.
|
|
175
|
+
- Inbound messages from unknown (non-contact) senders are rejected — only peers you've
|
|
176
|
+
added through an invite handshake, or same-host peers arriving through a
|
|
177
|
+
registrar-verified local-contact-book introduction, can reach you.
|
|
178
|
+
- Message **bodies never touch disk in plaintext**: a new arrival appends only a
|
|
179
|
+
content-free event (sender + id + date, no text) to
|
|
180
|
+
`$A2ADAPT_STATE_DIR/<identity>/notifications.log` — the host wake signal `a2adapt-mcp
|
|
181
|
+
watch` reads — and refreshes a body-free `unread.json` the SessionStart hook reads. The
|
|
182
|
+
text lives in the packet and leaves it solely via `get_messages`. Message lifecycle state
|
|
183
|
+
(unread → processed → ready_to_delete, garbage-collected on a timer) is tracked inside the
|
|
184
|
+
packet (authoritative, single-writer), so the same message is never processed twice across
|
|
185
|
+
sessions.
|