@ouro.bot/friends 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +514 -0
  3. package/changelog.json +34 -0
  4. package/dist/a2a/index.d.ts +102 -0
  5. package/dist/a2a/index.js +198 -0
  6. package/dist/agent-peer.d.ts +17 -0
  7. package/dist/agent-peer.js +57 -0
  8. package/dist/channel.d.ts +11 -0
  9. package/dist/channel.js +132 -0
  10. package/dist/consent.d.ts +34 -0
  11. package/dist/consent.js +62 -0
  12. package/dist/coordination.d.ts +100 -0
  13. package/dist/coordination.js +255 -0
  14. package/dist/file-bundle.d.ts +12 -0
  15. package/dist/file-bundle.js +23 -0
  16. package/dist/grant-store-file.d.ts +16 -0
  17. package/dist/grant-store-file.js +136 -0
  18. package/dist/grant-store.d.ts +7 -0
  19. package/dist/grant-store.js +8 -0
  20. package/dist/grants.d.ts +39 -0
  21. package/dist/grants.js +84 -0
  22. package/dist/group-context.d.ts +21 -0
  23. package/dist/group-context.js +144 -0
  24. package/dist/index.d.ts +49 -0
  25. package/dist/index.js +105 -0
  26. package/dist/link-identity.d.ts +14 -0
  27. package/dist/link-identity.js +88 -0
  28. package/dist/mcp/bin.d.ts +2 -0
  29. package/dist/mcp/bin.js +16 -0
  30. package/dist/mcp/dispatch.d.ts +14 -0
  31. package/dist/mcp/dispatch.js +432 -0
  32. package/dist/mcp/index.d.ts +6 -0
  33. package/dist/mcp/index.js +14 -0
  34. package/dist/mcp/run-main.d.ts +7 -0
  35. package/dist/mcp/run-main.js +45 -0
  36. package/dist/mcp/schemas.d.ts +10 -0
  37. package/dist/mcp/schemas.js +398 -0
  38. package/dist/mcp/server.d.ts +21 -0
  39. package/dist/mcp/server.js +194 -0
  40. package/dist/mission-share.d.ts +94 -0
  41. package/dist/mission-share.js +232 -0
  42. package/dist/mission-store-file.d.ts +18 -0
  43. package/dist/mission-store-file.js +153 -0
  44. package/dist/mission-store.d.ts +10 -0
  45. package/dist/mission-store.js +9 -0
  46. package/dist/missions.d.ts +31 -0
  47. package/dist/missions.js +98 -0
  48. package/dist/notes.d.ts +11 -0
  49. package/dist/notes.js +90 -0
  50. package/dist/observability.d.ts +27 -0
  51. package/dist/observability.js +31 -0
  52. package/dist/outcomes.d.ts +9 -0
  53. package/dist/outcomes.js +51 -0
  54. package/dist/resolver.d.ts +28 -0
  55. package/dist/resolver.js +187 -0
  56. package/dist/results.d.ts +8 -0
  57. package/dist/results.js +2 -0
  58. package/dist/room.d.ts +22 -0
  59. package/dist/room.js +40 -0
  60. package/dist/share.d.ts +106 -0
  61. package/dist/share.js +223 -0
  62. package/dist/standing.d.ts +83 -0
  63. package/dist/standing.js +111 -0
  64. package/dist/store-file.d.ts +21 -0
  65. package/dist/store-file.js +264 -0
  66. package/dist/store.d.ts +9 -0
  67. package/dist/store.js +4 -0
  68. package/dist/tokens.d.ts +8 -0
  69. package/dist/tokens.js +26 -0
  70. package/dist/trust-explanation.d.ts +16 -0
  71. package/dist/trust-explanation.js +74 -0
  72. package/dist/trust-mutation.d.ts +4 -0
  73. package/dist/trust-mutation.js +29 -0
  74. package/dist/types.d.ts +164 -0
  75. package/dist/types.js +51 -0
  76. package/dist/util/cap-string.d.ts +7 -0
  77. package/dist/util/cap-string.js +35 -0
  78. package/dist/verifier.d.ts +11 -0
  79. package/dist/verifier.js +29 -0
  80. package/dist/whoami.d.ts +7 -0
  81. package/dist/whoami.js +39 -0
  82. package/package.json +68 -0
