@ouro.bot/friends 0.1.0-alpha.6 → 0.1.0-alpha.7
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/README.md +56 -18
- package/changelog.json +6 -0
- package/dist/audit.d.ts +8 -4
- package/dist/connect-authority.d.ts +43 -0
- package/dist/connect-authority.js +84 -0
- package/dist/connect.d.ts +55 -0
- package/dist/connect.js +160 -0
- package/dist/coordination.d.ts +17 -1
- package/dist/coordination.js +80 -6
- package/dist/index.d.ts +7 -0
- package/dist/index.js +22 -2
- package/dist/mailbox/index.d.ts +13 -10
- package/dist/mailbox/index.js +1 -1
- package/dist/mcp/dispatch.d.ts +15 -0
- package/dist/mcp/dispatch.js +65 -0
- package/dist/mcp/schemas.js +50 -3
- package/dist/mission-result.d.ts +82 -0
- package/dist/mission-result.js +200 -0
- package/dist/mission-store-file.js +8 -0
- package/dist/types.d.ts +53 -0
- package/package.json +2 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prepareMissionResult = prepareMissionResult;
|
|
4
|
+
exports.importMissionResult = importMissionResult;
|
|
5
|
+
// prepareMissionResult (producer) + importMissionResult (consumer) — the result-return
|
|
6
|
+
// envelope (gap-2, p11 inc2). The honest north-star DELIVERABLE channel. Structural twin
|
|
7
|
+
// of mission-share.ts, re-aimed from a SHARE (outcomes/learnings) at a RESULT (B's actual
|
|
8
|
+
// produced artifact, attributed to B, correlated to A's delegation via missionKey +
|
|
9
|
+
// requestId). Q1 resolved: a NEW result envelope, NOT a mission_share reuse.
|
|
10
|
+
//
|
|
11
|
+
// Store-only + transport-agnostic: prepareMissionResult returns an envelope,
|
|
12
|
+
// importMissionResult consumes one; the WIRE is the caller's job (the result rides the
|
|
13
|
+
// mailbox under kind:"mission_result"). Pure — the only node builtin is `node:crypto`,
|
|
14
|
+
// mirroring mission-share.ts. Core-clean (no a2a-client import).
|
|
15
|
+
//
|
|
16
|
+
// Through-line invariants (every one tested in mission-result.test.ts):
|
|
17
|
+
// - the mission is named by its JOIN KEY (missionKey), NEVER the local UUID;
|
|
18
|
+
// - fromAgentId === the producing self (attributed to B);
|
|
19
|
+
// - the result correlates to A's delegation via requestId;
|
|
20
|
+
// - consent rides the "coordinate" identity-tier scope — a result is B answering A's OWN
|
|
21
|
+
// delegation request (no third-party content), so trust ≥ friend suffices, exactly like
|
|
22
|
+
// coordinate; NO new ShareScope, NO new content grant;
|
|
23
|
+
// - on import the deliverable lands QUARANTINED under importedResults[agentId][requestId],
|
|
24
|
+
// attributed to B + stamped imported, WITHOUT touching first-party;
|
|
25
|
+
// - correlation honesty: a result whose requestId matches NO prior first-party delegation
|
|
26
|
+
// on A is REJECTED (no_delegation) — A only accepts results for work it delegated;
|
|
27
|
+
// - assignee honesty (security-review inc-2 finding 1): a result whose source is NOT the
|
|
28
|
+
// agent A delegated TO (delegation.assignee.agentId !== fromAgentId) is REJECTED
|
|
29
|
+
// (assignee_mismatch), even from a trusted peer with the right requestId; a legacy
|
|
30
|
+
// delegation with no recorded assignee FAILS CLOSED;
|
|
31
|
+
// - NO seeding: an unknown mission → no_mission (a result never creates a mission);
|
|
32
|
+
// - the source agent's trust CAPS acceptance (a stranger/over-trust source writes nothing),
|
|
33
|
+
// checked BEFORE correlation;
|
|
34
|
+
// - non-transitive: status/participants are NEVER recomputed; first-party byte-untouched;
|
|
35
|
+
// - idempotent on replay (same (agentId, requestId) → no double-land).
|
|
36
|
+
const observability_1 = require("./observability");
|
|
37
|
+
const consent_1 = require("./consent");
|
|
38
|
+
const verifier_1 = require("./verifier");
|
|
39
|
+
/**
|
|
40
|
+
* Producer half of the result-return. Resolves the local mission by `missionId`; names
|
|
41
|
+
* it by its `missionKey` (NEVER the local UUID); attributes the result to `selfAgentId`
|
|
42
|
+
* (B); correlates by `requestId`. Consent-gated via the `"coordinate"` identity-tier
|
|
43
|
+
* scope (a result is B answering A's own delegation — trust ≥ friend suffices under the
|
|
44
|
+
* tiered default, ZERO new scope). Records the result first-party on B's own record under
|
|
45
|
+
* `results[requestId]`.
|
|
46
|
+
*/
|
|
47
|
+
async function prepareMissionResult(missions, store, grants, input, consent = consent_1.DEFAULT_CONSENT_POLICY) {
|
|
48
|
+
const record = await missions.get(input.missionId);
|
|
49
|
+
if (!record) {
|
|
50
|
+
return { ok: false, status: "not_found" };
|
|
51
|
+
}
|
|
52
|
+
// The recipient's trust is read from this agent's own knowledge of it (the a2a-agent
|
|
53
|
+
// friend record for toAgentId). An unknown recipient defaults to stranger.
|
|
54
|
+
const recipientRecord = await store.findByExternalId("a2a-agent", input.toAgentId);
|
|
55
|
+
const recipientTrust = recipientRecord?.trustLevel ?? "stranger";
|
|
56
|
+
const recipient = { agentId: input.toAgentId, trustLevel: recipientTrust };
|
|
57
|
+
// Consent rides the EXISTING "coordinate" identity-tier scope — a result is B
|
|
58
|
+
// answering A's OWN delegation, so trust ≥ friend suffices under the tiered default,
|
|
59
|
+
// with ZERO change to consent logic and NO new scope.
|
|
60
|
+
const consented = await consent.consents({
|
|
61
|
+
subjectKey: record.missionKey,
|
|
62
|
+
recipient,
|
|
63
|
+
scope: "coordinate",
|
|
64
|
+
grants,
|
|
65
|
+
});
|
|
66
|
+
if (!consented) {
|
|
67
|
+
return { ok: false, status: "no_consent" };
|
|
68
|
+
}
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
const envelope = {
|
|
71
|
+
subject: { missionKey: record.missionKey, title: record.title },
|
|
72
|
+
fromAgentId: input.selfAgentId,
|
|
73
|
+
requestId: input.requestId,
|
|
74
|
+
result: {
|
|
75
|
+
requestId: input.requestId,
|
|
76
|
+
summary: input.result.summary,
|
|
77
|
+
...(input.result.artifact !== undefined ? { artifact: input.result.artifact } : {}),
|
|
78
|
+
...(input.result.outputs !== undefined ? { outputs: input.result.outputs } : {}),
|
|
79
|
+
},
|
|
80
|
+
issuedAt: now,
|
|
81
|
+
...(input.proof !== undefined ? { proof: input.proof } : {}),
|
|
82
|
+
};
|
|
83
|
+
// Record the result first-party on B's own mission under results[requestId].
|
|
84
|
+
const firstPartyResult = { ...envelope.result, provenance: { origin: "first_party" } };
|
|
85
|
+
const updated = {
|
|
86
|
+
...record,
|
|
87
|
+
results: { ...(record.results ?? {}), [input.requestId]: firstPartyResult },
|
|
88
|
+
updatedAt: now,
|
|
89
|
+
};
|
|
90
|
+
await missions.put(updated.id, updated);
|
|
91
|
+
(0, observability_1.emitNervesEvent)({
|
|
92
|
+
component: "friends",
|
|
93
|
+
event: "friends.mission_result_prepared",
|
|
94
|
+
message: "prepared mission result envelope",
|
|
95
|
+
meta: { toAgentId: input.toAgentId, requestId: input.requestId, consentPolicy: consent.name },
|
|
96
|
+
});
|
|
97
|
+
return { ok: true, envelope };
|
|
98
|
+
}
|
|
99
|
+
// ── Consumer ──
|
|
100
|
+
const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
|
|
101
|
+
/** Land B's deliverable under `importedResults[fromAgentId][requestId]`, returning a NEW
|
|
102
|
+
* namespace (never mutates the input). First-party `results` are NOT passed in and stay
|
|
103
|
+
* physically untouched. Idempotent per (agentId, requestId): an entry that already exists
|
|
104
|
+
* is preserved unchanged (never re-stamped). */
|
|
105
|
+
function mergeImportedResult(record, result, fromAgentId, now) {
|
|
106
|
+
const existing = record.importedResults ?? {};
|
|
107
|
+
const forAgent = existing[fromAgentId] ?? {};
|
|
108
|
+
if (forAgent[result.requestId])
|
|
109
|
+
return existing;
|
|
110
|
+
return {
|
|
111
|
+
...existing,
|
|
112
|
+
[fromAgentId]: {
|
|
113
|
+
...forAgent,
|
|
114
|
+
[result.requestId]: {
|
|
115
|
+
requestId: result.requestId,
|
|
116
|
+
summary: result.summary,
|
|
117
|
+
...(result.artifact !== undefined ? { artifact: result.artifact } : {}),
|
|
118
|
+
...(result.outputs !== undefined ? { outputs: result.outputs } : {}),
|
|
119
|
+
provenance: { origin: "imported", assertedBy: { agentId: fromAgentId }, importedAt: now },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Consumer half of the result-return — the non-clobbering merge. Order (PINNED):
|
|
126
|
+
* (1) TOFU verifier + trust cap (both must pass, else `untrusted_source`, write nothing);
|
|
127
|
+
* (2) unknown mission (no findByMissionKey hit) → `no_mission` (NO seeding — a result
|
|
128
|
+
* never creates a mission);
|
|
129
|
+
* (3) the result's `requestId` not present in the record's FIRST-PARTY `delegations`
|
|
130
|
+
* (A never delegated this) → `no_delegation` — correlation honesty;
|
|
131
|
+
* (3b) the matched delegation's recorded `assignee` is not the result's source
|
|
132
|
+
* (`delegation.assignee.agentId !== fromAgentId`) → `assignee_mismatch` — assignee
|
|
133
|
+
* honesty (security-review inc-2 finding 1). FAILS CLOSED on a legacy delegation with
|
|
134
|
+
* no recorded assignee. A mismatch writes NOTHING (not even quarantined);
|
|
135
|
+
* (4) otherwise land under `importedResults[agentId][requestId]` (dedupe on replay),
|
|
136
|
+
* stamped imported + attributed + importedAt, NEVER touching first-party
|
|
137
|
+
* `learnings`/`notes`/`status`/`delegations`/`results`, NEVER recomputing
|
|
138
|
+
* status/participants (non-transitive).
|
|
139
|
+
*/
|
|
140
|
+
async function importMissionResult(missions, input, options = {}) {
|
|
141
|
+
const verifier = options.verifier ?? verifier_1.DEFAULT_AGENT_VERIFIER;
|
|
142
|
+
const minTrust = options.minTrustToAccept ?? "acquaintance";
|
|
143
|
+
// (1) Authentication (caller's seam) AND authorization (trust ladder) must BOTH pass —
|
|
144
|
+
// checked BEFORE correlation, so a stranger never even learns whether the mission exists.
|
|
145
|
+
const authenticated = verifier.verify(input.fromAgentId, input.envelope.proof);
|
|
146
|
+
const trustedEnough = TRUST_RANK[input.trustOfSource] >= TRUST_RANK[minTrust];
|
|
147
|
+
if (!authenticated || !trustedEnough) {
|
|
148
|
+
(0, observability_1.emitNervesEvent)({
|
|
149
|
+
component: "friends",
|
|
150
|
+
event: "friends.mission_result_refused",
|
|
151
|
+
message: "refused mission result from untrusted source",
|
|
152
|
+
meta: { fromAgentId: input.fromAgentId, trustOfSource: input.trustOfSource, authenticated },
|
|
153
|
+
});
|
|
154
|
+
return { ok: false, status: "untrusted_source" };
|
|
155
|
+
}
|
|
156
|
+
// (2) Unknown mission → no_mission. A result NEVER seeds a mission (distinct from
|
|
157
|
+
// mission-share's friend-seed).
|
|
158
|
+
const existing = await missions.findByMissionKey(input.envelope.subject.missionKey);
|
|
159
|
+
if (!existing) {
|
|
160
|
+
return { ok: false, status: "no_mission" };
|
|
161
|
+
}
|
|
162
|
+
// (3) Correlation honesty — the requestId must name a delegation THIS agent issued
|
|
163
|
+
// first-party (A only accepts results for work it actually delegated).
|
|
164
|
+
const delegation = existing.delegations?.[input.envelope.requestId];
|
|
165
|
+
if (!delegation) {
|
|
166
|
+
return { ok: false, status: "no_delegation" };
|
|
167
|
+
}
|
|
168
|
+
// (3b) Assignee honesty (security-review inc-2 finding 1) — the result's SOURCE must be
|
|
169
|
+
// the very agent A delegated TO. The requestId being delegated is NOT enough: a peer C
|
|
170
|
+
// that A trusts (≥ the cap) who learned a requestId A delegated to a DIFFERENT agent B
|
|
171
|
+
// could otherwise inject a forged result. FAILS CLOSED on a legacy/orphan delegation with
|
|
172
|
+
// no recorded assignee (reject, never land). A mismatch writes NOTHING — not even
|
|
173
|
+
// quarantined.
|
|
174
|
+
if (delegation.assignee?.agentId !== input.fromAgentId) {
|
|
175
|
+
(0, observability_1.emitNervesEvent)({
|
|
176
|
+
component: "friends",
|
|
177
|
+
event: "friends.mission_result_refused",
|
|
178
|
+
message: "refused mission result — source is not the delegation's assignee",
|
|
179
|
+
meta: {
|
|
180
|
+
fromAgentId: input.fromAgentId,
|
|
181
|
+
requestId: input.envelope.requestId,
|
|
182
|
+
expectedAssignee: delegation.assignee?.agentId ?? null,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return { ok: false, status: "assignee_mismatch" };
|
|
186
|
+
}
|
|
187
|
+
// (4) Land the deliverable quarantined + attributed, never touching first-party, never
|
|
188
|
+
// recomputing status/participants (non-transitive).
|
|
189
|
+
const now = new Date().toISOString();
|
|
190
|
+
const importedResults = mergeImportedResult(existing, input.envelope.result, input.fromAgentId, now);
|
|
191
|
+
const updated = { ...existing, importedResults, updatedAt: now };
|
|
192
|
+
await missions.put(updated.id, updated);
|
|
193
|
+
(0, observability_1.emitNervesEvent)({
|
|
194
|
+
component: "friends",
|
|
195
|
+
event: "friends.mission_result_imported",
|
|
196
|
+
message: "imported mission result into existing mission",
|
|
197
|
+
meta: { missionId: updated.id, fromAgentId: input.fromAgentId, requestId: input.envelope.requestId },
|
|
198
|
+
});
|
|
199
|
+
return { ok: true, status: "imported", record: updated };
|
|
200
|
+
}
|
|
@@ -126,6 +126,14 @@ class FileMissionStore {
|
|
|
126
126
|
// present iff the record carries one, so a legacy mission with no coordination
|
|
127
127
|
// round-trips unchanged (absent ⇒ unclaimed).
|
|
128
128
|
...(raw.coordination && typeof raw.coordination === "object" ? { coordination: raw.coordination } : {}),
|
|
129
|
+
// The delegation/result namespaces (gap-1 + gap-2, p11 inc2) pass through the same
|
|
130
|
+
// way: present iff the record carries one, so a legacy mission round-trips unchanged
|
|
131
|
+
// (absent ⇒ none). Without these, a FILE-backed store would silently drop them,
|
|
132
|
+
// breaking the result-return correlation (which reads first-party `delegations`).
|
|
133
|
+
...(raw.delegations && typeof raw.delegations === "object" ? { delegations: raw.delegations } : {}),
|
|
134
|
+
...(raw.importedDelegations && typeof raw.importedDelegations === "object" ? { importedDelegations: raw.importedDelegations } : {}),
|
|
135
|
+
...(raw.results && typeof raw.results === "object" ? { results: raw.results } : {}),
|
|
136
|
+
...(raw.importedResults && typeof raw.importedResults === "object" ? { importedResults: raw.importedResults } : {}),
|
|
129
137
|
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : new Date().toISOString(),
|
|
130
138
|
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : new Date().toISOString(),
|
|
131
139
|
schemaVersion: typeof raw.schemaVersion === "number" ? raw.schemaVersion : 1,
|
package/dist/types.d.ts
CHANGED
|
@@ -75,6 +75,48 @@ export interface ImportedLearning {
|
|
|
75
75
|
assertedBy?: AgentAttribution;
|
|
76
76
|
originallyAssertedBy?: AgentAttribution;
|
|
77
77
|
}
|
|
78
|
+
export interface MissionTaskSpec {
|
|
79
|
+
/** Correlation key the result-return matches (PINNED). Minted by the producer. */
|
|
80
|
+
requestId: string;
|
|
81
|
+
/** What B is being asked to do (the headline ask). */
|
|
82
|
+
summary: string;
|
|
83
|
+
/** Optional longer brief. */
|
|
84
|
+
details?: string;
|
|
85
|
+
/** Optional structured inputs. */
|
|
86
|
+
inputs?: Record<string, string>;
|
|
87
|
+
}
|
|
88
|
+
export interface MissionResult {
|
|
89
|
+
/** Correlates to the gap-1 task-spec's requestId (PINNED) — A only accepts a result
|
|
90
|
+
* for a requestId it actually delegated. */
|
|
91
|
+
requestId: string;
|
|
92
|
+
/** The headline deliverable — what B produced. */
|
|
93
|
+
summary: string;
|
|
94
|
+
/** Optional larger produced artifact body. */
|
|
95
|
+
artifact?: string;
|
|
96
|
+
/** Optional structured outputs. */
|
|
97
|
+
outputs?: Record<string, string>;
|
|
98
|
+
/** first_party on B's side; imported (attributed to B) on A's. */
|
|
99
|
+
provenance?: NoteProvenance;
|
|
100
|
+
}
|
|
101
|
+
export interface MissionResultEnvelope {
|
|
102
|
+
/** The mission, named by its join key — `missionKey` + a human title. */
|
|
103
|
+
subject: {
|
|
104
|
+
missionKey: string;
|
|
105
|
+
title: string;
|
|
106
|
+
};
|
|
107
|
+
/** The agent that produced this result (B's join-key agentId) — the attribution.
|
|
108
|
+
* NOTE (security review inc-2 finding 5, by-design): this is SELF-ASSERTED and is
|
|
109
|
+
* vestigial on import — importMissionResult attributes + enforces against the
|
|
110
|
+
* TRANSPORT-supplied `ImportMissionResultInput.fromAgentId` (the authenticated channel
|
|
111
|
+
* identity), never this envelope field, so a forged value here changes nothing. */
|
|
112
|
+
fromAgentId: string;
|
|
113
|
+
/** The delegation correlation key (matches the gap-1 task-spec's requestId). */
|
|
114
|
+
requestId: string;
|
|
115
|
+
result: MissionResult;
|
|
116
|
+
/** Opaque, verifier-specific proof slot. The TOFU verifier ignores it. */
|
|
117
|
+
proof?: string;
|
|
118
|
+
issuedAt: string;
|
|
119
|
+
}
|
|
78
120
|
export type CoordinationIntent = "request" | "offer" | "accept" | "decline" | "handoff";
|
|
79
121
|
export declare function isCoordinationIntent(value: unknown): value is CoordinationIntent;
|
|
80
122
|
export interface CoordinationLogEntry {
|
|
@@ -99,6 +141,17 @@ export interface MissionRecord {
|
|
|
99
141
|
learnings: Record<string, MissionLearning>;
|
|
100
142
|
importedLearnings?: Record<string, Record<string, ImportedLearning>>;
|
|
101
143
|
coordination?: MissionCoordination;
|
|
144
|
+
delegations?: Record<string, {
|
|
145
|
+
task: MissionTaskSpec;
|
|
146
|
+
assignee?: AgentAttribution;
|
|
147
|
+
provenance: NoteProvenance;
|
|
148
|
+
}>;
|
|
149
|
+
importedDelegations?: Record<string, Record<string, {
|
|
150
|
+
task: MissionTaskSpec;
|
|
151
|
+
provenance: NoteProvenance;
|
|
152
|
+
}>>;
|
|
153
|
+
results?: Record<string, MissionResult>;
|
|
154
|
+
importedResults?: Record<string, Record<string, MissionResult>>;
|
|
102
155
|
createdAt: string;
|
|
103
156
|
updatedAt: string;
|
|
104
157
|
schemaVersion: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/friends",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.7",
|
|
4
4
|
"description": "The who's-who / identity / relationship substrate for agents — trust ladder (family/friend/acquaintance/stranger), multi-party and multi-agent, consumed via the FriendStore interface + FriendResolver.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"example:cross-agent-mission-memory": "npm run build && tsx examples/cross-agent-mission-memory.ts",
|
|
47
47
|
"example:cross-agent-standing": "npm run build && tsx examples/cross-agent-standing.ts",
|
|
48
48
|
"example:cross-agent-coordination": "npm run build && tsx examples/cross-agent-coordination.ts",
|
|
49
|
+
"example:cross-agent-delegation": "npm run build && tsx examples/cross-agent-delegation.ts",
|
|
49
50
|
"example:cross-agent-a2a-relay": "npm run build && tsx examples/cross-agent-a2a-relay.ts",
|
|
50
51
|
"release:bump": "node scripts/release-bump.cjs",
|
|
51
52
|
"release:preflight": "node scripts/release-preflight.cjs",
|