@ouro.bot/friends 0.1.0-alpha.4

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.
Files changed (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +514 -0
  3. package/changelog.json +34 -0
  4. package/dist/a2a/index.d.ts +102 -0
  5. package/dist/a2a/index.js +198 -0
  6. package/dist/agent-peer.d.ts +17 -0
  7. package/dist/agent-peer.js +57 -0
  8. package/dist/channel.d.ts +11 -0
  9. package/dist/channel.js +132 -0
  10. package/dist/consent.d.ts +34 -0
  11. package/dist/consent.js +62 -0
  12. package/dist/coordination.d.ts +100 -0
  13. package/dist/coordination.js +255 -0
  14. package/dist/file-bundle.d.ts +12 -0
  15. package/dist/file-bundle.js +23 -0
  16. package/dist/grant-store-file.d.ts +16 -0
  17. package/dist/grant-store-file.js +136 -0
  18. package/dist/grant-store.d.ts +7 -0
  19. package/dist/grant-store.js +8 -0
  20. package/dist/grants.d.ts +39 -0
  21. package/dist/grants.js +84 -0
  22. package/dist/group-context.d.ts +21 -0
  23. package/dist/group-context.js +144 -0
  24. package/dist/index.d.ts +49 -0
  25. package/dist/index.js +105 -0
  26. package/dist/link-identity.d.ts +14 -0
  27. package/dist/link-identity.js +88 -0
  28. package/dist/mcp/bin.d.ts +2 -0
  29. package/dist/mcp/bin.js +16 -0
  30. package/dist/mcp/dispatch.d.ts +14 -0
  31. package/dist/mcp/dispatch.js +432 -0
  32. package/dist/mcp/index.d.ts +6 -0
  33. package/dist/mcp/index.js +14 -0
  34. package/dist/mcp/run-main.d.ts +7 -0
  35. package/dist/mcp/run-main.js +45 -0
  36. package/dist/mcp/schemas.d.ts +10 -0
  37. package/dist/mcp/schemas.js +398 -0
  38. package/dist/mcp/server.d.ts +21 -0
  39. package/dist/mcp/server.js +194 -0
  40. package/dist/mission-share.d.ts +94 -0
  41. package/dist/mission-share.js +232 -0
  42. package/dist/mission-store-file.d.ts +18 -0
  43. package/dist/mission-store-file.js +153 -0
  44. package/dist/mission-store.d.ts +10 -0
  45. package/dist/mission-store.js +9 -0
  46. package/dist/missions.d.ts +31 -0
  47. package/dist/missions.js +98 -0
  48. package/dist/notes.d.ts +11 -0
  49. package/dist/notes.js +90 -0
  50. package/dist/observability.d.ts +27 -0
  51. package/dist/observability.js +31 -0
  52. package/dist/outcomes.d.ts +9 -0
  53. package/dist/outcomes.js +51 -0
  54. package/dist/resolver.d.ts +28 -0
  55. package/dist/resolver.js +187 -0
  56. package/dist/results.d.ts +8 -0
  57. package/dist/results.js +2 -0
  58. package/dist/room.d.ts +22 -0
  59. package/dist/room.js +40 -0
  60. package/dist/share.d.ts +106 -0
  61. package/dist/share.js +223 -0
  62. package/dist/standing.d.ts +83 -0
  63. package/dist/standing.js +111 -0
  64. package/dist/store-file.d.ts +21 -0
  65. package/dist/store-file.js +264 -0
  66. package/dist/store.d.ts +9 -0
  67. package/dist/store.js +4 -0
  68. package/dist/tokens.d.ts +8 -0
  69. package/dist/tokens.js +26 -0
  70. package/dist/trust-explanation.d.ts +16 -0
  71. package/dist/trust-explanation.js +74 -0
  72. package/dist/trust-mutation.d.ts +4 -0
  73. package/dist/trust-mutation.js +29 -0
  74. package/dist/types.d.ts +164 -0
  75. package/dist/types.js +51 -0
  76. package/dist/util/cap-string.d.ts +7 -0
  77. package/dist/util/cap-string.js +35 -0
  78. package/dist/verifier.d.ts +11 -0
  79. package/dist/verifier.js +29 -0
  80. package/dist/whoami.d.ts +7 -0
  81. package/dist/whoami.js +39 -0
  82. package/package.json +68 -0
package/README.md ADDED
@@ -0,0 +1,514 @@
1
+ # @ouro.bot/friends
2
+
3
+ **An open identity, relationship, and multiplayer substrate for AI agents.**
4
+ *Who am I, who are you, who else is in the room* — for any harness, any agent.
5
+
6
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](./LICENSE)
7
+ [![npm](https://img.shields.io/badge/npm-%40ouro.bot%2Ffriends-cb3837.svg)](https://www.npmjs.com/package/@ouro.bot/friends)
8
+  ·  store-only  ·  transport-agnostic  ·  no daemon  ·  alpha
9
+
10
+ <!-- OPERATOR: enrich the vision/soul here (the "taming" / Little-Prince framing is yours to voice) -->
11
+
12
+ > "It is the time you have wasted for your rose that makes your rose so important. […]
13
+ > People have forgotten this truth," said the fox. "But you must not forget it. You become
14
+ > responsible, forever, for what you have tamed."
15
+ > — Antoine de Saint-Exupéry, *The Little Prince*
16
+
17
+ An agent meets the same people over and over — across a CLI, a chat thread, an email, a voice
18
+ call — and it meets other agents. `friends` is where it keeps track of **who it knows**: a single
19
+ merged identity per person (who they are across every channel), the notes it has written about
20
+ them, and where each relationship sits on a **trust ladder**. A stranger is just another voice
21
+ until ties are established; establishing those ties — *taming*, in the book's word — is what moves
22
+ someone from `stranger` to `acquaintance` to `friend` to `family`, and what makes the agent
23
+ behave differently toward them.
24
+
25
+ ---
26
+
27
+ ## What `friends` is
28
+
29
+ `friends` is a **library + an MCP server** that gives an agent a who's-who. It is deliberately
30
+ narrow:
31
+
32
+ - **Store-only.** Every tool reads or writes *records*. There is no agent turn, no LLM call, no
33
+ session — which is exactly what makes it harness-agnostic. The same package serves Claude Code,
34
+ Codex, a Copilot CLI, or anything else that can call a function or speak MCP.
35
+ - **Transport-agnostic.** When two agents need to exchange something, `friends` produces and
36
+ consumes a plain envelope; **the wire between them is the caller's job.** An optional
37
+ git-mailbox transport ships alongside, but the core never opens a socket.
38
+ - **No daemon.** Nothing to run in the background. Point it at a directory and call it.
39
+ - **Bring your own storage.** The library never decides *where* or *how* your data lives — you
40
+ pass a path (or a connection string) and, if you want, your own storage backend.
41
+
42
+ It is built as **five additive capability layers**. Each is a minimal primitive on the one before
43
+ it; none is a workflow engine; removing any layer leaves the ones beneath it unchanged.
44
+
45
+ ---
46
+
47
+ ## What it does — the five capabilities
48
+
49
+ ### 1. Identity + the cross-agent moat
50
+
51
+ The foundation: **recognize a person across every channel, and decide how much to trust them.**
52
+
53
+ Every person or peer the agent meets becomes a `FriendRecord` — one merged identity that collapses
54
+ all of someone's channel handles together, keyed by a **join key** (`provider:externalId`, never a
55
+ local UUID). The same person reached on a CLI today and a chat thread tomorrow resolves to the
56
+ *same* record.
57
+
58
+ Relationships sit on a four-rung **trust ladder** (`family` / `friend` / `acquaintance` /
59
+ `stranger`), and the agent's behavior is gated by where someone sits on it. Two agents that have
60
+ never shared a database can agree a party is the same person **and** share what they know about
61
+ them — **with consent, and without first-party knowledge ever being clobbered**. First-party
62
+ knowledge is **structurally inviolable** and trust is **non-transitive**: an import can add an
63
+ attributed, quarantined note, but it can never change who you trust. (See
64
+ [Trust & consent model](#trust--consent-model).)
65
+
66
+ ### 2. Connectivity — the git-backed A2A transport
67
+
68
+ How two agents actually reach each other, without a server in the middle.
69
+
70
+ The optional `@ouro.bot/friends/a2a` sub-export is a **pure git-mailbox transport**: zero runtime
71
+ dependencies, and it does **no git or network itself**. The host does every git op (clone / pull /
72
+ add / commit / push); the library only **computes a message file's path + bytes** and
73
+ **parses / validates / orders / dedups** the files the host hands back. Two agents authenticate as
74
+ two distinct git identities sharing a private mailbox repo; each agent is the single writer of its
75
+ own outbox.
76
+
77
+ The mailbox is treated as **untrusted infrastructure**: a hostile mailbox can only **deny or
78
+ replay** — never **escalate** — because an import never touches first-party notes or trust.
79
+
80
+ ### 3. Shared memory — the mission ledger
81
+
82
+ What two agents collectively *learned* doing work together.
83
+
84
+ A **mission** is named by a cross-agent `missionKey` (a ticket id, `repo#PR`, a slug two agents
85
+ agree on out of band). A `MissionRecord` remembers the work: its status, participants, outcomes,
86
+ and `learnings`. The same import discipline as the moat applies — first-party `learnings` are
87
+ physically separated from `importedLearnings` accepted from a peer, and an imported learning can
88
+ never masquerade as first-party.
89
+
90
+ ### 4. Earned standing — advisory reputation, never on the wire
91
+
92
+ A read-only assessment of how a peer has actually performed on work *you personally did with it*.
93
+
94
+ `standing` is **derived from your own first-party outcomes** — a tier
95
+ (`proven` / `reliable` / `mixed` / `untested` / `troubled`) computed on read, persisted nowhere.
96
+ The bright line: **trust decides, standing informs.** Standing **never auto-changes trust** and
97
+ **never crosses the wire** — there is no envelope field and no message type to express it, which is
98
+ the anti-Sybil core (a collusion ring cannot vouch each other into your standing).
99
+
100
+ ### 5. Coordination — negotiate who does the work
101
+
102
+ The five layers close the loop: agents can now negotiate **who does a mission.**
103
+
104
+ Five verbs — `request` / `offer` / `accept` / `decline` / `handoff` — ride one new transport `kind`
105
+ over the same mailbox. The **only** persisted effect is one additive sub-object on the mission an
106
+ agent already shares: its **assignment** (who currently holds it) plus an append-only log of every
107
+ ask, bid, and answer. It is a single negotiated *field*, not a scheduler: a `handoff` never *forces*
108
+ an assignee onto anyone (the receiver's own `accept` confirms it — non-transitive), assignment is
109
+ advisory metadata rather than a granted capability, and conflicts resolve last-writer-wins by
110
+ timestamp. No queue, no DAG, no workflow DSL.
111
+
112
+ > The stack, in one line: agents **recognize** each other (1), **reach** each other (2),
113
+ > **remember** shared work (3), **assess** each other (4), and **negotiate** who does what (5) —
114
+ > each a minimal primitive on the last.
115
+
116
+ ---
117
+
118
+ ## Quickstart
119
+
120
+ `friends` is consumed two ways. Use the **library** when you're writing code that owns the agent;
121
+ use the **MCP server** when you want any MCP-speaking harness to call the same surface as tools.
122
+
123
+ ### Install
124
+
125
+ ```sh
126
+ npm install @ouro.bot/friends
127
+ ```
128
+
129
+ ### A) The library — the `FriendStore` seam + the core API
130
+
131
+ Two seams. You bring a **store**; you resolve through the **resolver**.
132
+
133
+ ```ts
134
+ import { openFileBundle, FriendResolver, describeTrustContext } from "@ouro.bot/friends"
135
+
136
+ // 1. A store — where friend records live. openFileBundle persists one JSON file per
137
+ // friend under the directory you give it (and wires the sibling _grants/ /
138
+ // _missions/ collections). Or implement FriendStore yourself — see "Bring your
139
+ // own storage".
140
+ const { store } = openFileBundle("/path/to/bundle/friends")
141
+
142
+ // 2. A resolver — turns an incoming external identity into a FriendRecord + the
143
+ // capabilities of the channel it arrived on. Created per incoming message.
144
+ const { friend, channel } = await new FriendResolver(store, {
145
+ provider: "aad",
146
+ externalId: "aad-object-id",
147
+ tenantId: "tenant-guid",
148
+ displayName: "Jordan",
149
+ channel: "teams",
150
+ }).resolve()
151
+
152
+ // 3. Gate behavior on trust.
153
+ const trust = describeTrustContext({ friend, channel: channel.channel })
154
+ // → { level, basis: "direct" | "shared_group" | "unknown", permits, constraints, ... }
155
+ ```
156
+
157
+ `FriendStore` is the injectable abstraction — no friend code touches `fs` directly except the
158
+ `FileFriendStore` adapter — so you can back friends with anything (in-memory, a database, a remote
159
+ service) by implementing the interface. The full public surface is listed under
160
+ [Public API](#public-api).
161
+
162
+ ### B) The MCP server — `friends-mcp`
163
+
164
+ `@ouro.bot/friends` ships an MCP server that exposes the library as a tool surface for any
165
+ MCP-speaking harness. **The server runs no agent turn — it is a pure record read/write surface over
166
+ the library, which is exactly what makes it harness-agnostic.** Each tool call reads or writes
167
+ friend records against a directory you point it at.
168
+
169
+ The store directory is the **only** coupling between the server and a bundle. Provide it with
170
+ `--dir <path>` or the `FRIENDS_DIR` environment variable (**the flag wins** when both are set, and
171
+ one of them is required — the server exits otherwise). It points at the bundle's `friends/`
172
+ directory — the same directory a `FileFriendStore` persists to.
173
+
174
+ A sample `.mcp.json`:
175
+
176
+ ```json
177
+ {
178
+ "mcpServers": {
179
+ "friends": {
180
+ "command": "npx",
181
+ "args": ["-y", "--package", "@ouro.bot/friends", "friends-mcp", "--dir", "<path-to-friends-dir>"]
182
+ }
183
+ }
184
+ }
185
+ ```
186
+
187
+ For local development against a checkout, point at the built binary instead:
188
+
189
+ ```json
190
+ {
191
+ "mcpServers": {
192
+ "friends": {
193
+ "command": "node",
194
+ "args": ["<repo>/dist/mcp/bin.js", "--dir", "<path-to-friends-dir>"]
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ You can also `npm pack` then
201
+ `npx -y --package ./ouro.bot-friends-<version>.tgz friends-mcp --dir <path>`, or `npm link` then
202
+ `friends-mcp --dir <path>`. The server speaks JSON-RPC 2.0 over stdio with **dual framing** —
203
+ Content-Length and newline-delimited JSON — auto-detected from the first message, so it works with
204
+ harnesses on either convention.
205
+
206
+ #### The tool surface — 29 tools
207
+
208
+ A thin 1:1 mapping over the library (no domain logic in the server):
209
+
210
+ | Tool | What it does |
211
+ |---|---|
212
+ | `resolve_party` | Resolve an external identity into a friend record (creating one on first contact); returns `{ friend, channel, created }`. |
213
+ | `describe_trust` | Explain a friend's trust context (level, basis, permits, constraints). |
214
+ | `get_friend` | Fetch one friend record by uuid or name. |
215
+ | `list_friends` | List friends, optionally filtered by trust / kind and limited. |
216
+ | `save_note` | Save a friend's name, a tool preference, or a general note (with `override`). |
217
+ | `record_interaction` | Accumulate token usage and/or append a shared-mission outcome. |
218
+ | `upsert_group` | Link participants to a shared group, promoting strangers to acquaintances. |
219
+ | `set_trust` | Set a friend's trust level (mirrored onto `role`). |
220
+ | `link_identity` | Link an external identity, merging any orphan record that holds it. |
221
+ | `unlink_identity` | Remove an external identity from a friend. |
222
+ | `onboard_agent` | Upsert an agent-peer record from resolved coordinates (no HTTP fetch). |
223
+ | `whoami` | Resolve the machine owner and which record represents the self. |
224
+ | `channel_caps` | Return a channel's capabilities. |
225
+ | `resolve_room` | Resolve a room (a group's external id) into its members, each with trust context and `knownVia`. |
226
+ | `share_profile` | **Producer** — prepare a consent-gated, scope-filtered, provenance-preserving profile-share envelope for another agent. |
227
+ | `import_profile` | **Consumer** — import a profile-share envelope (non-clobbering merge into the imported namespace; never touches first-party notes or trust). |
228
+ | `grant_share` | Mint an explicit, revocable consent grant (an agent may receive a scope of a subject — a friend's profile or a mission). |
229
+ | `revoke_share` | Revoke a consent grant by id (tombstones it; the right-to-be-forgotten lever). |
230
+ | `list_shares` | List consent grants with their effective state (the audit + revoke surface). |
231
+ | `record_mission` | Upsert a shared **mission** by its `missionKey` — append first-party learnings / participants / outcomes, set status. |
232
+ | `get_mission` | Fetch one mission record by its local uuid id. |
233
+ | `list_missions` | List mission records, optionally limited. |
234
+ | `share_mission` | **Producer** — prepare a consent-gated, scope-filtered mission-share envelope (`mission` = shareable learnings; `outcomes` = the result rows). |
235
+ | `import_mission` | **Consumer** — import a mission-share envelope (non-clobbering merge into the imported namespace; never touches first-party learnings or status). |
236
+ | `assess_standing` | Assess a peer's **earned standing** from your first-party outcomes — a tier + basis count + tally. Advisory; never writes trust, never shared. |
237
+ | `explain_standing` | Explain a peer's earned standing in words (tier, why, advisory notes — never an instruction to change trust). |
238
+ | `coordinate` | **Producer** — prepare a coordination message (`request` / `offer` / `accept` / `decline` / `handoff`) that negotiates **who** does a mission. |
239
+ | `import_coordination` | **Consumer** — import a coordination message (appends to the mission's coordination log; only a self-`accept` sets the assignee; a `handoff` never forces one). |
240
+ | `get_coordination` | Read a mission's coordination state — its current assignee + the append-only negotiation log. |
241
+
242
+ The consent tools (`share_profile` / `import_profile` / `grant_share` / `revoke_share` /
243
+ `list_shares`) need a **grant store**; the mission and coordination tools need a **mission store**.
244
+ The `friends-mcp` binary wires both automatically at sibling `_grants/` and `_missions/` directories
245
+ under `--dir`. An embedded server gets them by passing `grants` / `missions` to
246
+ `createFriendsMcpServer`. Without the relevant store, those tools report
247
+ `{ ok: false, status: "unsupported" }` and everything else works store-only.
248
+
249
+ The server module is consumed in code from the `@ouro.bot/friends/mcp` subpath, exporting
250
+ `createFriendsMcpServer`, `getToolSchemas`, and `runMain`.
251
+
252
+ ---
253
+
254
+ ## Trust & consent model
255
+
256
+ This is the differentiator. The whole package is built so that **what you know stays yours**, and
257
+ **what crosses between agents is deliberate, scoped, audited, and revocable.**
258
+
259
+ ### The trust ladder
260
+
261
+ | Level | Meaning | Grants |
262
+ |---|---|---|
263
+ | `family` | The machine owner and those closest. | Full tool access, proactive follow-through, local operations. |
264
+ | `friend` | A directly-trusted relationship. | Full collaborative access (same as family for gating purposes). |
265
+ | `acquaintance` | Known through a **shared group** context, not direct endorsement. | Group-safe coordination; guarded local actions. |
266
+ | `stranger` | Cold first contact. | Safe orientation only; no privileged actions. |
267
+
268
+ `family` and `friend` are the **trusted** levels (`TRUSTED_LEVELS` / `isTrustedLevel`) — they
269
+ unlock full tool access and proactive sends. `acquaintance` and `stranger` are gated.
270
+
271
+ Trust is **assigned, not guessed**:
272
+
273
+ - **First contact** on a populated bundle starts at `stranger`.
274
+ - The **machine owner** (the OS user running the agent) resolves to `family` — they own the agent
275
+ and its bundle, so they are never a stranger.
276
+ - A **shared group** (a group chat) promotes its participants from `stranger` to `acquaintance` —
277
+ the agent now knows them *through* a context it trusts (`upsertGroupContextParticipants`).
278
+
279
+ ### Consent-gated sharing
280
+
281
+ Two *different* agents (different owners) can agree a party is the same person **and** share what
282
+ they know about them — **with consent**. The package does the **authorization** (how much a verified
283
+ peer's claims count, via the trust ladder); **authentication of the wire** is plugged in through an
284
+ `AgentVerifier` (defaulting to trust-on-first-use, upgradable to DID/VC with no envelope change).
285
+
286
+ Consent itself is an explicit, auditable, **revocable** grant — `grant_share` / `revoke_share` /
287
+ `list_shares` are the right-to-be-forgotten seam. The producer is gated by a **`ConsentPolicy`**, and
288
+ three postures ship behind one swap point (`DEFAULT_CONSENT_POLICY` in `src/consent.ts`):
289
+
290
+ - **`strictPolicy`** — consented only by a non-revoked, non-expired explicit grant.
291
+ - **`trustImpliedPolicy`** — an explicit grant, *or* recipient trust ≥ `friend` (any scope).
292
+ - **`tieredPolicy`** *(default)* — identity-scope shares (the join key) are consented on recipient
293
+ trust ≥ `friend`; any **note-content scope** requires an explicit grant. *(Trust agrees on who;
294
+ content still needs consent.)*
295
+
296
+ ### The safety invariants
297
+
298
+ Each is **structurally enforced** and tested — they are properties of the domain logic, not of any
299
+ particular storage backend, so they hold even if you bring your own:
300
+
301
+ - **First-party is inviolable.** Imported facts land in a **separate namespace** (`importedNotes` /
302
+ `importedLearnings`, stamped `origin: "imported"` + `assertedBy` + `importedAt`). First-party
303
+ `notes` / `learnings` are **physically untouchable; first-party always wins.**
304
+ - **Trust is non-transitive.** An import **never** changes the party's trust level — the single most
305
+ important invariant. A peer vouching for someone cannot promote them in *your* graph.
306
+ - **Source trust caps acceptance.** A `stranger` source is refused; the floor is configurable.
307
+ Seeding an *unknown* party (at `acquaintance`) requires a `friend`/`family` introducer.
308
+ - **No laundering.** A first-party note shared onward is attributed to *this* agent; an imported note
309
+ carries its `originallyAssertedBy` through, so an imported fact can never be re-shared as
310
+ first-party.
311
+ - **Reputation stays home.** `standing` is first-party-only, never writes trust, and **never crosses
312
+ the wire** — there is no type to express it on a message (the anti-Sybil core).
313
+ - **Coordination grants no authority.** A mission's `assignee` is advisory metadata; claiming a
314
+ mission gives a peer no capability it didn't already have, and a `handoff` never forces an
315
+ assignment onto a receiver (only their own `accept` sets it).
316
+
317
+ The load-bearing consequence: **the security of the system does not depend on the security of the
318
+ transport.** A hostile mailbox can deny or replay, but never escalate.
319
+
320
+ ---
321
+
322
+ ## Bring your own storage
323
+
324
+ **`friends` never decides where or how your data lives.** *Where* is the path / connection string
325
+ you pass; *how* is a `FriendStore` / `GrantStore` / `MissionStore` implementation you choose or write.
326
+ The core domain logic — resolver, trust, notes, consent, share, import, mission ledger, standing,
327
+ coordination — is **100% persistence-agnostic**: it only ever calls the store interfaces.
328
+
329
+ `openFileBundle` is the one-liner for the filesystem case, encapsulating the sibling collection
330
+ conventions (the explicit construction stays available):
331
+
332
+ ```ts
333
+ import { openFileBundle } from "@ouro.bot/friends"
334
+
335
+ const { store, grants, missions } = openFileBundle("/bundle/friends")
336
+ // grants → /bundle/friends/_grants
337
+ // missions → /bundle/friends/_missions
338
+ ```
339
+
340
+ ### The store seams as a contract
341
+
342
+ A third-party backend implements the store interfaces. Get these three behaviors right or
343
+ cross-channel / cross-agent unification breaks:
344
+
345
+ - **`findByExternalId(provider, externalId, tenantId?)`** — the cross-agent join-key lookup. A match
346
+ requires `provider` + `externalId` **and** (`tenantId` undefined ⇒ any tenant, else an exact
347
+ tenant match). This is how the same person is recognized across channels and how an import resolves
348
+ its subject by join key.
349
+ - **`get(id)` — UUID-then-name fallback.** Look up by UUID first; if not found, fall back to a
350
+ **case-insensitive name** lookup (the documented path for proactive sends). A DB backend should
351
+ index the UUID and MAY implement the name fallback.
352
+ - **Round-trip discipline (load-bearing).** A backend MUST preserve the **full `FriendRecord`
353
+ losslessly** — including `importedNotes` **and future additive fields**. Storing a lossy projection
354
+ breaks the schemaVersion-1 guarantee for non-file backends. Prefer storing the **whole record as a
355
+ JSON blob keyed by id**, with side indexes for lookups.
356
+
357
+ ### Sketch: a SQLite backend (illustrative — not shipped code)
358
+
359
+ The entire moat works **unchanged** over a database, because the domain only ever calls the
360
+ `FriendStore` interface. Store the record as a JSON blob (lossless) with an index table for the
361
+ join-key lookup:
362
+
363
+ ```ts
364
+ // friends(id TEXT PRIMARY KEY, name TEXT, record TEXT /* JSON */)
365
+ // external_ids(provider TEXT, external_id TEXT, tenant_id TEXT, friend_id TEXT)
366
+ class SqliteFriendStore implements FriendStore {
367
+ constructor(private readonly db: Database) {}
368
+
369
+ async put(id: string, record: FriendRecord): Promise<void> {
370
+ // Lossless: the WHOLE record as JSON — importedNotes + any additive field survive.
371
+ this.db.run("INSERT OR REPLACE INTO friends (id, name, record) VALUES (?, ?, ?)",
372
+ id, record.name, JSON.stringify(record))
373
+ this.db.run("DELETE FROM external_ids WHERE friend_id = ?", id)
374
+ for (const ext of record.externalIds) {
375
+ this.db.run("INSERT INTO external_ids (provider, external_id, tenant_id, friend_id) VALUES (?, ?, ?, ?)",
376
+ ext.provider, ext.externalId, ext.tenantId ?? null, id)
377
+ }
378
+ }
379
+
380
+ async get(id: string): Promise<FriendRecord | null> {
381
+ const byId = this.db.get("SELECT record FROM friends WHERE id = ?", id)
382
+ if (byId) return JSON.parse(byId.record)
383
+ // UUID-then-name fallback (case-insensitive).
384
+ const byName = this.db.get("SELECT record FROM friends WHERE LOWER(name) = LOWER(?)", id)
385
+ return byName ? JSON.parse(byName.record) : null
386
+ }
387
+
388
+ async findByExternalId(provider: string, externalId: string, tenantId?: string): Promise<FriendRecord | null> {
389
+ const row = this.db.get(
390
+ "SELECT friend_id FROM external_ids WHERE provider = ? AND external_id = ? AND (? IS NULL OR tenant_id = ?)",
391
+ provider, externalId, tenantId ?? null, tenantId ?? null)
392
+ return row ? this.get(row.friend_id) : null
393
+ }
394
+ // delete / listAll / hasAnyFriends follow the same id-keyed-blob shape.
395
+ }
396
+ ```
397
+
398
+ `GrantStore` and `MissionStore` are the same shape — an id-keyed JSON blob. Swap any store in and
399
+ **every import-safety invariant still holds**, because they are structural properties of the domain
400
+ logic, not of the filesystem.
401
+
402
+ ---
403
+
404
+ ## Examples — runnable, cross-agent proofs
405
+
406
+ Every guarantee above is demonstrated by a runnable script under [`examples/`](./examples). Each
407
+ spins up **two separate stores** (often two separate `friends-mcp` processes) — two *different*
408
+ agents — exchanges real envelopes between them, and **hard-asserts every invariant**, printing a
409
+ green transcript per step and exiting non-zero (with a loud banner) on any violation. They are
410
+ **git-free** (the A2A demos exchange through a temp mailbox dir), so they reproduce anywhere with no
411
+ network.
412
+
413
+ ```sh
414
+ npm run example:cross-agent-moat # identity join key + consent-gated profile share,
415
+ # first-party-inviolable, trust non-transitive
416
+ npm run example:a2a-git-mailbox # the git-mailbox transport: path-binding, replay-safety,
417
+ # spoof rejection, hostile-mailbox tamper
418
+ npm run example:cross-agent-mission-memory # the mission ledger: shareable vs private learnings,
419
+ # first-party-wins, status non-transitive
420
+ npm run example:cross-agent-standing # earned standing: first-party-only, never-on-the-wire,
421
+ # inert on trust
422
+ npm run example:cross-agent-coordination # the five coordination verbs end-to-end: assignment,
423
+ # non-transitive handoff, last-writer-wins, seeding gate
424
+ ```
425
+
426
+ Read them as the honest spec of what the package promises: if a guarantee weren't real, the
427
+ matching example would exit 1.
428
+
429
+ ---
430
+
431
+ ## Channels & observability
432
+
433
+ Each channel an agent speaks on (`cli`, `teams`, `bluebubbles`, `mail`, `voice`, `a2a`, `inner`,
434
+ `mcp`) has fixed **capabilities** — its sense type (`open` / `closed` / `local` / `internal`), which
435
+ integrations it exposes, and whether it supports markdown, streaming, and rich cards. Look them up
436
+ with `getChannelCapabilities`. The sense type, combined with trust, is what decides whether a
437
+ first-contact stranger reaches the full model on an open channel.
438
+
439
+ The package emits structured events through `emitNervesEvent`. By default these are **dropped**
440
+ (no-op), so the package is fully self-contained. To forward them to your logging / observability
441
+ pipeline, inject an emitter once at startup:
442
+
443
+ ```ts
444
+ import { setNervesEmitter } from "@ouro.bot/friends"
445
+
446
+ setNervesEmitter((event) => {
447
+ // forward `event` to your logging / observability pipeline
448
+ })
449
+ ```
450
+
451
+ ---
452
+
453
+ ## Design notes & status
454
+
455
+ - **Store-only, transport-agnostic, additive.** The five layers were each built as a minimal
456
+ primitive that does not modify the layers beneath it. The cross-agent envelopes are plain data; the
457
+ wire is always the caller's job (the `./a2a` mailbox is one optional, host-driven choice). A
458
+ CI-enforced dependency rule keeps the core from ever importing the transport.
459
+ - **One persisted schema, additively grown.** Records are `schemaVersion: 1`; every layer added
460
+ optional fields and sibling collections rather than changing existing meaning, so older data reads
461
+ clean.
462
+ - **Not a workflow engine.** Each layer deliberately refuses the larger machine it brushes against:
463
+ the mission ledger is not a knowledge base, standing is not a reputation engine, coordination is not
464
+ a scheduler. The discipline is the point.
465
+ - **Alpha.** The surface is feature-complete across the five layers but pre-1.0 — expect additive
466
+ changes, and pin a version. Feedback and issues are welcome.
467
+
468
+ ### Public API
469
+
470
+ **Types:** `FriendRecord`, `FriendConnection`, `ExternalId`, `IdentityProvider`, `Integration`,
471
+ `Channel`, `TrustLevel`, `AgentMeta`, `AgentAttribution`, `RelationshipOutcome`, `NoteProvenance`,
472
+ `ImportedNote`, `ShareScope`, `ShareGrant`, `MissionKey`, `MissionLearning`, `ImportedLearning`,
473
+ `MissionRecord`, `CoordinationIntent`, `CoordinationLogEntry`, `MissionCoordination`,
474
+ `ChannelCapabilities`, `ResolvedContext`, `SenseType`, `Facing`, `TrustExplanation`, `TrustBasis`,
475
+ `Standing`, `StandingTier`, `StandingTally`, `StandingExplanation`, `StandingRule`,
476
+ `StandingRuleInput`, `FriendStore`, `GrantStore`, `MissionStore`, `FriendResolverParams`,
477
+ `GroupContextParticipant`, `GroupContextUpsertResult`, `UsageData`, `FriendOpResult`,
478
+ `FriendOpStatus`, `ApplyFriendNoteInput`, `WhoamiResult`, `RoomView`, `RoomMember`, `RoomKnownVia`,
479
+ `ConsentPolicy`, `ConsentRecipient`, `ConsentDecisionInput`, `AgentVerifier`, `ProfileShareEnvelope`,
480
+ `SharedNote`, `PrepareProfileShareInput`, `PrepareProfileShareResult`, `PrepareProfileShareStatus`,
481
+ `ImportProfileShareInput`, `ImportProfileShareOptions`, `ImportProfileShareResult`,
482
+ `ImportProfileShareStatus`, `GrantShareInput`, `RevokeShareResult`, `ListSharesFilter`, `ListedShare`,
483
+ `FileBundle`, `NervesEvent`, `NervesEmitter`, `LogLevel`, `RecordMissionInput`,
484
+ `MissionShareEnvelope`, `SharedLearning`, `PrepareMissionShareInput`, `PrepareMissionShareResult`,
485
+ `PrepareMissionShareStatus`, `ImportMissionShareInput`, `ImportMissionShareOptions`,
486
+ `ImportMissionShareResult`, `ImportMissionShareStatus`, `CoordinationEnvelope`,
487
+ `PrepareCoordinationInput`, `PrepareCoordinationResult`, `PrepareCoordinationStatus`,
488
+ `ImportCoordinationInput`, `ImportCoordinationOptions`, `ImportCoordinationResult`,
489
+ `ImportCoordinationStatus`.
490
+
491
+ **Values:** `TRUSTED_LEVELS`, `IDENTITY_SCOPES`, `isTrustedLevel`, `isIdentityProvider`,
492
+ `isIntegration`, `isShareScope`, `isCoordinationIntent`, `FileFriendStore`, `FileGrantStore`,
493
+ `grantsDirFor`, `FileMissionStore`, `missionsDirFor`, `openFileBundle`, `FriendResolver`,
494
+ `machineOwnerUsername`, `isLocalMachineOwnerIdentity`, `getChannelCapabilities`, `channelToFacing`,
495
+ `isRemoteChannel`, `getAlwaysOnSenseNames`, `describeTrustContext`, `assessStanding`,
496
+ `explainStanding`, `DEFAULT_STANDING_RULE`, `upsertGroupContextParticipants`, `accumulateFriendTokens`,
497
+ `applyFriendNote`, `setFriendTrust`, `linkExternalId`, `unlinkExternalId`, `upsertAgentPeer`,
498
+ `recordRelationshipOutcome`, `recordMission`, `whoami`, `resolveRoom`, `strictPolicy`,
499
+ `trustImpliedPolicy`, `tieredPolicy`, `DEFAULT_CONSENT_POLICY`, `tofuVerifier`,
500
+ `DEFAULT_AGENT_VERIFIER`, `prepareProfileShare`, `importProfileShare`, `prepareMissionShare`,
501
+ `importMissionShare`, `prepareCoordination`, `importCoordination`, `grantShare`, `revokeShare`,
502
+ `listShares`, `isGrantEffective`, `setNervesEmitter`, `emitNervesEvent`.
503
+
504
+ **From `@ouro.bot/friends/mcp`:** `createFriendsMcpServer`, `getToolSchemas`, `runMain` (plus the
505
+ `McpToolSchema`, `FriendsMcpServer`, and `RunMainIo` types).
506
+
507
+ **From `@ouro.bot/friends/a2a`:** `buildOutgoing`, `readIncoming`, `markSeen`, `isSeen`,
508
+ `compareReady`, `MAILBOX_VERSION` (plus the `MailboxMessage`, `BuildOutgoingInput`,
509
+ `BuildOutgoingResult`, `IncomingFile`, `IncomingMessage`, `ReadIncomingInput`, `ReadIncomingResult`,
510
+ `RejectedMessage`, `SeenLedger` types).
511
+
512
+ ## License
513
+
514
+ [Apache-2.0](./LICENSE)
package/changelog.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "versions": [
3
+ {
4
+ "version": "0.1.0-alpha.4",
5
+ "changes": [
6
+ "docs: rewrite README as an adoption-focused guide for the feature-complete five-layer surface (identity moat, A2A transport, mission ledger, earned standing, coordination); lead with what friends is + the five capabilities, a library/MCP quickstart with the full 29-tool surface, the trust & consent model and safety invariants, BYO-storage, and the runnable cross-agent proofs."
7
+ ]
8
+ },
9
+ {
10
+ "version": "0.1.0-alpha.3",
11
+ "changes": [
12
+ "Add coordination / delegation (brick five): five coordination verbs (request/offer/accept/decline/handoff) over a new kind:\"coordination\" mailbox transport that negotiate WHO does a mission; an additive coordination sub-object on MissionRecord (assignee + append-only log) as the only persisted effect; a prepareCoordination/importCoordination producer/consumer pair mirroring the mission share (trust-gated, consent-gated via a new identity-tier \"coordinate\" scope, first-party-inviolable, non-transitive — a handoff never forces an assignee, the receiver's own accept confirms); last-writer-wins assignee-conflict resolution by issuedAt; 3 MCP tools (coordinate, import_coordination, get_coordination); and a git-free three-agent cross-agent proof. The moat, A2A transport (buildOutgoing/readIncoming unchanged), consent stack, mission ledger, and standing are untouched."
13
+ ]
14
+ },
15
+ {
16
+ "version": "0.1.0-alpha.2",
17
+ "changes": [
18
+ "Add earned standing (brick four): a first-party, advisory, derived `standing` (assessStanding/explainStanding) read from agentMeta.outcomes — filtered to first-party, never writes trustLevel, never on the wire; an injectable StandingRule + DEFAULT_STANDING_RULE tier ladder; 2 read-only MCP tools (assess_standing, explain_standing); and a git-free cross-agent proof."
19
+ ]
20
+ },
21
+ {
22
+ "version": "0.1.0-alpha.1",
23
+ "changes": [
24
+ "Add the cross-agent mission ledger (shared work memory): a first-class mission record with first-party/imported learnings, a mission share producer/consumer mirroring the profile moat, a kind:\"mission_share\" mailbox transport, 5 MCP mission tools, and a git-free cross-agent proof. Widen grant subjects (subjectFriendId → subjectKey), backward-compatibly."
25
+ ]
26
+ },
27
+ {
28
+ "version": "0.1.0-alpha.0",
29
+ "changes": [
30
+ "Add the npm CI publish pipeline: an OIDC trusted-publishing `publish` job on push to main, an alpha-channel release-bump script, a changelog gate with freshness enforcement, and a trusted-publisher contract check wired into the coverage-gate preflight."
31
+ ]
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,102 @@
1
+ import type { ProfileShareEnvelope } from "../share";
2
+ import type { MissionShareEnvelope } from "../mission-share";
3
+ import type { CoordinationEnvelope } from "../coordination";
4
+ /** The mailbox wire-format version. Bumped only on a breaking message change. */
5
+ export declare const MAILBOX_VERSION = 1;
6
+ /** A mailbox message: the TRANSPORT wrapper around a verbatim share envelope. The
7
+ * wrapper's from/to are the post-office addressing (validated against the path);
8
+ * the envelope's own `fromAgentId` is the producing-agent claim and is NEVER
9
+ * mutated by this module. */
10
+ export interface MailboxMessage {
11
+ mailboxVersion: number;
12
+ messageId: string;
13
+ fromAgentId: string;
14
+ toAgentId: string;
15
+ issuedAt: string;
16
+ /** The payload discriminant. The host branches on it to call importProfileShare
17
+ * vs importMissionShare vs importCoordination. The mailbox itself is
18
+ * payload-agnostic — this union grows by one leaf per brick (additive, backward-
19
+ * compatible); buildOutgoing/readIncoming carry any of them unchanged. */
20
+ kind: "profile_share" | "mission_share" | "coordination";
21
+ envelope: ProfileShareEnvelope | MissionShareEnvelope | CoordinationEnvelope;
22
+ }
23
+ export interface BuildOutgoingInput {
24
+ envelope: ProfileShareEnvelope | MissionShareEnvelope | CoordinationEnvelope;
25
+ fromAgentId: string;
26
+ toAgentId: string;
27
+ /** The payload discriminant. Defaults to "profile_share" for backward-compat;
28
+ * a mission share passes "mission_share", a coordination message "coordination". */
29
+ kind?: "profile_share" | "mission_share" | "coordination";
30
+ /** Injectable ISO clock for deterministic tests; defaults to now. */
31
+ now?: string;
32
+ }
33
+ export interface BuildOutgoingResult {
34
+ /** git-relative POSIX path: agents/<from>/outbox/<to>/<issuedAt>--<msgId>.json */
35
+ relativePath: string;
36
+ /** The exact file contents the host writes (pretty-printed JSON). */
37
+ bytes: string;
38
+ messageId: string;
39
+ }
40
+ /** Compute the mailbox file (path + bytes) for one outgoing share. Does NOT
41
+ * write anything — the host does the git op. The envelope is carried verbatim
42
+ * (by reference, never cloned or mutated). */
43
+ export declare function buildOutgoing(input: BuildOutgoingInput): BuildOutgoingResult;
44
+ /** One file handed to `readIncoming`: its git-relative POSIX path + raw bytes. */
45
+ export interface IncomingFile {
46
+ relativePath: string;
47
+ bytes: string;
48
+ }
49
+ /** A validated, path-bound, self-addressed message ready to import. The `kind`
50
+ * is the load-bearing routing primitive — the host branches on it to call
51
+ * importProfileShare vs importMissionShare vs importCoordination. */
52
+ export interface IncomingMessage {
53
+ messageId: string;
54
+ fromAgentId: string;
55
+ toAgentId: string;
56
+ issuedAt: string;
57
+ kind: "profile_share" | "mission_share" | "coordination";
58
+ envelope: ProfileShareEnvelope | MissionShareEnvelope | CoordinationEnvelope;
59
+ relativePath: string;
60
+ }
61
+ export interface ReadIncomingInput {
62
+ files: IncomingFile[];
63
+ selfAgentId: string;
64
+ seen: SeenLedger;
65
+ /** Injectable ISO clock for the audit emit; defaults to now. */
66
+ now?: string;
67
+ }
68
+ /** A file that failed validation, with the specific reason. */
69
+ export interface RejectedMessage {
70
+ relativePath: string;
71
+ reason: string;
72
+ }
73
+ export interface ReadIncomingResult {
74
+ ready: IncomingMessage[];
75
+ /** messageIds skipped because already in the seen ledger (replay-safe). */
76
+ skippedSeen: string[];
77
+ rejected: RejectedMessage[];
78
+ }
79
+ /** Deterministic delivery order: issuedAt ascending, tiebroken by messageId
80
+ * ascending. Exported so the ordering contract is independently testable in both
81
+ * argument orders (every branch reachable). */
82
+ export declare function compareReady(a: IncomingMessage, b: IncomingMessage): number;
83
+ /** Parse, validate, path-bind, address-filter, and dedup a batch of mailbox
84
+ * files. The security-critical reader: every reject reason is distinct so the
85
+ * caller can tell a spoof (path mismatch) from malformed input. Order of checks:
86
+ * path → JSON → object → wrapper shape → version → path-binding → addressing →
87
+ * dedup. A message addressed to someone else is silently skipped (not ours);
88
+ * only a malformed PATH makes a non-self message visible (as rejected). */
89
+ export declare function readIncoming(input: ReadIncomingInput): ReadIncomingResult;
90
+ /** The exactly-once dedup ledger: messageId → ISO timestamp it was first seen.
91
+ * Host-owned (the proof/host persists it, e.g. `_a2a/seen.json`) — this module
92
+ * only reads and functionally updates it. */
93
+ export interface SeenLedger {
94
+ seen: Record<string, string>;
95
+ }
96
+ /** Whether a messageId is already in the ledger. Uses hasOwnProperty so an
97
+ * inherited prototype key (e.g. "toString") never reads as seen. */
98
+ export declare function isSeen(seen: SeenLedger, messageId: string): boolean;
99
+ /** Return a NEW ledger with `messageId` recorded (immutable — never mutates the
100
+ * input). `at` defaults to now; that single `new Date()` is the only ambient
101
+ * time minted here and matches share.ts's idiom. */
102
+ export declare function markSeen(seen: SeenLedger, messageId: string, at?: string): SeenLedger;