package/dist/share.js ADDED
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.originalAsserterOf = originalAsserterOf;
4
+ exports.prepareProfileShare = prepareProfileShare;
5
+ exports.importProfileShare = importProfileShare;
6
+ // prepareProfileShare (producer) + importProfileShare (consumer) — the moat (N12).
7
+ //
8
+ // Two DIFFERENT agents agreeing a party is the same person AND sharing what they
9
+ // know — WITH CONSENT, without first-party knowledge being clobbered. The package
10
+ // stays store-only + transport-agnostic: prepareProfileShare returns an envelope,
11
+ // importProfileShare consumes one; the WIRE between them is the caller's job.
12
+ //
13
+ // Through-line invariants (every one is tested):
14
+ // - the party is named by its JOIN KEY (externalIds), NEVER the local UUID;
15
+ // - the share is consent-gated (a ConsentPolicy) and scope-filtered;
16
+ // - imported facts NEVER touch first-party `notes` (a separate `importedNotes`
17
+ // namespace) — first-party always wins, structurally;
18
+ // - the source agent's trust CAPS acceptance (a stranger peer is refused);
19
+ // - imports NEVER change the party's trust level (non-transitive — the key one);
20
+ // - an unknown party may be SEEDED only by a friend/family introducing peer
21
+ // (Fork E); a stranger/acquaintance peer may not seed a new record.
22
+ const node_crypto_1 = require("node:crypto");
23
+ const observability_1 = require("./observability");
24
+ const types_1 = require("./types");
25
+ const consent_1 = require("./consent");
26
+ const verifier_1 = require("./verifier");
27
+ /** The original asserter of a note: for an imported note, whoever the import
28
+ * recorded as `originallyAssertedBy` (falling back to its `assertedBy`); for a
29
+ * first-party note, this agent itself. Never launders imported → first-party.
30
+ * Always returns an attribution (a shared fact is always attributable). Exported
31
+ * so the mission-share producer reuses it (a MissionLearning is structurally
32
+ * compatible with the inline param type) — single-sourced, tested once. */
33
+ function originalAsserterOf(note, selfAgentId) {
34
+ if (note.provenance?.origin === "imported") {
35
+ return note.provenance.assertedBy ?? { agentId: selfAgentId };
36
+ }
37
+ return { agentId: selfAgentId };
38
+ }
39
+ function buildSharedNotes(record, scope, selfAgentId) {
40
+ return Object.entries(record.notes)
41
+ .filter(([, note]) => scope === "notes:all" || note.shareable === true)
42
+ .map(([key, note]) => ({
43
+ key,
44
+ value: note.value,
45
+ originallyAssertedBy: originalAsserterOf(note, selfAgentId),
46
+ }));
47
+ }
48
+ /**
49
+ * Producer half of the moat. Consent-gated (via the injected ConsentPolicy, or
50
+ * the module default), scope-filtered, provenance-preserving. Names the party by
51
+ * join key, never the local UUID. The recipient's trust level — read off this
52
+ * agent's own record for `toAgentId` — is the authorization input the policy
53
+ * uses. Returns `{ ok:true, envelope }` or `{ ok:false, status }`.
54
+ */
55
+ async function prepareProfileShare(store, grants, input, consent = consent_1.DEFAULT_CONSENT_POLICY) {
56
+ const record = await store.get(input.friendId);
57
+ if (!record) {
58
+ return { ok: false, status: "not_found" };
59
+ }
60
+ // The recipient's trust level is read from this agent's own knowledge of it
61
+ // (the a2a-agent record for toAgentId). An unknown recipient defaults to
62
+ // stranger — never trusted by default.
63
+ const recipientRecord = await store.findByExternalId("a2a-agent", input.toAgentId);
64
+ const recipientTrust = recipientRecord?.trustLevel ?? "stranger";
65
+ const recipient = { agentId: input.toAgentId, trustLevel: recipientTrust };
66
+ const consented = await consent.consents({
67
+ subjectKey: record.id,
68
+ recipient,
69
+ scope: input.scope,
70
+ grants,
71
+ });
72
+ if (!consented) {
73
+ return { ok: false, status: "no_consent" };
74
+ }
75
+ const now = new Date().toISOString();
76
+ const isIdentityScope = types_1.IDENTITY_SCOPES.has(input.scope);
77
+ const envelope = {
78
+ subject: {
79
+ externalIds: record.externalIds,
80
+ displayName: record.name,
81
+ },
82
+ fromAgentId: input.selfAgentId,
83
+ scope: input.scope,
84
+ issuedAt: now,
85
+ ...(input.proof !== undefined ? { proof: input.proof } : {}),
86
+ ...(input.scope === "outcomes"
87
+ ? { outcomes: record.agentMeta?.outcomes ?? [] }
88
+ : {}),
89
+ ...(!isIdentityScope && input.scope !== "outcomes"
90
+ ? { notes: buildSharedNotes(record, input.scope, input.selfAgentId) }
91
+ : {}),
92
+ };
93
+ (0, observability_1.emitNervesEvent)({
94
+ component: "friends",
95
+ event: "friends.profile_share_prepared",
96
+ message: "prepared profile share envelope",
97
+ meta: { scope: input.scope, toAgentId: input.toAgentId, consentPolicy: consent.name },
98
+ });
99
+ return { ok: true, envelope };
100
+ }
101
+ // ── Consumer ──
102
+ /** Trust levels a peer must hold to INTRODUCE a previously-unknown party (Fork E).
103
+ * A friend/family peer may seed a new record at acquaintance; a stranger /
104
+ * acquaintance peer may not. */
105
+ const SEEDING_TRUST = new Set(["family", "friend"]);
106
+ const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
107
+ /** Find the local friend the envelope's subject refers to, by join key — the
108
+ * FIRST of the subject's externalIds that resolves to an existing record. */
109
+ async function resolveSubject(store, envelope) {
110
+ for (const ext of envelope.subject.externalIds) {
111
+ const found = await store.findByExternalId(ext.provider, ext.externalId, ext.tenantId);
112
+ if (found)
113
+ return found;
114
+ }
115
+ return null;
116
+ }
117
+ function importedNoteFrom(note, fromAgentId, now) {
118
+ return {
119
+ value: note.value,
120
+ importedAt: now,
121
+ assertedBy: { agentId: fromAgentId },
122
+ ...(note.originallyAssertedBy ? { originallyAssertedBy: note.originallyAssertedBy } : {}),
123
+ };
124
+ }
125
+ /** Merge the envelope's notes into the record's `importedNotes` namespace under
126
+ * the source agentId. First-party `notes` are NOT passed in and are physically
127
+ * untouched. Within the namespace, the newest import wins on key collision. */
128
+ function mergeImportedNotes(record, notes, fromAgentId, now) {
129
+ const existing = record.importedNotes ?? {};
130
+ const forAgent = { ...(existing[fromAgentId] ?? {}) };
131
+ for (const note of notes) {
132
+ forAgent[note.key] = importedNoteFrom(note, fromAgentId, now);
133
+ }
134
+ return { ...existing, [fromAgentId]: forAgent };
135
+ }
136
+ /** Create a freshly-seeded record for a previously-unknown party (Fork E). Always
137
+ * `acquaintance`, kind `human`, carrying the subject's join-key externalIds. */
138
+ function seedRecord(envelope, now) {
139
+ return {
140
+ id: (0, node_crypto_1.randomUUID)(),
141
+ name: envelope.subject.displayName,
142
+ role: "acquaintance",
143
+ trustLevel: "acquaintance",
144
+ connections: [],
145
+ externalIds: envelope.subject.externalIds.map((ext) => ({ ...ext, linkedAt: now })),
146
+ tenantMemberships: [],
147
+ toolPreferences: {},
148
+ notes: {},
149
+ totalTokens: 0,
150
+ createdAt: now,
151
+ updatedAt: now,
152
+ schemaVersion: 1,
153
+ kind: "human",
154
+ };
155
+ }
156
+ /**
157
+ * Consumer half of the moat — the non-clobbering merge. Resolves the party by
158
+ * join key; lands imported facts in the `importedNotes` namespace (origin
159
+ * "imported" + assertedBy + importedAt) WITHOUT ever touching first-party `notes`;
160
+ * the source agent's trust caps acceptance; NEVER changes the party's trust level
161
+ * (the key safety invariant); seeds an unknown party only when a friend/family
162
+ * peer introduces it. Returns `{ ok, status, record }`.
163
+ */
164
+ async function importProfileShare(store, input, options = {}) {
165
+ const verifier = options.verifier ?? verifier_1.DEFAULT_AGENT_VERIFIER;
166
+ const minTrust = options.minTrustToAccept ?? "acquaintance";
167
+ // Authentication (caller's seam) AND authorization (trust ladder) must BOTH
168
+ // pass. The verifier authenticates the wire; the trust cap is the package's
169
+ // own gate and applies regardless of what the verifier returns.
170
+ const authenticated = verifier.verify(input.fromAgentId, input.envelope.proof);
171
+ const trustedEnough = TRUST_RANK[input.trustOfSource] >= TRUST_RANK[minTrust];
172
+ if (!authenticated || !trustedEnough) {
173
+ (0, observability_1.emitNervesEvent)({
174
+ component: "friends",
175
+ event: "friends.profile_share_refused",
176
+ message: "refused profile share from untrusted source",
177
+ meta: { fromAgentId: input.fromAgentId, trustOfSource: input.trustOfSource, authenticated },
178
+ });
179
+ return { ok: false, status: "untrusted_source" };
180
+ }
181
+ const now = new Date().toISOString();
182
+ const existing = await resolveSubject(store, input.envelope);
183
+ if (!existing) {
184
+ // Unknown party. Fork E: only a friend/family peer may seed a new record.
185
+ if (!SEEDING_TRUST.has(input.trustOfSource)) {
186
+ return { ok: false, status: "untrusted_introduction" };
187
+ }
188
+ const seeded = seedRecord(input.envelope, now);
189
+ const withNotes = applyEnvelopeToRecord(seeded, input.envelope, input.fromAgentId, now);
190
+ await store.put(withNotes.id, withNotes);
191
+ (0, observability_1.emitNervesEvent)({
192
+ component: "friends",
193
+ event: "friends.profile_share_seeded",
194
+ message: "seeded new party from profile share",
195
+ meta: { friendId: withNotes.id, fromAgentId: input.fromAgentId },
196
+ });
197
+ return { ok: true, status: "seeded", record: withNotes };
198
+ }
199
+ const updated = applyEnvelopeToRecord(existing, input.envelope, input.fromAgentId, now);
200
+ await store.put(updated.id, updated);
201
+ (0, observability_1.emitNervesEvent)({
202
+ component: "friends",
203
+ event: "friends.profile_share_imported",
204
+ message: "imported profile share into existing party",
205
+ meta: { friendId: updated.id, fromAgentId: input.fromAgentId, scope: input.envelope.scope },
206
+ });
207
+ return { ok: true, status: "imported", record: updated };
208
+ }
209
+ /** Apply an envelope's payload to a record WITHOUT changing its trust level or
210
+ * touching first-party `notes`. Only `importedNotes` (and `updatedAt`) change.
211
+ * `trustLevel` and `role` are copied through verbatim — imports are non-transitive. */
212
+ function applyEnvelopeToRecord(record, envelope, fromAgentId, now) {
213
+ const importedNotes = envelope.notes && envelope.notes.length > 0
214
+ ? mergeImportedNotes(record, envelope.notes, fromAgentId, now)
215
+ : record.importedNotes;
216
+ return {
217
+ ...record,
218
+ // trustLevel / role are intentionally NOT recomputed — an import must never
219
+ // change the party's trust (the single most important safety invariant).
220
+ ...(importedNotes ? { importedNotes } : {}),
221
+ updatedAt: now,
222
+ };
223
+ }
@@ -0,0 +1,83 @@
1
+ import type { FriendRecord } from "./types";
2
+ /** A peer's earned standing tier - derived from the first-party outcomes you
3
+ * personally recorded with them. Ordered worst→best for reference:
4
+ * `troubled` < `untested` < `mixed` < `reliable` < `proven`. */
5
+ export type StandingTier = "proven" | "reliable" | "mixed" | "untested" | "troubled";
6
+ /** The tally of first-party outcomes by result (imported outcomes excluded). */
7
+ export interface StandingTally {
8
+ success: number;
9
+ partial: number;
10
+ failed: number;
11
+ }
12
+ /** A derived, advisory assessment of a peer from your first-party outcomes.
13
+ * Computed on read; persisted nowhere; never crosses the wire. */
14
+ export interface Standing {
15
+ tier: StandingTier;
16
+ /** How many first-party outcomes the tier rests on (imported excluded). */
17
+ basisCount: number;
18
+ tally: StandingTally;
19
+ /** The peer's familiarity counter (read through from `agentMeta`). */
20
+ familiarity: number;
21
+ /** ISO timestamp at which this assessment was computed. */
22
+ assessedAt: string;
23
+ }
24
+ /** A human gloss of a `Standing` plus advisory notes that frame it as input to a
25
+ * MANUAL trust decision - never an instruction to change trust. */
26
+ export interface StandingExplanation {
27
+ standing: Standing;
28
+ summary: string;
29
+ why: string;
30
+ /** Advisory notes. ALWAYS includes the guardrail that standing does not change
31
+ * the peer's trust level (the anti-auto-promote firewall). */
32
+ advisory: string[];
33
+ }
34
+ /** The inputs a tier rule maps to a `StandingTier`: the first-party tally, the
35
+ * basis count, and the peer's familiarity. */
36
+ export interface StandingRuleInput {
37
+ tally: StandingTally;
38
+ basisCount: number;
39
+ familiarity: number;
40
+ }
41
+ /**
42
+ * A pluggable tier rule - the swap point (mirrors `ConsentPolicy`). `tier`
43
+ * deterministically maps `(tally, basisCount, familiarity)` to a `StandingTier`.
44
+ * Inject a custom rule to change the ladder (e.g. add time-decay later) without
45
+ * touching `assessStanding`/`explainStanding`.
46
+ */
47
+ export interface StandingRule {
48
+ readonly name: string;
49
+ tier(input: StandingRuleInput): StandingTier;
50
+ }
51
+ /**
52
+ * The familiarity floor a peer must reach (alongside ≥3 clean successes) to earn
53
+ * `proven`. Equal to the proven success floor - a peer is only "proven" once you
54
+ * have both enough good outcomes AND enough lived history. Tunable.
55
+ */
56
+ export declare const FAMILIARITY_THRESHOLD = 3;
57
+ /**
58
+ * ── STANDING-RULE SWAP POINT (the default tier ladder) ──
59
+ * The active tier rule. A fixed, transparent, count-based ladder - NOT ML:
60
+ * • no basis at all → `untested`
61
+ * • failures outnumber wins → `troubled`
62
+ * • ≥3 clean wins + familiar → `proven`
63
+ * • ≥1 clean win → `reliable`
64
+ * • otherwise (mixed signal) → `mixed`
65
+ * Swap this assignment (or inject a `rule` per-call) to change the ladder; e.g. a
66
+ * later recency/decay rule is an additive swap here, not a rebuild.
67
+ */
68
+ export declare const DEFAULT_STANDING_RULE: StandingRule;
69
+ /**
70
+ * Assess a peer's earned standing from the first-party outcomes you personally
71
+ * recorded with them. Pure + store-free: reads `agentMeta.outcomes`, FILTERS to
72
+ * first-party (firewall 1), tallies by result, reads `familiarity`, and maps via
73
+ * the (injectable) tier rule. Emits `friends.standing_assessed`. Returns a value;
74
+ * never writes trust (firewall 2); never produces a wire artifact (firewall 3).
75
+ */
76
+ export declare function assessStanding(record: FriendRecord, now?: Date, rule?: StandingRule): Standing;
77
+ /**
78
+ * Explain a peer's earned standing in words: the tier with a human `summary` +
79
+ * `why`, plus `advisory` notes that explicitly frame standing as input to a
80
+ * MANUAL trust decision (firewall 4 - never an instruction to change trust).
81
+ * Mirrors `describeTrustContext`'s shape.
82
+ */
83
+ export declare function explainStanding(record: FriendRecord, now?: Date, rule?: StandingRule): StandingExplanation;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_STANDING_RULE = exports.FAMILIARITY_THRESHOLD = void 0;
4
+ exports.assessStanding = assessStanding;
5
+ exports.explainStanding = explainStanding;
6
+ const observability_1 = require("./observability");
7
+ /**
8
+ * The familiarity floor a peer must reach (alongside ≥3 clean successes) to earn
9
+ * `proven`. Equal to the proven success floor - a peer is only "proven" once you
10
+ * have both enough good outcomes AND enough lived history. Tunable.
11
+ */
12
+ exports.FAMILIARITY_THRESHOLD = 3;
13
+ /**
14
+ * ── STANDING-RULE SWAP POINT (the default tier ladder) ──
15
+ * The active tier rule. A fixed, transparent, count-based ladder - NOT ML:
16
+ * • no basis at all → `untested`
17
+ * • failures outnumber wins → `troubled`
18
+ * • ≥3 clean wins + familiar → `proven`
19
+ * • ≥1 clean win → `reliable`
20
+ * • otherwise (mixed signal) → `mixed`
21
+ * Swap this assignment (or inject a `rule` per-call) to change the ladder; e.g. a
22
+ * later recency/decay rule is an additive swap here, not a rebuild.
23
+ */
24
+ exports.DEFAULT_STANDING_RULE = {
25
+ name: "count_based",
26
+ tier({ tally, basisCount, familiarity }) {
27
+ if (basisCount === 0)
28
+ return "untested";
29
+ if (tally.failed > tally.success)
30
+ return "troubled";
31
+ if (tally.success >= 3 && tally.failed === 0 && familiarity >= exports.FAMILIARITY_THRESHOLD)
32
+ return "proven";
33
+ if (tally.success >= 1 && tally.failed === 0)
34
+ return "reliable";
35
+ return "mixed";
36
+ },
37
+ };
38
+ /**
39
+ * Assess a peer's earned standing from the first-party outcomes you personally
40
+ * recorded with them. Pure + store-free: reads `agentMeta.outcomes`, FILTERS to
41
+ * first-party (firewall 1), tallies by result, reads `familiarity`, and maps via
42
+ * the (injectable) tier rule. Emits `friends.standing_assessed`. Returns a value;
43
+ * never writes trust (firewall 2); never produces a wire artifact (firewall 3).
44
+ */
45
+ function assessStanding(record, now, rule) {
46
+ const outcomes = record.agentMeta?.outcomes ?? [];
47
+ // FIREWALL 1: first-party only. An absent `origin` is treated as first-party
48
+ // (the safe-merge predicate); only an explicit "imported" is excluded.
49
+ const firstParty = outcomes.filter((outcome) => outcome.provenance?.origin !== "imported");
50
+ const tally = { success: 0, partial: 0, failed: 0 };
51
+ for (const outcome of firstParty) {
52
+ tally[outcome.result] += 1;
53
+ }
54
+ const basisCount = firstParty.length;
55
+ const familiarity = record.agentMeta?.familiarity ?? 0;
56
+ const tier = (rule ?? exports.DEFAULT_STANDING_RULE).tier({ tally, basisCount, familiarity });
57
+ const assessedAt = (now ?? new Date()).toISOString();
58
+ (0, observability_1.emitNervesEvent)({
59
+ component: "friends",
60
+ event: "friends.standing_assessed",
61
+ message: "assessed earned standing",
62
+ meta: { tier, basisCount },
63
+ });
64
+ return { tier, basisCount, tally, familiarity, assessedAt };
65
+ }
66
+ /**
67
+ * Explain a peer's earned standing in words: the tier with a human `summary` +
68
+ * `why`, plus `advisory` notes that explicitly frame standing as input to a
69
+ * MANUAL trust decision (firewall 4 - never an instruction to change trust).
70
+ * Mirrors `describeTrustContext`'s shape.
71
+ */
72
+ function explainStanding(record, now, rule) {
73
+ const standing = assessStanding(record, now, rule);
74
+ const { tier, basisCount, tally } = standing;
75
+ // FIREWALL 4: the guardrail. ALWAYS present - frames standing as input to a
76
+ // manual trust decision, never an instruction. It states, in plain words, that
77
+ // standing does not change the peer's trust level.
78
+ const guardrail = "Standing is advisory only - it does not change this peer's trust level. Adjust trust deliberately with set_trust if warranted.";
79
+ // Per-tier human gloss (count-based language; references the basis where it
80
+ // helps). Mirrors describeTrustContext's per-level branch. Every tier carries a
81
+ // tier-flavored `note`, paired with the always-present guardrail in `advisory`.
82
+ const gloss = {
83
+ proven: {
84
+ summary: "proven on our shared work",
85
+ why: `every one of the ${basisCount} outcomes I personally recorded with this peer succeeded, across enough shared work to be confident.`,
86
+ note: "A strong track record - still your call whether it warrants more trust.",
87
+ },
88
+ reliable: {
89
+ summary: "reliable so far",
90
+ why: `the ${basisCount} first-party outcome${basisCount === 1 ? "" : "s"} I recorded with this peer succeeded, with no failures yet.`,
91
+ note: "Promising, but on a thin basis - weigh it as one input, not a verdict.",
92
+ },
93
+ mixed: {
94
+ summary: "a mixed track record",
95
+ why: `the outcomes I recorded with this peer are mixed (${tally.success} succeeded, ${tally.partial} partial, ${tally.failed} failed) - not yet a clear signal either way.`,
96
+ note: "Signals are mixed; lean on direct judgement here.",
97
+ },
98
+ untested: {
99
+ summary: "untested - no first-party basis",
100
+ why: "I have not recorded any first-party outcomes with this peer, so there is nothing earned to assess.",
101
+ note: "No basis yet - standing offers no guidance until you have shared outcomes.",
102
+ },
103
+ troubled: {
104
+ summary: "troubled - failures outweigh successes",
105
+ why: `the outcomes I recorded with this peer skew negative (${tally.failed} failed vs ${tally.success} succeeded).`,
106
+ note: "Recent results are poor; weigh carefully before granting more authority.",
107
+ },
108
+ };
109
+ const chosen = gloss[tier];
110
+ return { standing, summary: chosen.summary, why: chosen.why, advisory: [chosen.note, guardrail] };
111
+ }
@@ -0,0 +1,21 @@
1
+ import type { FriendStore } from "./store";
2
+ import type { FriendRecord } from "./types";
3
+ export declare class FileFriendStore implements FriendStore {
4
+ private readonly friendsPath;
5
+ constructor(friendsPath: string);
6
+ get(id: string): Promise<FriendRecord | null>;
7
+ put(id: string, record: FriendRecord): Promise<void>;
8
+ delete(id: string): Promise<void>;
9
+ findByExternalId(provider: string, externalId: string, tenantId?: string): Promise<FriendRecord | null>;
10
+ hasAnyFriends(): Promise<boolean>;
11
+ listAll(): Promise<FriendRecord[]>;
12
+ private normalize;
13
+ private normalizeAgentMeta;
14
+ private normalizeA2AMeta;
15
+ /** Preserve an additive a2a.mailbox coord only when both fields are strings;
16
+ * otherwise drop it (absent ⇒ unchanged — the additive guarantee). */
17
+ private normalizeMailbox;
18
+ private readJson;
19
+ private writeJson;
20
+ private removeFile;
21
+ }