@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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAILBOX_VERSION = void 0;
|
|
4
|
+
exports.buildOutgoing = buildOutgoing;
|
|
5
|
+
exports.compareReady = compareReady;
|
|
6
|
+
exports.readIncoming = readIncoming;
|
|
7
|
+
exports.isSeen = isSeen;
|
|
8
|
+
exports.markSeen = markSeen;
|
|
9
|
+
// src/a2a — the pure git-mailbox format/routing/dedup library (brick two).
|
|
10
|
+
//
|
|
11
|
+
// A consumer agent and a producer agent that authenticate as two DISTINCT git
|
|
12
|
+
// identities share a dedicated PRIVATE mailbox repo. This module computes the
|
|
13
|
+
// per-message file PATH + BYTES the host writes, and parses/validates/orders/
|
|
14
|
+
// dedups the files the host hands back — nothing more. It is PURE:
|
|
15
|
+
// • ZERO runtime deps; the ONLY node builtin is `node:crypto` (randomUUID),
|
|
16
|
+
// mirroring share.ts / agent-peer.ts;
|
|
17
|
+
// • NO fs / net / http / child_process / process.env / git anywhere — the wire
|
|
18
|
+
// (clone / pull / add / commit / push) is entirely the caller's job.
|
|
19
|
+
// Type-only imports of `ProfileShareEnvelope` (../share) + `MissionShareEnvelope`
|
|
20
|
+
// (../mission-share) carry no runtime edge. Both are CORE modules, so the a2a→core
|
|
21
|
+
// import direction is eslint-legal (a2a may import core; the reverse is forbidden).
|
|
22
|
+
//
|
|
23
|
+
// Security model (the git-native TOFU): addressing lives in the PATH, and a
|
|
24
|
+
// single-writer-per-outbox-dir layout means a forged sender can't write into
|
|
25
|
+
// another agent's outbox dir without that git identity. `readIncoming` binds the
|
|
26
|
+
// wrapper's claimed from/to against the path and REJECTS any mismatch, so a
|
|
27
|
+
// hostile mailbox can only DENY or REPLAY — never escalate (content trust is the
|
|
28
|
+
// import layer's job; this layer never touches first-party notes or trust).
|
|
29
|
+
const node_crypto_1 = require("node:crypto");
|
|
30
|
+
const observability_1 = require("../observability");
|
|
31
|
+
/** The mailbox wire-format version. Bumped only on a breaking message change. */
|
|
32
|
+
exports.MAILBOX_VERSION = 1;
|
|
33
|
+
/** Compute the mailbox file (path + bytes) for one outgoing share. Does NOT
|
|
34
|
+
* write anything — the host does the git op. The envelope is carried verbatim
|
|
35
|
+
* (by reference, never cloned or mutated). */
|
|
36
|
+
function buildOutgoing(input) {
|
|
37
|
+
const now = input.now ?? new Date().toISOString();
|
|
38
|
+
const messageId = (0, node_crypto_1.randomUUID)();
|
|
39
|
+
const message = {
|
|
40
|
+
mailboxVersion: exports.MAILBOX_VERSION,
|
|
41
|
+
messageId,
|
|
42
|
+
fromAgentId: input.fromAgentId,
|
|
43
|
+
toAgentId: input.toAgentId,
|
|
44
|
+
issuedAt: now,
|
|
45
|
+
kind: input.kind ?? "profile_share",
|
|
46
|
+
envelope: input.envelope,
|
|
47
|
+
};
|
|
48
|
+
// Mailbox paths are git-relative POSIX (always `/`), intentionally NOT
|
|
49
|
+
// path.join — that would pull an fs-adjacent builtin and be platform-sep
|
|
50
|
+
// sensitive. A template literal keeps this module fs-free.
|
|
51
|
+
const relativePath = `agents/${input.fromAgentId}/outbox/${input.toAgentId}/${now}--${messageId}.json`;
|
|
52
|
+
(0, observability_1.emitNervesEvent)({
|
|
53
|
+
component: "friends",
|
|
54
|
+
event: "friends.a2a_outgoing_built",
|
|
55
|
+
message: "built outgoing mailbox message",
|
|
56
|
+
meta: { toAgentId: input.toAgentId },
|
|
57
|
+
});
|
|
58
|
+
return { relativePath, bytes: JSON.stringify(message, null, 2), messageId };
|
|
59
|
+
}
|
|
60
|
+
/** Parse the post-office path. Returns the owner/routing dirs, or null when the
|
|
61
|
+
* path doesn't match `agents/<from>/outbox/<to>/<file>.json` exactly. */
|
|
62
|
+
function parsePath(relativePath) {
|
|
63
|
+
const parts = relativePath.split("/");
|
|
64
|
+
if (parts.length !== 5)
|
|
65
|
+
return null;
|
|
66
|
+
if (parts[0] !== "agents" || parts[2] !== "outbox")
|
|
67
|
+
return null;
|
|
68
|
+
if (parts.some((segment) => segment.length === 0))
|
|
69
|
+
return null;
|
|
70
|
+
if (!parts[4].endsWith(".json"))
|
|
71
|
+
return null;
|
|
72
|
+
return { from: parts[1], to: parts[3] };
|
|
73
|
+
}
|
|
74
|
+
/** Whether a parsed value is a well-formed mailbox wrapper. */
|
|
75
|
+
function isWellFormedWrapper(value) {
|
|
76
|
+
return (typeof value.mailboxVersion === "number" &&
|
|
77
|
+
typeof value.messageId === "string" &&
|
|
78
|
+
value.messageId.length > 0 &&
|
|
79
|
+
typeof value.fromAgentId === "string" &&
|
|
80
|
+
typeof value.toAgentId === "string" &&
|
|
81
|
+
typeof value.issuedAt === "string" &&
|
|
82
|
+
(value.kind === "profile_share" || value.kind === "mission_share" || value.kind === "coordination") &&
|
|
83
|
+
typeof value.envelope === "object" &&
|
|
84
|
+
value.envelope !== null &&
|
|
85
|
+
!Array.isArray(value.envelope));
|
|
86
|
+
}
|
|
87
|
+
/** Lexicographic compare of two strings → -1 | 0 | 1. */
|
|
88
|
+
function cmp(a, b) {
|
|
89
|
+
if (a < b)
|
|
90
|
+
return -1;
|
|
91
|
+
if (a > b)
|
|
92
|
+
return 1;
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
/** Deterministic delivery order: issuedAt ascending, tiebroken by messageId
|
|
96
|
+
* ascending. Exported so the ordering contract is independently testable in both
|
|
97
|
+
* argument orders (every branch reachable). */
|
|
98
|
+
function compareReady(a, b) {
|
|
99
|
+
const byTime = cmp(a.issuedAt, b.issuedAt);
|
|
100
|
+
return byTime !== 0 ? byTime : cmp(a.messageId, b.messageId);
|
|
101
|
+
}
|
|
102
|
+
/** Parse, validate, path-bind, address-filter, and dedup a batch of mailbox
|
|
103
|
+
* files. The security-critical reader: every reject reason is distinct so the
|
|
104
|
+
* caller can tell a spoof (path mismatch) from malformed input. Order of checks:
|
|
105
|
+
* path → JSON → object → wrapper shape → version → path-binding → addressing →
|
|
106
|
+
* dedup. A message addressed to someone else is silently skipped (not ours);
|
|
107
|
+
* only a malformed PATH makes a non-self message visible (as rejected). */
|
|
108
|
+
function readIncoming(input) {
|
|
109
|
+
const ready = [];
|
|
110
|
+
const skippedSeen = [];
|
|
111
|
+
const rejected = [];
|
|
112
|
+
for (const file of input.files) {
|
|
113
|
+
const path = parsePath(file.relativePath);
|
|
114
|
+
if (!path) {
|
|
115
|
+
rejected.push({ relativePath: file.relativePath, reason: "malformed_path" });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(file.bytes);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
rejected.push({ relativePath: file.relativePath, reason: "invalid_json" });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
127
|
+
rejected.push({ relativePath: file.relativePath, reason: "not_an_object" });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const message = parsed;
|
|
131
|
+
if (!isWellFormedWrapper(message)) {
|
|
132
|
+
rejected.push({ relativePath: file.relativePath, reason: "malformed_message" });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (message.mailboxVersion !== exports.MAILBOX_VERSION) {
|
|
136
|
+
rejected.push({ relativePath: file.relativePath, reason: "unsupported_version" });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Path-binding (TOFU): a forged sender that doesn't own the outbox dir, or a
|
|
140
|
+
// wrapper routed to a dir it doesn't address, is rejected — never delivered.
|
|
141
|
+
if (message.fromAgentId !== path.from) {
|
|
142
|
+
rejected.push({ relativePath: file.relativePath, reason: "from_path_mismatch" });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (message.toAgentId !== path.to) {
|
|
146
|
+
rejected.push({ relativePath: file.relativePath, reason: "to_path_mismatch" });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// Addressing: a message for someone else is not ours to read — skip silently.
|
|
150
|
+
if (message.toAgentId !== input.selfAgentId)
|
|
151
|
+
continue;
|
|
152
|
+
const messageId = message.messageId;
|
|
153
|
+
if (isSeen(input.seen, messageId)) {
|
|
154
|
+
skippedSeen.push(messageId);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
ready.push({
|
|
158
|
+
messageId,
|
|
159
|
+
fromAgentId: message.fromAgentId,
|
|
160
|
+
toAgentId: message.toAgentId,
|
|
161
|
+
issuedAt: message.issuedAt,
|
|
162
|
+
kind: message.kind,
|
|
163
|
+
envelope: message.envelope,
|
|
164
|
+
relativePath: file.relativePath,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Deterministic delivery order: issuedAt ascending, tiebroken by messageId.
|
|
168
|
+
ready.sort(compareReady);
|
|
169
|
+
(0, observability_1.emitNervesEvent)({
|
|
170
|
+
component: "friends",
|
|
171
|
+
event: "friends.a2a_incoming_read",
|
|
172
|
+
message: "read incoming mailbox files",
|
|
173
|
+
meta: {
|
|
174
|
+
ready: ready.length,
|
|
175
|
+
skipped: skippedSeen.length,
|
|
176
|
+
rejected: rejected.length,
|
|
177
|
+
at: input.now ?? new Date().toISOString(),
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
return { ready, skippedSeen, rejected };
|
|
181
|
+
}
|
|
182
|
+
/** Whether a messageId is already in the ledger. Uses hasOwnProperty so an
|
|
183
|
+
* inherited prototype key (e.g. "toString") never reads as seen. */
|
|
184
|
+
function isSeen(seen, messageId) {
|
|
185
|
+
return Object.prototype.hasOwnProperty.call(seen.seen, messageId);
|
|
186
|
+
}
|
|
187
|
+
/** Return a NEW ledger with `messageId` recorded (immutable — never mutates the
|
|
188
|
+
* input). `at` defaults to now; that single `new Date()` is the only ambient
|
|
189
|
+
* time minted here and matches share.ts's idiom. */
|
|
190
|
+
function markSeen(seen, messageId, at) {
|
|
191
|
+
(0, observability_1.emitNervesEvent)({
|
|
192
|
+
component: "friends",
|
|
193
|
+
event: "friends.a2a_marked_seen",
|
|
194
|
+
message: "marked mailbox message seen",
|
|
195
|
+
meta: {},
|
|
196
|
+
});
|
|
197
|
+
return { seen: { ...seen.seen, [messageId]: at ?? new Date().toISOString() } };
|
|
198
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FriendStore } from "./store";
|
|
2
|
+
import type { AgentMeta, FriendRecord, TrustLevel } from "./types";
|
|
3
|
+
export interface UpsertAgentPeerInput {
|
|
4
|
+
name: string;
|
|
5
|
+
agentId: string;
|
|
6
|
+
trustLevel?: TrustLevel;
|
|
7
|
+
a2a?: AgentMeta["a2a"];
|
|
8
|
+
/** Optional A2A git-mailbox coords — the ergonomic top-level path the MCP
|
|
9
|
+
* `onboard_agent` tool uses. Folded into the rebuilt `a2a`; if also set inside
|
|
10
|
+
* `a2a`, this explicit value wins (last spread). Absent ⇒ no mailbox key. */
|
|
11
|
+
mailbox?: {
|
|
12
|
+
repo: string;
|
|
13
|
+
selfOutboxAgentId: string;
|
|
14
|
+
};
|
|
15
|
+
bundleName?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function upsertAgentPeer(store: FriendStore, input: UpsertAgentPeerInput): Promise<FriendRecord>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.upsertAgentPeer = upsertAgentPeer;
|
|
4
|
+
// upsertAgentPeer — the record-shaping half of the harness's `onboardA2APeer`.
|
|
5
|
+
//
|
|
6
|
+
// Mints or updates an agent-peer friend record from already-resolved inputs. The
|
|
7
|
+
// HTTP agent-card fetch (`fetchA2AAgentCard` / `endpointForCard` / URL parsing)
|
|
8
|
+
// stays harness-side; this helper takes `agentId` and the `a2a` coords directly,
|
|
9
|
+
// so the MCP server can onboard a peer without any network call.
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const observability_1 = require("./observability");
|
|
12
|
+
async function upsertAgentPeer(store, input) {
|
|
13
|
+
const { name, agentId, a2a, bundleName } = input;
|
|
14
|
+
const existing = await store.findByExternalId("a2a-agent", agentId);
|
|
15
|
+
const now = new Date().toISOString();
|
|
16
|
+
const trustLevel = input.trustLevel ?? existing?.trustLevel ?? "acquaintance";
|
|
17
|
+
const baseMeta = existing?.agentMeta ?? {
|
|
18
|
+
bundleName: bundleName ?? name,
|
|
19
|
+
familiarity: 0,
|
|
20
|
+
sharedMissions: [],
|
|
21
|
+
outcomes: [],
|
|
22
|
+
};
|
|
23
|
+
const record = {
|
|
24
|
+
...(existing ?? {
|
|
25
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
26
|
+
createdAt: now,
|
|
27
|
+
externalIds: [],
|
|
28
|
+
tenantMemberships: [],
|
|
29
|
+
toolPreferences: {},
|
|
30
|
+
notes: {},
|
|
31
|
+
totalTokens: 0,
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
}),
|
|
34
|
+
name,
|
|
35
|
+
role: "agent-peer",
|
|
36
|
+
trustLevel,
|
|
37
|
+
kind: "agent",
|
|
38
|
+
agentMeta: {
|
|
39
|
+
...baseMeta,
|
|
40
|
+
bundleName: baseMeta.bundleName || bundleName || name,
|
|
41
|
+
a2a: { ...(a2a ?? {}), agentId, ...(input.mailbox ? { mailbox: input.mailbox } : {}) },
|
|
42
|
+
},
|
|
43
|
+
externalIds: [
|
|
44
|
+
...(existing?.externalIds.filter((id) => !(id.provider === "a2a-agent" && id.externalId === agentId)) ?? []),
|
|
45
|
+
{ provider: "a2a-agent", externalId: agentId, linkedAt: now },
|
|
46
|
+
],
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
};
|
|
49
|
+
await store.put(record.id, record);
|
|
50
|
+
(0, observability_1.emitNervesEvent)({
|
|
51
|
+
component: "friends",
|
|
52
|
+
event: "friends.agent_peer_upserted",
|
|
53
|
+
message: "upserted agent peer record",
|
|
54
|
+
meta: { friendId: record.id, trustLevel },
|
|
55
|
+
});
|
|
56
|
+
return record;
|
|
57
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ChannelCapabilities, Channel } from "./types";
|
|
2
|
+
export type Facing = "human" | "agent";
|
|
3
|
+
export declare function channelToFacing(channel?: Channel | string): Facing;
|
|
4
|
+
export declare function getChannelCapabilities(channel: string): ChannelCapabilities;
|
|
5
|
+
/** Whether the channel is remote (open or closed) vs local/internal. */
|
|
6
|
+
export declare function isRemoteChannel(capabilities?: ChannelCapabilities): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Returns channel names whose senseType is "open" or "closed" -- i.e. channels
|
|
9
|
+
* that are always-on (daemon-managed) rather than interactive or internal.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getAlwaysOnSenseNames(): string[];
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Channel capabilities -- hardcoded const map keyed by channel identifier.
|
|
3
|
+
// Pure lookup, no I/O, cannot fail. Unknown channel gets minimal defaults.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.channelToFacing = channelToFacing;
|
|
6
|
+
exports.getChannelCapabilities = getChannelCapabilities;
|
|
7
|
+
exports.isRemoteChannel = isRemoteChannel;
|
|
8
|
+
exports.getAlwaysOnSenseNames = getAlwaysOnSenseNames;
|
|
9
|
+
const observability_1 = require("./observability");
|
|
10
|
+
const AGENT_FACING_CHANNELS = new Set(["inner", "mcp", "a2a"]);
|
|
11
|
+
function channelToFacing(channel) {
|
|
12
|
+
const facing = channel && AGENT_FACING_CHANNELS.has(channel) ? "agent" : "human";
|
|
13
|
+
(0, observability_1.emitNervesEvent)({
|
|
14
|
+
component: "channels",
|
|
15
|
+
event: "channel.facing_lookup",
|
|
16
|
+
message: "channel facing lookup",
|
|
17
|
+
meta: { channel: channel ?? "undefined", facing },
|
|
18
|
+
});
|
|
19
|
+
return facing;
|
|
20
|
+
}
|
|
21
|
+
const CHANNEL_CAPABILITIES = {
|
|
22
|
+
cli: {
|
|
23
|
+
channel: "cli",
|
|
24
|
+
senseType: "local",
|
|
25
|
+
availableIntegrations: [],
|
|
26
|
+
supportsMarkdown: false,
|
|
27
|
+
supportsStreaming: true,
|
|
28
|
+
supportsRichCards: false,
|
|
29
|
+
maxMessageLength: Infinity,
|
|
30
|
+
},
|
|
31
|
+
teams: {
|
|
32
|
+
channel: "teams",
|
|
33
|
+
senseType: "closed",
|
|
34
|
+
availableIntegrations: ["ado", "graph", "github"],
|
|
35
|
+
supportsMarkdown: true,
|
|
36
|
+
supportsStreaming: true,
|
|
37
|
+
supportsRichCards: true,
|
|
38
|
+
maxMessageLength: Infinity,
|
|
39
|
+
},
|
|
40
|
+
bluebubbles: {
|
|
41
|
+
channel: "bluebubbles",
|
|
42
|
+
senseType: "open",
|
|
43
|
+
availableIntegrations: [],
|
|
44
|
+
supportsMarkdown: false,
|
|
45
|
+
supportsStreaming: false,
|
|
46
|
+
supportsRichCards: false,
|
|
47
|
+
maxMessageLength: Infinity,
|
|
48
|
+
},
|
|
49
|
+
mail: {
|
|
50
|
+
channel: "mail",
|
|
51
|
+
senseType: "open",
|
|
52
|
+
availableIntegrations: [],
|
|
53
|
+
supportsMarkdown: false,
|
|
54
|
+
supportsStreaming: false,
|
|
55
|
+
supportsRichCards: false,
|
|
56
|
+
maxMessageLength: Infinity,
|
|
57
|
+
},
|
|
58
|
+
voice: {
|
|
59
|
+
channel: "voice",
|
|
60
|
+
senseType: "local",
|
|
61
|
+
availableIntegrations: [],
|
|
62
|
+
supportsMarkdown: false,
|
|
63
|
+
supportsStreaming: true,
|
|
64
|
+
supportsRichCards: false,
|
|
65
|
+
maxMessageLength: Infinity,
|
|
66
|
+
},
|
|
67
|
+
a2a: {
|
|
68
|
+
channel: "a2a",
|
|
69
|
+
senseType: "open",
|
|
70
|
+
availableIntegrations: [],
|
|
71
|
+
supportsMarkdown: true,
|
|
72
|
+
supportsStreaming: false,
|
|
73
|
+
supportsRichCards: false,
|
|
74
|
+
maxMessageLength: Infinity,
|
|
75
|
+
},
|
|
76
|
+
inner: {
|
|
77
|
+
channel: "inner",
|
|
78
|
+
senseType: "internal",
|
|
79
|
+
availableIntegrations: [],
|
|
80
|
+
supportsMarkdown: false,
|
|
81
|
+
supportsStreaming: true,
|
|
82
|
+
supportsRichCards: false,
|
|
83
|
+
maxMessageLength: Infinity,
|
|
84
|
+
},
|
|
85
|
+
mcp: {
|
|
86
|
+
channel: "mcp",
|
|
87
|
+
senseType: "local",
|
|
88
|
+
availableIntegrations: [],
|
|
89
|
+
supportsMarkdown: true,
|
|
90
|
+
supportsStreaming: false,
|
|
91
|
+
supportsRichCards: false,
|
|
92
|
+
maxMessageLength: Infinity,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
const DEFAULT_CAPABILITIES = {
|
|
96
|
+
channel: "cli",
|
|
97
|
+
senseType: "local",
|
|
98
|
+
availableIntegrations: [],
|
|
99
|
+
supportsMarkdown: false,
|
|
100
|
+
supportsStreaming: false,
|
|
101
|
+
supportsRichCards: false,
|
|
102
|
+
maxMessageLength: Infinity,
|
|
103
|
+
};
|
|
104
|
+
function getChannelCapabilities(channel) {
|
|
105
|
+
(0, observability_1.emitNervesEvent)({
|
|
106
|
+
component: "channels",
|
|
107
|
+
event: "channel.capabilities_lookup",
|
|
108
|
+
message: "channel capabilities lookup",
|
|
109
|
+
meta: { channel },
|
|
110
|
+
});
|
|
111
|
+
return CHANNEL_CAPABILITIES[channel] ?? DEFAULT_CAPABILITIES;
|
|
112
|
+
}
|
|
113
|
+
/** Whether the channel is remote (open or closed) vs local/internal. */
|
|
114
|
+
function isRemoteChannel(capabilities) {
|
|
115
|
+
const senseType = capabilities?.senseType;
|
|
116
|
+
return senseType !== undefined && senseType !== "local" && senseType !== "internal";
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Returns channel names whose senseType is "open" or "closed" -- i.e. channels
|
|
120
|
+
* that are always-on (daemon-managed) rather than interactive or internal.
|
|
121
|
+
*/
|
|
122
|
+
function getAlwaysOnSenseNames() {
|
|
123
|
+
(0, observability_1.emitNervesEvent)({
|
|
124
|
+
component: "channels",
|
|
125
|
+
event: "channel.always_on_lookup",
|
|
126
|
+
message: "always-on sense names lookup",
|
|
127
|
+
meta: {},
|
|
128
|
+
});
|
|
129
|
+
return Object.entries(CHANNEL_CAPABILITIES)
|
|
130
|
+
.filter(([, cap]) => cap.senseType === "open" || cap.senseType === "closed")
|
|
131
|
+
.map(([channel]) => channel);
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { GrantStore } from "./grant-store";
|
|
2
|
+
import type { ShareScope, TrustLevel } from "./types";
|
|
3
|
+
/** The recipient of a share, as the consent layer sees it: its join-key agentId
|
|
4
|
+
* and its resolved trust level on this graph (the authorization input). */
|
|
5
|
+
export interface ConsentRecipient {
|
|
6
|
+
agentId: string;
|
|
7
|
+
trustLevel: TrustLevel;
|
|
8
|
+
}
|
|
9
|
+
export interface ConsentDecisionInput {
|
|
10
|
+
/** The subject whose data may be shared — a friend UUID for a profile share, a
|
|
11
|
+
* missionKey for a mission share (Fork D: opaque subject key). */
|
|
12
|
+
subjectKey: string;
|
|
13
|
+
recipient: ConsentRecipient;
|
|
14
|
+
scope: ShareScope;
|
|
15
|
+
grants: GrantStore;
|
|
16
|
+
now?: Date;
|
|
17
|
+
}
|
|
18
|
+
/** A pluggable consent posture. `consents` resolves true iff the share is
|
|
19
|
+
* permitted under this posture. */
|
|
20
|
+
export interface ConsentPolicy {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
consents(input: ConsentDecisionInput): Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
export declare const strictPolicy: ConsentPolicy;
|
|
25
|
+
export declare const trustImpliedPolicy: ConsentPolicy;
|
|
26
|
+
export declare const tieredPolicy: ConsentPolicy;
|
|
27
|
+
/**
|
|
28
|
+
* ── CONSENT-POLICY SWAP POINT (the operator's one-line default) ──
|
|
29
|
+
* The active consent posture. Swap this assignment to `strictPolicy` or
|
|
30
|
+
* `trustImpliedPolicy` to change the product's privacy posture; `tieredPolicy`
|
|
31
|
+
* is the recommended default (identity via trust, note content via explicit
|
|
32
|
+
* grant). `prepareProfileShare` uses this when no `consent` policy is injected.
|
|
33
|
+
*/
|
|
34
|
+
export declare const DEFAULT_CONSENT_POLICY: ConsentPolicy;
|
package/dist/consent.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CONSENT_POLICY = exports.tieredPolicy = exports.trustImpliedPolicy = exports.strictPolicy = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
const grants_1 = require("./grants");
|
|
6
|
+
const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
|
|
7
|
+
/** True when `level` is at least `friend` (the "trusted" floor). */
|
|
8
|
+
function isAtLeastFriend(level) {
|
|
9
|
+
return TRUST_RANK[level] >= TRUST_RANK.friend;
|
|
10
|
+
}
|
|
11
|
+
/** Whether an effective, non-revoked, non-expired grant covers exactly
|
|
12
|
+
* (subject, recipient, scope). The shared machinery all three policies build on. */
|
|
13
|
+
async function hasEffectiveGrant(input) {
|
|
14
|
+
const now = input.now ?? new Date();
|
|
15
|
+
const all = await input.grants.listAll();
|
|
16
|
+
return all.some((g) => g.subjectKey === input.subjectKey &&
|
|
17
|
+
g.recipientAgentId === input.recipient.agentId &&
|
|
18
|
+
g.scope === input.scope &&
|
|
19
|
+
(0, grants_1.isGrantEffective)(g, now));
|
|
20
|
+
}
|
|
21
|
+
// ── A1: strict ──
|
|
22
|
+
// Consented ONLY if a non-revoked, non-expired explicit grant covers
|
|
23
|
+
// (subject, recipient, scope). Safest; trust alone never implies a share.
|
|
24
|
+
exports.strictPolicy = {
|
|
25
|
+
name: "strict",
|
|
26
|
+
async consents(input) {
|
|
27
|
+
return hasEffectiveGrant(input);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
// ── A2: trust-implied ──
|
|
31
|
+
// Consented if an explicit grant covers it, OR the recipient's trust ≥ friend
|
|
32
|
+
// (any scope). Fast; can surprise on privacy because trust unlocks note content.
|
|
33
|
+
exports.trustImpliedPolicy = {
|
|
34
|
+
name: "trust_implied",
|
|
35
|
+
async consents(input) {
|
|
36
|
+
if (isAtLeastFriend(input.recipient.trustLevel))
|
|
37
|
+
return true;
|
|
38
|
+
return hasEffectiveGrant(input);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
// ── A3: tiered (the recommended default) ──
|
|
42
|
+
// Identity-scope shares (the join key only — "name"/"identity") are consented if
|
|
43
|
+
// the recipient's trust ≥ friend; but any note-content scope (`notes:*`,
|
|
44
|
+
// `outcomes`) ALWAYS requires an explicit grant. Trust agrees on WHO; content
|
|
45
|
+
// still requires consent.
|
|
46
|
+
exports.tieredPolicy = {
|
|
47
|
+
name: "tiered",
|
|
48
|
+
async consents(input) {
|
|
49
|
+
if (types_1.IDENTITY_SCOPES.has(input.scope)) {
|
|
50
|
+
return isAtLeastFriend(input.recipient.trustLevel);
|
|
51
|
+
}
|
|
52
|
+
return hasEffectiveGrant(input);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* ── CONSENT-POLICY SWAP POINT (the operator's one-line default) ──
|
|
57
|
+
* The active consent posture. Swap this assignment to `strictPolicy` or
|
|
58
|
+
* `trustImpliedPolicy` to change the product's privacy posture; `tieredPolicy`
|
|
59
|
+
* is the recommended default (identity via trust, note content via explicit
|
|
60
|
+
* grant). `prepareProfileShare` uses this when no `consent` policy is injected.
|
|
61
|
+
*/
|
|
62
|
+
exports.DEFAULT_CONSENT_POLICY = exports.tieredPolicy;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { MissionStore } from "./mission-store";
|
|
2
|
+
import type { FriendStore } from "./store";
|
|
3
|
+
import type { GrantStore } from "./grant-store";
|
|
4
|
+
import type { AgentAttribution, CoordinationIntent, MissionRecord, TrustLevel } from "./types";
|
|
5
|
+
import type { ConsentPolicy } from "./consent";
|
|
6
|
+
import type { AgentVerifier } from "./verifier";
|
|
7
|
+
/** The cross-agent coordination envelope (brick 5). Names the subject by JOIN KEY
|
|
8
|
+
* (`missionKey`) + title only — NEVER a local UUID. A SIBLING of
|
|
9
|
+
* `MissionShareEnvelope` (Fork A — per-kind compiler-enforced type safety), not a
|
|
10
|
+
* widening: a coordination message is always *about* a mission both agents can name
|
|
11
|
+
* out of band. */
|
|
12
|
+
export interface CoordinationEnvelope {
|
|
13
|
+
/** The mission, named by its join key — `missionKey` + a human title. */
|
|
14
|
+
subject: {
|
|
15
|
+
missionKey: string;
|
|
16
|
+
title: string;
|
|
17
|
+
};
|
|
18
|
+
/** The agent that produced this envelope (its join-key agentId). */
|
|
19
|
+
fromAgentId: string;
|
|
20
|
+
/** The verb (one of the five coordination intents). */
|
|
21
|
+
intent: CoordinationIntent;
|
|
22
|
+
/** Optional free text ("can you take the API side?"). */
|
|
23
|
+
note?: string;
|
|
24
|
+
/** The handoff target, present ONLY on intent:"handoff": the agent the sender
|
|
25
|
+
* PROPOSES as the new assignee (named by join-key agentId). The receiver's own
|
|
26
|
+
* accept is what actually sets it — a handoff never forces an assignment. */
|
|
27
|
+
proposedAssignee?: AgentAttribution;
|
|
28
|
+
/** Opaque, verifier-specific proof slot. The TOFU verifier ignores it. */
|
|
29
|
+
proof?: string;
|
|
30
|
+
issuedAt: string;
|
|
31
|
+
}
|
|
32
|
+
export interface PrepareCoordinationInput {
|
|
33
|
+
/** The LOCAL mission to coordinate, by its local UUID id (resolved via the store). */
|
|
34
|
+
missionId: string;
|
|
35
|
+
/** The recipient agent's join-key agentId. */
|
|
36
|
+
toAgentId: string;
|
|
37
|
+
/** The coordination verb. */
|
|
38
|
+
intent: CoordinationIntent;
|
|
39
|
+
/** Optional free text carried on the envelope + logged. */
|
|
40
|
+
note?: string;
|
|
41
|
+
/** The proposed new assignee — meaningful ONLY on intent:"handoff". */
|
|
42
|
+
proposedAssignee?: AgentAttribution;
|
|
43
|
+
/** This agent's own join-key agentId — the asserter of the first-party log entry
|
|
44
|
+
* (and, on an `accept`, the assignee it claims for itself). */
|
|
45
|
+
selfAgentId: string;
|
|
46
|
+
/** Optional proof to stamp on the envelope (for a non-TOFU recipient verifier). */
|
|
47
|
+
proof?: string;
|
|
48
|
+
}
|
|
49
|
+
export type PrepareCoordinationStatus = "not_found" | "no_consent" | "not_assignee";
|
|
50
|
+
export type PrepareCoordinationResult = {
|
|
51
|
+
ok: true;
|
|
52
|
+
envelope: CoordinationEnvelope;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
status: PrepareCoordinationStatus;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Producer half of the coordination primitive. Consent-gated (subject = the
|
|
59
|
+
* mission's `missionKey`, scope = `"coordinate"`), names the mission by its join
|
|
60
|
+
* key (never the local UUID). The recipient's trust — read off this agent's own
|
|
61
|
+
* friend record for `toAgentId` — is the authorization input the policy uses. The
|
|
62
|
+
* ONLY precondition: `handoff` requires this agent to hold the assignment. Also
|
|
63
|
+
* records the outgoing intent on the local mission as a first-party log step.
|
|
64
|
+
*/
|
|
65
|
+
export declare function prepareCoordination(missions: MissionStore, store: FriendStore, grants: GrantStore, input: PrepareCoordinationInput, consent?: ConsentPolicy): Promise<PrepareCoordinationResult>;
|
|
66
|
+
export interface ImportCoordinationInput {
|
|
67
|
+
envelope: CoordinationEnvelope;
|
|
68
|
+
/** The agent the envelope arrived from (its join-key agentId). */
|
|
69
|
+
fromAgentId: string;
|
|
70
|
+
/** This agent's resolved trust in the source agent — the cap on acceptance. */
|
|
71
|
+
trustOfSource: TrustLevel;
|
|
72
|
+
}
|
|
73
|
+
export type ImportCoordinationStatus = "logged" | "assigned" | "seeded" | "no_mission" | "untrusted_source" | "untrusted_introduction";
|
|
74
|
+
export type ImportCoordinationResult = {
|
|
75
|
+
ok: true;
|
|
76
|
+
status: "logged" | "assigned" | "seeded";
|
|
77
|
+
record: MissionRecord;
|
|
78
|
+
} | {
|
|
79
|
+
ok: false;
|
|
80
|
+
status: "no_mission" | "untrusted_source" | "untrusted_introduction";
|
|
81
|
+
};
|
|
82
|
+
export interface ImportCoordinationOptions {
|
|
83
|
+
/** Authentication seam. Defaults to TOFU. Authorization (trust) is still applied
|
|
84
|
+
* regardless of what the verifier says. */
|
|
85
|
+
verifier?: AgentVerifier;
|
|
86
|
+
/** Minimum trust a source must hold for its messages to be accepted at all.
|
|
87
|
+
* Default `acquaintance`: a stranger source is refused. */
|
|
88
|
+
minTrustToAccept?: TrustLevel;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Consumer half of the coordination primitive — the non-clobbering merge. Resolves
|
|
92
|
+
* the mission by `findByMissionKey`; appends the incoming intent to
|
|
93
|
+
* `coordination.log` stamped `origin:imported` WITHOUT touching first-party
|
|
94
|
+
* `learnings`/`notes`/`status`; applies the bounded assignee effect (only `accept`
|
|
95
|
+
* sets it; a `handoff` never forces it; conflicts are last-writer-wins by
|
|
96
|
+
* `issuedAt`); NEVER recomputes status / participants / trust / standing; the
|
|
97
|
+
* source agent's trust caps acceptance; seeds an unknown mission only when a
|
|
98
|
+
* friend/family peer introduces it.
|
|
99
|
+
*/
|
|
100
|
+
export declare function importCoordination(missions: MissionStore, input: ImportCoordinationInput, options?: ImportCoordinationOptions): Promise<ImportCoordinationResult>;
|