@ouro.bot/cli 0.1.0-alpha.671 → 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 +6 -0
- package/dist/a2a/card.js +2 -1
- package/dist/a2a/did-resolution.js +93 -0
- package/dist/a2a/identity.js +137 -0
- package/dist/a2a/inbound-share.js +107 -0
- package/dist/a2a/pin-store.js +132 -0
- package/dist/a2a/seen-ledger.js +120 -0
- package/dist/a2a/server.js +73 -5
- package/dist/senses/a2a-entry.js +31 -1
- package/package.json +2 -2
package/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
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
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.671",
|
|
6
12
|
"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
|
+
}
|
|
@@ -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;
|
package/dist/a2a/server.js
CHANGED
|
@@ -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
|
|
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)({
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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))
|
package/dist/senses/a2a-entry.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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#250d1a4bbbad5cd50687975afc463e60e3ff8366",
|
|
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",
|