@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
@@ -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[];
@@ -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;
@@ -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>;