@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.
- package/LICENSE +201 -0
- package/README.md +514 -0
- package/changelog.json +34 -0
- package/dist/a2a/index.d.ts +102 -0
- package/dist/a2a/index.js +198 -0
- package/dist/agent-peer.d.ts +17 -0
- package/dist/agent-peer.js +57 -0
- package/dist/channel.d.ts +11 -0
- package/dist/channel.js +132 -0
- package/dist/consent.d.ts +34 -0
- package/dist/consent.js +62 -0
- package/dist/coordination.d.ts +100 -0
- package/dist/coordination.js +255 -0
- package/dist/file-bundle.d.ts +12 -0
- package/dist/file-bundle.js +23 -0
- package/dist/grant-store-file.d.ts +16 -0
- package/dist/grant-store-file.js +136 -0
- package/dist/grant-store.d.ts +7 -0
- package/dist/grant-store.js +8 -0
- package/dist/grants.d.ts +39 -0
- package/dist/grants.js +84 -0
- package/dist/group-context.d.ts +21 -0
- package/dist/group-context.js +144 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +105 -0
- package/dist/link-identity.d.ts +14 -0
- package/dist/link-identity.js +88 -0
- package/dist/mcp/bin.d.ts +2 -0
- package/dist/mcp/bin.js +16 -0
- package/dist/mcp/dispatch.d.ts +14 -0
- package/dist/mcp/dispatch.js +432 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.js +14 -0
- package/dist/mcp/run-main.d.ts +7 -0
- package/dist/mcp/run-main.js +45 -0
- package/dist/mcp/schemas.d.ts +10 -0
- package/dist/mcp/schemas.js +398 -0
- package/dist/mcp/server.d.ts +21 -0
- package/dist/mcp/server.js +194 -0
- package/dist/mission-share.d.ts +94 -0
- package/dist/mission-share.js +232 -0
- package/dist/mission-store-file.d.ts +18 -0
- package/dist/mission-store-file.js +153 -0
- package/dist/mission-store.d.ts +10 -0
- package/dist/mission-store.js +9 -0
- package/dist/missions.d.ts +31 -0
- package/dist/missions.js +98 -0
- package/dist/notes.d.ts +11 -0
- package/dist/notes.js +90 -0
- package/dist/observability.d.ts +27 -0
- package/dist/observability.js +31 -0
- package/dist/outcomes.d.ts +9 -0
- package/dist/outcomes.js +51 -0
- package/dist/resolver.d.ts +28 -0
- package/dist/resolver.js +187 -0
- package/dist/results.d.ts +8 -0
- package/dist/results.js +2 -0
- package/dist/room.d.ts +22 -0
- package/dist/room.js +40 -0
- package/dist/share.d.ts +106 -0
- package/dist/share.js +223 -0
- package/dist/standing.d.ts +83 -0
- package/dist/standing.js +111 -0
- package/dist/store-file.d.ts +21 -0
- package/dist/store-file.js +264 -0
- package/dist/store.d.ts +9 -0
- package/dist/store.js +4 -0
- package/dist/tokens.d.ts +8 -0
- package/dist/tokens.js +26 -0
- package/dist/trust-explanation.d.ts +16 -0
- package/dist/trust-explanation.js +74 -0
- package/dist/trust-mutation.d.ts +4 -0
- package/dist/trust-mutation.js +29 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +51 -0
- package/dist/util/cap-string.d.ts +7 -0
- package/dist/util/cap-string.js +35 -0
- package/dist/verifier.d.ts +11 -0
- package/dist/verifier.js +29 -0
- package/dist/whoami.d.ts +7 -0
- package/dist/whoami.js +39 -0
- 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)
|
|
7
|
+
[](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;
|