@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/notes.js ADDED
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyFriendNote = applyFriendNote;
4
+ // applyFriendNote — structured-result port of the harness's `save_friend_note`.
5
+ //
6
+ // Writes a friend's name, a tool preference, or a general note. Returns a
7
+ // FriendOpResult instead of the harness's English strings so the MCP layer can
8
+ // serialize and branch on the outcome. The override-conflict case stays
9
+ // distinguishable (`status: "override_required"`, `ok: false`) from a real
10
+ // write, and a missing friend is a normal `not_found` result — never a throw.
11
+ const observability_1 = require("./observability");
12
+ async function applyFriendNote(store, friendId, input) {
13
+ (0, observability_1.emitNervesEvent)({
14
+ component: "friends",
15
+ event: "friends.note_applied",
16
+ message: "applied friend note",
17
+ meta: { type: input.type },
18
+ });
19
+ const { type, key, content, override, provenance } = input;
20
+ // Validate inputs up front so the helper is self-contained (the harness
21
+ // returned English here; we return a structured `invalid` result).
22
+ if (!content) {
23
+ return { ok: false, status: "invalid", message: "a content value is required" };
24
+ }
25
+ if ((type === "tool_preference" || type === "note") && !key) {
26
+ return { ok: false, status: "invalid", message: "a key is required for tool_preference or note" };
27
+ }
28
+ try {
29
+ const record = await store.get(friendId);
30
+ if (!record) {
31
+ return { ok: false, status: "not_found", message: "friend record not found" };
32
+ }
33
+ const now = new Date().toISOString();
34
+ if (type === "name") {
35
+ const updated = { ...record, name: content, updatedAt: now };
36
+ await store.put(friendId, updated);
37
+ return { ok: true, status: "saved", record: updated };
38
+ }
39
+ if (type === "tool_preference") {
40
+ const existing = record.toolPreferences[key];
41
+ if (existing && !override) {
42
+ return {
43
+ ok: false,
44
+ status: "override_required",
45
+ message: `a tool preference already exists for '${key}': "${existing}"`,
46
+ };
47
+ }
48
+ const updated = {
49
+ ...record,
50
+ toolPreferences: { ...record.toolPreferences, [key]: content },
51
+ updatedAt: now,
52
+ };
53
+ await store.put(friendId, updated);
54
+ return { ok: true, status: "saved", record: updated };
55
+ }
56
+ // type === "note"
57
+ // Redirect a "name" key to the name field rather than storing it as a note.
58
+ if (key === "name") {
59
+ const updated = { ...record, name: content, updatedAt: now };
60
+ await store.put(friendId, updated);
61
+ return { ok: true, status: "redirected_to_name", record: updated };
62
+ }
63
+ const existing = record.notes[key];
64
+ if (existing && !override) {
65
+ return {
66
+ ok: false,
67
+ status: "override_required",
68
+ message: `a note already exists for '${key}': "${existing.value}"`,
69
+ };
70
+ }
71
+ const updated = {
72
+ ...record,
73
+ notes: {
74
+ ...record.notes,
75
+ [key]: { value: content, savedAt: now, ...(provenance ? { provenance } : {}) },
76
+ },
77
+ updatedAt: now,
78
+ };
79
+ await store.put(friendId, updated);
80
+ return { ok: true, status: "saved", record: updated };
81
+ }
82
+ catch (err) {
83
+ return {
84
+ ok: false,
85
+ status: "error",
86
+ /* v8 ignore next -- defensive: non-Error throw is unreachable in tests; we inject an Error @preserve */
87
+ message: err instanceof Error ? err.message : String(err),
88
+ };
89
+ }
90
+ }
@@ -0,0 +1,27 @@
1
+ /** Log severity for an emitted event. Mirrors the harness's `LogLevel`. */
2
+ export type LogLevel = "debug" | "info" | "warn" | "error";
3
+ /**
4
+ * A structured observability event. Field-for-field identical to the harness's
5
+ * `NervesEvent` so the harness's real emitter can be injected without adaptation.
6
+ */
7
+ export interface NervesEvent {
8
+ level?: LogLevel;
9
+ event: string;
10
+ trace_id?: string;
11
+ component: string;
12
+ message: string;
13
+ meta?: Record<string, unknown>;
14
+ }
15
+ /** A function that consumes emitted events. */
16
+ export type NervesEmitter = (event: NervesEvent) => void;
17
+ /**
18
+ * Inject the emitter that `emitNervesEvent` should forward to. Pass `null` to
19
+ * reset back to the default no-op. The harness passes its real nerves emitter
20
+ * here so extracted friend code reports through the same observability pipeline.
21
+ */
22
+ export declare function setNervesEmitter(emitter: NervesEmitter | null): void;
23
+ /**
24
+ * Emit a structured observability event. No-op by default; forwards to whatever
25
+ * was last passed to `setNervesEmitter`.
26
+ */
27
+ export declare function emitNervesEvent(event: NervesEvent): void;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ // Observability seam.
3
+ //
4
+ // In the Ouroboros harness, the friend model emits structured "nerves" events
5
+ // through `emitNervesEvent` (from `nerves/runtime`). The standalone package must
6
+ // stay self-contained, so this module ships a no-op `emitNervesEvent` with the
7
+ // SAME signature as the harness's, plus `setNervesEmitter(fn)` — an injection
8
+ // point the harness (or any consumer) can use to wire its real emitter back in.
9
+ //
10
+ // Default behavior: events are dropped. Call `setNervesEmitter` once at startup
11
+ // to forward them somewhere real.
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.setNervesEmitter = setNervesEmitter;
14
+ exports.emitNervesEvent = emitNervesEvent;
15
+ const noopEmitter = () => { };
16
+ let activeEmitter = noopEmitter;
17
+ /**
18
+ * Inject the emitter that `emitNervesEvent` should forward to. Pass `null` to
19
+ * reset back to the default no-op. The harness passes its real nerves emitter
20
+ * here so extracted friend code reports through the same observability pipeline.
21
+ */
22
+ function setNervesEmitter(emitter) {
23
+ activeEmitter = emitter ?? noopEmitter;
24
+ }
25
+ /**
26
+ * Emit a structured observability event. No-op by default; forwards to whatever
27
+ * was last passed to `setNervesEmitter`.
28
+ */
29
+ function emitNervesEvent(event) {
30
+ activeEmitter(event);
31
+ }
@@ -0,0 +1,9 @@
1
+ import type { FriendStore } from "./store";
2
+ import type { FriendRecord, NoteProvenance } from "./types";
3
+ export interface RecordOutcomeInput {
4
+ missionId: string;
5
+ result: "success" | "partial" | "failed";
6
+ note?: string;
7
+ provenance?: NoteProvenance;
8
+ }
9
+ export declare function recordRelationshipOutcome(store: FriendStore, friendId: string, input: RecordOutcomeInput, familiarityDelta?: number): Promise<FriendRecord | null>;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recordRelationshipOutcome = recordRelationshipOutcome;
4
+ // recordRelationshipOutcome — appends a shared-mission outcome to a friend's
5
+ // agentMeta, dedupes the mission into sharedMissions, and bumps familiarity.
6
+ //
7
+ // D3: if the record has no `agentMeta` (e.g. a human record), it is
8
+ // auto-initialized so the helper is usable on any friendId. The record's `kind`
9
+ // is intentionally NOT flipped to "agent". CAVEAT: `FileFriendStore.normalize`
10
+ // drops `agentMeta` on read when `kind !== "agent"`, so on a human record the
11
+ // outcome persists in-process and round-trips through a MemoryStore, but a
12
+ // FileFriendStore reload normalizes it away. For an agent record it persists in
13
+ // full. Returns the updated record, or null when the friend is missing.
14
+ const observability_1 = require("./observability");
15
+ async function recordRelationshipOutcome(store, friendId, input, familiarityDelta) {
16
+ const record = await store.get(friendId);
17
+ if (!record)
18
+ return null;
19
+ const meta = record.agentMeta ?? {
20
+ bundleName: record.name,
21
+ familiarity: 0,
22
+ sharedMissions: [],
23
+ outcomes: [],
24
+ };
25
+ const now = new Date().toISOString();
26
+ const outcome = {
27
+ missionId: input.missionId,
28
+ result: input.result,
29
+ timestamp: now,
30
+ ...(input.note ? { note: input.note } : {}),
31
+ ...(input.provenance ? { provenance: input.provenance } : {}),
32
+ };
33
+ const outcomes = [...meta.outcomes, outcome];
34
+ const sharedMissions = meta.sharedMissions.includes(input.missionId)
35
+ ? meta.sharedMissions
36
+ : [...meta.sharedMissions, input.missionId];
37
+ const familiarity = meta.familiarity + (familiarityDelta ?? 1);
38
+ const updated = {
39
+ ...record,
40
+ agentMeta: { ...meta, outcomes, sharedMissions, familiarity },
41
+ updatedAt: now,
42
+ };
43
+ await store.put(friendId, updated);
44
+ (0, observability_1.emitNervesEvent)({
45
+ component: "friends",
46
+ event: "friends.outcome_recorded",
47
+ message: "recorded relationship outcome",
48
+ meta: { friendId, result: input.result },
49
+ });
50
+ return updated;
51
+ }
@@ -0,0 +1,28 @@
1
+ import type { FriendStore } from "./store";
2
+ import type { IdentityProvider, ResolvedContext } from "./types";
3
+ export interface FriendResolverParams {
4
+ provider: IdentityProvider;
5
+ externalId: string;
6
+ tenantId?: string;
7
+ displayName: string;
8
+ channel: string;
9
+ }
10
+ export declare function _setMachineOwnerUsernameForTest(value: string | null | undefined): void;
11
+ /**
12
+ * The OS username that owns this daemon process, or null if undetectable. The
13
+ * person running the daemon owns this agent + its bundle, so the local friend
14
+ * that names them is the machine owner (family), not a stranger.
15
+ */
16
+ export declare function machineOwnerUsername(): string | null;
17
+ /**
18
+ * True when (provider, externalId) names the local machine owner — the OS user
19
+ * running the daemon. Matches the bare username or a `user@host` external id.
20
+ */
21
+ export declare function isLocalMachineOwnerIdentity(provider: string, externalId: string, ownerUsername: string | null): boolean;
22
+ export declare class FriendResolver {
23
+ private readonly store;
24
+ private readonly params;
25
+ constructor(store: FriendStore, params: FriendResolverParams);
26
+ resolve(): Promise<ResolvedContext>;
27
+ private resolveOrCreate;
28
+ }
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ // FriendResolver -- resolves external identity into a FriendRecord + channel capabilities.
3
+ // Created per-request (per-incoming-message), per-friend.
4
+ // Replaces the old ContextResolver: no authority checker, no separate note resolution.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FriendResolver = void 0;
7
+ exports._setMachineOwnerUsernameForTest = _setMachineOwnerUsernameForTest;
8
+ exports.machineOwnerUsername = machineOwnerUsername;
9
+ exports.isLocalMachineOwnerIdentity = isLocalMachineOwnerIdentity;
10
+ const crypto_1 = require("crypto");
11
+ const os_1 = require("os");
12
+ const channel_1 = require("./channel");
13
+ const observability_1 = require("./observability");
14
+ const CURRENT_SCHEMA_VERSION = 1;
15
+ // Test seam: when set (including to null), overrides OS detection of the
16
+ // machine-owner username so resolver tests are deterministic.
17
+ let machineOwnerOverride;
18
+ function _setMachineOwnerUsernameForTest(value) {
19
+ machineOwnerOverride = value;
20
+ }
21
+ /**
22
+ * The OS username that owns this daemon process, or null if undetectable. The
23
+ * person running the daemon owns this agent + its bundle, so the local friend
24
+ * that names them is the machine owner (family), not a stranger.
25
+ */
26
+ function machineOwnerUsername() {
27
+ if (machineOwnerOverride !== undefined)
28
+ return machineOwnerOverride;
29
+ try {
30
+ return (0, os_1.userInfo)().username;
31
+ }
32
+ catch {
33
+ /* v8 ignore next -- defensive: userInfo() only throws when the running user has no passwd entry @preserve */
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * True when (provider, externalId) names the local machine owner — the OS user
39
+ * running the daemon. Matches the bare username or a `user@host` external id.
40
+ */
41
+ function isLocalMachineOwnerIdentity(provider, externalId, ownerUsername) {
42
+ if (provider !== "local" || !ownerUsername)
43
+ return false;
44
+ return externalId === ownerUsername || externalId.startsWith(`${ownerUsername}@`);
45
+ }
46
+ class FriendResolver {
47
+ store;
48
+ params;
49
+ constructor(store, params) {
50
+ this.store = store;
51
+ this.params = params;
52
+ }
53
+ async resolve() {
54
+ const friend = await this.resolveOrCreate();
55
+ const channel = (0, channel_1.getChannelCapabilities)(this.params.channel);
56
+ return { friend, channel };
57
+ }
58
+ async resolveOrCreate() {
59
+ // Try to find existing friend by external ID
60
+ let existing = null;
61
+ try {
62
+ existing = await this.store.findByExternalId(this.params.provider, this.params.externalId, this.params.tenantId);
63
+ }
64
+ catch {
65
+ // Store search failure -- fall through to create new (D16)
66
+ }
67
+ if (existing)
68
+ return existing;
69
+ // Migration: local provider previously used "${username}@${hostname}" format.
70
+ // If no exact match, try finding a friend with old-format external ID.
71
+ /* v8 ignore start -- migration path: only fires when legacy hostname-format friend exists @preserve */
72
+ if (this.params.provider === "local" && !this.params.externalId.includes("@")) {
73
+ try {
74
+ const all = typeof this.store.listAll === "function" ? await this.store.listAll() : [];
75
+ /* v8 ignore start -- migration path: only fires when legacy hostname-format friend exists @preserve */
76
+ const migrationMatch = all.find((f) => f.externalIds.some((eid) => eid.provider === "local" && eid.externalId.startsWith(this.params.externalId + "@")));
77
+ if (migrationMatch) {
78
+ const now = new Date().toISOString();
79
+ migrationMatch.externalIds.push({
80
+ provider: this.params.provider,
81
+ externalId: this.params.externalId,
82
+ linkedAt: now,
83
+ });
84
+ migrationMatch.updatedAt = now;
85
+ try {
86
+ await this.store.put(migrationMatch.id, migrationMatch);
87
+ }
88
+ catch {
89
+ // best-effort persist
90
+ }
91
+ (0, observability_1.emitNervesEvent)({
92
+ component: "friends",
93
+ event: "friends.local_id_migrated",
94
+ message: `migrated local friend identity from hostname format to username-only`,
95
+ meta: { friendId: migrationMatch.id, newExternalId: this.params.externalId },
96
+ });
97
+ return migrationMatch;
98
+ }
99
+ /* v8 ignore stop */
100
+ }
101
+ catch {
102
+ // fall through to create new
103
+ }
104
+ }
105
+ /* v8 ignore stop */
106
+ // First encounter -- create new FriendRecord
107
+ const now = new Date().toISOString();
108
+ const externalId = {
109
+ provider: this.params.provider,
110
+ externalId: this.params.externalId,
111
+ linkedAt: now,
112
+ ...(this.params.tenantId !== undefined ? { tenantId: this.params.tenantId } : {}),
113
+ };
114
+ const tenantMemberships = this.params.tenantId ? [this.params.tenantId] : [];
115
+ let hasAnyFriends = false;
116
+ try {
117
+ if (typeof this.store.hasAnyFriends === "function") {
118
+ hasAnyFriends = await this.store.hasAnyFriends();
119
+ }
120
+ }
121
+ catch {
122
+ hasAnyFriends = false;
123
+ }
124
+ const isFirstImprint = !hasAnyFriends;
125
+ const isA2AAgent = this.params.provider === "a2a-agent";
126
+ // The local friend that names the OS user running the daemon is the machine
127
+ // owner (family) — they own the agent + its bundle. Usually this friend already
128
+ // exists as a family/primary hatch imprint; this covers the un-imprinted boss
129
+ // path (e.g. a Workbench boss check-in on a bundle that skipped imprint).
130
+ const isLocalMachineOwner = isLocalMachineOwnerIdentity(this.params.provider, this.params.externalId, machineOwnerUsername());
131
+ // BlueBubbles group chats route through here as `imessage-handle` with an
132
+ // externalId of the form `group:any;+;<chatHash>`. When the harness auto-
133
+ // creates the group friend at stranger trust, we mark the record so that
134
+ // the trust gate can surface the relationship for explicit acknowledgment
135
+ // later instead of letting messages accumulate silently.
136
+ const isImessageGroup = this.params.provider === "imessage-handle" &&
137
+ typeof this.params.externalId === "string" &&
138
+ this.params.externalId.startsWith("group:");
139
+ const notes = {};
140
+ if (this.params.displayName !== "Unknown") {
141
+ notes.name = { value: this.params.displayName, savedAt: now };
142
+ }
143
+ if (isImessageGroup && !isFirstImprint) {
144
+ notes.autoCreatedGroup = { value: "true", savedAt: now };
145
+ }
146
+ const friend = {
147
+ id: (0, crypto_1.randomUUID)(),
148
+ name: this.params.displayName,
149
+ role: isA2AAgent ? "agent-peer" : isFirstImprint ? "primary" : isLocalMachineOwner ? "family" : "stranger",
150
+ trustLevel: isA2AAgent ? "stranger" : (isFirstImprint || isLocalMachineOwner) ? "family" : "stranger",
151
+ connections: [],
152
+ externalIds: [externalId],
153
+ tenantMemberships,
154
+ toolPreferences: {},
155
+ notes,
156
+ totalTokens: 0,
157
+ createdAt: now,
158
+ updatedAt: now,
159
+ schemaVersion: CURRENT_SCHEMA_VERSION,
160
+ kind: isA2AAgent ? "agent" : "human",
161
+ ...(isA2AAgent ? {
162
+ agentMeta: {
163
+ bundleName: this.params.displayName,
164
+ familiarity: 0,
165
+ sharedMissions: [],
166
+ outcomes: [],
167
+ a2a: { agentId: this.params.externalId },
168
+ },
169
+ } : {}),
170
+ };
171
+ // Persist -- log and continue on failure (D16)
172
+ try {
173
+ await this.store.put(friend.id, friend);
174
+ }
175
+ catch (err) {
176
+ (0, observability_1.emitNervesEvent)({
177
+ level: "error",
178
+ event: "friends.persist_error",
179
+ component: "friends",
180
+ message: "failed to persist friend record",
181
+ meta: { reason: err instanceof Error ? err.message : String(err) },
182
+ });
183
+ }
184
+ return friend;
185
+ }
186
+ }
187
+ exports.FriendResolver = FriendResolver;
@@ -0,0 +1,8 @@
1
+ import type { FriendRecord } from "./types";
2
+ export type FriendOpStatus = "saved" | "updated" | "linked" | "unlinked" | "merged" | "noop" | "not_found" | "override_required" | "redirected_to_name" | "invalid" | "error";
3
+ export interface FriendOpResult {
4
+ ok: boolean;
5
+ status: FriendOpStatus;
6
+ message?: string;
7
+ record?: FriendRecord;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/room.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { FriendStore } from "./store";
2
+ import type { Channel, FriendRecord } from "./types";
3
+ import type { TrustExplanation } from "./trust-explanation";
4
+ /** How the agent knows a room member:
5
+ * - "direct" — the member carries a per-person identity (a non-group externalId),
6
+ * so the agent knows them as an individual, not merely as a name in a roster.
7
+ * - "group_only" — the member is known ONLY through this room: the only identities
8
+ * they carry are group ids. */
9
+ export type RoomKnownVia = "direct" | "group_only";
10
+ export interface RoomMember {
11
+ friend: FriendRecord;
12
+ trust: TrustExplanation;
13
+ knownVia: RoomKnownVia;
14
+ }
15
+ export interface RoomView {
16
+ groupExternalId: string;
17
+ members: RoomMember[];
18
+ }
19
+ /** Resolve the room identified by `groupExternalId` into its members + each
20
+ * member's trust context + how the agent knows them. `channel` selects the lens
21
+ * for the trust explanation (defaults to the agent-facing "mcp" channel). */
22
+ export declare function resolveRoom(store: FriendStore, groupExternalId: string, channel?: Channel): Promise<RoomView>;
package/dist/room.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveRoom = resolveRoom;
4
+ // resolveRoom — the team / room view (N11). Pure read, NO new persisted state.
5
+ //
6
+ // A "room" IS its group ExternalId; membership is already materialized on each
7
+ // member (every participant carries the group's externalId, via
8
+ // upsertGroupContextParticipants). resolveRoom reverse-looks-up every friend
9
+ // carrying that group id and composes the per-member trust context that already
10
+ // exists — proof N10's shape is right (per-party trust + who-said-what +
11
+ // provenance all compose into the room view without any new state).
12
+ const observability_1 = require("./observability");
13
+ const trust_explanation_1 = require("./trust-explanation");
14
+ function isGroupExternalId(externalId) {
15
+ return externalId.startsWith("group:");
16
+ }
17
+ function knownViaFor(friend) {
18
+ const hasNonGroupIdentity = friend.externalIds.some((ext) => !isGroupExternalId(ext.externalId));
19
+ return hasNonGroupIdentity ? "direct" : "group_only";
20
+ }
21
+ /** Resolve the room identified by `groupExternalId` into its members + each
22
+ * member's trust context + how the agent knows them. `channel` selects the lens
23
+ * for the trust explanation (defaults to the agent-facing "mcp" channel). */
24
+ async function resolveRoom(store, groupExternalId, channel = "mcp") {
25
+ const all = typeof store.listAll === "function" ? await store.listAll() : [];
26
+ const members = all
27
+ .filter((friend) => friend.externalIds.some((ext) => ext.externalId === groupExternalId))
28
+ .map((friend) => ({
29
+ friend,
30
+ trust: (0, trust_explanation_1.describeTrustContext)({ friend, channel, isGroupChat: true }),
31
+ knownVia: knownViaFor(friend),
32
+ }));
33
+ (0, observability_1.emitNervesEvent)({
34
+ component: "friends",
35
+ event: "friends.room_resolved",
36
+ message: "resolved room view",
37
+ meta: { groupExternalId, memberCount: members.length },
38
+ });
39
+ return { groupExternalId, members };
40
+ }
@@ -0,0 +1,106 @@
1
+ import type { FriendStore } from "./store";
2
+ import type { GrantStore } from "./grant-store";
3
+ import type { AgentAttribution, ExternalId, FriendRecord, RelationshipOutcome, ShareScope, TrustLevel } from "./types";
4
+ import type { ConsentPolicy } from "./consent";
5
+ import type { AgentVerifier } from "./verifier";
6
+ /** A note as carried on the wire: its value plus who FIRST asserted it
7
+ * (`originallyAssertedBy`), so the consumer can attribute it without laundering
8
+ * an imported fact into first-party. */
9
+ export interface SharedNote {
10
+ key: string;
11
+ value: string;
12
+ originallyAssertedBy?: AgentAttribution;
13
+ }
14
+ /** The cross-agent profile-share envelope. Names the subject by JOIN KEY only. */
15
+ export interface ProfileShareEnvelope {
16
+ /** The party, named by join key — externalIds + display name, NEVER a local UUID. */
17
+ subject: {
18
+ externalIds: ExternalId[];
19
+ displayName: string;
20
+ };
21
+ /** The agent that produced this envelope (its join-key agentId). */
22
+ fromAgentId: string;
23
+ scope: ShareScope;
24
+ /** Scope-filtered notes (present for `notes:*` scopes). */
25
+ notes?: SharedNote[];
26
+ /** Scope-filtered relationship outcomes (present for the `outcomes` scope). */
27
+ outcomes?: RelationshipOutcome[];
28
+ /** Opaque, verifier-specific proof slot (Fork B). The TOFU verifier ignores it;
29
+ * reserved day one so a stronger verifier needs no envelope change. */
30
+ proof?: string;
31
+ issuedAt: string;
32
+ }
33
+ export interface PrepareProfileShareInput {
34
+ /** The local friend to share (UUID or name — resolved via the store). */
35
+ friendId: string;
36
+ /** The recipient agent's join-key agentId. */
37
+ toAgentId: string;
38
+ scope: ShareScope;
39
+ /** This agent's own join-key agentId — the original asserter of first-party
40
+ * facts (so a shared first-party note is attributed to self, not laundered). */
41
+ selfAgentId: string;
42
+ /** Optional proof to stamp on the envelope (for a non-TOFU recipient verifier). */
43
+ proof?: string;
44
+ }
45
+ export type PrepareProfileShareStatus = "not_found" | "no_consent" | "no_recipient";
46
+ export type PrepareProfileShareResult = {
47
+ ok: true;
48
+ envelope: ProfileShareEnvelope;
49
+ } | {
50
+ ok: false;
51
+ status: PrepareProfileShareStatus;
52
+ };
53
+ /** The original asserter of a note: for an imported note, whoever the import
54
+ * recorded as `originallyAssertedBy` (falling back to its `assertedBy`); for a
55
+ * first-party note, this agent itself. Never launders imported → first-party.
56
+ * Always returns an attribution (a shared fact is always attributable). Exported
57
+ * so the mission-share producer reuses it (a MissionLearning is structurally
58
+ * compatible with the inline param type) — single-sourced, tested once. */
59
+ export declare function originalAsserterOf(note: {
60
+ provenance?: {
61
+ origin?: "first_party" | "imported";
62
+ assertedBy?: AgentAttribution;
63
+ };
64
+ }, selfAgentId: string): AgentAttribution;
65
+ /**
66
+ * Producer half of the moat. Consent-gated (via the injected ConsentPolicy, or
67
+ * the module default), scope-filtered, provenance-preserving. Names the party by
68
+ * join key, never the local UUID. The recipient's trust level — read off this
69
+ * agent's own record for `toAgentId` — is the authorization input the policy
70
+ * uses. Returns `{ ok:true, envelope }` or `{ ok:false, status }`.
71
+ */
72
+ export declare function prepareProfileShare(store: FriendStore, grants: GrantStore, input: PrepareProfileShareInput, consent?: ConsentPolicy): Promise<PrepareProfileShareResult>;
73
+ export interface ImportProfileShareInput {
74
+ envelope: ProfileShareEnvelope;
75
+ /** The agent the envelope arrived from (its join-key agentId). */
76
+ fromAgentId: string;
77
+ /** This agent's resolved trust in the source agent — the cap on acceptance.
78
+ * A stranger source's facts are refused (see `minTrustToAccept`). */
79
+ trustOfSource: TrustLevel;
80
+ }
81
+ export type ImportProfileShareStatus = "imported" | "seeded" | "no_party" | "untrusted_source" | "untrusted_introduction";
82
+ export type ImportProfileShareResult = {
83
+ ok: true;
84
+ status: "imported" | "seeded";
85
+ record: FriendRecord;
86
+ } | {
87
+ ok: false;
88
+ status: "no_party" | "untrusted_source" | "untrusted_introduction";
89
+ };
90
+ export interface ImportProfileShareOptions {
91
+ /** Authentication seam (Fork B). Defaults to TOFU. Authorization (trust) is
92
+ * still applied regardless of what the verifier says. */
93
+ verifier?: AgentVerifier;
94
+ /** Minimum trust a source agent must hold for its facts to be accepted at all.
95
+ * Default `acquaintance`: a stranger source is refused. */
96
+ minTrustToAccept?: TrustLevel;
97
+ }
98
+ /**
99
+ * Consumer half of the moat — the non-clobbering merge. Resolves the party by
100
+ * join key; lands imported facts in the `importedNotes` namespace (origin
101
+ * "imported" + assertedBy + importedAt) WITHOUT ever touching first-party `notes`;
102
+ * the source agent's trust caps acceptance; NEVER changes the party's trust level
103
+ * (the key safety invariant); seeds an unknown party only when a friend/family
104
+ * peer introduces it. Returns `{ ok, status, record }`.
105
+ */
106
+ export declare function importProfileShare(store: FriendStore, input: ImportProfileShareInput, options?: ImportProfileShareOptions): Promise<ImportProfileShareResult>;