@uuaid/pillar 0.2.0-alpha.9 → 2.0.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/HOSTING-LIMITS.md +432 -0
- package/INTERACTION-CONVENTION.md +222 -1
- package/README.md +358 -17
- package/bin/carrier.mjs +7 -1
- package/bin/pillar-ask.mjs +1 -0
- package/bin/pillar-blob.mjs +139 -0
- package/bin/pillar-directory.mjs +232 -0
- package/bin/pillar-escrow.mjs +675 -0
- package/bin/pillar-responder.mjs +49 -3
- package/bin/pillar-seed.mjs +113 -13
- package/bin/pillar.mjs +299 -7
- package/package.json +30 -7
- package/src/blob/blob.mjs +510 -0
- package/src/blob/broker.mjs +129 -0
- package/src/blob/http.mjs +334 -0
- package/src/blob/store.mjs +55 -0
- package/src/escrow/box.mjs +43 -0
- package/src/escrow/escrow-service.mjs +538 -0
- package/src/escrow/rails/evm-rail.mjs +427 -0
- package/src/escrow/rails/index.mjs +41 -0
- package/src/escrow/rails/zilligon-rail.mjs +73 -0
- package/src/escrow/terms.mjs +368 -0
- package/src/escrow/token-registry.mjs +231 -0
- package/src/escrow/verify.mjs +119 -0
- package/src/escrow/zilligon.mjs +266 -0
- package/src/identity/keychain.mjs +27 -10
- package/src/identity/tier.mjs +130 -0
- package/src/identity/trust.mjs +63 -0
- package/src/index.mjs +28 -2
- package/src/mailbox/mailbox.mjs +33 -2
- package/src/net/carrier-client.mjs +93 -14
- package/src/net/carrier-server.mjs +212 -15
- package/src/net/delegation.mjs +209 -0
- package/src/net/envelope.mjs +81 -0
- package/src/net/link-offer.mjs +107 -0
- package/src/net/refusals.mjs +126 -0
- package/src/net/seed-registry.mjs +467 -0
- package/src/net/seed-tier.mjs +185 -0
- package/src/node-check.mjs +32 -0
- package/src/observability.mjs +141 -0
- package/src/pillar.mjs +180 -17
- package/src/vda/core.mjs +290 -0
- package/src/vda/directory.mjs +87 -0
- package/src/vda/instruments.mjs +246 -0
- package/src/vda/shims.mjs +77 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Pillar Interaction Convention v1
|
|
2
2
|
|
|
3
3
|
App-layer standard for bidirectional interactive conversations over the
|
|
4
|
-
pillar carrier (v0.2.0-alpha.5
|
|
4
|
+
pillar carrier (written against v0.2.0-alpha.5; still current as of
|
|
5
|
+
2.0.1). No core protocol changes — this is a
|
|
5
6
|
convention any enrolled agent opts into by mutual agreement. LLM-agnostic:
|
|
6
7
|
pure JSON envelopes; any client that can HTTPS + Ed25519-seal can speak it.
|
|
7
8
|
|
|
@@ -14,6 +15,34 @@ pure JSON envelopes; any client that can HTTPS + Ed25519-seal can speak it.
|
|
|
14
15
|
| `ack` | receipt for any envelope; echoes the id being acked |
|
|
15
16
|
| `message` | fire-and-forget (default kind, unchanged) |
|
|
16
17
|
|
|
18
|
+
### Reserved prefixes
|
|
19
|
+
|
|
20
|
+
| prefix | owner | status |
|
|
21
|
+
|----------|------------------------|---------------------------------------|
|
|
22
|
+
| `esig:*` | esig-suite (Esig-Lead) | reserved by convention — NOT enforced |
|
|
23
|
+
|
|
24
|
+
`kind` is unvalidated except against a delegation: `seal()` takes
|
|
25
|
+
`body.kind ?? "message"` with no enum, allowlist or regex, and neither `open()`
|
|
26
|
+
nor the carrier's `_handlePost()` checks it on a plainly signed envelope. The
|
|
27
|
+
one exception is a DELEGATED envelope: `open()` refuses a capability name used
|
|
28
|
+
as a kind (`inbox`, even under `"*"`) and a kind outside the delegation's
|
|
29
|
+
`kinds` (`src/net/envelope.mjs`, `kind-reserved-capability` and
|
|
30
|
+
`kind-not-covered`). That limits what a delegate may *emit*; it never grants a
|
|
31
|
+
recipient anything. A reserved prefix is therefore a naming convention between
|
|
32
|
+
lanes, **not an access control** — any enrolled agent can emit `esig:sealed`.
|
|
33
|
+
|
|
34
|
+
What `kind` does give you: it sits inside the signed canonical body, and
|
|
35
|
+
`open()` binds the sender key to `sender`, so a kind is unforgeable *as another
|
|
36
|
+
sender* — it is authentic as "this sender asserted this kind." What it does not
|
|
37
|
+
give you: any authorization value. An `esig:*` envelope from an unrelated uuaid
|
|
38
|
+
is a well-formed envelope with a misleading label; every authorization decision
|
|
39
|
+
MUST rest on the sealed payload's own signature, never on `kind`.
|
|
40
|
+
|
|
41
|
+
Per rule 5 below, `kind` is cleartext to the carrier, so `esig:sign-request`
|
|
42
|
+
discloses "uuaid X asked uuaid Y to sign something, at time T" to every relay.
|
|
43
|
+
Lanes running signature workflows SHOULD use an opaque routing kind (`esig:m`)
|
|
44
|
+
and put the real verb inside the sealed payload.
|
|
45
|
+
|
|
17
46
|
## Payload fields (all optional unless stated)
|
|
18
47
|
|
|
19
48
|
- `text` — human/agent-readable body
|
|
@@ -42,3 +71,195 @@ pure JSON envelopes; any client that can HTTPS + Ed25519-seal can speak it.
|
|
|
42
71
|
sends a `request` and blocks until the `ack`+`reply` arrive (default
|
|
43
72
|
120s timeout), printing: request id → ack latency → reply latency → text.
|
|
44
73
|
Synchronous Q→A over the same carrier as everything else.
|
|
74
|
+
|
|
75
|
+
## App-level convention: read receipts (`pillar-read-receipt`)
|
|
76
|
+
|
|
77
|
+
*Status: app-level convention, first implemented by the Pillar mobile app.
|
|
78
|
+
No wire change — it rides the existing `ack` kind inside an ordinary
|
|
79
|
+
E2E-encrypted payload; carriers and non-participating clients are unaffected.*
|
|
80
|
+
|
|
81
|
+
A client signals "I have seen your messages up to here" by sending a sealed
|
|
82
|
+
`ack` envelope whose decrypted payload is exactly:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{ "v": 1, "type": "pillar-read-receipt", "upTo": "env-…", "thread": "env-…" }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `upTo` — id of the most recent inbound envelope the reader has seen.
|
|
89
|
+
- `thread` — the thread root id (rule 1) the receipt applies to.
|
|
90
|
+
- `inReplyTo` is deliberately omitted; `upTo` plays that role, and keeping
|
|
91
|
+
the payload free of it lets older senders ignore the ack without
|
|
92
|
+
mis-marking a single message as delivered.
|
|
93
|
+
|
|
94
|
+
Semantics for the receiving (original sender) side:
|
|
95
|
+
|
|
96
|
+
- Mark every outbound envelope in `thread` created at or before `upTo` as
|
|
97
|
+
**read**; read implies delivered, so a missing ack state is backfilled.
|
|
98
|
+
- Only honor receipts whose sender is the thread's counterparty.
|
|
99
|
+
|
|
100
|
+
Etiquette for the sending (reader) side:
|
|
101
|
+
|
|
102
|
+
- Send when a thread is opened and when a new inbound envelope arrives while
|
|
103
|
+
the thread is open — at most one receipt per `upTo` value per view.
|
|
104
|
+
- Only send to known contacts (same gate as auto-acks; sealing needs the
|
|
105
|
+
peer's pubkey anyway).
|
|
106
|
+
- Read receipts MUST NOT be re-acked (rule 2 covers `request`/`reply` only),
|
|
107
|
+
SHOULD NOT be rendered as messages, and MUST NOT increment unread
|
|
108
|
+
counters.
|
|
109
|
+
- Privacy is symmetric (WhatsApp-style): a client configured not to send
|
|
110
|
+
read receipts also ignores incoming ones.
|
|
111
|
+
|
|
112
|
+
### What a read receipt does NOT mean
|
|
113
|
+
|
|
114
|
+
**A receipt proves that the endpoint holding the key acknowledged the
|
|
115
|
+
envelope. It does NOT prove that the human, agent or seat capable of acting
|
|
116
|
+
on the content has read it.** Implementations MUST NOT present it as more
|
|
117
|
+
than that, and MUST NOT let a UI label ("Read", a blue tick) — or a
|
|
118
|
+
machine-readable field name in a ledger or API response (e.g.
|
|
119
|
+
`delivery_verified: true`) — imply comprehension, agreement, or that a task
|
|
120
|
+
has been picked up. The prohibition binds the data model, not just the
|
|
121
|
+
presentation layer: a field name no human ever renders can oversell a
|
|
122
|
+
receipt exactly as effectively as a checkmark can.
|
|
123
|
+
|
|
124
|
+
This is not hypothetical. In a fleet deployment (2026-09-04) a governance
|
|
125
|
+
halt was delivered, acked in 21 s and replied to in one inbox surface while
|
|
126
|
+
the working session that actually held the capability never saw it: a fully
|
|
127
|
+
green receipt above a structurally ignorant actor. A receipt is evidence
|
|
128
|
+
about a *mailbox*, never about an *understanding*.
|
|
129
|
+
|
|
130
|
+
Consequences for anyone wiring receipts into an ack/receipt ledger:
|
|
131
|
+
|
|
132
|
+
- Label the state after the surface that produced it ("acknowledged in
|
|
133
|
+
inbox"), never after the actor you hope is behind it.
|
|
134
|
+
- A receipt is a lower bound on delivery and says nothing about routing
|
|
135
|
+
*inside* the recipient. If the recipient fans work out to other sessions,
|
|
136
|
+
seats or tabs, that hop is invisible here and needs its own evidence —
|
|
137
|
+
and that evidence has an owner: the RECIPIENT, not the sender and not the
|
|
138
|
+
courier, owes a second signal originating from the acting seat. An
|
|
139
|
+
unowned requirement is exactly how a green receipt ended up sitting above
|
|
140
|
+
a structurally ignorant actor in the incident above.
|
|
141
|
+
- Never close, escalate or exonerate on a receipt alone. It answers "did the
|
|
142
|
+
bytes arrive and get opened", which is strictly weaker than "was this
|
|
143
|
+
acted on" — and it is the second question that decisions usually turn on.
|
|
144
|
+
|
|
145
|
+
The receipt is still worth having: it is signed by the recipient's key, so it
|
|
146
|
+
is stronger evidence of arrival than any sender-side or render-side proxy
|
|
147
|
+
(e.g. reading a UI surface back). Claim exactly that much and no more.
|
|
148
|
+
|
|
149
|
+
## Handle convention (`...`)
|
|
150
|
+
|
|
151
|
+
*Status: app-level convention, first implemented by the Pillar mobile app.
|
|
152
|
+
Display/UX-level only — the wire protocol continues to carry full UUAIDs in
|
|
153
|
+
every envelope; nothing in this section changes any protocol behavior.*
|
|
154
|
+
|
|
155
|
+
The handle prefix in the Pillar ecosystem is **`...` (three dots)** — NOT
|
|
156
|
+
`@` or `#`. The ecosystem already truncates UUAIDs for display as
|
|
157
|
+
`…a2e0d9c6`; `...` formalizes that visual language into a handle syntax.
|
|
158
|
+
|
|
159
|
+
### Grammar
|
|
160
|
+
|
|
161
|
+
- `...<name>` — a directory-resolved handle: three dots, then
|
|
162
|
+
`[a-z0-9-]+` (case-insensitive). Matched case-insensitively against the
|
|
163
|
+
carrier directory's agent `name` and `role` fields (e.g. `...my-agent`,
|
|
164
|
+
`...helper`). Resolution prefers an exact name/role match, then a prefix
|
|
165
|
+
match, then a substring match; the first non-empty tier wins. Multiple
|
|
166
|
+
matches → the client shows a picker; zero matches → a clear
|
|
167
|
+
"no agent found" error.
|
|
168
|
+
- `...<hex-suffix>` — three dots, then 6+ hex chars (case-insensitive).
|
|
169
|
+
Matched against the tail of directory agents' UUAID localIds AND their
|
|
170
|
+
pubkey-derived localIds (dashes stripped before comparison, e.g.
|
|
171
|
+
`...7c801eba`). If a suffix matches nobody in the directory it is NOT
|
|
172
|
+
usable on its own: the user must also supply the full UUAID + public key
|
|
173
|
+
— a suffix alone is never enough to trust.
|
|
174
|
+
|
|
175
|
+
Parsing is strict: exactly three leading dots (whitespace-trimmed). `..x`,
|
|
176
|
+
`....x`, a bare `...`, and characters outside the two charsets all fail to
|
|
177
|
+
parse. A value that is both a legal name and 6+ hex chars (`...cafe`,
|
|
178
|
+
`...decade`) is tried as a NAME first; suffix matching applies only when
|
|
179
|
+
name resolution finds nobody (documented decision: name-then-suffix, so a
|
|
180
|
+
directory agent literally named `decade` always beats a localId-tail
|
|
181
|
+
collision).
|
|
182
|
+
|
|
183
|
+
### Resolution and trust rules
|
|
184
|
+
|
|
185
|
+
- A handle ALWAYS resolves to a `(uuaid, publicKey)` pair taken from the
|
|
186
|
+
directory or an existing contact. A pubkey-less resolution MUST NOT be
|
|
187
|
+
accepted for sending — candidates without a valid 64-hex Ed25519 public
|
|
188
|
+
key are dropped before matching.
|
|
189
|
+
- When a handle resolves, the client shows the matched name + full UUAID +
|
|
190
|
+
public-key suffix and lets the user eyeball the binding before adding
|
|
191
|
+
(trust-on-first-use with visible binding). The resolved fields remain
|
|
192
|
+
editable.
|
|
193
|
+
|
|
194
|
+
Reference implementation: `apps/mobile/src/services/handles.ts`
|
|
195
|
+
(`parseHandle` / `resolveHandle`), exercised by
|
|
196
|
+
`scripts/test-handles.mjs`.
|
|
197
|
+
|
|
198
|
+
## App-level convention: group chats (`pillar-group` v1)
|
|
199
|
+
|
|
200
|
+
*Status: app-level convention, first implemented by the Pillar mobile app
|
|
201
|
+
(2026-09-03). Superset of the reference `Pillar.sendGroup` shape. No wire
|
|
202
|
+
change: every group message is an ordinary 1:1 envelope; carriers and
|
|
203
|
+
non-participating clients see nothing new.*
|
|
204
|
+
|
|
205
|
+
There is deliberately **no group key**. A group message is sealed separately
|
|
206
|
+
to every member (per-member fan-out, one envelope each, kind `message`), so
|
|
207
|
+
membership changes never require rekeying and a carrier still cannot read
|
|
208
|
+
anything or even tell that a group exists. The group context rides INSIDE the
|
|
209
|
+
sealed payload under `_group`:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"text": "hello crew",
|
|
214
|
+
"_group": {
|
|
215
|
+
"v": 1,
|
|
216
|
+
"id": "grp-0123456789abcdef",
|
|
217
|
+
"name": "Weekend crew",
|
|
218
|
+
"members": ["uuaid:…sender", "uuaid:…", "uuaid:…"],
|
|
219
|
+
"keys": { "uuaid:…": "<ed25519 pubkey hex>" },
|
|
220
|
+
"msgId": "gm-…",
|
|
221
|
+
"op": "message"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- `id` — stable group id, `grp-<16 hex>`. The reference `sendGroup` sends
|
|
227
|
+
only `{ name, members }`; a client receiving a tag WITHOUT `id` derives a
|
|
228
|
+
local id from the name plus the sorted roster so the conversation still
|
|
229
|
+
groups. `id` is a grouping key, not an authority.
|
|
230
|
+
- `members` — the full roster, uuaid strings, including the sender (same as
|
|
231
|
+
the reference). A recipient that is sealed to but missing from the roster
|
|
232
|
+
adds itself locally.
|
|
233
|
+
- `keys` — OPTIONAL trust-on-first-use hints so members can seal to each
|
|
234
|
+
other without a prior contact exchange. A receiver MUST binding-check every
|
|
235
|
+
hint (the key must derive the uuaid) before storing it, and MUST prefer a
|
|
236
|
+
key already known from a contact or from a plainly verified envelope's
|
|
237
|
+
`transportSignature` (first key wins). Delegated envelopes are signed by a
|
|
238
|
+
device key and therefore teach nothing about the persona key.
|
|
239
|
+
- `msgId` — `gm-…`, identical across the per-member envelopes of one group
|
|
240
|
+
message; receivers show one bubble per msgId and senders aggregate delivery
|
|
241
|
+
state across the fan-out (read only when EVERY recipient read).
|
|
242
|
+
- `op` — `message` (default, may be omitted) | `create` | `rename` | `add` |
|
|
243
|
+
`leave`. Control ops carry an empty or informational `text` (`add` lists the
|
|
244
|
+
added uuaids comma-separated) and render as system lines, not bubbles.
|
|
245
|
+
|
|
246
|
+
Roster rules for receivers: an unknown `id` creates the group; only a current
|
|
247
|
+
MEMBER may rename or add; nobody can remove anyone but themselves (`leave`
|
|
248
|
+
drops the sender). A member who left keeps history and stops sending.
|
|
249
|
+
|
|
250
|
+
Acks and read receipts: group traffic uses kind `message`, so implementations
|
|
251
|
+
SHOULD ack it like `request`/`reply` (rule 2 covers the minimum, not the
|
|
252
|
+
maximum). A read receipt for a group envelope names that envelope's own thread
|
|
253
|
+
(group messages carry no `thread`); the original sender applies it to every
|
|
254
|
+
earlier outbound envelope it sealed to that reader in the same group.
|
|
255
|
+
|
|
256
|
+
## App-level payload fields: `quote` and `senderName`
|
|
257
|
+
|
|
258
|
+
Both optional, both inside the sealed payload, both ignorable.
|
|
259
|
+
|
|
260
|
+
- `quote: { "id": "env-…", "text": "…", "from": "uuaid:…" }` — reply-with-quote.
|
|
261
|
+
`text` is a ≤300-char excerpt copied at reply time; receivers render it as
|
|
262
|
+
a quoted block and may scroll to `id` when they hold that envelope.
|
|
263
|
+
- `senderName: "Ada"` — the sender's self-declared display name (≤64 chars).
|
|
264
|
+
UX-level only: a client uses it as a fallback after its own contact name
|
|
265
|
+
and MUST NOT treat it as an identity claim (the uuaid + key are).
|