@ouro.bot/friends 0.1.0-alpha.4 → 0.1.0-alpha.6
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/README.md +79 -8
- package/changelog.json +12 -0
- package/dist/a2a-client/a2a-message.d.ts +39 -0
- package/dist/a2a-client/a2a-message.js +54 -0
- package/dist/a2a-client/adapter.d.ts +97 -0
- package/dist/a2a-client/adapter.js +114 -0
- package/dist/a2a-client/agent-card.d.ts +50 -0
- package/dist/a2a-client/agent-card.js +32 -0
- package/dist/a2a-client/did-key.d.ts +38 -0
- package/dist/a2a-client/did-key.js +120 -0
- package/dist/a2a-client/did-verifier.d.ts +109 -0
- package/dist/a2a-client/did-verifier.js +163 -0
- package/dist/a2a-client/did-web.d.ts +26 -0
- package/dist/a2a-client/did-web.js +140 -0
- package/dist/a2a-client/index.d.ts +24 -0
- package/dist/a2a-client/index.js +76 -0
- package/dist/a2a-client/jcs.d.ts +5 -0
- package/dist/a2a-client/jcs.js +84 -0
- package/dist/a2a-client/reachability.d.ts +22 -0
- package/dist/a2a-client/reachability.js +17 -0
- package/dist/a2a-client/roster-verify.d.ts +15 -0
- package/dist/a2a-client/roster-verify.js +61 -0
- package/dist/a2a-client/seal.d.ts +47 -0
- package/dist/a2a-client/seal.js +95 -0
- package/dist/a2a-client/sealed-envelope.d.ts +55 -0
- package/dist/a2a-client/sealed-envelope.js +94 -0
- package/dist/a2a-client/sign.d.ts +42 -0
- package/dist/a2a-client/sign.js +87 -0
- package/dist/a2a-client/sodium.d.ts +5 -0
- package/dist/a2a-client/sodium.js +19 -0
- package/dist/account-roster.d.ts +52 -0
- package/dist/account-roster.js +108 -0
- package/dist/agent-peer.js +10 -2
- package/dist/audit.d.ts +38 -0
- package/dist/audit.js +86 -0
- package/dist/file-bundle.d.ts +5 -0
- package/dist/file-bundle.js +4 -0
- package/dist/friend-lookup.d.ts +9 -0
- package/dist/friend-lookup.js +69 -0
- package/dist/identity.d.ts +17 -0
- package/dist/identity.js +68 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +31 -2
- package/dist/{a2a → mailbox}/index.js +10 -3
- package/dist/mcp/bin.js +0 -0
- package/dist/mcp/dispatch.d.ts +12 -1
- package/dist/mcp/dispatch.js +45 -3
- package/dist/mcp/run-main.js +8 -5
- package/dist/mcp/schemas.js +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +2 -2
- package/dist/resolver.d.ts +32 -1
- package/dist/resolver.js +50 -3
- package/dist/roster-store-file.d.ts +16 -0
- package/dist/roster-store-file.js +125 -0
- package/dist/roster-store-memory.d.ts +9 -0
- package/dist/roster-store-memory.js +20 -0
- package/dist/roster-store.d.ts +29 -0
- package/dist/roster-store.js +9 -0
- package/dist/roster-verifier.d.ts +23 -0
- package/dist/roster-verifier.js +47 -0
- package/dist/store-file.d.ts +6 -2
- package/dist/store-file.js +28 -5
- package/dist/trust-explanation.d.ts +7 -1
- package/dist/trust-explanation.js +52 -34
- package/dist/trust-mutation.d.ts +13 -1
- package/dist/trust-mutation.js +31 -2
- package/dist/types.d.ts +33 -7
- package/package.json +15 -6
- /package/dist/{a2a → mailbox}/index.d.ts +0 -0
package/README.md
CHANGED
|
@@ -63,11 +63,13 @@ knowledge is **structurally inviolable** and trust is **non-transitive**: an imp
|
|
|
63
63
|
attributed, quarantined note, but it can never change who you trust. (See
|
|
64
64
|
[Trust & consent model](#trust--consent-model).)
|
|
65
65
|
|
|
66
|
-
### 2. Connectivity — the git-backed
|
|
66
|
+
### 2. Connectivity — the git-backed mailbox fallback
|
|
67
67
|
|
|
68
|
-
How two agents actually reach each other, without a server in the middle.
|
|
68
|
+
How two agents actually reach each other, without a server in the middle. (This git-mailbox is the
|
|
69
|
+
demoted **offline/no-endpoint fallback** — real A2A + the friends E2E overlay is the primary path;
|
|
70
|
+
see `@ouro.bot/friends/a2a-client`.)
|
|
69
71
|
|
|
70
|
-
The optional `@ouro.bot/friends/
|
|
72
|
+
The optional `@ouro.bot/friends/mailbox` sub-export is a **pure git-mailbox transport**: zero runtime
|
|
71
73
|
dependencies, and it does **no git or network itself**. The host does every git op (clone / pull /
|
|
72
74
|
add / commit / push); the library only **computes a message file's path + bytes** and
|
|
73
75
|
**parses / validates / orders / dedups** the files the host hands back. Two agents authenticate as
|
|
@@ -77,6 +79,53 @@ own outbox.
|
|
|
77
79
|
The mailbox is treated as **untrusted infrastructure**: a hostile mailbox can only **deny or
|
|
78
80
|
replay** — never **escalate** — because an import never touches first-party notes or trust.
|
|
79
81
|
|
|
82
|
+
### 2b. The primary transport — real A2A + an end-to-end security overlay
|
|
83
|
+
|
|
84
|
+
The **`@ouro.bot/friends/a2a-client`** sub-export is the host-side adapter that makes friends agents
|
|
85
|
+
speak the **real A2A (Agent2Agent) standard** — `message/send`, agent cards at a well-known URL, a
|
|
86
|
+
single structured `DataPart` per envelope — and adds the **end-to-end security overlay** that keeps
|
|
87
|
+
the wire safe even when a relay sits in the middle. It is the only part of the package that has a
|
|
88
|
+
runtime dependency (`libsodium-wrappers`); the core stays zero-dep and transport-agnostic.
|
|
89
|
+
|
|
90
|
+
A friends exchange is one A2A message whose DataPart carries a **sealed envelope**. Before it ever
|
|
91
|
+
hits the wire, the envelope is:
|
|
92
|
+
|
|
93
|
+
- **signed** by the sender — Ed25519 over the [RFC 8785 JCS](https://www.rfc-editor.org/rfc/rfc8785)
|
|
94
|
+
canonical bytes, carried in the envelope's reserved `proof` slot; and
|
|
95
|
+
- **sealed** to the recipient — XChaCha20-Poly1305 AEAD over an ephemeral X25519 ECDH key, with the
|
|
96
|
+
**recipient's DID bound into the AEAD associated-data** so a blob cannot be re-targeted.
|
|
97
|
+
|
|
98
|
+
The signature lives **inside** the ciphertext (sign-then-seal), so a relay never even learns who
|
|
99
|
+
signed. Cryptographic identity is **`did:key`** (zero-infra — the agent's DID *is* its Ed25519 key,
|
|
100
|
+
and the X25519 keyAgreement key is derived from it) or **`did:web`** (resolved behind an injectable
|
|
101
|
+
hook). Identity is `agentId === did`, pinned trust-on-first-use, with **trust-tiered key rotation**
|
|
102
|
+
(a family/friend peer may present a *signed* successor proof; acquaintances/strangers re-confirm out
|
|
103
|
+
of band).
|
|
104
|
+
|
|
105
|
+
**The friends relay (`ourostack/friends-relay`)** is the friends-family communication layer for any
|
|
106
|
+
agent using the friends library — a relay (agents with no reachable endpoint register; it forwards
|
|
107
|
+
A2A messages) plus a directory (discovery). It is built and deployed as a separate component from
|
|
108
|
+
this store-only library, and it is **UNTRUSTED INFRASTRUCTURE by design**: standard A2A is TLS-only
|
|
109
|
+
and terminates at the server, so a plain A2A relay would read every payload. The friends overlay
|
|
110
|
+
closes exactly that gap. The relay carries **ciphertext and a routing handle and nothing else** — it
|
|
111
|
+
can never **read**, **forge**, **tamper**, **re-target**, **replay-to-effect**, or **escalate**. The
|
|
112
|
+
only residual it has is the ability to **deny, delay, or observe handle-level metadata**.
|
|
113
|
+
|
|
114
|
+
That claim is not a promise — it is a **proof**. `examples/cross-agent-a2a-relay.ts` stands up a
|
|
115
|
+
deliberately-malicious in-process relay and asserts all eight properties hold against real
|
|
116
|
+
libsodium crypto:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
npm run example:cross-agent-a2a-relay # the malicious-relay proof: 8 hard assertions —
|
|
120
|
+
# ciphertext-only, can't-forge/tamper/re-target,
|
|
121
|
+
# replay-inert, moat-invariants, direct-equivalence,
|
|
122
|
+
# reachability ladder (direct → relay → mailbox → none)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Reachability is a deterministic ladder: a directly-reachable **A2A endpoint** first, else the
|
|
126
|
+
**relay**, else the **git-mailbox fallback** (§2), else **unreachable**. The *same* sealed envelope
|
|
127
|
+
rides every rung — the security never depends on which path it took.
|
|
128
|
+
|
|
80
129
|
### 3. Shared memory — the mission ledger
|
|
81
130
|
|
|
82
131
|
What two agents collectively *learned* doing work together.
|
|
@@ -413,7 +462,7 @@ network.
|
|
|
413
462
|
```sh
|
|
414
463
|
npm run example:cross-agent-moat # identity join key + consent-gated profile share,
|
|
415
464
|
# first-party-inviolable, trust non-transitive
|
|
416
|
-
npm run example:
|
|
465
|
+
npm run example:mailbox-fallback # the git-mailbox FALLBACK: path-binding, replay-safety,
|
|
417
466
|
# spoof rejection, hostile-mailbox tamper
|
|
418
467
|
npm run example:cross-agent-mission-memory # the mission ledger: shareable vs private learnings,
|
|
419
468
|
# first-party-wins, status non-transitive
|
|
@@ -454,7 +503,7 @@ setNervesEmitter((event) => {
|
|
|
454
503
|
|
|
455
504
|
- **Store-only, transport-agnostic, additive.** The five layers were each built as a minimal
|
|
456
505
|
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 `./
|
|
506
|
+
wire is always the caller's job (the `./mailbox` git-mailbox is one optional, host-driven fallback). A
|
|
458
507
|
CI-enforced dependency rule keeps the core from ever importing the transport.
|
|
459
508
|
- **One persisted schema, additively grown.** Records are `schemaVersion: 1`; every layer added
|
|
460
509
|
optional fields and sibling collections rather than changing existing meaning, so older data reads
|
|
@@ -486,7 +535,13 @@ setNervesEmitter((event) => {
|
|
|
486
535
|
`ImportMissionShareResult`, `ImportMissionShareStatus`, `CoordinationEnvelope`,
|
|
487
536
|
`PrepareCoordinationInput`, `PrepareCoordinationResult`, `PrepareCoordinationStatus`,
|
|
488
537
|
`ImportCoordinationInput`, `ImportCoordinationOptions`, `ImportCoordinationResult`,
|
|
489
|
-
`ImportCoordinationStatus
|
|
538
|
+
`ImportCoordinationStatus`, `SetFriendTrustContext`, `AuditSink`, `ControlPlaneAuditRecord`,
|
|
539
|
+
`ResolvedAgentIdentity`, `RosterStore`, `AccountRoster`, `RosterPin`, `RosterVerifier`,
|
|
540
|
+
`AccountMembershipDecision`, `AccountMembershipResult`, `EvaluateAccountMembershipInput`,
|
|
541
|
+
`FriendResolverRosterContext`. (`TrustBasis` additively gains the `"same_account"` member — the basis
|
|
542
|
+
for family granted via the signed account roster — and `AgentMeta` additively gains an optional
|
|
543
|
+
`identity { did, pinnedKey?, handle?, pinnedAt? }` durable-identity home; both are schemaVersion-1
|
|
544
|
+
additive, and a legacy `a2a.did` migrates-on-read into `identity.did`.)
|
|
490
545
|
|
|
491
546
|
**Values:** `TRUSTED_LEVELS`, `IDENTITY_SCOPES`, `isTrustedLevel`, `isIdentityProvider`,
|
|
492
547
|
`isIntegration`, `isShareScope`, `isCoordinationIntent`, `FileFriendStore`, `FileGrantStore`,
|
|
@@ -499,16 +554,32 @@ setNervesEmitter((event) => {
|
|
|
499
554
|
`trustImpliedPolicy`, `tieredPolicy`, `DEFAULT_CONSENT_POLICY`, `tofuVerifier`,
|
|
500
555
|
`DEFAULT_AGENT_VERIFIER`, `prepareProfileShare`, `importProfileShare`, `prepareMissionShare`,
|
|
501
556
|
`importMissionShare`, `prepareCoordination`, `importCoordination`, `grantShare`, `revokeShare`,
|
|
502
|
-
`listShares`, `isGrantEffective`, `setNervesEmitter`, `emitNervesEvent
|
|
557
|
+
`listShares`, `isGrantEffective`, `setNervesEmitter`, `emitNervesEvent`, `resolveAgentIdentity`,
|
|
558
|
+
`withMigratedIdentity`, `findFriendByDid`, `MemoryAuditSink`, `FileAuditSink`, `auditPathFor`,
|
|
559
|
+
`FileRosterStore`, `rostersDirFor`, `MemoryRosterStore`, `identityRosterVerifier`,
|
|
560
|
+
`DEFAULT_ROSTER_VERIFIER`, `evaluateAccountMembership`.
|
|
503
561
|
|
|
504
562
|
**From `@ouro.bot/friends/mcp`:** `createFriendsMcpServer`, `getToolSchemas`, `runMain` (plus the
|
|
505
563
|
`McpToolSchema`, `FriendsMcpServer`, and `RunMainIo` types).
|
|
506
564
|
|
|
507
|
-
**From `@ouro.bot/friends/
|
|
565
|
+
**From `@ouro.bot/friends/mailbox`:** `buildOutgoing`, `readIncoming`, `markSeen`, `isSeen`,
|
|
508
566
|
`compareReady`, `MAILBOX_VERSION` (plus the `MailboxMessage`, `BuildOutgoingInput`,
|
|
509
567
|
`BuildOutgoingResult`, `IncomingFile`, `IncomingMessage`, `ReadIncomingInput`, `ReadIncomingResult`,
|
|
510
568
|
`RejectedMessage`, `SeenLedger` types).
|
|
511
569
|
|
|
570
|
+
**From `@ouro.bot/friends/a2a-client`** (the real-A2A adapter + the E2E overlay): `sendShare`,
|
|
571
|
+
`receiveShare`, `resolveReachability`; `sealEnvelope` / `openSealedEnvelope`; `wrapInDataPart` /
|
|
572
|
+
`unwrapDataPart`; `buildFriendsAgentCard`; `DidVerifier`, `evaluateRotation`, `signSuccessor`,
|
|
573
|
+
`verifyCardDidBinding`, `pinOnFirstContact` / `isPinned` / `getPinned`, `MemoryPinStore`; the
|
|
574
|
+
identity helpers `parseDidKey` / `keyAgreementFromDidKey` / `didKeyIdentityFromEd25519` /
|
|
575
|
+
`ed25519PubToDidKey` and `didWebToUrl` / `resolveDidWeb` / `parseDidDocument`; the primitives
|
|
576
|
+
`sealTo` / `openSealed`, `signEnvelope` / `verifyEnvelopeSignature`, `jcsString` / `jcsBytes`, and
|
|
577
|
+
the `ready` init seam; and the account-roster Ed25519 verify `ed25519RosterVerifier` / `signRoster`
|
|
578
|
+
(the crypto implementation of the core `RosterVerifier` seam — host-injected, so the core stays
|
|
579
|
+
transport-free) (plus the `A2ATransport`, `DidResolution`, `SealedEnvelope`, `StructuredProof`,
|
|
580
|
+
`ReachabilityPlan`, `FriendsAgentCard`, `DidKeyIdentity`, `DidDocument` types). The transports
|
|
581
|
+
(direct A2A / relay / git op) are injected by the host — this module does no network or git itself.
|
|
582
|
+
|
|
512
583
|
## License
|
|
513
584
|
|
|
514
585
|
[Apache-2.0](./LICENSE)
|
package/changelog.json
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.1.0-alpha.6",
|
|
5
|
+
"changes": [
|
|
6
|
+
"p11 increment 1 — own-fleet foundation. Bug A: cold A2A contact defaults to stranger (safe-by-default; explicit owner-initiated trustLevel still wins). Bug B: append-only control-plane audit (AuditSink + MemoryAuditSink/FileAuditSink) on every setFriendTrust, now wired into the live MCP dispatch path (set_trust + the onboard_agent trust seat) via openFileBundle's FileAuditSink, attributed to the owner-only stdio boundary. Bug C: FriendResolver is roster-aware — a key-verified member of the pinned account roster is recognized as family across OS users, without loosening the cold-A2A stranger default. DID re-key (additive, schemaVersion still 1): AgentMeta.identity durable home with a2a.did migrate-on-read; did-aware findFriendByDid. Account roster: RosterStore/FileRosterStore/MemoryRosterStore + the RosterVerifier seam (core identity-only default; a2a-client ed25519RosterVerifier Ed25519 impl) granting family via a new same_account TrustBasis only to a TOFU-pinned, key-verified roster member (changed roster key hard-fails). Security hardening of the trust primitives: the family-grant path FAILS CLOSED — the identity-only default can never grant family (only a cryptographic verifier can, gated by a grantsFamily capability), with a loud one-time warning; evaluateAccountMembership requires an unforgeable VerifiedCandidate so the did-control precondition can't be skipped; findFriendByDid rejects falsy-did queries and no longer rewards back-dating on a duplicate-did anomaly (prefers the pinned record, warns); empty-string DIDs are never matchable keys; and FileRosterStore guards accountId against path traversal. Core remains transport-free (the CI core⊥a2a-client dependency rule is unmodified)."
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.1.0-alpha.5",
|
|
11
|
+
"changes": [
|
|
12
|
+
"a2a-client sub-export: real A2A + E2E sign-then-seal overlay (libsodium), did:key/did:web identity, untrusted-relay-safe; git-mailbox demoted to ./mailbox fallback"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
3
15
|
{
|
|
4
16
|
"version": "0.1.0-alpha.4",
|
|
5
17
|
"changes": [
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SealedBlob } from "./seal";
|
|
2
|
+
/** The opaque payload carried in the A2A DataPart `data`. Nothing here reveals the
|
|
3
|
+
* sender, the content, or the friends kind. */
|
|
4
|
+
export interface FriendsDataPartPayload {
|
|
5
|
+
v: number;
|
|
6
|
+
sealed: SealedBlob;
|
|
7
|
+
recipientDid: string;
|
|
8
|
+
}
|
|
9
|
+
/** An A2A Part (only the `data` kind is used by friends). */
|
|
10
|
+
export interface A2ADataPart {
|
|
11
|
+
kind: "data";
|
|
12
|
+
data: FriendsDataPartPayload;
|
|
13
|
+
}
|
|
14
|
+
/** An A2A Message carrying exactly one friends DataPart. */
|
|
15
|
+
export interface A2AMessage {
|
|
16
|
+
messageId: string;
|
|
17
|
+
role: "agent";
|
|
18
|
+
parts: A2ADataPart[];
|
|
19
|
+
}
|
|
20
|
+
/** The `SealedEnvelope` shape (re-declared minimally to avoid a cycle; the full
|
|
21
|
+
* type lives in sealed-envelope.ts). */
|
|
22
|
+
interface SealedEnvelopeLike {
|
|
23
|
+
v: number;
|
|
24
|
+
sealed: SealedBlob;
|
|
25
|
+
}
|
|
26
|
+
export interface WrapInDataPartInput {
|
|
27
|
+
sealedEnvelope: SealedEnvelopeLike;
|
|
28
|
+
recipientDid: string;
|
|
29
|
+
v?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Wrap a SealedEnvelope into an A2A Message with one relay-blind DataPart. No
|
|
32
|
+
* `metadata["ouro.friends/kind"]` hint (omitted by default — §3.5); no
|
|
33
|
+
* `friendsKind` on the wire. */
|
|
34
|
+
export declare function wrapInDataPart(input: WrapInDataPartInput): A2AMessage;
|
|
35
|
+
/** Extract + validate the single friends DataPart. Returns null on any malformed
|
|
36
|
+
* shape: wrong part count (≠1), non-data kind, or a missing/ill-typed
|
|
37
|
+
* sealed/recipientDid/v. */
|
|
38
|
+
export declare function unwrapDataPart(msg: A2AMessage): FriendsDataPartPayload | null;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wrapInDataPart = wrapInDataPart;
|
|
4
|
+
exports.unwrapDataPart = unwrapDataPart;
|
|
5
|
+
// A2A DataPart mapping — a friends exchange = one A2A `message/send` whose Message
|
|
6
|
+
// carries ONE DataPart. The DataPart `data` is RELAY-BLIND: it carries only the
|
|
7
|
+
// routing-necessary `{ v, sealed, recipientDid }`. `friendsKind` travels INSIDE the
|
|
8
|
+
// sealed plaintext (see sealed-envelope.ts) — the relay never learns the friends
|
|
9
|
+
// taxonomy. `recipientDid` is unavoidable (it is the AD-reconstruction target AND
|
|
10
|
+
// the relay's routing handle — the relay sees the recipient regardless).
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
12
|
+
/** Wrap a SealedEnvelope into an A2A Message with one relay-blind DataPart. No
|
|
13
|
+
* `metadata["ouro.friends/kind"]` hint (omitted by default — §3.5); no
|
|
14
|
+
* `friendsKind` on the wire. */
|
|
15
|
+
function wrapInDataPart(input) {
|
|
16
|
+
const v = input.v ?? input.sealedEnvelope.v;
|
|
17
|
+
return {
|
|
18
|
+
messageId: (0, node_crypto_1.randomUUID)(),
|
|
19
|
+
role: "agent",
|
|
20
|
+
parts: [{ kind: "data", data: { v, sealed: input.sealedEnvelope.sealed, recipientDid: input.recipientDid } }],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Extract + validate the single friends DataPart. Returns null on any malformed
|
|
24
|
+
* shape: wrong part count (≠1), non-data kind, or a missing/ill-typed
|
|
25
|
+
* sealed/recipientDid/v. */
|
|
26
|
+
function unwrapDataPart(msg) {
|
|
27
|
+
if (!msg || typeof msg !== "object")
|
|
28
|
+
return null;
|
|
29
|
+
const parts = msg.parts;
|
|
30
|
+
if (!Array.isArray(parts) || parts.length !== 1)
|
|
31
|
+
return null;
|
|
32
|
+
const part = parts[0];
|
|
33
|
+
if (!part || typeof part !== "object" || part.kind !== "data")
|
|
34
|
+
return null;
|
|
35
|
+
const data = part.data;
|
|
36
|
+
if (!data || typeof data !== "object")
|
|
37
|
+
return null;
|
|
38
|
+
if (typeof data.recipientDid !== "string")
|
|
39
|
+
return null;
|
|
40
|
+
if (typeof data.v !== "number")
|
|
41
|
+
return null;
|
|
42
|
+
if (!isSealedBlob(data.sealed))
|
|
43
|
+
return null;
|
|
44
|
+
return { v: data.v, sealed: data.sealed, recipientDid: data.recipientDid };
|
|
45
|
+
}
|
|
46
|
+
function isSealedBlob(value) {
|
|
47
|
+
if (!value || typeof value !== "object")
|
|
48
|
+
return false;
|
|
49
|
+
const b = value;
|
|
50
|
+
return (typeof b.v === "number" &&
|
|
51
|
+
typeof b.ePk === "string" &&
|
|
52
|
+
typeof b.n === "string" &&
|
|
53
|
+
typeof b.ct === "string");
|
|
54
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { FriendStore } from "../store";
|
|
2
|
+
import type { MissionStore } from "../mission-store";
|
|
3
|
+
import type { TrustLevel } from "../types";
|
|
4
|
+
import type { PinStore } from "./did-verifier";
|
|
5
|
+
import type { A2AMessage } from "./a2a-message";
|
|
6
|
+
import type { FriendsKind, FromIdentity, RecipientIdentity } from "./sealed-envelope";
|
|
7
|
+
import type { Sodium } from "./sodium";
|
|
8
|
+
/** An injectable A2A transport: deliver one A2A message to a target. Direct + relay
|
|
9
|
+
* are both "send an A2A message"; the proof's relay stub implements this maliciously.
|
|
10
|
+
*/
|
|
11
|
+
export interface A2ATransport {
|
|
12
|
+
send(target: {
|
|
13
|
+
rung: "direct" | "relay" | "mailbox";
|
|
14
|
+
address: string;
|
|
15
|
+
}, message: A2AMessage): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** The async DID resolve + pin step (wraps did:key / did:web + TOFU + binding). It
|
|
18
|
+
* runs BEFORE the importer so the verifier handed to the importer is pure-sync. */
|
|
19
|
+
export interface DidResolution {
|
|
20
|
+
/** Resolve `did` to its pinned Ed25519 (+ optionally X25519) key material,
|
|
21
|
+
* pinning on first contact and verifying against the pin thereafter. Returns
|
|
22
|
+
* null on an unresolvable / failed-binding / failed-rotation DID. */
|
|
23
|
+
resolveAndPin(input: {
|
|
24
|
+
fromAgentId: string;
|
|
25
|
+
did: string;
|
|
26
|
+
pinStore: PinStore;
|
|
27
|
+
trustOfSource: TrustLevel;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
ed25519Pub: Uint8Array;
|
|
30
|
+
} | null>;
|
|
31
|
+
}
|
|
32
|
+
export type SendShareResult = {
|
|
33
|
+
ok: true;
|
|
34
|
+
rung: "direct" | "relay" | "mailbox";
|
|
35
|
+
message: A2AMessage;
|
|
36
|
+
} | {
|
|
37
|
+
ok: false;
|
|
38
|
+
reason: "unreachable" | "resolve_failed";
|
|
39
|
+
};
|
|
40
|
+
export interface SendShareInput {
|
|
41
|
+
sodium: Sodium;
|
|
42
|
+
transport: A2ATransport;
|
|
43
|
+
fromIdentity: FromIdentity;
|
|
44
|
+
/** The recipient peer's coords. */
|
|
45
|
+
toPeer: {
|
|
46
|
+
a2a?: {
|
|
47
|
+
endpointUrl?: string;
|
|
48
|
+
relay?: {
|
|
49
|
+
url: string;
|
|
50
|
+
handle: string;
|
|
51
|
+
};
|
|
52
|
+
did?: string;
|
|
53
|
+
};
|
|
54
|
+
mailbox?: {
|
|
55
|
+
repo: string;
|
|
56
|
+
selfOutboxAgentId: string;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
recipientDid: string;
|
|
60
|
+
recipientX25519Pub: Uint8Array;
|
|
61
|
+
plaintextEnvelope: Record<string, unknown>;
|
|
62
|
+
friendsKind: FriendsKind;
|
|
63
|
+
}
|
|
64
|
+
/** Seal+sign a friends envelope and deliver it over the resolved rung. The SAME
|
|
65
|
+
* SealedEnvelope is built regardless of rung (direct/relay/mailbox) — only the
|
|
66
|
+
* transport target differs. */
|
|
67
|
+
export declare function sendShare(input: SendShareInput): Promise<SendShareResult>;
|
|
68
|
+
/** A2A TaskState mapping for an inbound share. `completed` carries the importer
|
|
69
|
+
* status; `rejected` carries the reason code. */
|
|
70
|
+
export type ReceiveShareResult = {
|
|
71
|
+
state: "completed";
|
|
72
|
+
friendsKind: FriendsKind;
|
|
73
|
+
status: string;
|
|
74
|
+
} | {
|
|
75
|
+
state: "rejected";
|
|
76
|
+
reason: "malformed_message" | "unseal_failed" | "recipient_mismatch" | "malformed_plaintext" | "sender_binding_mismatch" | "resolve_failed" | "replayed" | "untrusted_source" | "import_failed";
|
|
77
|
+
};
|
|
78
|
+
export interface SeenLedgerLike {
|
|
79
|
+
isSeen(nonce: string): boolean;
|
|
80
|
+
markSeen(nonce: string): void;
|
|
81
|
+
}
|
|
82
|
+
export interface ReceiveShareInput {
|
|
83
|
+
sodium: Sodium;
|
|
84
|
+
store: FriendStore;
|
|
85
|
+
missionStore: MissionStore;
|
|
86
|
+
pinStore: PinStore;
|
|
87
|
+
didResolution: DidResolution;
|
|
88
|
+
seen: SeenLedgerLike;
|
|
89
|
+
a2aMessage: A2AMessage;
|
|
90
|
+
recipientDid: string;
|
|
91
|
+
recipientIdentity: RecipientIdentity;
|
|
92
|
+
trustOfSource: TrustLevel;
|
|
93
|
+
}
|
|
94
|
+
/** Receive a sealed A2A message: unwrap → unseal → resolve+pin the SENDER → build a
|
|
95
|
+
* sync DidVerifier → branch on the (unsealed) friendsKind → call the UNCHANGED
|
|
96
|
+
* importer → map to A2A TaskState. Replay is deduped on the seal nonce. */
|
|
97
|
+
export declare function receiveShare(input: ReceiveShareInput): Promise<ReceiveShareResult>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendShare = sendShare;
|
|
4
|
+
exports.receiveShare = receiveShare;
|
|
5
|
+
// adapter — the host-side send/receive that ties everything together. Transports
|
|
6
|
+
// are INJECTABLE (no real HTTP/git in this module; the host supplies them; the
|
|
7
|
+
// malicious-relay proof supplies a hostile stub). The async DID resolve + pin runs
|
|
8
|
+
// BEFORE the (sync) core importer, so the importer's verifier stays sync.
|
|
9
|
+
const coordination_1 = require("../coordination");
|
|
10
|
+
const mission_share_1 = require("../mission-share");
|
|
11
|
+
const share_1 = require("../share");
|
|
12
|
+
const did_verifier_1 = require("./did-verifier");
|
|
13
|
+
const a2a_message_1 = require("./a2a-message");
|
|
14
|
+
const reachability_1 = require("./reachability");
|
|
15
|
+
const sealed_envelope_1 = require("./sealed-envelope");
|
|
16
|
+
/** Seal+sign a friends envelope and deliver it over the resolved rung. The SAME
|
|
17
|
+
* SealedEnvelope is built regardless of rung (direct/relay/mailbox) — only the
|
|
18
|
+
* transport target differs. */
|
|
19
|
+
async function sendShare(input) {
|
|
20
|
+
const plan = (0, reachability_1.resolveReachability)(input.toPeer.a2a, input.toPeer.mailbox);
|
|
21
|
+
if (plan.rung === "unreachable") {
|
|
22
|
+
return { ok: false, reason: "unreachable" };
|
|
23
|
+
}
|
|
24
|
+
const sealed = (0, sealed_envelope_1.sealEnvelope)({
|
|
25
|
+
sodium: input.sodium,
|
|
26
|
+
envelope: input.plaintextEnvelope,
|
|
27
|
+
friendsKind: input.friendsKind,
|
|
28
|
+
fromIdentity: input.fromIdentity,
|
|
29
|
+
recipientDid: input.recipientDid,
|
|
30
|
+
recipientX25519Pub: input.recipientX25519Pub,
|
|
31
|
+
});
|
|
32
|
+
const message = (0, a2a_message_1.wrapInDataPart)({ sealedEnvelope: sealed, recipientDid: input.recipientDid });
|
|
33
|
+
const address = plan.rung === "direct" ? plan.endpointUrl : plan.rung === "relay" ? plan.relay.handle : plan.mailbox.repo;
|
|
34
|
+
await input.transport.send({ rung: plan.rung, address }, message);
|
|
35
|
+
return { ok: true, rung: plan.rung, message };
|
|
36
|
+
}
|
|
37
|
+
/** Receive a sealed A2A message: unwrap → unseal → resolve+pin the SENDER → build a
|
|
38
|
+
* sync DidVerifier → branch on the (unsealed) friendsKind → call the UNCHANGED
|
|
39
|
+
* importer → map to A2A TaskState. Replay is deduped on the seal nonce. */
|
|
40
|
+
async function receiveShare(input) {
|
|
41
|
+
const payload = (0, a2a_message_1.unwrapDataPart)(input.a2aMessage);
|
|
42
|
+
if (!payload)
|
|
43
|
+
return { state: "rejected", reason: "malformed_message" };
|
|
44
|
+
// Replay dedup BEFORE any state change, keyed on the seal nonce.
|
|
45
|
+
if (input.seen.isSeen(payload.sealed.n)) {
|
|
46
|
+
return { state: "rejected", reason: "replayed" };
|
|
47
|
+
}
|
|
48
|
+
const opened = (0, sealed_envelope_1.openSealedEnvelope)({
|
|
49
|
+
sodium: input.sodium,
|
|
50
|
+
sealedEnvelope: { v: payload.v, sealed: payload.sealed },
|
|
51
|
+
recipientDid: input.recipientDid,
|
|
52
|
+
recipientIdentity: input.recipientIdentity,
|
|
53
|
+
});
|
|
54
|
+
if (!opened.ok) {
|
|
55
|
+
// unseal_failed / malformed_plaintext / recipient_mismatch map 1:1.
|
|
56
|
+
return { state: "rejected", reason: opened.error };
|
|
57
|
+
}
|
|
58
|
+
// SECURITY (binding): the trustworthy sender identity is `opened.fromAgentId` —
|
|
59
|
+
// it lives INSIDE the signed envelope bytes, so it is authentic once the
|
|
60
|
+
// signature verifies. `opened.signerDid` comes from the OUTER, UNSIGNED sealed
|
|
61
|
+
// plaintext and is ADVISORY only. We pin/verify/route on the SIGNED
|
|
62
|
+
// `fromAgentId`, and require the advisory `signerDid` to match it (a divergence
|
|
63
|
+
// is a malformed/spoofed bundle). The real gate remains DidVerifier: the pinned
|
|
64
|
+
// key (keyed to `fromAgentId`) must have signed THIS envelope.
|
|
65
|
+
const senderDid = opened.fromAgentId;
|
|
66
|
+
if (senderDid.length === 0 || opened.signerDid !== senderDid) {
|
|
67
|
+
return { state: "rejected", reason: "sender_binding_mismatch" };
|
|
68
|
+
}
|
|
69
|
+
// Resolve + pin the SENDER's DID (async — BEFORE the sync importer/verifier).
|
|
70
|
+
const resolved = await input.didResolution.resolveAndPin({
|
|
71
|
+
fromAgentId: senderDid,
|
|
72
|
+
did: senderDid,
|
|
73
|
+
pinStore: input.pinStore,
|
|
74
|
+
trustOfSource: input.trustOfSource,
|
|
75
|
+
});
|
|
76
|
+
if (!resolved)
|
|
77
|
+
return { state: "rejected", reason: "resolve_failed" };
|
|
78
|
+
// Build the sync verifier bound to THIS envelope + the pinned sender key.
|
|
79
|
+
const verifier = new did_verifier_1.DidVerifier({
|
|
80
|
+
sodium: input.sodium,
|
|
81
|
+
pinnedEd25519Pub: resolved.ed25519Pub,
|
|
82
|
+
pinnedDid: senderDid,
|
|
83
|
+
envelope: opened.envelope,
|
|
84
|
+
});
|
|
85
|
+
// Mark seen now (idempotent imports + the replay guard above keep this safe).
|
|
86
|
+
input.seen.markSeen(payload.sealed.n);
|
|
87
|
+
// Branch on the unsealed friendsKind → the UNCHANGED importer. fromAgentId is the
|
|
88
|
+
// SIGNED sender DID, so the verifier's binding (proof.signerDid === fromAgentId
|
|
89
|
+
// === pinnedDid) anchors on authentic, signature-covered material.
|
|
90
|
+
const fromAgentId = senderDid;
|
|
91
|
+
const importInput = { envelope: opened.envelope, fromAgentId, trustOfSource: input.trustOfSource };
|
|
92
|
+
if (opened.friendsKind === "profile_share") {
|
|
93
|
+
const r = await (0, share_1.importProfileShare)(input.store, importInput, { verifier });
|
|
94
|
+
return mapImport(r, opened.friendsKind);
|
|
95
|
+
}
|
|
96
|
+
if (opened.friendsKind === "mission_share") {
|
|
97
|
+
const r = await (0, mission_share_1.importMissionShare)(input.missionStore, importInput, { verifier });
|
|
98
|
+
return mapImport(r, opened.friendsKind);
|
|
99
|
+
}
|
|
100
|
+
const r = await (0, coordination_1.importCoordination)(input.missionStore, importInput, { verifier });
|
|
101
|
+
return mapImport(r, opened.friendsKind);
|
|
102
|
+
}
|
|
103
|
+
/** Map an importer result to the A2A TaskState shape. */
|
|
104
|
+
function mapImport(r, friendsKind) {
|
|
105
|
+
if (r.ok) {
|
|
106
|
+
return { state: "completed", friendsKind, status: r.status };
|
|
107
|
+
}
|
|
108
|
+
// The importer returns `untrusted_source` when the verifier fails (forge) OR the
|
|
109
|
+
// trust cap is too low (stranger) — both map to the A2A `rejected` taxonomy.
|
|
110
|
+
if (r.status === "untrusted_source") {
|
|
111
|
+
return { state: "rejected", reason: "untrusted_source" };
|
|
112
|
+
}
|
|
113
|
+
return { state: "rejected", reason: "import_failed" };
|
|
114
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FriendsKind } from "./sealed-envelope";
|
|
2
|
+
/** A single A2A skill descriptor. */
|
|
3
|
+
export interface A2ASkill {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
tags: string[];
|
|
8
|
+
}
|
|
9
|
+
/** The A2A capabilities block. */
|
|
10
|
+
export interface A2ACapabilities {
|
|
11
|
+
streaming: boolean;
|
|
12
|
+
pushNotifications: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** The friends agent card (the A2A card + the friends overlay). */
|
|
15
|
+
export interface FriendsAgentCard {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
url: string;
|
|
19
|
+
version: string;
|
|
20
|
+
protocolVersion: string;
|
|
21
|
+
capabilities: A2ACapabilities;
|
|
22
|
+
defaultInputModes: string[];
|
|
23
|
+
defaultOutputModes: string[];
|
|
24
|
+
skills: A2ASkill[];
|
|
25
|
+
/** No transport security scheme for the local proof (the E2E overlay is the
|
|
26
|
+
* real security; transport authn is host-config). */
|
|
27
|
+
securitySchemes: Record<string, never>;
|
|
28
|
+
security: never[];
|
|
29
|
+
/** The friends overlay binding: the agent's DID (== its agentId). */
|
|
30
|
+
did: string;
|
|
31
|
+
/** The friends extension: advertises the relay handle when the agent is reached
|
|
32
|
+
* via a relay. Absent when the agent has a direct endpoint. */
|
|
33
|
+
ouroRelay?: {
|
|
34
|
+
handle: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface BuildFriendsAgentCardInput {
|
|
38
|
+
name: string;
|
|
39
|
+
url: string;
|
|
40
|
+
version: string;
|
|
41
|
+
protocolVersion: string;
|
|
42
|
+
did: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
relayHandle?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Build a minimal friends agent card. The relay handle is advertised only when
|
|
47
|
+
* supplied. */
|
|
48
|
+
export declare function buildFriendsAgentCard(input: BuildFriendsAgentCardInput): FriendsAgentCard;
|
|
49
|
+
/** Re-export the friendsKind type for convenience (it is NOT placed on the card). */
|
|
50
|
+
export type { FriendsKind };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildFriendsAgentCard = buildFriendsAgentCard;
|
|
4
|
+
/** The friends-exchange skill IDs are part of the public surface; the friends
|
|
5
|
+
* KIND taxonomy is intentionally NOT exposed (only this single skill). */
|
|
6
|
+
const FRIENDS_EXCHANGE_SKILL = {
|
|
7
|
+
id: "friends-exchange",
|
|
8
|
+
// Deliberately generic: the friends KIND taxonomy is never spelled out on the
|
|
9
|
+
// card (metadata-minimization — a directory/relay learns no friends internals).
|
|
10
|
+
name: "friends exchange",
|
|
11
|
+
description: "Exchange consent-gated friends envelopes over an end-to-end sign-then-seal overlay.",
|
|
12
|
+
tags: ["friends", "a2a", "e2e"],
|
|
13
|
+
};
|
|
14
|
+
/** Build a minimal friends agent card. The relay handle is advertised only when
|
|
15
|
+
* supplied. */
|
|
16
|
+
function buildFriendsAgentCard(input) {
|
|
17
|
+
return {
|
|
18
|
+
name: input.name,
|
|
19
|
+
description: input.description ?? "A friends-using agent (A2A + the friends E2E overlay).",
|
|
20
|
+
url: input.url,
|
|
21
|
+
version: input.version,
|
|
22
|
+
protocolVersion: input.protocolVersion,
|
|
23
|
+
capabilities: { streaming: false, pushNotifications: false },
|
|
24
|
+
defaultInputModes: ["application/json"],
|
|
25
|
+
defaultOutputModes: ["application/json"],
|
|
26
|
+
skills: [FRIENDS_EXCHANGE_SKILL],
|
|
27
|
+
securitySchemes: {},
|
|
28
|
+
security: [],
|
|
29
|
+
did: input.did,
|
|
30
|
+
...(input.relayHandle !== undefined ? { ouroRelay: { handle: input.relayHandle } } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Sodium } from "./sodium";
|
|
2
|
+
/** Encode bytes as base58btc (Bitcoin alphabet). */
|
|
3
|
+
export declare function base58btcEncode(bytes: Uint8Array): string;
|
|
4
|
+
/** Decode a base58btc string. Returns null on an invalid character. */
|
|
5
|
+
export declare function base58btcDecode(str: string): Uint8Array | null;
|
|
6
|
+
/** Parse a `did:key:z…` (Ed25519) into its 32-byte public key. Returns null on:
|
|
7
|
+
* wrong scheme, missing `z` multibase prefix, bad base58, wrong multicodec, or
|
|
8
|
+
* wrong key length. */
|
|
9
|
+
export declare function parseDidKey(did: string): {
|
|
10
|
+
ed25519Pub: Uint8Array;
|
|
11
|
+
} | null;
|
|
12
|
+
/** Encode an Ed25519 public key as a `did:key:z…`. Throws on a wrong-length key
|
|
13
|
+
* (a guard — callers pass real 32-byte keys). */
|
|
14
|
+
export declare function ed25519PubToDidKey(pub: Uint8Array): string;
|
|
15
|
+
/** Derive the X25519 keyAgreement PUBLIC key from an Ed25519 public key. */
|
|
16
|
+
export declare function keyAgreementFromDidKey(input: {
|
|
17
|
+
sodium: Sodium;
|
|
18
|
+
ed25519Pub: Uint8Array;
|
|
19
|
+
}): Uint8Array;
|
|
20
|
+
/** A self-contained did:key identity (signing + derived sealing keys). The keyId
|
|
21
|
+
* convention for did:key is `${did}#${zBase}` where the z-fragment repeats the
|
|
22
|
+
* did:key's multibase body (did:key is self-describing — the fragment IS the key).
|
|
23
|
+
*/
|
|
24
|
+
export interface DidKeyIdentity {
|
|
25
|
+
did: string;
|
|
26
|
+
ed25519Pub: Uint8Array;
|
|
27
|
+
ed25519Priv: Uint8Array;
|
|
28
|
+
x25519Pub: Uint8Array;
|
|
29
|
+
x25519Priv: Uint8Array;
|
|
30
|
+
keyId: string;
|
|
31
|
+
}
|
|
32
|
+
/** Build a did:key identity from an Ed25519 keypair (the signing + the derived
|
|
33
|
+
* X25519 keyAgreement keys). */
|
|
34
|
+
export declare function didKeyIdentityFromEd25519(input: {
|
|
35
|
+
sodium: Sodium;
|
|
36
|
+
ed25519Pub: Uint8Array;
|
|
37
|
+
ed25519Priv: Uint8Array;
|
|
38
|
+
}): DidKeyIdentity;
|