@ouro.bot/friends 0.1.0-alpha.5 → 0.1.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/changelog.json +6 -0
- package/dist/a2a-client/index.d.ts +1 -0
- package/dist/a2a-client/index.js +5 -1
- package/dist/a2a-client/roster-verify.d.ts +15 -0
- package/dist/a2a-client/roster-verify.js +61 -0
- package/dist/account-roster.d.ts +52 -0
- package/dist/account-roster.js +108 -0
- package/dist/agent-peer.js +5 -1
- package/dist/audit.d.ts +38 -0
- package/dist/audit.js +86 -0
- package/dist/file-bundle.d.ts +5 -0
- package/dist/file-bundle.js +4 -0
- package/dist/friend-lookup.d.ts +9 -0
- package/dist/friend-lookup.js +69 -0
- package/dist/identity.d.ts +17 -0
- package/dist/identity.js +68 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +31 -2
- package/dist/mcp/dispatch.d.ts +12 -1
- package/dist/mcp/dispatch.js +45 -3
- package/dist/mcp/run-main.js +8 -5
- package/dist/mcp/schemas.js +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +2 -2
- package/dist/resolver.d.ts +32 -1
- package/dist/resolver.js +50 -3
- package/dist/roster-store-file.d.ts +16 -0
- package/dist/roster-store-file.js +125 -0
- package/dist/roster-store-memory.d.ts +9 -0
- package/dist/roster-store-memory.js +20 -0
- package/dist/roster-store.d.ts +29 -0
- package/dist/roster-store.js +9 -0
- package/dist/roster-verifier.d.ts +23 -0
- package/dist/roster-verifier.js +47 -0
- package/dist/trust-explanation.d.ts +7 -1
- package/dist/trust-explanation.js +52 -34
- package/dist/trust-mutation.d.ts +13 -1
- package/dist/trust-mutation.js +31 -2
- package/dist/types.d.ts +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -535,7 +535,13 @@ setNervesEmitter((event) => {
|
|
|
535
535
|
`ImportMissionShareResult`, `ImportMissionShareStatus`, `CoordinationEnvelope`,
|
|
536
536
|
`PrepareCoordinationInput`, `PrepareCoordinationResult`, `PrepareCoordinationStatus`,
|
|
537
537
|
`ImportCoordinationInput`, `ImportCoordinationOptions`, `ImportCoordinationResult`,
|
|
538
|
-
`ImportCoordinationStatus
|
|
538
|
+
`ImportCoordinationStatus`, `SetFriendTrustContext`, `AuditSink`, `ControlPlaneAuditRecord`,
|
|
539
|
+
`ResolvedAgentIdentity`, `RosterStore`, `AccountRoster`, `RosterPin`, `RosterVerifier`,
|
|
540
|
+
`AccountMembershipDecision`, `AccountMembershipResult`, `EvaluateAccountMembershipInput`,
|
|
541
|
+
`FriendResolverRosterContext`. (`TrustBasis` additively gains the `"same_account"` member — the basis
|
|
542
|
+
for family granted via the signed account roster — and `AgentMeta` additively gains an optional
|
|
543
|
+
`identity { did, pinnedKey?, handle?, pinnedAt? }` durable-identity home; both are schemaVersion-1
|
|
544
|
+
additive, and a legacy `a2a.did` migrates-on-read into `identity.did`.)
|
|
539
545
|
|
|
540
546
|
**Values:** `TRUSTED_LEVELS`, `IDENTITY_SCOPES`, `isTrustedLevel`, `isIdentityProvider`,
|
|
541
547
|
`isIntegration`, `isShareScope`, `isCoordinationIntent`, `FileFriendStore`, `FileGrantStore`,
|
|
@@ -548,7 +554,10 @@ setNervesEmitter((event) => {
|
|
|
548
554
|
`trustImpliedPolicy`, `tieredPolicy`, `DEFAULT_CONSENT_POLICY`, `tofuVerifier`,
|
|
549
555
|
`DEFAULT_AGENT_VERIFIER`, `prepareProfileShare`, `importProfileShare`, `prepareMissionShare`,
|
|
550
556
|
`importMissionShare`, `prepareCoordination`, `importCoordination`, `grantShare`, `revokeShare`,
|
|
551
|
-
`listShares`, `isGrantEffective`, `setNervesEmitter`, `emitNervesEvent
|
|
557
|
+
`listShares`, `isGrantEffective`, `setNervesEmitter`, `emitNervesEvent`, `resolveAgentIdentity`,
|
|
558
|
+
`withMigratedIdentity`, `findFriendByDid`, `MemoryAuditSink`, `FileAuditSink`, `auditPathFor`,
|
|
559
|
+
`FileRosterStore`, `rostersDirFor`, `MemoryRosterStore`, `identityRosterVerifier`,
|
|
560
|
+
`DEFAULT_ROSTER_VERIFIER`, `evaluateAccountMembership`.
|
|
552
561
|
|
|
553
562
|
**From `@ouro.bot/friends/mcp`:** `createFriendsMcpServer`, `getToolSchemas`, `runMain` (plus the
|
|
554
563
|
`McpToolSchema`, `FriendsMcpServer`, and `RunMainIo` types).
|
|
@@ -565,7 +574,9 @@ setNervesEmitter((event) => {
|
|
|
565
574
|
identity helpers `parseDidKey` / `keyAgreementFromDidKey` / `didKeyIdentityFromEd25519` /
|
|
566
575
|
`ed25519PubToDidKey` and `didWebToUrl` / `resolveDidWeb` / `parseDidDocument`; the primitives
|
|
567
576
|
`sealTo` / `openSealed`, `signEnvelope` / `verifyEnvelopeSignature`, `jcsString` / `jcsBytes`, and
|
|
568
|
-
the `ready` init seam
|
|
577
|
+
the `ready` init seam; and the account-roster Ed25519 verify `ed25519RosterVerifier` / `signRoster`
|
|
578
|
+
(the crypto implementation of the core `RosterVerifier` seam — host-injected, so the core stays
|
|
579
|
+
transport-free) (plus the `A2ATransport`, `DidResolution`, `SealedEnvelope`, `StructuredProof`,
|
|
569
580
|
`ReachabilityPlan`, `FriendsAgentCard`, `DidKeyIdentity`, `DidDocument` types). The transports
|
|
570
581
|
(direct A2A / relay / git op) are injected by the host — this module does no network or git itself.
|
|
571
582
|
|
package/changelog.json
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"versions": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.1.0-alpha.6",
|
|
5
|
+
"changes": [
|
|
6
|
+
"p11 increment 1 — own-fleet foundation. Bug A: cold A2A contact defaults to stranger (safe-by-default; explicit owner-initiated trustLevel still wins). Bug B: append-only control-plane audit (AuditSink + MemoryAuditSink/FileAuditSink) on every setFriendTrust, now wired into the live MCP dispatch path (set_trust + the onboard_agent trust seat) via openFileBundle's FileAuditSink, attributed to the owner-only stdio boundary. Bug C: FriendResolver is roster-aware — a key-verified member of the pinned account roster is recognized as family across OS users, without loosening the cold-A2A stranger default. DID re-key (additive, schemaVersion still 1): AgentMeta.identity durable home with a2a.did migrate-on-read; did-aware findFriendByDid. Account roster: RosterStore/FileRosterStore/MemoryRosterStore + the RosterVerifier seam (core identity-only default; a2a-client ed25519RosterVerifier Ed25519 impl) granting family via a new same_account TrustBasis only to a TOFU-pinned, key-verified roster member (changed roster key hard-fails). Security hardening of the trust primitives: the family-grant path FAILS CLOSED — the identity-only default can never grant family (only a cryptographic verifier can, gated by a grantsFamily capability), with a loud one-time warning; evaluateAccountMembership requires an unforgeable VerifiedCandidate so the did-control precondition can't be skipped; findFriendByDid rejects falsy-did queries and no longer rewards back-dating on a duplicate-did anomaly (prefers the pinned record, warns); empty-string DIDs are never matchable keys; and FileRosterStore guards accountId against path traversal. Core remains transport-free (the CI core⊥a2a-client dependency rule is unmodified)."
|
|
7
|
+
]
|
|
8
|
+
},
|
|
3
9
|
{
|
|
4
10
|
"version": "0.1.0-alpha.5",
|
|
5
11
|
"changes": [
|
|
@@ -19,5 +19,6 @@ export { buildFriendsAgentCard } from "./agent-card";
|
|
|
19
19
|
export type { A2ACapabilities, A2ASkill, FriendsAgentCard } from "./agent-card";
|
|
20
20
|
export { resolveReachability } from "./reachability";
|
|
21
21
|
export type { ReachabilityPlan } from "./reachability";
|
|
22
|
+
export { ed25519RosterVerifier, signRoster } from "./roster-verify";
|
|
22
23
|
export { receiveShare, sendShare } from "./adapter";
|
|
23
24
|
export type { A2ATransport, DidResolution, ReceiveShareInput, ReceiveShareResult, SeenLedgerLike, SendShareInput, SendShareResult, } from "./adapter";
|
package/dist/a2a-client/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// recipient DID bound into the AEAD AD), so a relay carries CIPHERTEXT ONLY — it
|
|
11
11
|
// can never read, forge, tamper, re-target, replay-to-effect, or escalate.
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.sendShare = exports.receiveShare = exports.resolveReachability = exports.buildFriendsAgentCard = exports.wrapInDataPart = exports.unwrapDataPart = exports.sealEnvelope = exports.openSealedEnvelope = exports.verifyCardDidBinding = exports.signSuccessor = exports.pinOnFirstContact = exports.MemoryPinStore = exports.isPinned = exports.getPinned = exports.evaluateRotation = exports.DidVerifier = exports.resolveDidWeb = exports.parseDidDocument = exports.didWebToUrl = exports.parseDidKey = exports.keyAgreementFromDidKey = exports.ed25519PubToDidKey = exports.didKeyIdentityFromEd25519 = exports.base58btcEncode = exports.base58btcDecode = exports.verifyEnvelopeSignature = exports.signEnvelope = exports.serializeProof = exports.parseProof = exports.SealOpenError = exports.sealTo = exports.openSealed = exports.jcsString = exports.jcsBytes = exports.ready = void 0;
|
|
13
|
+
exports.sendShare = exports.receiveShare = exports.signRoster = exports.ed25519RosterVerifier = exports.resolveReachability = exports.buildFriendsAgentCard = exports.wrapInDataPart = exports.unwrapDataPart = exports.sealEnvelope = exports.openSealedEnvelope = exports.verifyCardDidBinding = exports.signSuccessor = exports.pinOnFirstContact = exports.MemoryPinStore = exports.isPinned = exports.getPinned = exports.evaluateRotation = exports.DidVerifier = exports.resolveDidWeb = exports.parseDidDocument = exports.didWebToUrl = exports.parseDidKey = exports.keyAgreementFromDidKey = exports.ed25519PubToDidKey = exports.didKeyIdentityFromEd25519 = exports.base58btcEncode = exports.base58btcDecode = exports.verifyEnvelopeSignature = exports.signEnvelope = exports.serializeProof = exports.parseProof = exports.SealOpenError = exports.sealTo = exports.openSealed = exports.jcsString = exports.jcsBytes = exports.ready = void 0;
|
|
14
14
|
// ── init seam ──
|
|
15
15
|
var sodium_1 = require("./sodium");
|
|
16
16
|
Object.defineProperty(exports, "ready", { enumerable: true, get: function () { return sodium_1.ready; } });
|
|
@@ -66,6 +66,10 @@ Object.defineProperty(exports, "buildFriendsAgentCard", { enumerable: true, get:
|
|
|
66
66
|
// ── reachability ladder ──
|
|
67
67
|
var reachability_1 = require("./reachability");
|
|
68
68
|
Object.defineProperty(exports, "resolveReachability", { enumerable: true, get: function () { return reachability_1.resolveReachability; } });
|
|
69
|
+
// ── account-roster Ed25519 verify (the RosterVerifier seam's crypto impl) ──
|
|
70
|
+
var roster_verify_1 = require("./roster-verify");
|
|
71
|
+
Object.defineProperty(exports, "ed25519RosterVerifier", { enumerable: true, get: function () { return roster_verify_1.ed25519RosterVerifier; } });
|
|
72
|
+
Object.defineProperty(exports, "signRoster", { enumerable: true, get: function () { return roster_verify_1.signRoster; } });
|
|
69
73
|
// ── send / receive adapter ──
|
|
70
74
|
var adapter_1 = require("./adapter");
|
|
71
75
|
Object.defineProperty(exports, "receiveShare", { enumerable: true, get: function () { return adapter_1.receiveShare; } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Sodium } from "./sodium";
|
|
2
|
+
import type { RosterVerifier } from "../roster-verifier";
|
|
3
|
+
import type { AccountRoster } from "../roster-store";
|
|
4
|
+
/** A RosterVerifier that checks the roster's Ed25519 signature against the base64
|
|
5
|
+
* `rosterKey`. Returns false (never throws) on a malformed key/sig or a bad
|
|
6
|
+
* signature. (Unit 7a stub — verify not implemented.) */
|
|
7
|
+
export declare function ed25519RosterVerifier(sodium: Sodium): RosterVerifier;
|
|
8
|
+
/** Test/host helper: produce a valid roster `sig` by signing `rosterSigningBytes`
|
|
9
|
+
* with the account's Ed25519 private key (mirrors `signSuccessor`/`signEnvelope`).
|
|
10
|
+
* Returns the base64 (ORIGINAL) detached signature. */
|
|
11
|
+
export declare function signRoster(input: {
|
|
12
|
+
sodium: Sodium;
|
|
13
|
+
accountKeyPriv: Uint8Array;
|
|
14
|
+
roster: Omit<AccountRoster, "sig">;
|
|
15
|
+
}): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ed25519RosterVerifier = ed25519RosterVerifier;
|
|
4
|
+
exports.signRoster = signRoster;
|
|
5
|
+
// ed25519RosterVerifier — the RosterVerifier seam's REAL crypto implementation
|
|
6
|
+
// (a2a-client side; MAY import libsodium/jcs/sign). Mirrors how DidVerifier
|
|
7
|
+
// implements the AgentVerifier seam. The host injects this so the core stays
|
|
8
|
+
// transport-free.
|
|
9
|
+
//
|
|
10
|
+
// Contract (shared with src/roster-verifier.ts): the roster `sig` is an Ed25519
|
|
11
|
+
// detached signature over `jcsBytes({ accountId, members, epoch })` — the roster
|
|
12
|
+
// MINUS `sig` — exactly how `verifyEnvelopeSignature` signs the proof-stripped
|
|
13
|
+
// envelope. `rosterKey` is the base64 (ORIGINAL) Ed25519 public key.
|
|
14
|
+
const jcs_1 = require("./jcs");
|
|
15
|
+
/** The canonical bytes the roster `sig` is computed over: the roster minus its
|
|
16
|
+
* `sig` field, JCS-canonicalized. Both signer and verifier MUST produce these
|
|
17
|
+
* identical bytes. */
|
|
18
|
+
function rosterSigningBytes(roster) {
|
|
19
|
+
return (0, jcs_1.jcsBytes)({ accountId: roster.accountId, members: roster.members, epoch: roster.epoch });
|
|
20
|
+
}
|
|
21
|
+
/** A RosterVerifier that checks the roster's Ed25519 signature against the base64
|
|
22
|
+
* `rosterKey`. Returns false (never throws) on a malformed key/sig or a bad
|
|
23
|
+
* signature. (Unit 7a stub — verify not implemented.) */
|
|
24
|
+
function ed25519RosterVerifier(sodium) {
|
|
25
|
+
return {
|
|
26
|
+
// SECURITY (finding 1): the REAL cryptographic verifier — the only RosterVerifier
|
|
27
|
+
// strong enough to back a family grant. `evaluateAccountMembership` checks this
|
|
28
|
+
// flag and fails closed (→ unverified) under any verifier that lacks it.
|
|
29
|
+
grantsFamily: true,
|
|
30
|
+
verify(roster, rosterKey) {
|
|
31
|
+
let pub;
|
|
32
|
+
let sig;
|
|
33
|
+
try {
|
|
34
|
+
pub = sodium.from_base64(rosterKey, sodium.base64_variants.ORIGINAL);
|
|
35
|
+
sig = sodium.from_base64(roster.sig, sodium.base64_variants.ORIGINAL);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Malformed base64 in the key or sig → a failed verification, never a throw.
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const msg = rosterSigningBytes(roster);
|
|
42
|
+
try {
|
|
43
|
+
return sodium.crypto_sign_verify_detached(sig, msg, pub);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A wrong-length key/sig can throw inside libsodium — treat as a failed
|
|
47
|
+
// verification, never an uncaught error (mirrors verifyEnvelopeSignature).
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Test/host helper: produce a valid roster `sig` by signing `rosterSigningBytes`
|
|
54
|
+
* with the account's Ed25519 private key (mirrors `signSuccessor`/`signEnvelope`).
|
|
55
|
+
* Returns the base64 (ORIGINAL) detached signature. */
|
|
56
|
+
function signRoster(input) {
|
|
57
|
+
const { sodium, accountKeyPriv, roster } = input;
|
|
58
|
+
const msg = (0, jcs_1.jcsBytes)({ accountId: roster.accountId, members: roster.members, epoch: roster.epoch });
|
|
59
|
+
const sig = sodium.crypto_sign_detached(msg, accountKeyPriv);
|
|
60
|
+
return sodium.to_base64(sig, sodium.base64_variants.ORIGINAL);
|
|
61
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AccountRoster, RosterStore } from "./roster-store";
|
|
2
|
+
import type { RosterVerifier } from "./roster-verifier";
|
|
3
|
+
export type AccountMembershipDecision = "family_same_account" | "not_member" | "unverified" | "roster_key_mismatch";
|
|
4
|
+
/** SECURITY (finding 2, HIGH): an opaque "the caller has verified this peer controls
|
|
5
|
+
* this did" token. `evaluateAccountMembership` grants family ONLY for a value of this
|
|
6
|
+
* type, and the ONLY way to produce one is `verifiedCandidate(did)` — so the
|
|
7
|
+
* candidate-DID precondition is impossible to forget at a call site (a bare string
|
|
8
|
+
* does not type-check). The private brand makes it unforgeable from a plain object. */
|
|
9
|
+
export interface VerifiedCandidate {
|
|
10
|
+
readonly did: string;
|
|
11
|
+
/** Private brand — prevents `{ did }` from structurally satisfying the type. */
|
|
12
|
+
readonly [VERIFIED_BRAND]: true;
|
|
13
|
+
}
|
|
14
|
+
declare const VERIFIED_BRAND: unique symbol;
|
|
15
|
+
/** Mint a {@link VerifiedCandidate}. CALLING THIS IS AN ASSERTION: the caller has
|
|
16
|
+
* already proven (via a DID/pinned-key handshake — e.g. the a2a-client sealed-envelope
|
|
17
|
+
* gate that runs `DidVerifier` before this) that the peer controls `did`. Never call
|
|
18
|
+
* it on an attacker-supplied did that has not been authenticated. */
|
|
19
|
+
export declare function verifiedCandidate(did: string): VerifiedCandidate;
|
|
20
|
+
export interface EvaluateAccountMembershipInput {
|
|
21
|
+
roster: AccountRoster;
|
|
22
|
+
/** SECURITY (finding 2): the verified candidate — only mintable via
|
|
23
|
+
* `verifiedCandidate(did)` after the caller has authenticated the peer's control of
|
|
24
|
+
* the did. The roster membership + sig checks are NOT a proof of did-control on
|
|
25
|
+
* their own; this token supplies that missing precondition. */
|
|
26
|
+
candidate: VerifiedCandidate;
|
|
27
|
+
rosterKey: string;
|
|
28
|
+
store: RosterStore;
|
|
29
|
+
verifier?: RosterVerifier;
|
|
30
|
+
}
|
|
31
|
+
export interface AccountMembershipResult {
|
|
32
|
+
decision: AccountMembershipDecision;
|
|
33
|
+
reason?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Test seam: reset the one-time-warning latch so a test can assert the loud warning
|
|
36
|
+
* fires (and de-dupes) deterministically, independent of test order. */
|
|
37
|
+
export declare function _resetRosterVerifierWarningForTest(): void;
|
|
38
|
+
/** Decide whether the VERIFIED `candidate` is family-via-same-account under `roster`.
|
|
39
|
+
*
|
|
40
|
+
* Preconditions (all enforced, not merely documented):
|
|
41
|
+
* - The caller has authenticated that the peer controls `candidate.did` (carried by
|
|
42
|
+
* the unforgeable {@link VerifiedCandidate} — finding 2). Membership + sig are NOT
|
|
43
|
+
* a substitute for did-control.
|
|
44
|
+
* - A real cryptographic `verifier` (`grantsFamily: true`) is injected. The
|
|
45
|
+
* identity-only default fails closed: it can verify identity for non-grant checks
|
|
46
|
+
* but can NEVER produce a `family_same_account` grant (finding 1).
|
|
47
|
+
*
|
|
48
|
+
* Flow: TOFU-pin the roster key on first contact; a changed key hard-fails; the
|
|
49
|
+
* verifier must accept the roster; the verifier must be family-granting; the
|
|
50
|
+
* candidate's did must be in the roster. Any miss yields a non-family decision. */
|
|
51
|
+
export declare function evaluateAccountMembership(input: EvaluateAccountMembershipInput): Promise<AccountMembershipResult>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifiedCandidate = verifiedCandidate;
|
|
4
|
+
exports._resetRosterVerifierWarningForTest = _resetRosterVerifierWarningForTest;
|
|
5
|
+
exports.evaluateAccountMembership = evaluateAccountMembership;
|
|
6
|
+
// evaluateAccountMembership — the Increment-1 payoff (Item 3). Grants `family` via
|
|
7
|
+
// TrustBasis "same_account" ONLY to a peer whose `did` is in the pinned roster AND
|
|
8
|
+
// whose roster verifies under the TOFU-pinned roster key. A changed roster key
|
|
9
|
+
// HARD-FAILS (no silent re-pin).
|
|
10
|
+
//
|
|
11
|
+
// CORE module: it uses the INJECTED RosterVerifier + RosterStore seams and does NO
|
|
12
|
+
// direct crypto (no a2a-client / libsodium import) — the Ed25519 verifier is
|
|
13
|
+
// injected by the host/test. The lint enforces the dependency direction.
|
|
14
|
+
const observability_1 = require("./observability");
|
|
15
|
+
const roster_verifier_1 = require("./roster-verifier");
|
|
16
|
+
/** Mint a {@link VerifiedCandidate}. CALLING THIS IS AN ASSERTION: the caller has
|
|
17
|
+
* already proven (via a DID/pinned-key handshake — e.g. the a2a-client sealed-envelope
|
|
18
|
+
* gate that runs `DidVerifier` before this) that the peer controls `did`. Never call
|
|
19
|
+
* it on an attacker-supplied did that has not been authenticated. */
|
|
20
|
+
function verifiedCandidate(did) {
|
|
21
|
+
return { did };
|
|
22
|
+
}
|
|
23
|
+
/** One-time loud-warning latch: we warn at most once per process when a family grant
|
|
24
|
+
* is refused purely because the active verifier is not cryptographic (finding 1). */
|
|
25
|
+
let warnedNonCryptographicVerifier = false;
|
|
26
|
+
/** Test seam: reset the one-time-warning latch so a test can assert the loud warning
|
|
27
|
+
* fires (and de-dupes) deterministically, independent of test order. */
|
|
28
|
+
function _resetRosterVerifierWarningForTest() {
|
|
29
|
+
warnedNonCryptographicVerifier = false;
|
|
30
|
+
}
|
|
31
|
+
/** Decide whether the VERIFIED `candidate` is family-via-same-account under `roster`.
|
|
32
|
+
*
|
|
33
|
+
* Preconditions (all enforced, not merely documented):
|
|
34
|
+
* - The caller has authenticated that the peer controls `candidate.did` (carried by
|
|
35
|
+
* the unforgeable {@link VerifiedCandidate} — finding 2). Membership + sig are NOT
|
|
36
|
+
* a substitute for did-control.
|
|
37
|
+
* - A real cryptographic `verifier` (`grantsFamily: true`) is injected. The
|
|
38
|
+
* identity-only default fails closed: it can verify identity for non-grant checks
|
|
39
|
+
* but can NEVER produce a `family_same_account` grant (finding 1).
|
|
40
|
+
*
|
|
41
|
+
* Flow: TOFU-pin the roster key on first contact; a changed key hard-fails; the
|
|
42
|
+
* verifier must accept the roster; the verifier must be family-granting; the
|
|
43
|
+
* candidate's did must be in the roster. Any miss yields a non-family decision. */
|
|
44
|
+
async function evaluateAccountMembership(input) {
|
|
45
|
+
const { roster, candidate, rosterKey, store } = input;
|
|
46
|
+
const accountId = roster.accountId;
|
|
47
|
+
// 1) Roster-key pin (TOFU). First contact pins the key; an EXISTING pin for a
|
|
48
|
+
// DIFFERENT key HARD-FAILS (no silent re-pin); a matching pin proceeds.
|
|
49
|
+
const existingPin = await store.getPin(accountId);
|
|
50
|
+
if (!existingPin) {
|
|
51
|
+
await store.putPin({ accountId, rosterKey, pinnedAt: new Date().toISOString() });
|
|
52
|
+
}
|
|
53
|
+
else if (existingPin.rosterKey !== rosterKey) {
|
|
54
|
+
const result = {
|
|
55
|
+
decision: "roster_key_mismatch",
|
|
56
|
+
reason: "presented roster key does not match the pinned key",
|
|
57
|
+
};
|
|
58
|
+
emit(result.decision, accountId);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
// 2) Authenticity: the injected verifier (or the identity default) must accept
|
|
62
|
+
// the roster under the pinned/presented key.
|
|
63
|
+
const verifier = input.verifier ?? roster_verifier_1.DEFAULT_ROSTER_VERIFIER;
|
|
64
|
+
if (!verifier.verify(roster, rosterKey)) {
|
|
65
|
+
const result = { decision: "unverified", reason: "roster signature did not verify" };
|
|
66
|
+
emit(result.decision, accountId);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
// 2b) SECURITY (finding 1, HIGH): FAIL CLOSED on the family-granting path. The
|
|
70
|
+
// identity-only default accepts any well-formed roster (it ignores the sig), so it
|
|
71
|
+
// MUST NOT be allowed to grant family — only a real cryptographic verifier
|
|
72
|
+
// (`grantsFamily: true`) can. Without one, the strongest tier is unreachable: a
|
|
73
|
+
// would-be member is `unverified`, never `family_same_account`. Warn LOUDLY once.
|
|
74
|
+
if (verifier.grantsFamily !== true) {
|
|
75
|
+
if (!warnedNonCryptographicVerifier) {
|
|
76
|
+
warnedNonCryptographicVerifier = true;
|
|
77
|
+
(0, observability_1.emitNervesEvent)({
|
|
78
|
+
level: "warn",
|
|
79
|
+
component: "friends",
|
|
80
|
+
event: "friends.roster_verifier_not_cryptographic",
|
|
81
|
+
message: "REFUSING to grant family_same_account: no cryptographic RosterVerifier injected (the identity-only default cannot back a family grant). Inject ed25519RosterVerifier to enable same-account family.",
|
|
82
|
+
meta: { accountId },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const result = {
|
|
86
|
+
decision: "unverified",
|
|
87
|
+
reason: "no cryptographic roster verifier injected — family grant withheld (fail-closed)",
|
|
88
|
+
};
|
|
89
|
+
emit(result.decision, accountId);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
// 3) Membership: the candidate's did must be in the verified roster.
|
|
93
|
+
const isMember = roster.members.some((m) => m.did === candidate.did);
|
|
94
|
+
const result = isMember
|
|
95
|
+
? { decision: "family_same_account" }
|
|
96
|
+
: { decision: "not_member", reason: "candidate did is not in the roster" };
|
|
97
|
+
emit(result.decision, accountId);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
/** Emit the membership-evaluated nerves event. */
|
|
101
|
+
function emit(decision, accountId) {
|
|
102
|
+
(0, observability_1.emitNervesEvent)({
|
|
103
|
+
component: "friends",
|
|
104
|
+
event: "friends.account_membership_evaluated",
|
|
105
|
+
message: "evaluated account-roster membership",
|
|
106
|
+
meta: { accountId, decision },
|
|
107
|
+
});
|
|
108
|
+
}
|
package/dist/agent-peer.js
CHANGED
|
@@ -13,7 +13,11 @@ async function upsertAgentPeer(store, input) {
|
|
|
13
13
|
const { name, agentId, a2a, bundleName } = input;
|
|
14
14
|
const existing = await store.findByExternalId("a2a-agent", agentId);
|
|
15
15
|
const now = new Date().toISOString();
|
|
16
|
-
|
|
16
|
+
// Bug A — cold contact is safe-by-default: a brand-new peer with no explicit
|
|
17
|
+
// trustLevel and no existing record lands at `stranger`, not `acquaintance`. An
|
|
18
|
+
// owner-initiated onboard that passes an explicit `trustLevel`, and an existing
|
|
19
|
+
// record's level, both still win (they precede this fallback).
|
|
20
|
+
const trustLevel = input.trustLevel ?? existing?.trustLevel ?? "stranger";
|
|
17
21
|
const baseMeta = existing?.agentMeta ?? {
|
|
18
22
|
bundleName: bundleName ?? name,
|
|
19
23
|
familiarity: 0,
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TrustBasis } from "./trust-explanation";
|
|
2
|
+
import type { TrustLevel } from "./types";
|
|
3
|
+
/** One append-only control-plane audit record. Captures a single trust mutation:
|
|
4
|
+
* WHO (`actor`), to WHOM (`targetId` / optional `targetDid`), the new `level`, the
|
|
5
|
+
* `basis` it was granted on, the `originSense` it came through, and WHEN (`ts`). */
|
|
6
|
+
export interface ControlPlaneAuditRecord {
|
|
7
|
+
action: "set_trust";
|
|
8
|
+
targetId: string;
|
|
9
|
+
targetDid?: string;
|
|
10
|
+
level: TrustLevel;
|
|
11
|
+
basis?: TrustBasis;
|
|
12
|
+
actor: string;
|
|
13
|
+
originSense?: string;
|
|
14
|
+
ts: string;
|
|
15
|
+
}
|
|
16
|
+
/** The append-only sink a control-plane mutation writes through. The host
|
|
17
|
+
* implements it (in-memory in tests, a file/JSONL adapter in production). */
|
|
18
|
+
export interface AuditSink {
|
|
19
|
+
append(record: ControlPlaneAuditRecord): Promise<void> | void;
|
|
20
|
+
}
|
|
21
|
+
/** In-memory append-only sink — test/host convenience, mirroring MemoryPinStore.
|
|
22
|
+
* `list()` exposes the records in append order; there is no overwrite. */
|
|
23
|
+
export declare class MemoryAuditSink implements AuditSink {
|
|
24
|
+
private readonly records;
|
|
25
|
+
append(record: ControlPlaneAuditRecord): void;
|
|
26
|
+
list(): ControlPlaneAuditRecord[];
|
|
27
|
+
}
|
|
28
|
+
/** The append-only control-plane log file for a given friends directory:
|
|
29
|
+
* `<friendsDir>/_audit/control.jsonl`. A reserved `_`-prefixed sibling (like
|
|
30
|
+
* `_grants/`) so one `--dir` covers it; JSONL so appends never rewrite history. */
|
|
31
|
+
export declare function auditPathFor(friendsDir: string): string;
|
|
32
|
+
/** Filesystem AuditSink — appends each record as one JSON line to
|
|
33
|
+
* `_audit/control.jsonl`. mkdir-on-construct, mirroring FileGrantStore. */
|
|
34
|
+
export declare class FileAuditSink implements AuditSink {
|
|
35
|
+
private readonly filePath;
|
|
36
|
+
constructor(filePath: string);
|
|
37
|
+
append(record: ControlPlaneAuditRecord): Promise<void>;
|
|
38
|
+
}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
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.FileAuditSink = exports.MemoryAuditSink = void 0;
|
|
37
|
+
exports.auditPathFor = auditPathFor;
|
|
38
|
+
// Control-plane audit (Bug B) — an append-only record of every trust mutation.
|
|
39
|
+
//
|
|
40
|
+
// The control plane is "who changed a peer's standing, from where, and why". The
|
|
41
|
+
// package must stay storage-agnostic (and 100%-coverable), so the audit is an
|
|
42
|
+
// injectable SINK — not a hard-wired `fs` write — mirroring the observability
|
|
43
|
+
// seam and the GrantStore/FileGrantStore split. `setFriendTrust` writes one record
|
|
44
|
+
// on a successful mutation; the host wires a `FileAuditSink` (or its own) to
|
|
45
|
+
// persist it. With no sink injected, the mutation is unchanged (no-op audit).
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const fsPromises = __importStar(require("fs/promises"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const observability_1 = require("./observability");
|
|
50
|
+
/** In-memory append-only sink — test/host convenience, mirroring MemoryPinStore.
|
|
51
|
+
* `list()` exposes the records in append order; there is no overwrite. */
|
|
52
|
+
class MemoryAuditSink {
|
|
53
|
+
records = [];
|
|
54
|
+
append(record) {
|
|
55
|
+
this.records.push(record);
|
|
56
|
+
}
|
|
57
|
+
list() {
|
|
58
|
+
return [...this.records];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.MemoryAuditSink = MemoryAuditSink;
|
|
62
|
+
/** The append-only control-plane log file for a given friends directory:
|
|
63
|
+
* `<friendsDir>/_audit/control.jsonl`. A reserved `_`-prefixed sibling (like
|
|
64
|
+
* `_grants/`) so one `--dir` covers it; JSONL so appends never rewrite history. */
|
|
65
|
+
function auditPathFor(friendsDir) {
|
|
66
|
+
return path.join(friendsDir, "_audit", "control.jsonl");
|
|
67
|
+
}
|
|
68
|
+
/** Filesystem AuditSink — appends each record as one JSON line to
|
|
69
|
+
* `_audit/control.jsonl`. mkdir-on-construct, mirroring FileGrantStore. */
|
|
70
|
+
class FileAuditSink {
|
|
71
|
+
filePath;
|
|
72
|
+
constructor(filePath) {
|
|
73
|
+
this.filePath = filePath;
|
|
74
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
75
|
+
(0, observability_1.emitNervesEvent)({
|
|
76
|
+
component: "friends",
|
|
77
|
+
event: "friends.audit_sink_init",
|
|
78
|
+
message: "file audit sink initialized",
|
|
79
|
+
meta: {},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async append(record) {
|
|
83
|
+
await fsPromises.appendFile(this.filePath, JSON.stringify(record) + "\n", "utf-8");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.FileAuditSink = FileAuditSink;
|
package/dist/file-bundle.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { FileFriendStore } from "./store-file";
|
|
2
2
|
import { FileGrantStore } from "./grant-store-file";
|
|
3
3
|
import { FileMissionStore } from "./mission-store-file";
|
|
4
|
+
import { FileAuditSink } from "./audit";
|
|
4
5
|
export interface FileBundle {
|
|
5
6
|
store: FileFriendStore;
|
|
6
7
|
grants: FileGrantStore;
|
|
7
8
|
missions: FileMissionStore;
|
|
9
|
+
/** Control-plane audit sink (Bug B, finding 3) over the sibling `_audit/control.jsonl`,
|
|
10
|
+
* so the live MCP `set_trust` / `onboard_agent` trust seat write audit records. */
|
|
11
|
+
audit: FileAuditSink;
|
|
8
12
|
friendsDir: string;
|
|
9
13
|
grantsDir: string;
|
|
10
14
|
missionsDir: string;
|
|
15
|
+
auditPath: string;
|
|
11
16
|
}
|
|
12
17
|
export declare function openFileBundle(friendsDir: string): FileBundle;
|
package/dist/file-bundle.js
CHANGED
|
@@ -7,17 +7,21 @@ exports.openFileBundle = openFileBundle;
|
|
|
7
7
|
const store_file_1 = require("./store-file");
|
|
8
8
|
const grant_store_file_1 = require("./grant-store-file");
|
|
9
9
|
const mission_store_file_1 = require("./mission-store-file");
|
|
10
|
+
const audit_1 = require("./audit");
|
|
10
11
|
const observability_1 = require("./observability");
|
|
11
12
|
function openFileBundle(friendsDir) {
|
|
12
13
|
const grantsDir = (0, grant_store_file_1.grantsDirFor)(friendsDir);
|
|
13
14
|
const missionsDir = (0, mission_store_file_1.missionsDirFor)(friendsDir);
|
|
15
|
+
const auditPath = (0, audit_1.auditPathFor)(friendsDir);
|
|
14
16
|
(0, observability_1.emitNervesEvent)({ component: "friends", event: "friends.file_bundle_opened", message: "opened file bundle", meta: {} });
|
|
15
17
|
return {
|
|
16
18
|
store: new store_file_1.FileFriendStore(friendsDir),
|
|
17
19
|
grants: new grant_store_file_1.FileGrantStore(grantsDir),
|
|
18
20
|
missions: new mission_store_file_1.FileMissionStore(missionsDir),
|
|
21
|
+
audit: new audit_1.FileAuditSink(auditPath),
|
|
19
22
|
friendsDir,
|
|
20
23
|
grantsDir,
|
|
21
24
|
missionsDir,
|
|
25
|
+
auditPath,
|
|
22
26
|
};
|
|
23
27
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FriendStore } from "./store";
|
|
2
|
+
import type { FriendRecord } from "./types";
|
|
3
|
+
/** Find the friend record whose durable identity DID equals `did`. A DUPLICATE did is
|
|
4
|
+
* an anomaly: it emits a loud `friends.duplicate_did` warning and resolves
|
|
5
|
+
* deterministically WITHOUT rewarding back-dating — a pinned/verified record wins, else
|
|
6
|
+
* the lowest record `id` (a stable, non-temporal tie-break) — see {@link preferOverBest}.
|
|
7
|
+
* Returns null when no record matches, the query did is falsy, or the store has no
|
|
8
|
+
* `listAll`. */
|
|
9
|
+
export declare function findFriendByDid(store: FriendStore, did: string): Promise<FriendRecord | null>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findFriendByDid = findFriendByDid;
|
|
4
|
+
// did-aware friend lookup (p11 Item 2 — the DID re-key).
|
|
5
|
+
//
|
|
6
|
+
// `did` is the durable cross-agent primary key. This pure helper finds a friend
|
|
7
|
+
// record by did WITHOUT changing the FriendStore interface contract: it scans
|
|
8
|
+
// `store.listAll?.()` and matches on the record's resolved identity
|
|
9
|
+
// (`resolveAgentIdentity(f.agentMeta).did`, which already prefers identity.did and
|
|
10
|
+
// migrates a2a.did on read). Additive — `findByExternalId` is untouched. A store
|
|
11
|
+
// with no `listAll` yields null (the lookup is best-effort, never a throw).
|
|
12
|
+
const observability_1 = require("./observability");
|
|
13
|
+
const identity_1 = require("./identity");
|
|
14
|
+
/** Whether `candidate` should replace the current best among duplicate-did records.
|
|
15
|
+
*
|
|
16
|
+
* SECURITY (finding 5, MEDIUM): the tie-break must NOT reward back-dating — the old
|
|
17
|
+
* "lowest createdAt wins" rule let an attacker mint a duplicate-did record with an
|
|
18
|
+
* earlier createdAt to silently shadow a legit one. Instead:
|
|
19
|
+
* 1) Prefer a trust-relevant signal — a record carrying a TOFU-pinned key
|
|
20
|
+
* (`pinnedKey`) is the verified one and beats an unpinned duplicate.
|
|
21
|
+
* 2) When pinned-status is equal, break the tie by the record `id` (a stable,
|
|
22
|
+
* non-temporal key) — back-dating `createdAt` no longer gains anything. */
|
|
23
|
+
function preferOverBest(candidate, best) {
|
|
24
|
+
const candidatePinned = (0, identity_1.resolveAgentIdentity)(candidate.agentMeta).pinnedKey !== undefined;
|
|
25
|
+
const bestPinned = (0, identity_1.resolveAgentIdentity)(best.agentMeta).pinnedKey !== undefined;
|
|
26
|
+
if (candidatePinned !== bestPinned)
|
|
27
|
+
return candidatePinned; // pinned beats unpinned
|
|
28
|
+
return candidate.id < best.id; // stable, non-temporal tie-break
|
|
29
|
+
}
|
|
30
|
+
/** Find the friend record whose durable identity DID equals `did`. A DUPLICATE did is
|
|
31
|
+
* an anomaly: it emits a loud `friends.duplicate_did` warning and resolves
|
|
32
|
+
* deterministically WITHOUT rewarding back-dating — a pinned/verified record wins, else
|
|
33
|
+
* the lowest record `id` (a stable, non-temporal tie-break) — see {@link preferOverBest}.
|
|
34
|
+
* Returns null when no record matches, the query did is falsy, or the store has no
|
|
35
|
+
* `listAll`. */
|
|
36
|
+
async function findFriendByDid(store, did) {
|
|
37
|
+
// SECURITY (finding 4, MEDIUM): a falsy did query must never match. Without this,
|
|
38
|
+
// findFriendByDid(store, undefined|"") matched the first did-less record (a did-less
|
|
39
|
+
// record resolves to `undefined`, and `undefined !== undefined` is false → match).
|
|
40
|
+
if (!did)
|
|
41
|
+
return null;
|
|
42
|
+
if (typeof store.listAll !== "function")
|
|
43
|
+
return null;
|
|
44
|
+
const all = await store.listAll();
|
|
45
|
+
let best = null;
|
|
46
|
+
let matchCount = 0;
|
|
47
|
+
for (const f of all) {
|
|
48
|
+
const resolvedDid = (0, identity_1.resolveAgentIdentity)(f.agentMeta).did;
|
|
49
|
+
// Skip records whose resolved did is falsy (absent/empty) so they can never match —
|
|
50
|
+
// belt-and-braces with resolveAgentIdentity's own empty-string guard (finding 6).
|
|
51
|
+
if (!resolvedDid || resolvedDid !== did)
|
|
52
|
+
continue;
|
|
53
|
+
matchCount += 1;
|
|
54
|
+
if (best === null || preferOverBest(f, best))
|
|
55
|
+
best = f;
|
|
56
|
+
}
|
|
57
|
+
// SECURITY (finding 5): a duplicate did is itself an anomaly — surface it loudly so a
|
|
58
|
+
// shadowing attempt is visible, rather than silently resolving it away.
|
|
59
|
+
if (matchCount > 1) {
|
|
60
|
+
(0, observability_1.emitNervesEvent)({
|
|
61
|
+
level: "warn",
|
|
62
|
+
component: "friends",
|
|
63
|
+
event: "friends.duplicate_did",
|
|
64
|
+
message: `duplicate did detected across ${matchCount} friend records — resolving to the pinned/lowest-id record (NOT lowest-createdAt); investigate possible record shadowing`,
|
|
65
|
+
meta: { did, matchCount },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return best;
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentMeta } from "./types";
|
|
2
|
+
/** The resolved durable identity of an agent peer — independent of which on-disk
|
|
3
|
+
* shape carried it. All fields optional: a did-less legacy record reads clean. */
|
|
4
|
+
export interface ResolvedAgentIdentity {
|
|
5
|
+
did?: string;
|
|
6
|
+
pinnedKey?: string;
|
|
7
|
+
handle?: string;
|
|
8
|
+
pinnedAt?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Read an agent's durable identity, preferring `meta.identity` and lifting the
|
|
11
|
+
* legacy `meta.a2a.did` on a miss (migrate-on-read). Returns `{}` for a did-less
|
|
12
|
+
* or absent meta. (Unit 4a stub — not implemented.) */
|
|
13
|
+
export declare function resolveAgentIdentity(meta: AgentMeta | undefined): ResolvedAgentIdentity;
|
|
14
|
+
/** Return a meta whose `identity.did` is backfilled from `a2a.did` when the durable
|
|
15
|
+
* home is absent (migrate-on-write); a meta already carrying `identity` is returned
|
|
16
|
+
* unchanged (no clobber). Absent meta is returned as-is. (Unit 4a stub.) */
|
|
17
|
+
export declare function withMigratedIdentity(meta: AgentMeta | undefined): AgentMeta | undefined;
|