@ouro.bot/cli 0.1.0-alpha.670 → 0.1.0-alpha.672

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/changelog.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.672",
6
+ "changes": [
7
+ "A2A inbound authentication foundation (friends @ouro.bot/friends alpha.7): the agent mints/loads an Ed25519 did:key self-identity (machine-local vault seed) and serves it on its agent card; an inbound friends sealed DataPart is now unwrapped, unsealed, signature-verified, and replay-checked, and the turn runs keyed on the VERIFIED sender DID at that peer's real trust level (read from the friend store by DID, defaulting to stranger) — replacing the previous unauthenticated-a2a-peer collapse. Adds durable harness-owned PinStore (state/a2a/pins, TOFU + signed-rotation evaluation) and SeenLedger (state/a2a/seen, restart-safe replay defense). The legacy unsigned-text inbound path is behavior-preserved (stranger)."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.671",
12
+ "changes": [
13
+ "Consolidate friend-domain record logic into @ouro.bot/friends: re-pin the git dependency and route save_friend_note, the friend update/link/unlink CLI commands, and A2A peer onboarding through the package's applyFriendNote/setFriendTrust/linkExternalId/unlinkExternalId/upsertAgentPeer helpers, removing the duplicated in-harness record-mutation logic while keeping the MCP tool wrappers, CLI framing, and A2A HTTP card-fetch glue."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.670",
6
18
  "changes": [
package/dist/a2a/card.js CHANGED
@@ -9,6 +9,7 @@ function buildA2AAgentCard(options) {
9
9
  name: options.agentName,
10
10
  description: options.description ?? `Ouroboros agent ${options.agentName}`,
11
11
  protocolVersion: config_1.A2A_DEFAULT_PROTOCOL_VERSION,
12
+ ...(options.did ? { did: options.did } : {}),
12
13
  url: endpoint,
13
14
  preferredTransport: "JSONRPC",
14
15
  supportedInterfaces: [{
@@ -50,7 +51,7 @@ function buildA2AAgentCard(options) {
50
51
  component: "channels",
51
52
  event: "channel.a2a_card_built",
52
53
  message: "built A2A agent card",
53
- meta: { agentName: options.agentName, endpoint },
54
+ meta: { agentName: options.agentName, endpoint, ...(options.did ? { did: options.did } : {}) },
54
55
  });
55
56
  return card;
56
57
  }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeDidResolution = makeDidResolution;
4
+ const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
5
+ const runtime_1 = require("../nerves/runtime");
6
+ /**
7
+ * Build the friends `DidResolution` for did:key peers. `resolveAndPin` is the
8
+ * verify-and-pin core the inbound bridge consumes:
9
+ * parse the did:key → derive the presented Ed25519 key →
10
+ * first contact (no pin under `fromAgentId`) → TOFU `pinOnFirstContact`
11
+ * already pinned → `evaluateRotation` against the pin:
12
+ * unchanged → pin-hit, return the pinned key
13
+ * accepted → (re-pinned by evaluateRotation) return the NEW key
14
+ * rejected → loud security event, return null
15
+ *
16
+ * The pin is keyed by the STABLE `fromAgentId` (NOT the did:key string), so a
17
+ * rotation is "same `fromAgentId`, new (did, key)". An unparseable or empty DID is
18
+ * refused (null) and never matchable. The crypto is the friends primitives' — this
19
+ * only sequences them.
20
+ */
21
+ function makeDidResolution(input) {
22
+ const { sodium, rotationProofFor } = input;
23
+ return {
24
+ async resolveAndPin(args) {
25
+ const { fromAgentId, did, pinStore, trustOfSource } = args;
26
+ // Empty DID is never a matchable key.
27
+ if (!did) {
28
+ (0, runtime_1.emitNervesEvent)({
29
+ component: "channels",
30
+ event: "channel.a2a_did_resolve_refused",
31
+ message: "A2A DID resolve refused (empty DID)",
32
+ meta: { fromAgentId, reason: "empty_did" },
33
+ });
34
+ return null;
35
+ }
36
+ const parsed = (0, a2a_client_1.parseDidKey)(did);
37
+ if (!parsed) {
38
+ (0, runtime_1.emitNervesEvent)({
39
+ component: "channels",
40
+ event: "channel.a2a_did_resolve_refused",
41
+ message: "A2A DID resolve refused (unparseable did:key)",
42
+ meta: { fromAgentId, reason: "parse_failed" },
43
+ });
44
+ return null;
45
+ }
46
+ const pinned = pinStore.get(fromAgentId);
47
+ if (!pinned) {
48
+ // First contact (TOFU): accept + pin the (did, key) under fromAgentId.
49
+ (0, a2a_client_1.pinOnFirstContact)({ pinStore, fromAgentId, did, ed25519Pub: parsed.ed25519Pub });
50
+ (0, runtime_1.emitNervesEvent)({
51
+ component: "channels",
52
+ event: "channel.a2a_pin_first_contact",
53
+ message: "A2A DID pinned on first contact",
54
+ meta: { fromAgentId, did },
55
+ });
56
+ return { ed25519Pub: parsed.ed25519Pub };
57
+ }
58
+ // Already pinned: evaluate the presented key against the pin.
59
+ const rotationProof = rotationProofFor?.(fromAgentId, did, parsed.ed25519Pub);
60
+ const decision = (0, a2a_client_1.evaluateRotation)({
61
+ sodium,
62
+ pinStore,
63
+ fromAgentId,
64
+ trustOfSource,
65
+ newDid: did,
66
+ newEd25519Pub: parsed.ed25519Pub,
67
+ rotationProof,
68
+ });
69
+ if (decision.decision === "unchanged") {
70
+ // Same key — pin-hit. (FileA2APinStore.get already emitted a2a_pin_hit.)
71
+ return { ed25519Pub: parsed.ed25519Pub };
72
+ }
73
+ if (decision.decision === "accepted") {
74
+ // evaluateRotation already re-pinned to the new key under fromAgentId.
75
+ (0, runtime_1.emitNervesEvent)({
76
+ component: "channels",
77
+ event: "channel.a2a_rotation_accepted",
78
+ message: "A2A signed key rotation accepted",
79
+ meta: { fromAgentId, newDid: did, trustOfSource },
80
+ });
81
+ return { ed25519Pub: parsed.ed25519Pub };
82
+ }
83
+ // Rejected — loud security signal (carries the reason).
84
+ (0, runtime_1.emitNervesEvent)({
85
+ component: "channels",
86
+ event: "channel.a2a_rotation_rejected",
87
+ message: "A2A key rotation rejected",
88
+ meta: { fromAgentId, newDid: did, trustOfSource, reason: decision.reason },
89
+ });
90
+ return null;
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readStoredA2ASeed = readStoredA2ASeed;
37
+ exports.loadOrMintA2AIdentity = loadOrMintA2AIdentity;
38
+ const crypto = __importStar(require("node:crypto"));
39
+ const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
40
+ const runtime_1 = require("../nerves/runtime");
41
+ /** The number of raw bytes in an Ed25519 seed. */
42
+ const ED25519_SEED_BYTES = 32;
43
+ /** The machine-config key path for the persisted seed: `a2a.identity.ed25519Seed`. */
44
+ function a2aIdentityConfig(config) {
45
+ const a2a = config.a2a;
46
+ if (!a2a || typeof a2a !== "object" || Array.isArray(a2a))
47
+ return undefined;
48
+ const identity = a2a.identity;
49
+ if (!identity || typeof identity !== "object" || Array.isArray(identity))
50
+ return undefined;
51
+ return identity;
52
+ }
53
+ /** Read the stored Ed25519 seed string from a machine-config object, or undefined. */
54
+ function readStoredA2ASeed(config) {
55
+ const identity = a2aIdentityConfig(config);
56
+ const seed = identity?.ed25519Seed;
57
+ return typeof seed === "string" && seed.length > 0 ? seed : undefined;
58
+ }
59
+ /** Decode + validate a stored seed string into 32 raw bytes. Throws on malformed. */
60
+ function decodeSeed(seedString) {
61
+ let decoded;
62
+ try {
63
+ decoded = Buffer.from(seedString, "base64url");
64
+ }
65
+ catch {
66
+ /* v8 ignore next -- Buffer.from(base64url) does not throw on bad chars; this guards future decoders @preserve */
67
+ throw new Error("malformed A2A identity seed: not decodable");
68
+ }
69
+ // base64url is lenient (it silently drops invalid chars), so the length check is
70
+ // the real gate: a real Ed25519 seed is exactly 32 bytes.
71
+ if (decoded.length !== ED25519_SEED_BYTES) {
72
+ throw new Error(`malformed A2A identity seed: expected ${ED25519_SEED_BYTES}-byte seed, got ${decoded.length}`);
73
+ }
74
+ return new Uint8Array(decoded);
75
+ }
76
+ /** Mint a fresh 32-byte Ed25519 seed (Node keygen → JWK `d`, the raw seed). */
77
+ function mintSeed() {
78
+ const { privateKey } = crypto.generateKeyPairSync("ed25519");
79
+ const jwk = privateKey.export({ format: "jwk" });
80
+ /* v8 ignore next -- Node always emits the `d` field for an Ed25519 private JWK; guard protects future runtimes @preserve */
81
+ if (typeof jwk.d !== "string")
82
+ throw new Error("failed to mint A2A identity seed: missing JWK d");
83
+ return decodeSeed(jwk.d);
84
+ }
85
+ /** Derive the full did:key identity from a 32-byte seed (deterministic). */
86
+ function deriveIdentity(sodium, seed) {
87
+ const keypair = sodium.crypto_sign_seed_keypair(seed);
88
+ const didIdentity = (0, a2a_client_1.didKeyIdentityFromEd25519)({
89
+ sodium,
90
+ ed25519Pub: keypair.publicKey,
91
+ ed25519Priv: keypair.privateKey,
92
+ });
93
+ return { ...didIdentity, seed: Buffer.from(seed).toString("base64url") };
94
+ }
95
+ /**
96
+ * Load the agent's A2A identity from the machine-local config, or mint + persist
97
+ * one on first use. A malformed stored seed FAILS FAST (it never silently
98
+ * re-mints, which would change the agent's DID). The mint path persists the new
99
+ * seed via `upsert` before deriving; the load path performs no writes.
100
+ */
101
+ async function loadOrMintA2AIdentity(input) {
102
+ const stored = readStoredA2ASeed(input.config);
103
+ if (stored) {
104
+ // Load path: a malformed seed throws (no re-mint).
105
+ const seed = decodeSeed(stored);
106
+ const identity = deriveIdentity(input.sodium, seed);
107
+ (0, runtime_1.emitNervesEvent)({
108
+ component: "channels",
109
+ event: "channel.a2a_identity_loaded",
110
+ message: "loaded A2A identity from machine config",
111
+ meta: { agentName: input.agentName, did: identity.did },
112
+ });
113
+ return identity;
114
+ }
115
+ // Mint path: generate a seed, persist it under a2a.identity.*, then derive.
116
+ const seed = mintSeed();
117
+ const seedString = Buffer.from(seed).toString("base64url");
118
+ const a2a = (input.config.a2a && typeof input.config.a2a === "object" && !Array.isArray(input.config.a2a)
119
+ ? input.config.a2a
120
+ : {});
121
+ const identityBlock = (a2a.identity && typeof a2a.identity === "object" && !Array.isArray(a2a.identity)
122
+ ? a2a.identity
123
+ : {});
124
+ const nextConfig = {
125
+ ...input.config,
126
+ a2a: { ...a2a, identity: { ...identityBlock, ed25519Seed: seedString } },
127
+ };
128
+ await input.upsert(nextConfig);
129
+ const identity = deriveIdentity(input.sodium, seed);
130
+ (0, runtime_1.emitNervesEvent)({
131
+ component: "channels",
132
+ event: "channel.a2a_identity_minted",
133
+ message: "minted new A2A identity and persisted seed to machine config",
134
+ meta: { agentName: input.agentName, did: identity.did },
135
+ });
136
+ return identity;
137
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.receiveInboundShare = receiveInboundShare;
4
+ const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
5
+ const friends_1 = require("@ouro.bot/friends");
6
+ const runtime_1 = require("../nerves/runtime");
7
+ /** Whether the inbound message carries a friends data part (kind:"data"). */
8
+ function hasDataPart(message) {
9
+ return Array.isArray(message.parts) && message.parts.some((p) => p && p.kind === "data");
10
+ }
11
+ /**
12
+ * Bridge an inbound A2A message through friends' `receiveShare`, keying the result
13
+ * on the VERIFIED sender DID.
14
+ *
15
+ * Ordering (the security-critical trap — `trustOfSource` is read from the store BY
16
+ * the just-verified DID, never defaulted, never from the envelope):
17
+ * 1. `unwrapDataPart` — no friends part ⇒ `not-a-share` (caller does text path).
18
+ * 2. `openSealedEnvelope` (pre-unseal) — extract the SIGNED sender DID. Unseal /
19
+ * recipient-mismatch failures ⇒ `rejected` (no turn). (This open does NOT
20
+ * verify the signature; the real auth gate is the `DidVerifier` inside
21
+ * `receiveShare`, so a forged-high-trust claim here is harmless — the forge is
22
+ * rejected by the verifier regardless of the trust we read.)
23
+ * 3. `findFriendByDid(store, verifiedDid)` — read trust off that record; an
24
+ * unknown DID ⇒ `stranger` (never a default-up).
25
+ * 4. `receiveShare({ ..., trustOfSource })` — re-unseal (deterministic) + resolve
26
+ * & pin + run `DidVerifier` (forge fails HERE) + replay-dedup + import.
27
+ * 5. Map the result. `completed` ⇒ run the turn at `verifiedDid`/`trust`.
28
+ */
29
+ async function receiveInboundShare(message, deps) {
30
+ const friendsMessage = message;
31
+ const payload = (0, a2a_client_1.unwrapDataPart)(friendsMessage);
32
+ if (!payload) {
33
+ // No (valid) friends part. If there was a data part at all, it was a malformed
34
+ // friends attempt → reject; otherwise it's a legacy text message → not-a-share.
35
+ if (hasDataPart(message)) {
36
+ (0, runtime_1.emitNervesEvent)({
37
+ component: "channels",
38
+ event: "channel.a2a_inbound_rejected",
39
+ message: "rejected malformed inbound A2A data part",
40
+ meta: { reason: "malformed_message" },
41
+ });
42
+ return { outcome: "rejected", reason: "malformed_message" };
43
+ }
44
+ return { outcome: "not-a-share" };
45
+ }
46
+ const recipientIdentity = {
47
+ x25519Priv: deps.identity.x25519Priv,
48
+ x25519Pub: deps.identity.x25519Pub,
49
+ };
50
+ // Pre-unseal to learn the SIGNED sender DID before we can read its trust.
51
+ const opened = (0, a2a_client_1.openSealedEnvelope)({
52
+ sodium: deps.sodium,
53
+ sealedEnvelope: { v: payload.v, sealed: payload.sealed },
54
+ recipientDid: deps.identity.did,
55
+ recipientIdentity,
56
+ });
57
+ if (!opened.ok) {
58
+ (0, runtime_1.emitNervesEvent)({
59
+ component: "channels",
60
+ event: "channel.a2a_inbound_rejected",
61
+ message: "rejected inbound A2A envelope (unseal/recipient)",
62
+ meta: { reason: opened.error },
63
+ });
64
+ return { outcome: "rejected", reason: opened.error };
65
+ }
66
+ const verifiedDid = opened.fromAgentId;
67
+ // Read trust from the friend store BY the verified DID — never defaulted, never
68
+ // from the envelope. Unknown (or empty) DID ⇒ stranger. `findFriendByDid` itself
69
+ // refuses a falsy did (never matches a did-less record), so this is safe to call
70
+ // unconditionally.
71
+ const record = await (0, friends_1.findFriendByDid)(deps.store, verifiedDid);
72
+ const trust = record?.trustLevel ?? "stranger";
73
+ const result = await (0, a2a_client_1.receiveShare)({
74
+ sodium: deps.sodium,
75
+ store: deps.store,
76
+ missionStore: deps.missionStore,
77
+ pinStore: deps.pinStore,
78
+ didResolution: deps.didResolution,
79
+ seen: deps.seen,
80
+ a2aMessage: friendsMessage,
81
+ recipientDid: deps.identity.did,
82
+ recipientIdentity,
83
+ trustOfSource: trust,
84
+ });
85
+ if (result.state === "completed") {
86
+ (0, runtime_1.emitNervesEvent)({
87
+ component: "channels",
88
+ event: "channel.a2a_inbound_verified",
89
+ message: "verified inbound A2A share; keying turn on sender DID",
90
+ meta: { verifiedDid, trust, friendsKind: result.friendsKind, status: result.status },
91
+ });
92
+ return {
93
+ outcome: "completed",
94
+ verifiedDid,
95
+ trust,
96
+ friendsKind: result.friendsKind,
97
+ status: result.status,
98
+ };
99
+ }
100
+ (0, runtime_1.emitNervesEvent)({
101
+ component: "channels",
102
+ event: "channel.a2a_inbound_rejected",
103
+ message: "rejected inbound A2A share",
104
+ meta: { reason: result.reason, verifiedDid, trust },
105
+ });
106
+ return { outcome: "rejected", reason: result.reason };
107
+ }
@@ -34,7 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.onboardA2APeer = onboardA2APeer;
37
- const node_crypto_1 = require("node:crypto");
38
37
  const path = __importStar(require("node:path"));
39
38
  const identity_1 = require("../heart/identity");
40
39
  const runtime_1 = require("../nerves/runtime");
@@ -52,60 +51,33 @@ function agentIdFor(_card, cardUrl) {
52
51
  return parsed.toString();
53
52
  }
54
53
  async function onboardA2APeer(options) {
54
+ // HTTP/URL glue stays harness-side: fetch the agent card, resolve its JSON-RPC
55
+ // endpoint, and derive the stable agentId. The record-shaping (mint/update the
56
+ // agent-peer friend) is delegated to @ouro.bot/friends' upsertAgentPeer.
55
57
  const card = await (0, client_1.fetchA2AAgentCard)(options.cardUrl, options.fetchImpl);
56
58
  const store = storeFor(options);
57
- const now = new Date().toISOString();
58
59
  const externalId = agentIdFor(card, options.cardUrl);
59
60
  /* v8 ignore next -- fetchA2AAgentCard validates a usable endpoint or legacy url before returning a card @preserve */
60
61
  const endpointUrl = (0, client_1.endpointForCard)(card) ?? options.cardUrl;
61
62
  const protocolVersion = card.supportedInterfaces?.find((entry) => entry.url === endpointUrl)?.protocolVersion
62
63
  ?? card.protocolVersion;
63
- const existing = await store.findByExternalId("a2a-agent", externalId);
64
64
  const name = options.name ?? card.name;
65
- const trustLevel = options.trustLevel ?? existing?.trustLevel ?? "acquaintance";
66
- const baseMeta = existing?.agentMeta ?? {
67
- bundleName: name,
68
- familiarity: 0,
69
- sharedMissions: [],
70
- outcomes: [],
71
- };
72
- const record = {
73
- ...(existing ?? {
74
- id: (0, node_crypto_1.randomUUID)(),
75
- createdAt: now,
76
- externalIds: [],
77
- tenantMemberships: [],
78
- toolPreferences: {},
79
- notes: {},
80
- totalTokens: 0,
81
- schemaVersion: 1,
82
- }),
65
+ const record = await (0, friends_1.upsertAgentPeer)(store, {
83
66
  name,
84
- role: "agent-peer",
85
- trustLevel,
86
- kind: "agent",
87
- agentMeta: {
88
- ...baseMeta,
89
- bundleName: baseMeta.bundleName || name,
90
- a2a: {
91
- cardUrl: options.cardUrl,
92
- endpointUrl,
93
- agentId: externalId,
94
- protocolVersion,
95
- },
67
+ agentId: externalId,
68
+ ...(options.trustLevel ? { trustLevel: options.trustLevel } : {}),
69
+ a2a: {
70
+ cardUrl: options.cardUrl,
71
+ endpointUrl,
72
+ agentId: externalId,
73
+ protocolVersion,
96
74
  },
97
- externalIds: [
98
- ...(existing?.externalIds.filter((id) => !(id.provider === "a2a-agent" && id.externalId === externalId)) ?? []),
99
- { provider: "a2a-agent", externalId, linkedAt: now },
100
- ],
101
- updatedAt: now,
102
- };
103
- await store.put(record.id, record);
75
+ });
104
76
  (0, runtime_1.emitNervesEvent)({
105
77
  component: "friends",
106
78
  event: "friends.a2a_peer_onboarded",
107
79
  message: "onboarded A2A peer into friend model",
108
- meta: { agentName: options.agentName, friendId: record.id, peerName: record.name, trustLevel },
80
+ meta: { agentName: options.agentName, friendId: record.id, peerName: record.name, trustLevel: record.trustLevel },
109
81
  });
110
82
  return record;
111
83
  }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FileA2APinStore = void 0;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const node_crypto_1 = require("node:crypto");
40
+ const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
41
+ const runtime_1 = require("../nerves/runtime");
42
+ function pinFileName(fromAgentId) {
43
+ return `${(0, node_crypto_1.createHash)("sha256").update(fromAgentId).digest("hex")}.json`;
44
+ }
45
+ class FileA2APinStore {
46
+ dir;
47
+ cache = new Map();
48
+ constructor(agentRoot) {
49
+ this.dir = path.join(agentRoot, "state", "a2a", "pins");
50
+ fs.mkdirSync(this.dir, { recursive: true });
51
+ this.loadExisting();
52
+ (0, runtime_1.emitNervesEvent)({
53
+ component: "channels",
54
+ event: "channel.a2a_pin_store_init",
55
+ message: "initialized durable A2A pin store",
56
+ meta: { dir: this.dir, pins: this.cache.size },
57
+ });
58
+ }
59
+ get size() {
60
+ return this.cache.size;
61
+ }
62
+ get(fromAgentId) {
63
+ if (!fromAgentId)
64
+ return undefined;
65
+ const pinned = this.cache.get(fromAgentId);
66
+ if (!pinned)
67
+ return undefined;
68
+ (0, runtime_1.emitNervesEvent)({
69
+ component: "channels",
70
+ event: "channel.a2a_pin_hit",
71
+ message: "A2A pin hit",
72
+ meta: { fromAgentId, did: pinned.did },
73
+ });
74
+ return pinned;
75
+ }
76
+ set(fromAgentId, pinned) {
77
+ // An empty-string DID is never a matchable key (security: it can never index a
78
+ // pin). Stay inert rather than writing an unkeyed pin file.
79
+ if (!fromAgentId)
80
+ return;
81
+ const existed = this.cache.has(fromAgentId);
82
+ // Faithfully persist the pinned identity's DID (which may differ from
83
+ // fromAgentId after a rotation), not fromAgentId itself.
84
+ const stored = {
85
+ fromAgentId,
86
+ did: pinned.did,
87
+ pinnedKey: (0, a2a_client_1.base58btcEncode)(pinned.ed25519Pub),
88
+ };
89
+ fs.writeFileSync(path.join(this.dir, pinFileName(fromAgentId)), `${JSON.stringify(stored, null, 2)}\n`, "utf-8");
90
+ this.cache.set(fromAgentId, { did: pinned.did, ed25519Pub: pinned.ed25519Pub });
91
+ (0, runtime_1.emitNervesEvent)({
92
+ component: "channels",
93
+ event: "channel.a2a_pin_set",
94
+ message: "set durable A2A pin",
95
+ meta: { fromAgentId, did: pinned.did, rotated: existed },
96
+ });
97
+ }
98
+ /** Reload every persisted pin from disk into the in-memory cache (the restart
99
+ * replay). Corrupt/undecodable files are skipped so one bad file can never
100
+ * wedge the store. */
101
+ loadExisting() {
102
+ let entries;
103
+ try {
104
+ entries = fs.readdirSync(this.dir);
105
+ }
106
+ catch {
107
+ /* v8 ignore next -- the constructor mkdirSync's the dir before this runs; unreadable dir is unreachable in practice @preserve */
108
+ return;
109
+ }
110
+ for (const name of entries) {
111
+ if (!name.endsWith(".json"))
112
+ continue;
113
+ let stored;
114
+ try {
115
+ stored = JSON.parse(fs.readFileSync(path.join(this.dir, name), "utf-8"));
116
+ }
117
+ catch {
118
+ continue;
119
+ }
120
+ // `fromAgentId` is the cache key; fall back to `did` for any pre-rotation
121
+ // file written before the key/did split (back-compat).
122
+ const key = stored.fromAgentId ?? stored.did;
123
+ if (!key || !stored.did || !stored.pinnedKey)
124
+ continue;
125
+ const ed25519Pub = (0, a2a_client_1.base58btcDecode)(stored.pinnedKey);
126
+ if (!ed25519Pub)
127
+ continue;
128
+ this.cache.set(key, { did: stored.did, ed25519Pub });
129
+ }
130
+ }
131
+ }
132
+ exports.FileA2APinStore = FileA2APinStore;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FileA2ASeenLedger = void 0;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const node_crypto_1 = require("node:crypto");
40
+ const runtime_1 = require("../nerves/runtime");
41
+ function nonceFileName(nonce) {
42
+ return `${(0, node_crypto_1.createHash)("sha256").update(nonce).digest("hex")}.json`;
43
+ }
44
+ class FileA2ASeenLedger {
45
+ dir;
46
+ cache = new Set();
47
+ constructor(agentRoot) {
48
+ this.dir = path.join(agentRoot, "state", "a2a", "seen");
49
+ fs.mkdirSync(this.dir, { recursive: true });
50
+ this.loadExisting();
51
+ (0, runtime_1.emitNervesEvent)({
52
+ component: "channels",
53
+ event: "channel.a2a_seen_ledger_init",
54
+ message: "initialized durable A2A seen ledger",
55
+ meta: { dir: this.dir, seen: this.cache.size },
56
+ });
57
+ }
58
+ get size() {
59
+ return this.cache.size;
60
+ }
61
+ isSeen(nonce) {
62
+ if (!nonce)
63
+ return false;
64
+ const seen = this.cache.has(nonce);
65
+ if (seen) {
66
+ // A nonce we already processed is arriving again — for a seal nonce this is
67
+ // a replay. Loud security signal.
68
+ (0, runtime_1.emitNervesEvent)({
69
+ component: "channels",
70
+ event: "channel.a2a_replay_rejected",
71
+ message: "A2A replay rejected (nonce already seen)",
72
+ meta: { nonceHash: nonceFileName(nonce).slice(0, 16) },
73
+ });
74
+ }
75
+ return seen;
76
+ }
77
+ markSeen(nonce) {
78
+ // An empty-string nonce is never a matchable key — stay inert.
79
+ if (!nonce)
80
+ return;
81
+ if (this.cache.has(nonce))
82
+ return;
83
+ fs.writeFileSync(path.join(this.dir, nonceFileName(nonce)), `${JSON.stringify({ nonce, markedAt: new Date().toISOString() }, null, 2)}\n`, "utf-8");
84
+ this.cache.add(nonce);
85
+ (0, runtime_1.emitNervesEvent)({
86
+ component: "channels",
87
+ event: "channel.a2a_seen_marked",
88
+ message: "marked A2A seal nonce as seen (first sight)",
89
+ meta: { nonceHash: nonceFileName(nonce).slice(0, 16) },
90
+ });
91
+ }
92
+ /** Reload every persisted nonce from disk into the in-memory set (the restart
93
+ * replay defense). Corrupt/unreadable artifacts are skipped so one bad file can
94
+ * never reopen the replay window for the rest. */
95
+ loadExisting() {
96
+ let entries;
97
+ try {
98
+ entries = fs.readdirSync(this.dir);
99
+ }
100
+ catch {
101
+ /* v8 ignore next -- the constructor mkdirSync's the dir before this runs; unreadable dir is unreachable in practice @preserve */
102
+ return;
103
+ }
104
+ for (const name of entries) {
105
+ if (!name.endsWith(".json"))
106
+ continue;
107
+ let record;
108
+ try {
109
+ record = JSON.parse(fs.readFileSync(path.join(this.dir, name), "utf-8"));
110
+ }
111
+ catch {
112
+ continue;
113
+ }
114
+ if (typeof record.nonce === "string" && record.nonce.length > 0) {
115
+ this.cache.add(record.nonce);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ exports.FileA2ASeenLedger = FileA2ASeenLedger;
@@ -36,11 +36,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.startA2AServer = startA2AServer;
37
37
  const http = __importStar(require("node:http"));
38
38
  const node_crypto_1 = require("node:crypto");
39
+ const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
40
+ const friends_1 = require("@ouro.bot/friends");
39
41
  const identity_1 = require("../heart/identity");
40
42
  const runtime_1 = require("../nerves/runtime");
41
43
  const shared_turn_1 = require("../senses/shared-turn");
42
44
  const card_1 = require("./card");
43
45
  const config_1 = require("./config");
46
+ const pin_store_1 = require("./pin-store");
47
+ const seen_ledger_1 = require("./seen-ledger");
48
+ const did_resolution_1 = require("./did-resolution");
49
+ const inbound_share_1 = require("./inbound-share");
44
50
  const task_store_1 = require("./task-store");
45
51
  const MAX_A2A_REQUEST_BYTES = 128 * 1024;
46
52
  const MAX_A2A_MESSAGE_TEXT_CHARS = 16_000;
@@ -160,6 +166,21 @@ function accessTokenScope(accessToken) {
160
166
  function untrustedTurnPeerExternalId() {
161
167
  return "unauthenticated-a2a-peer";
162
168
  }
169
+ /** Whether an inbound message carries a friends sealed DataPart (`kind:"data"`). */
170
+ function messageHasDataPart(message) {
171
+ /* v8 ignore next -- defensive: callers pass a parsed inbound whose `parts` is always an array; guards malformed direct input @preserve */
172
+ if (!Array.isArray(message.parts))
173
+ return false;
174
+ return message.parts.some((part) => part && part.kind === "data");
175
+ }
176
+ /** Map a friends `receiveShare` rejection reason → a JSON-RPC error code.
177
+ * Malformed-shape reasons map to invalid-params (-32602); auth/trust/replay
178
+ * rejections map to a dedicated A2A "rejected" code (-32003). */
179
+ function rejectionErrorCode(reason) {
180
+ if (reason === "malformed_message" || reason === "malformed_plaintext")
181
+ return -32602;
182
+ return -32003;
183
+ }
163
184
  function senderHintFromMessage(req, inbound) {
164
185
  const idHint = metadataString(inbound, "senderAgentId")
165
186
  ?? metadataString(inbound, "senderCardUrl")
@@ -315,9 +336,26 @@ async function startA2AServer(options) {
315
336
  const port = options.port ?? (0, config_1.defaultA2APort)(options.agentName);
316
337
  const a2aPath = (0, config_1.normalizeA2APath)(options.path);
317
338
  /* v8 ignore next -- foreground CLI/default daemon paths own the ambient agent-root fallback; protocol tests inject an isolated root @preserve */
318
- const taskStore = new task_store_1.FileA2ATaskStore(options.agentRoot ?? (0, identity_1.getAgentRoot)(options.agentName));
339
+ const agentRoot = options.agentRoot ?? (0, identity_1.getAgentRoot)(options.agentName);
340
+ const taskStore = new task_store_1.FileA2ATaskStore(agentRoot);
319
341
  /* v8 ignore next -- default runner crosses into the full live agent pipeline; shared-turn covers that pipeline and server tests inject a deterministic runner @preserve */
320
342
  const turnRunner = options.turnRunner ?? defaultTurnRunner;
343
+ // The inbound friends-DataPart bridge is available only when the agent has a
344
+ // cryptographic identity (its did:key + X25519 unseal keys). Build its durable
345
+ // stores once at start; without an identity, inbound stays on the text path.
346
+ let inboundShareDeps;
347
+ if (options.identity) {
348
+ const friendsDir = `${agentRoot}/friends`;
349
+ inboundShareDeps = {
350
+ sodium: await (0, a2a_client_1.ready)(),
351
+ store: new friends_1.FileFriendStore(friendsDir),
352
+ missionStore: new friends_1.FileMissionStore((0, friends_1.missionsDirFor)(friendsDir)),
353
+ pinStore: new pin_store_1.FileA2APinStore(agentRoot),
354
+ seen: new seen_ledger_1.FileA2ASeenLedger(agentRoot),
355
+ didResolution: (0, did_resolution_1.makeDidResolution)({ sodium: await (0, a2a_client_1.ready)() }),
356
+ identity: options.identity,
357
+ };
358
+ }
321
359
  let publicBaseUrl = options.baseUrl;
322
360
  const server = http.createServer(async (req, res) => {
323
361
  /* v8 ignore next -- Node always supplies url/host for accepted HTTP requests; fallback keeps malformed local calls safe @preserve */
@@ -325,7 +363,12 @@ async function startA2AServer(options) {
325
363
  /* v8 ignore next -- host fallback is defensive for malformed local requests; publicBaseUrl/default host branches are covered @preserve */
326
364
  const currentBaseUrl = publicBaseUrl ?? `http://${req.headers.host ?? `${host}:${port}`}`;
327
365
  if (req.method === "GET" && (requestUrl.pathname === "/.well-known/agent-card.json" || requestUrl.pathname === "/agent-card.json")) {
328
- writeJson(res, 200, (0, card_1.buildA2AAgentCard)({ agentName: options.agentName, baseUrl: currentBaseUrl, path: a2aPath }));
366
+ writeJson(res, 200, (0, card_1.buildA2AAgentCard)({
367
+ agentName: options.agentName,
368
+ baseUrl: currentBaseUrl,
369
+ path: a2aPath,
370
+ ...(options.identity ? { did: options.identity.did } : {}),
371
+ }));
329
372
  return;
330
373
  }
331
374
  if (req.method !== "POST" || requestUrl.pathname !== a2aPath) {
@@ -350,7 +393,30 @@ async function startA2AServer(options) {
350
393
  const responseStyle = rpc.method === "message/send" ? "legacy" : "latest";
351
394
  const parsedInbound = messageFromParams(rpc.params);
352
395
  const inbound = parsedInbound ? { ...parsedInbound, kind: parsedInbound.kind ?? "message" } : null;
353
- const text = textFromMessage(inbound);
396
+ // Inbound friends-DataPart branch (runs BEFORE the legacy text path). A
397
+ // sealed DataPart is unwrapped + verified through the bridge; the turn is
398
+ // then keyed on the VERIFIED sender DID (replacing the unauthenticated
399
+ // sentinel). A non-friends (text) message falls through unchanged.
400
+ let verifiedPeerAgentId;
401
+ let verifiedPeerName;
402
+ let verifiedShareText;
403
+ if (inbound && inboundShareDeps && messageHasDataPart(inbound)) {
404
+ const bridged = await (0, inbound_share_1.receiveInboundShare)(inbound, inboundShareDeps);
405
+ if (bridged.outcome === "rejected") {
406
+ writeJson(res, 200, errorResponse(rpc.id, rejectionErrorCode(bridged.reason), `A2A share rejected: ${bridged.reason}`));
407
+ return;
408
+ }
409
+ // A message with a data part always resolves to completed|rejected — the
410
+ // bridge only returns "not-a-share" when there is NO data part, which the
411
+ // `messageHasDataPart` gate above excludes. So this is the completed path.
412
+ /* v8 ignore next -- "not-a-share" is unreachable here (gated by messageHasDataPart) @preserve */
413
+ if (bridged.outcome === "completed") {
414
+ verifiedPeerAgentId = bridged.verifiedDid;
415
+ verifiedPeerName = bridged.verifiedDid;
416
+ verifiedShareText = `[a2a] received ${bridged.friendsKind} (${bridged.status}) from ${bridged.verifiedDid}`;
417
+ }
418
+ }
419
+ const text = verifiedShareText ?? textFromMessage(inbound);
354
420
  if (!inbound || !text) {
355
421
  writeJson(res, 200, errorResponse(rpc.id, -32602, "SendMessage requires a text message"));
356
422
  return;
@@ -360,8 +426,10 @@ async function startA2AServer(options) {
360
426
  return;
361
427
  }
362
428
  const senderHint = senderHintFromMessage(req, inbound);
363
- const peerAgentId = untrustedTurnPeerExternalId();
364
- const peerName = senderHint.name;
429
+ // The verified DID wins as the turn's peer identity; otherwise the legacy
430
+ // unauthenticated sentinel (text path, behavior-preserved).
431
+ const peerAgentId = verifiedPeerAgentId ?? untrustedTurnPeerExternalId();
432
+ const peerName = verifiedPeerName ?? senderHint.name;
365
433
  const continuationToken = accessTokenFromParams(rpc.params);
366
434
  const continuationTask = inbound.taskId && continuationToken
367
435
  ? taskStore.get(inbound.taskId, accessTokenScope(continuationToken))
@@ -5419,14 +5419,6 @@ async function performSystemSetup(deps) {
5419
5419
  await registerOuroBundleTypeNonBlocking(deps);
5420
5420
  }
5421
5421
  // ── Friend command execution ──
5422
- const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
5423
- /* v8 ignore start -- defensive: ?? fallbacks are unreachable when inputs are valid TrustLevel values @preserve */
5424
- function higherTrust(a, b) {
5425
- const rankA = TRUST_RANK[a ?? "stranger"] ?? 1;
5426
- const rankB = TRUST_RANK[b ?? "stranger"] ?? 1;
5427
- return rankA >= rankB ? (a ?? "stranger") : (b ?? "stranger");
5428
- }
5429
- /* v8 ignore stop */
5430
5422
  async function executeFriendCommand(command, store) {
5431
5423
  if (command.kind === "friend.list") {
5432
5424
  const listAll = store.listAll;
@@ -5467,65 +5459,32 @@ async function executeFriendCommand(command, store) {
5467
5459
  return `created: ${id} (${command.name}, ${trustLevel})`;
5468
5460
  }
5469
5461
  if (command.kind === "friend.update") {
5470
- const current = await store.get(command.friendId);
5471
- if (!current)
5462
+ const result = await (0, friends_1.setFriendTrust)(store, command.friendId, command.trustLevel);
5463
+ if (result.status === "not_found")
5472
5464
  return `friend not found: ${command.friendId}`;
5473
- const now = new Date().toISOString();
5474
- await store.put(command.friendId, {
5475
- ...current,
5476
- trustLevel: command.trustLevel,
5477
- role: command.trustLevel,
5478
- updatedAt: now,
5479
- });
5480
5465
  return `updated: ${command.friendId} → trust=${command.trustLevel}`;
5481
5466
  }
5482
5467
  if (command.kind === "friend.link") {
5483
- const current = await store.get(command.friendId);
5484
- if (!current)
5468
+ const result = await (0, friends_1.linkExternalId)(store, command.friendId, {
5469
+ provider: command.provider,
5470
+ externalId: command.externalId,
5471
+ });
5472
+ if (result.status === "not_found")
5485
5473
  return `friend not found: ${command.friendId}`;
5486
- const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
5487
- if (alreadyLinked)
5474
+ if (result.status === "noop")
5488
5475
  return `identity already linked: ${command.provider}:${command.externalId}`;
5489
- const now = new Date().toISOString();
5490
- const newExternalIds = [
5491
- ...current.externalIds,
5492
- { provider: command.provider, externalId: command.externalId, linkedAt: now },
5493
- ];
5494
- // Orphan cleanup: check if another friend has this externalId
5495
- const orphan = await store.findByExternalId(command.provider, command.externalId);
5496
- let mergeMessage = "";
5497
- let mergedNotes = { ...current.notes };
5498
- let mergedTrust = current.trustLevel;
5499
- let orphanExternalIds = [];
5500
- if (orphan && orphan.id !== command.friendId) {
5501
- // Merge orphan's notes (target's notes take priority)
5502
- mergedNotes = { ...orphan.notes, ...current.notes };
5503
- // Keep higher trust level
5504
- mergedTrust = higherTrust(current.trustLevel, orphan.trustLevel);
5505
- // Collect orphan's other externalIds (excluding the one being linked)
5506
- orphanExternalIds = orphan.externalIds.filter((ext) => !(ext.provider === command.provider && ext.externalId === command.externalId));
5507
- await store.delete(orphan.id);
5508
- mergeMessage = ` (merged orphan ${orphan.id})`;
5509
- }
5510
- await store.put(command.friendId, {
5511
- ...current,
5512
- externalIds: [...newExternalIds, ...orphanExternalIds],
5513
- notes: mergedNotes,
5514
- trustLevel: mergedTrust,
5515
- updatedAt: now,
5516
- });
5476
+ const mergeMessage = result.status === "merged" ? " (merged orphan)" : "";
5517
5477
  return `linked ${command.provider}:${command.externalId} to ${command.friendId}${mergeMessage}`;
5518
5478
  }
5519
5479
  // command.kind === "friend.unlink"
5520
- const current = await store.get(command.friendId);
5521
- if (!current)
5480
+ const result = await (0, friends_1.unlinkExternalId)(store, command.friendId, {
5481
+ provider: command.provider,
5482
+ externalId: command.externalId,
5483
+ });
5484
+ if (result.status === "not_found")
5522
5485
  return `friend not found: ${command.friendId}`;
5523
- const idx = current.externalIds.findIndex((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
5524
- if (idx === -1)
5486
+ if (result.status === "noop")
5525
5487
  return `identity not linked: ${command.provider}:${command.externalId}`;
5526
- const now = new Date().toISOString();
5527
- const filtered = current.externalIds.filter((_, i) => i !== idx);
5528
- await store.put(command.friendId, { ...current, externalIds: filtered, updatedAt: now });
5529
5488
  return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
5530
5489
  }
5531
5490
  async function executeA2ACommand(command, deps) {
@@ -40,6 +40,7 @@ const skills_1 = require("./skills");
40
40
  const config_1 = require("../heart/config");
41
41
  const runtime_1 = require("../nerves/runtime");
42
42
  const identity_1 = require("../heart/identity");
43
+ const friends_1 = require("@ouro.bot/friends");
43
44
  const diary_1 = require("../mind/diary");
44
45
  const record_paths_1 = require("../mind/record-paths");
45
46
  const provenance_trust_1 = require("../mind/provenance-trust");
@@ -414,7 +415,8 @@ exports.notesToolDefinitions = [
414
415
  const friendId = ctx.context.friend?.id;
415
416
  if (!friendId)
416
417
  return "i can't save notes -- no friend identity available";
417
- // Validate parameters
418
+ // Validate parameters (input-shape guards stay harness-side so we keep the
419
+ // first-person tool wording; applyFriendNote does the record mutation).
418
420
  if (!a.content)
419
421
  return "i need a content value to save";
420
422
  const validTypes = ["name", "tool_preference", "note"];
@@ -422,44 +424,33 @@ exports.notesToolDefinitions = [
422
424
  return `i don't recognize type '${a.type}' -- use name, tool_preference, or note`;
423
425
  if ((a.type === "tool_preference" || a.type === "note") && !a.key)
424
426
  return "i need a key for tool_preference or note type";
425
- try {
426
- // Read fresh record from disk
427
- const record = await ctx.friendStore.get(friendId);
428
- if (!record)
429
- return "i can't find the friend record on disk";
430
- const isOverride = a.override === "true";
431
- if (a.type === "name") {
432
- const updated = { ...record, name: a.content, updatedAt: new Date().toISOString() };
433
- await ctx.friendStore.put(friendId, updated);
434
- return `saved: name = ${a.content}`;
435
- }
436
- if (a.type === "tool_preference") {
437
- const existing = record.toolPreferences[a.key];
438
- if (existing && !isOverride) {
439
- return `i already have a preference for '${a.key}': "${existing}". if you want to replace it, call again with override: true. or merge both values into content and override.`;
440
- }
441
- const updated = { ...record, toolPreferences: { ...record.toolPreferences, [a.key]: a.content }, updatedAt: new Date().toISOString() };
442
- await ctx.friendStore.put(friendId, updated);
443
- return `saved: toolPreference ${a.key} = ${a.content}`;
444
- }
445
- // type === "note"
446
- // Redirect "name" key to name field
447
- if (a.key === "name") {
448
- const updated = { ...record, name: a.content, updatedAt: new Date().toISOString() };
449
- await ctx.friendStore.put(friendId, updated);
427
+ // Record-domain logic lives in @ouro.bot/friends; this wrapper maps the
428
+ // structured FriendOpResult back to the tool's first-person strings.
429
+ const result = await (0, friends_1.applyFriendNote)(ctx.friendStore, friendId, {
430
+ type: a.type,
431
+ ...(a.key !== undefined ? { key: a.key } : {}),
432
+ content: a.content,
433
+ override: a.override === "true",
434
+ });
435
+ switch (result.status) {
436
+ case "saved":
437
+ if (a.type === "name")
438
+ return `saved: name = ${a.content}`;
439
+ if (a.type === "tool_preference")
440
+ return `saved: toolPreference ${a.key} = ${a.content}`;
441
+ return `saved: note ${a.key} = ${a.content}`;
442
+ case "redirected_to_name":
450
443
  return `updated friend's name to '${a.content}' (stored as name, not a note)`;
451
- }
452
- const existing = record.notes[a.key];
453
- if (existing && !isOverride) {
454
- return `i already have a note for '${a.key}': "${existing.value}". if you want to replace it, call again with override: true. or merge both values into content and override.`;
455
- }
456
- const updated = { ...record, notes: { ...record.notes, [a.key]: { value: a.content, savedAt: new Date().toISOString() } }, updatedAt: new Date().toISOString() };
457
- await ctx.friendStore.put(friendId, updated);
458
- return `saved: note ${a.key} = ${a.content}`;
459
- }
460
- catch (err) {
461
- /* v8 ignore next -- defensive: non-Error branch for String(err) @preserve */
462
- return `error saving note: ${err instanceof Error ? err.message : String(err)}`;
444
+ case "override_required":
445
+ // result.message embeds the existing value; append the harness merge instruction.
446
+ return `${result.message}. if you want to replace it, call again with override: true. or merge both values into content and override.`;
447
+ case "not_found":
448
+ return "i can't find the friend record on disk";
449
+ /* v8 ignore next 2 -- invalid is pre-empted by the harness validation above; kept for exhaustiveness @preserve */
450
+ case "invalid":
451
+ return result.message ?? "i can't save that note";
452
+ default:
453
+ return `error saving note: ${result.message}`;
463
454
  }
464
455
  },
465
456
  summaryKeys: ["type", "key", "content"],
@@ -53,11 +53,41 @@ const runtime_1 = require("../nerves/runtime");
53
53
  message: "booting A2A entrypoint",
54
54
  meta: { entry: "a2a", agentName },
55
55
  });
56
- Promise.resolve().then(() => __importStar(require("../a2a/server"))).then(async ({ startA2AServer }) => {
56
+ Promise.all([
57
+ Promise.resolve().then(() => __importStar(require("../a2a/server"))),
58
+ Promise.resolve().then(() => __importStar(require("../a2a/identity"))),
59
+ Promise.resolve().then(() => __importStar(require("../heart/runtime-credentials"))),
60
+ Promise.resolve().then(() => __importStar(require("../heart/machine-identity"))),
61
+ Promise.resolve().then(() => __importStar(require("@ouro.bot/friends/a2a-client"))),
62
+ ])
63
+ .then(async ([{ startA2AServer }, { loadOrMintA2AIdentity }, { waitForRuntimeCredentialBootstrap, readMachineRuntimeCredentialConfig, upsertMachineRuntimeCredentialConfig }, { loadOrCreateMachineIdentity }, { ready },]) => {
64
+ const os = await Promise.resolve().then(() => __importStar(require("node:os")));
65
+ // Wait for the daemon to deliver the machine-local runtime config (carrying the
66
+ // a2a identity seed) into the in-memory cache before we read or mint identity.
67
+ const bootstrapped = await waitForRuntimeCredentialBootstrap(agentName);
68
+ (0, runtime_1.emitNervesEvent)({
69
+ component: "senses",
70
+ event: "senses.a2a_bootstrap_waited",
71
+ message: "awaited runtime-credential bootstrap before A2A identity load",
72
+ meta: { entry: "a2a", agentName, bootstrapped },
73
+ });
74
+ const sodium = await ready();
75
+ const machineId = loadOrCreateMachineIdentity({ homeDir: os.homedir() }).machineId;
76
+ const machineConfigRead = readMachineRuntimeCredentialConfig(agentName);
77
+ const machineConfig = machineConfigRead.ok ? machineConfigRead.config : {};
78
+ const identity = await loadOrMintA2AIdentity({
79
+ agentName,
80
+ sodium,
81
+ config: machineConfig,
82
+ upsert: async (next) => {
83
+ await upsertMachineRuntimeCredentialConfig(agentName, machineId, next);
84
+ },
85
+ });
57
86
  const rawPort = argValue("--port");
58
87
  const port = rawPort ? Number.parseInt(rawPort, 10) : undefined;
59
88
  await startA2AServer({
60
89
  agentName,
90
+ identity,
61
91
  ...(argValue("--host") ? { host: argValue("--host") } : {}),
62
92
  ...(Number.isInteger(port) ? { port } : {}),
63
93
  ...(argValue("--base-url") ? { baseUrl: argValue("--base-url") } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.670",
3
+ "version": "0.1.0-alpha.672",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -50,7 +50,6 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@anthropic-ai/sdk": "^0.78.0",
53
- "@ouro.bot/friends": "git+https://github.com/ourostack/friends.git#0095316eed6876a27f36a26ddc47ad9da800a652",
54
53
  "@azure/identity": "^4.13.0",
55
54
  "@azure/storage-blob": "^12.31.0",
56
55
  "@microsoft/teams.api": "2.0.11",
@@ -59,6 +58,7 @@
59
58
  "@microsoft/teams.common": "2.0.11",
60
59
  "@microsoft/teams.dev": "2.0.11",
61
60
  "@microsoft/teams.graph": "2.0.11",
61
+ "@ouro.bot/friends": "0.1.0-alpha.7",
62
62
  "@types/react": "^17.0.91",
63
63
  "@types/ws": "^8.18.1",
64
64
  "fast-glob": "^3.3.3",