@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.
- package/LICENSE +201 -0
- package/README.md +514 -0
- package/changelog.json +34 -0
- package/dist/a2a/index.d.ts +102 -0
- package/dist/a2a/index.js +198 -0
- package/dist/agent-peer.d.ts +17 -0
- package/dist/agent-peer.js +57 -0
- package/dist/channel.d.ts +11 -0
- package/dist/channel.js +132 -0
- package/dist/consent.d.ts +34 -0
- package/dist/consent.js +62 -0
- package/dist/coordination.d.ts +100 -0
- package/dist/coordination.js +255 -0
- package/dist/file-bundle.d.ts +12 -0
- package/dist/file-bundle.js +23 -0
- package/dist/grant-store-file.d.ts +16 -0
- package/dist/grant-store-file.js +136 -0
- package/dist/grant-store.d.ts +7 -0
- package/dist/grant-store.js +8 -0
- package/dist/grants.d.ts +39 -0
- package/dist/grants.js +84 -0
- package/dist/group-context.d.ts +21 -0
- package/dist/group-context.js +144 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +105 -0
- package/dist/link-identity.d.ts +14 -0
- package/dist/link-identity.js +88 -0
- package/dist/mcp/bin.d.ts +2 -0
- package/dist/mcp/bin.js +16 -0
- package/dist/mcp/dispatch.d.ts +14 -0
- package/dist/mcp/dispatch.js +432 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.js +14 -0
- package/dist/mcp/run-main.d.ts +7 -0
- package/dist/mcp/run-main.js +45 -0
- package/dist/mcp/schemas.d.ts +10 -0
- package/dist/mcp/schemas.js +398 -0
- package/dist/mcp/server.d.ts +21 -0
- package/dist/mcp/server.js +194 -0
- package/dist/mission-share.d.ts +94 -0
- package/dist/mission-share.js +232 -0
- package/dist/mission-store-file.d.ts +18 -0
- package/dist/mission-store-file.js +153 -0
- package/dist/mission-store.d.ts +10 -0
- package/dist/mission-store.js +9 -0
- package/dist/missions.d.ts +31 -0
- package/dist/missions.js +98 -0
- package/dist/notes.d.ts +11 -0
- package/dist/notes.js +90 -0
- package/dist/observability.d.ts +27 -0
- package/dist/observability.js +31 -0
- package/dist/outcomes.d.ts +9 -0
- package/dist/outcomes.js +51 -0
- package/dist/resolver.d.ts +28 -0
- package/dist/resolver.js +187 -0
- package/dist/results.d.ts +8 -0
- package/dist/results.js +2 -0
- package/dist/room.d.ts +22 -0
- package/dist/room.js +40 -0
- package/dist/share.d.ts +106 -0
- package/dist/share.js +223 -0
- package/dist/standing.d.ts +83 -0
- package/dist/standing.js +111 -0
- package/dist/store-file.d.ts +21 -0
- package/dist/store-file.js +264 -0
- package/dist/store.d.ts +9 -0
- package/dist/store.js +4 -0
- package/dist/tokens.d.ts +8 -0
- package/dist/tokens.js +26 -0
- package/dist/trust-explanation.d.ts +16 -0
- package/dist/trust-explanation.js +74 -0
- package/dist/trust-mutation.d.ts +4 -0
- package/dist/trust-mutation.js +29 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +51 -0
- package/dist/util/cap-string.d.ts +7 -0
- package/dist/util/cap-string.js +35 -0
- package/dist/verifier.d.ts +11 -0
- package/dist/verifier.js +29 -0
- package/dist/whoami.d.ts +7 -0
- package/dist/whoami.js +39 -0
- 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>;
|
package/dist/outcomes.js
ADDED
|
@@ -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
|
+
}
|
package/dist/resolver.js
ADDED
|
@@ -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
|
+
}
|
package/dist/results.js
ADDED
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
|
+
}
|
package/dist/share.d.ts
ADDED
|
@@ -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>;
|