@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.
@@ -49,8 +49,12 @@ function holdsAssignment(record, selfAgentId) {
49
49
  /** Apply the OUTGOING intent to the producer's own mission record as a first-party
50
50
  * step: always append the intent to `coordination.log`; on an `accept`, also claim
51
51
  * the assignment for self (the accepter is taking it). No other intent moves the
52
- * producer's `assignee`. Mirrors how recordMission stamps first-party provenance. */
53
- function applyOutgoingIntent(record, input, now) {
52
+ * producer's `assignee`. When a `taskSpec` is supplied (a request carrying a task,
53
+ * gap-1), ALSO record it first-party under `delegations[requestId]` — the task-spec, the
54
+ * delegated-TO `assignee` (`input.toAgentId`), and first-party provenance. The assignee is
55
+ * the anchor importMissionResult checks the result's source against (security-review inc-2
56
+ * finding 1). Mirrors how recordMission stamps first-party provenance. */
57
+ function applyOutgoingIntent(record, input, now, taskSpec) {
54
58
  const entry = {
55
59
  intent: input.intent,
56
60
  fromAgentId: input.selfAgentId,
@@ -62,7 +66,38 @@ function applyOutgoingIntent(record, input, now) {
62
66
  const coordination = input.intent === "accept"
63
67
  ? { ...withLog, assignee: { agentId: input.selfAgentId }, assignedAt: now }
64
68
  : withLog;
65
- return { ...record, coordination, updatedAt: now };
69
+ // gap-1: record the issued delegation first-party under delegations[requestId]. PERSIST
70
+ // the ASSIGNEE — the agent this task is delegated TO (input.toAgentId) — alongside the
71
+ // task-spec (security-review inc-2 finding 1). This is the anchor importMissionResult
72
+ // checks the result's SOURCE against: A only accepts a result for this requestId from the
73
+ // very agent it delegated TO. Without it, a trusted non-assignee who learned the
74
+ // requestId could inject a forged result.
75
+ const delegations = taskSpec
76
+ ? {
77
+ ...(record.delegations ?? {}),
78
+ [taskSpec.requestId]: { task: taskSpec, assignee: { agentId: input.toAgentId }, provenance: { origin: "first_party" } },
79
+ }
80
+ : record.delegations;
81
+ return {
82
+ ...record,
83
+ coordination,
84
+ ...(delegations ? { delegations } : {}),
85
+ updatedAt: now,
86
+ };
87
+ }
88
+ /** Build the MissionTaskSpec for a request carrying a task (gap-1): mint the
89
+ * `requestId` and carry the optional details/inputs only when present. Returns undefined
90
+ * when there is no task to attach, or the intent is not a request (a task on any other
91
+ * intent is ignored). */
92
+ function buildTaskSpec(input) {
93
+ if (input.intent !== "request" || input.task === undefined)
94
+ return undefined;
95
+ return {
96
+ requestId: (0, node_crypto_1.randomUUID)(),
97
+ summary: input.task.summary,
98
+ ...(input.task.details !== undefined ? { details: input.task.details } : {}),
99
+ ...(input.task.inputs !== undefined ? { inputs: input.task.inputs } : {}),
100
+ };
66
101
  }
67
102
  /**
68
103
  * Producer half of the coordination primitive. Consent-gated (subject = the
@@ -101,6 +136,10 @@ async function prepareCoordination(missions, store, grants, input, consent = con
101
136
  return { ok: false, status: "not_assignee" };
102
137
  }
103
138
  const now = new Date().toISOString();
139
+ // gap-1: a request carrying a task mints a requestId + a MissionTaskSpec (undefined on
140
+ // any non-request intent, or when no task was given). Minted ONCE so the envelope's
141
+ // task and the first-party delegations[requestId] share the same correlation key.
142
+ const taskSpec = buildTaskSpec(input);
104
143
  const envelope = {
105
144
  subject: { missionKey: record.missionKey, title: record.title },
106
145
  fromAgentId: input.selfAgentId,
@@ -110,11 +149,13 @@ async function prepareCoordination(missions, store, grants, input, consent = con
110
149
  ...(input.intent === "handoff" && input.proposedAssignee !== undefined
111
150
  ? { proposedAssignee: input.proposedAssignee }
112
151
  : {}),
152
+ ...(taskSpec ? { task: taskSpec } : {}),
113
153
  ...(input.proof !== undefined ? { proof: input.proof } : {}),
114
154
  };
115
155
  // Record the outgoing intent on the producer's own mission (first-party), so the
116
- // sender's record reflects "I asked / I offered / I accepted".
117
- const updated = applyOutgoingIntent(record, input, now);
156
+ // sender's record reflects "I asked / I offered / I accepted" — and, for a request
157
+ // with a task, the issued delegation under delegations[requestId].
158
+ const updated = applyOutgoingIntent(record, input, now, taskSpec);
118
159
  await missions.put(updated.id, updated);
119
160
  (0, observability_1.emitNervesEvent)({
120
161
  component: "friends",
@@ -174,8 +215,41 @@ function applyIncomingIntent(record, envelope, fromAgentId, now) {
174
215
  : withLog;
175
216
  return { record: { ...record, coordination, updatedAt: now }, assigned: isLater };
176
217
  }
218
+ // gap-1: a request carrying a task lands the task-spec QUARANTINED under
219
+ // importedDelegations[fromAgentId][requestId] (attributed, imported), NEVER touching
220
+ // first-party `delegations`/`learnings`/`notes`/`status`. Idempotent per (agentId,
221
+ // requestId): an existing entry is preserved (never re-stamped).
222
+ const importedDelegations = envelope.intent === "request" && envelope.task !== undefined
223
+ ? mergeImportedDelegation(record, envelope.task, fromAgentId, now)
224
+ : record.importedDelegations;
177
225
  // request / offer / decline / handoff → log only; assignee untouched.
178
- return { record: { ...record, coordination: withLog, updatedAt: now }, assigned: false };
226
+ return {
227
+ record: {
228
+ ...record,
229
+ coordination: withLog,
230
+ ...(importedDelegations ? { importedDelegations } : {}),
231
+ updatedAt: now,
232
+ },
233
+ assigned: false,
234
+ };
235
+ }
236
+ /** Land one imported task-spec under `importedDelegations[fromAgentId][requestId]`,
237
+ * returning a NEW namespace (never mutates the input). First-party `delegations` are NOT
238
+ * passed in and stay physically untouched. Idempotent per (agentId, requestId): an entry
239
+ * that already exists is preserved unchanged (never re-stamped with a new importedAt). */
240
+ function mergeImportedDelegation(record, task, fromAgentId, now) {
241
+ const existing = record.importedDelegations ?? {};
242
+ const forAgent = existing[fromAgentId] ?? {};
243
+ // Idempotent: keep the existing entry for this requestId (do not re-stamp on replay).
244
+ if (forAgent[task.requestId])
245
+ return existing;
246
+ return {
247
+ ...existing,
248
+ [fromAgentId]: {
249
+ ...forAgent,
250
+ [task.requestId]: { task, provenance: { origin: "imported", assertedBy: { agentId: fromAgentId }, importedAt: now } },
251
+ },
252
+ };
179
253
  }
180
254
  /** Create a freshly-seeded mission for a previously-unknown key, carrying the
181
255
  * subject's join key + title, `status:"active"`, empty first-party `learnings`. The
package/dist/index.d.ts CHANGED
@@ -34,6 +34,10 @@ export { MemoryAuditSink, FileAuditSink, auditPathFor } from "./audit";
34
34
  export type { AuditSink, ControlPlaneAuditRecord } from "./audit";
35
35
  export { linkExternalId, unlinkExternalId } from "./link-identity";
36
36
  export { upsertAgentPeer } from "./agent-peer";
37
+ export { connectAgents } from "./connect";
38
+ export type { ConnectPeer, ConnectAgentsInput, ConnectAgentsDeps, ConnectResult, ConnectStatus } from "./connect";
39
+ export { authorizeConnect } from "./connect-authority";
40
+ export type { AuthorizeConnectInput, ConnectAuthorization } from "./connect-authority";
37
41
  export { resolveAgentIdentity, withMigratedIdentity } from "./identity";
38
42
  export type { ResolvedAgentIdentity } from "./identity";
39
43
  export { findFriendByDid } from "./friend-lookup";
@@ -57,6 +61,9 @@ export { prepareMissionShare, importMissionShare } from "./mission-share";
57
61
  export type { MissionShareEnvelope, SharedLearning, PrepareMissionShareInput, PrepareMissionShareResult, PrepareMissionShareStatus, ImportMissionShareInput, ImportMissionShareOptions, ImportMissionShareResult, ImportMissionShareStatus, } from "./mission-share";
58
62
  export { prepareCoordination, importCoordination } from "./coordination";
59
63
  export type { CoordinationEnvelope, PrepareCoordinationInput, PrepareCoordinationResult, PrepareCoordinationStatus, ImportCoordinationInput, ImportCoordinationOptions, ImportCoordinationResult, ImportCoordinationStatus, } from "./coordination";
64
+ export { prepareMissionResult, importMissionResult } from "./mission-result";
65
+ export type { PrepareMissionResultInput, PrepareMissionResultResult, PrepareMissionResultStatus, ImportMissionResultInput, ImportMissionResultOptions, ImportMissionResultResult, ImportMissionResultStatus, } from "./mission-result";
66
+ export type { MissionTaskSpec, MissionResult, MissionResultEnvelope } from "./types";
60
67
  export { grantShare, revokeShare, listShares, isGrantEffective } from "./grants";
61
68
  export { emitNervesEvent, setNervesEmitter, } from "./observability";
62
69
  export type { NervesEvent, NervesEmitter, LogLevel } from "./observability";
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@
6
6
  // multi-agent (a2a peer) aware, consumed through the FriendStore interface +
7
7
  // FriendResolver.
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.resolveRoom = exports.whoami = exports.recordMission = exports.recordRelationshipOutcome = exports._resetRosterVerifierWarningForTest = exports.verifiedCandidate = exports.evaluateAccountMembership = exports.MemoryRosterStore = exports.DEFAULT_ROSTER_VERIFIER = exports.identityRosterVerifier = exports.rostersDirFor = exports.FileRosterStore = exports.findFriendByDid = exports.withMigratedIdentity = exports.resolveAgentIdentity = exports.upsertAgentPeer = exports.unlinkExternalId = exports.linkExternalId = exports.auditPathFor = exports.FileAuditSink = exports.MemoryAuditSink = exports.setFriendTrust = exports.applyFriendNote = exports.accumulateFriendTokens = exports.upsertGroupContextParticipants = exports.DEFAULT_STANDING_RULE = exports.explainStanding = exports.assessStanding = exports.describeTrustContext = exports.getAlwaysOnSenseNames = exports.isRemoteChannel = exports.channelToFacing = exports.getChannelCapabilities = exports._setMachineOwnerUsernameForTest = exports.isLocalMachineOwnerIdentity = exports.machineOwnerUsername = exports.FriendResolver = exports.openFileBundle = exports.missionsDirFor = exports.FileMissionStore = exports.grantsDirFor = exports.FileGrantStore = exports.FileFriendStore = exports.isCoordinationIntent = exports.isShareScope = exports.isIntegration = exports.isIdentityProvider = exports.isTrustedLevel = exports.IDENTITY_SCOPES = exports.TRUSTED_LEVELS = void 0;
10
- exports.setNervesEmitter = exports.emitNervesEvent = exports.isGrantEffective = exports.listShares = exports.revokeShare = exports.grantShare = exports.importCoordination = exports.prepareCoordination = exports.importMissionShare = exports.prepareMissionShare = exports.importProfileShare = exports.prepareProfileShare = exports.DEFAULT_AGENT_VERIFIER = exports.tofuVerifier = exports.DEFAULT_CONSENT_POLICY = exports.tieredPolicy = exports.trustImpliedPolicy = exports.strictPolicy = void 0;
9
+ exports.recordMission = exports.recordRelationshipOutcome = exports._resetRosterVerifierWarningForTest = exports.verifiedCandidate = exports.evaluateAccountMembership = exports.MemoryRosterStore = exports.DEFAULT_ROSTER_VERIFIER = exports.identityRosterVerifier = exports.rostersDirFor = exports.FileRosterStore = exports.findFriendByDid = exports.withMigratedIdentity = exports.resolveAgentIdentity = exports.authorizeConnect = exports.connectAgents = exports.upsertAgentPeer = exports.unlinkExternalId = exports.linkExternalId = exports.auditPathFor = exports.FileAuditSink = exports.MemoryAuditSink = exports.setFriendTrust = exports.applyFriendNote = exports.accumulateFriendTokens = exports.upsertGroupContextParticipants = exports.DEFAULT_STANDING_RULE = exports.explainStanding = exports.assessStanding = exports.describeTrustContext = exports.getAlwaysOnSenseNames = exports.isRemoteChannel = exports.channelToFacing = exports.getChannelCapabilities = exports._setMachineOwnerUsernameForTest = exports.isLocalMachineOwnerIdentity = exports.machineOwnerUsername = exports.FriendResolver = exports.openFileBundle = exports.missionsDirFor = exports.FileMissionStore = exports.grantsDirFor = exports.FileGrantStore = exports.FileFriendStore = exports.isCoordinationIntent = exports.isShareScope = exports.isIntegration = exports.isIdentityProvider = exports.isTrustedLevel = exports.IDENTITY_SCOPES = exports.TRUSTED_LEVELS = void 0;
10
+ exports.setNervesEmitter = exports.emitNervesEvent = exports.isGrantEffective = exports.listShares = exports.revokeShare = exports.grantShare = exports.importMissionResult = exports.prepareMissionResult = exports.importCoordination = exports.prepareCoordination = exports.importMissionShare = exports.prepareMissionShare = exports.importProfileShare = exports.prepareProfileShare = exports.DEFAULT_AGENT_VERIFIER = exports.tofuVerifier = exports.DEFAULT_CONSENT_POLICY = exports.tieredPolicy = exports.trustImpliedPolicy = exports.strictPolicy = exports.resolveRoom = exports.whoami = void 0;
11
11
  // -- Values --
12
12
  var types_1 = require("./types");
13
13
  Object.defineProperty(exports, "TRUSTED_LEVELS", { enumerable: true, get: function () { return types_1.TRUSTED_LEVELS; } });
@@ -62,6 +62,16 @@ Object.defineProperty(exports, "linkExternalId", { enumerable: true, get: functi
62
62
  Object.defineProperty(exports, "unlinkExternalId", { enumerable: true, get: function () { return link_identity_1.unlinkExternalId; } });
63
63
  var agent_peer_1 = require("./agent-peer");
64
64
  Object.defineProperty(exports, "upsertAgentPeer", { enumerable: true, get: function () { return agent_peer_1.upsertAgentPeer; } });
65
+ // -- connect_to (brick 8, p11 inc2): the owner links one of their OWN agents into the
66
+ // fleet, gated to a management sense (local/closed) — `local` commits; an `open` sense
67
+ // downgrades to a confirm-prompt; `closed` is gated by a roster/membership check (never a
68
+ // blanket allow); a bare name with no resolvable handle/DID returns needs_handle (never
69
+ // fabricated). Writes one `action:"connect"` control-plane audit. `authorizeConnect` is the
70
+ // pure authority predicate (consumes a pre-computed AccountMembershipResult — core-clean).
71
+ var connect_1 = require("./connect");
72
+ Object.defineProperty(exports, "connectAgents", { enumerable: true, get: function () { return connect_1.connectAgents; } });
73
+ var connect_authority_1 = require("./connect-authority");
74
+ Object.defineProperty(exports, "authorizeConnect", { enumerable: true, get: function () { return connect_authority_1.authorizeConnect; } });
65
75
  // -- Agent identity (p11 Item 2 — DID re-key): durable home + migrate-on-read --
66
76
  var identity_1 = require("./identity");
67
77
  Object.defineProperty(exports, "resolveAgentIdentity", { enumerable: true, get: function () { return identity_1.resolveAgentIdentity; } });
@@ -120,6 +130,16 @@ Object.defineProperty(exports, "importMissionShare", { enumerable: true, get: fu
120
130
  var coordination_1 = require("./coordination");
121
131
  Object.defineProperty(exports, "prepareCoordination", { enumerable: true, get: function () { return coordination_1.prepareCoordination; } });
122
132
  Object.defineProperty(exports, "importCoordination", { enumerable: true, get: function () { return coordination_1.importCoordination; } });
133
+ // -- Result-return / delegation deliverable (gap-2, p11 inc2): B returns its result --
134
+ // prepareMissionResult (producer) / importMissionResult (consumer) carry B's actual
135
+ // produced deliverable back to A — attributed to B, correlated to A's delegation via
136
+ // missionKey + requestId, consent-gated via the existing "coordinate" scope, lands
137
+ // quarantined + attributed on import, trust-capped, non-transitive, first-party-inviolable.
138
+ // `MissionTaskSpec` (gap-1) rides the CoordinationEnvelope; the new MissionRecord fields
139
+ // (delegations / importedDelegations / results / importedResults) are additive.
140
+ var mission_result_1 = require("./mission-result");
141
+ Object.defineProperty(exports, "prepareMissionResult", { enumerable: true, get: function () { return mission_result_1.prepareMissionResult; } });
142
+ Object.defineProperty(exports, "importMissionResult", { enumerable: true, get: function () { return mission_result_1.importMissionResult; } });
123
143
  var grants_1 = require("./grants");
124
144
  Object.defineProperty(exports, "grantShare", { enumerable: true, get: function () { return grants_1.grantShare; } });
125
145
  Object.defineProperty(exports, "revokeShare", { enumerable: true, get: function () { return grants_1.revokeShare; } });
@@ -1,6 +1,7 @@
1
1
  import type { ProfileShareEnvelope } from "../share";
2
2
  import type { MissionShareEnvelope } from "../mission-share";
3
3
  import type { CoordinationEnvelope } from "../coordination";
4
+ import type { MissionResultEnvelope } from "../types";
4
5
  /** The mailbox wire-format version. Bumped only on a breaking message change. */
5
6
  export declare const MAILBOX_VERSION = 1;
6
7
  /** A mailbox message: the TRANSPORT wrapper around a verbatim share envelope. The
@@ -14,19 +15,21 @@ export interface MailboxMessage {
14
15
  toAgentId: string;
15
16
  issuedAt: string;
16
17
  /** The payload discriminant. The host branches on it to call importProfileShare
17
- * vs importMissionShare vs importCoordination. The mailbox itself is
18
- * payload-agnostic — this union grows by one leaf per brick (additive, backward-
19
- * compatible); buildOutgoing/readIncoming carry any of them unchanged. */
20
- kind: "profile_share" | "mission_share" | "coordination";
21
- envelope: ProfileShareEnvelope | MissionShareEnvelope | CoordinationEnvelope;
18
+ * vs importMissionShare vs importCoordination vs importMissionResult. The mailbox
19
+ * itself is payload-agnostic — this union grows by one leaf per brick (additive,
20
+ * backward-compatible); buildOutgoing/readIncoming carry any of them unchanged.
21
+ * `mission_result` (gap-2) carries B's delegation deliverable back to A. */
22
+ kind: "profile_share" | "mission_share" | "coordination" | "mission_result";
23
+ envelope: ProfileShareEnvelope | MissionShareEnvelope | CoordinationEnvelope | MissionResultEnvelope;
22
24
  }
23
25
  export interface BuildOutgoingInput {
24
- envelope: ProfileShareEnvelope | MissionShareEnvelope | CoordinationEnvelope;
26
+ envelope: ProfileShareEnvelope | MissionShareEnvelope | CoordinationEnvelope | MissionResultEnvelope;
25
27
  fromAgentId: string;
26
28
  toAgentId: string;
27
29
  /** The payload discriminant. Defaults to "profile_share" for backward-compat;
28
- * a mission share passes "mission_share", a coordination message "coordination". */
29
- kind?: "profile_share" | "mission_share" | "coordination";
30
+ * a mission share passes "mission_share", a coordination message "coordination",
31
+ * a result-return "mission_result" (gap-2). */
32
+ kind?: "profile_share" | "mission_share" | "coordination" | "mission_result";
30
33
  /** Injectable ISO clock for deterministic tests; defaults to now. */
31
34
  now?: string;
32
35
  }
@@ -54,8 +57,8 @@ export interface IncomingMessage {
54
57
  fromAgentId: string;
55
58
  toAgentId: string;
56
59
  issuedAt: string;
57
- kind: "profile_share" | "mission_share" | "coordination";
58
- envelope: ProfileShareEnvelope | MissionShareEnvelope | CoordinationEnvelope;
60
+ kind: "profile_share" | "mission_share" | "coordination" | "mission_result";
61
+ envelope: ProfileShareEnvelope | MissionShareEnvelope | CoordinationEnvelope | MissionResultEnvelope;
59
62
  relativePath: string;
60
63
  }
61
64
  export interface ReadIncomingInput {
@@ -86,7 +86,7 @@ function isWellFormedWrapper(value) {
86
86
  typeof value.fromAgentId === "string" &&
87
87
  typeof value.toAgentId === "string" &&
88
88
  typeof value.issuedAt === "string" &&
89
- (value.kind === "profile_share" || value.kind === "mission_share" || value.kind === "coordination") &&
89
+ (value.kind === "profile_share" || value.kind === "mission_share" || value.kind === "coordination" || value.kind === "mission_result") &&
90
90
  typeof value.envelope === "object" &&
91
91
  value.envelope !== null &&
92
92
  !Array.isArray(value.envelope));
@@ -2,6 +2,8 @@ import type { FriendStore } from "../store";
2
2
  import type { GrantStore } from "../grant-store";
3
3
  import type { MissionStore } from "../mission-store";
4
4
  import type { AuditSink } from "../audit";
5
+ import type { SenseType } from "../types";
6
+ import type { AccountMembershipResult } from "../account-roster";
5
7
  type Args = Record<string, unknown>;
6
8
  export interface DispatchResult {
7
9
  result: unknown;
@@ -12,6 +14,19 @@ export interface DispatchResult {
12
14
  export interface ControlPlaneContext {
13
15
  actor?: string;
14
16
  originSense?: string;
17
+ /** The management SENSE the gate evaluates for `connect_to` (p11 inc2, brick 8).
18
+ * The stdio path is owner-only, so this defaults to `local` (`?? "local"`) — a
19
+ * `local` management sense COMMITS. A network/multi-tenant transport that constructs
20
+ * the server MUST pass its real senseType (`open` ⇒ confirm-prompt downgrade; `closed`
21
+ * ⇒ gated by `membership`). Distinct from `originSense` (a free-form audit string like
22
+ * "stdio"); this is the typed SenseType the authority predicate consumes. */
23
+ senseType?: SenseType;
24
+ /** The PRE-COMPUTED account-roster membership for a `closed`-sense `connect_to`
25
+ * (p11 inc2). The stdio `local` path never consults it (left `undefined`); a `closed`
26
+ * network transport supplies the membership it already evaluated against the roster.
27
+ * The boundary stays thin — it forwards this to the library, computing no membership
28
+ * itself (the MCP `resolve_party` path does not wire a roster context). */
29
+ membership?: AccountMembershipResult;
15
30
  }
16
31
  /** Whether wiring an AuditSink should also stamp a record for an `onboard_agent` trust
17
32
  * seat: only when the owner explicitly set a trustLevel (a deliberate trust decision).
@@ -32,6 +32,8 @@ const missions_1 = require("../missions");
32
32
  const mission_share_1 = require("../mission-share");
33
33
  const coordination_1 = require("../coordination");
34
34
  const types_2 = require("../types");
35
+ const connect_1 = require("../connect");
36
+ const mission_result_1 = require("../mission-result");
35
37
  /** SECURITY (finding 3-A): the friends MCP server speaks JSON-RPC over **stdio**, and
36
38
  * stdio is an owner-only channel — the local user who launched the process is the only
37
39
  * actor. So when no explicit controlContext is wired, audited mutations are attributed
@@ -259,6 +261,31 @@ async function dispatchTool(store, name, args, grants, missions, audit, controlC
259
261
  }
260
262
  return { result: record, isError: false };
261
263
  }
264
+ case "connect_to": {
265
+ // The management-sense control plane (p11 inc2, brick 8). The boundary stays
266
+ // thin — coerce the peer handles + level, resolve the gate's management sense
267
+ // (the stdio path is owner-only ⇒ `local`), and forward to the library, which
268
+ // owns the authority gate + disambiguation + introduce + audit. `isError` reflects
269
+ // ok===false (a `downgraded` / `needs_handle_or_introduction` result is an error
270
+ // result like the other mutation cases).
271
+ const result = await (0, connect_1.connectAgents)(store, {
272
+ peer: {
273
+ agentId: coerceOptionalString(args.agentId),
274
+ did: coerceOptionalString(args.did),
275
+ name: coerceOptionalString(args.name),
276
+ },
277
+ // The stdio default is `local` (owner-only); a network transport supplies its
278
+ // real senseType via controlContext. The proof's stdio path commits.
279
+ senseType: controlContext?.senseType ?? "local",
280
+ ...(controlContext?.membership ? { membership: controlContext.membership } : {}),
281
+ trustLevel: coerceOptionalString(args.trustLevel),
282
+ }, {
283
+ ...(audit ? { audit } : {}),
284
+ actor: auditActor,
285
+ originSense: auditOriginSense,
286
+ });
287
+ return { result, isError: result.ok === false };
288
+ }
262
289
  case "whoami": {
263
290
  return { result: await (0, whoami_1.whoami)(store), isError: false };
264
291
  }
@@ -438,6 +465,9 @@ async function dispatchTool(store, name, args, grants, missions, audit, controlC
438
465
  note: coerceOptionalString(args.note),
439
466
  proposedAssignee: parseMaybeJson(args.proposedAssignee),
440
467
  selfAgentId,
468
+ // gap-1 (p11 inc2): an optional task-spec, meaningful only on a `request`. The
469
+ // library mints the requestId + records the delegation first-party.
470
+ task: parseMaybeJson(args.task),
441
471
  proof: coerceOptionalString(args.proof),
442
472
  });
443
473
  return { result, isError: result.ok === false };
@@ -467,6 +497,41 @@ async function dispatchTool(store, name, args, grants, missions, audit, controlC
467
497
  }
468
498
  return { result: record.coordination ?? { assignee: undefined, log: [] }, isError: false };
469
499
  }
500
+ case "send_result": {
501
+ // Producer (gap-2): B returns its deliverable. Self identity comes from whoami
502
+ // (the dispatch is store-only); the mission is named by its missionKey inside the
503
+ // library. Gated on BOTH a GrantStore (consent via the "coordinate" scope) and a
504
+ // MissionStore — exactly like share_mission.
505
+ if (!missions || !grants)
506
+ return { result: NO_MISSION_STORE, isError: true };
507
+ const self = await (0, whoami_1.whoami)(store);
508
+ const selfAgentId = self.selfFriendId ?? "";
509
+ const result = await (0, mission_result_1.prepareMissionResult)(missions, store, grants, {
510
+ missionId: coerceString(args.missionId),
511
+ toAgentId: coerceString(args.toAgentId),
512
+ requestId: coerceString(args.requestId),
513
+ result: parseMaybeJson(args.result) ?? { summary: "" },
514
+ selfAgentId,
515
+ proof: coerceOptionalString(args.proof),
516
+ });
517
+ return { result, isError: result.ok === false };
518
+ }
519
+ case "import_result": {
520
+ // Consumer (gap-2): A imports B's deliverable. Gated on the MissionStore, like
521
+ // import_mission. An invalid/malformed envelope is a clean `invalid` result.
522
+ if (!missions)
523
+ return { result: NO_MISSION_STORE, isError: true };
524
+ const envelope = parseMaybeJson(args.envelope);
525
+ if (!envelope || typeof envelope !== "object") {
526
+ return { result: { ok: false, status: "invalid", message: "an envelope object is required" }, isError: true };
527
+ }
528
+ const result = await (0, mission_result_1.importMissionResult)(missions, {
529
+ envelope,
530
+ fromAgentId: coerceString(args.fromAgentId),
531
+ trustOfSource: coerceString(args.trustOfSource),
532
+ });
533
+ return { result, isError: result.ok === false };
534
+ }
470
535
  default: {
471
536
  return { result: { error: `Unknown tool: ${name}` }, isError: true };
472
537
  }
@@ -3,14 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getToolSchemas = getToolSchemas;
4
4
  // MCP tool schemas for the friends server.
5
5
  //
6
- // 29 tools — a thin 1:1 surface over the friends library (D7): the original 14,
6
+ // 32 tools — a thin 1:1 surface over the friends library (D7): the original 14,
7
7
  // the cross-agent moat surface (resolve_room, import_profile, grant_share,
8
8
  // revoke_share, list_shares; share_profile is de-stubbed in place), the brick-3
9
9
  // mission ledger (record_mission, get_mission, list_missions, share_mission,
10
10
  // import_mission), the brick-4 earned-standing lenses (assess_standing,
11
11
  // explain_standing — read-only, advisory; never write trust, never on the wire),
12
- // and the brick-5 coordination verbs (coordinate, import_coordination,
13
- // get_coordination — negotiate WHO does a mission; advisory assignment metadata).
12
+ // the brick-5 coordination verbs (coordinate, import_coordination,
13
+ // get_coordination — negotiate WHO does a mission; advisory assignment metadata),
14
+ // and the p11-inc2 own-fleet delegation surface (connect_to — the management-sense
15
+ // control plane that links two own agents + audits action:"connect"; send_result,
16
+ // import_result — the result-return / delegation deliverable channel that carries
17
+ // B's produced artifact back to A, attributed + correlated + quarantined on import).
14
18
  // Each schema follows JSON Schema for
15
19
  // `inputSchema` as required by MCP. The shape mirrors the harness's McpToolSchema
16
20
  // so the same client tooling consumes both.
@@ -195,6 +199,20 @@ function getToolSchemas() {
195
199
  required: ["name", "agentId"],
196
200
  },
197
201
  },
202
+ {
203
+ name: "connect_to",
204
+ description: "Management-sense control plane (brick 8): the owner links one of their OWN agents into this agent's fleet — introduce a peer by agentId/did/name at a trust level (default family for own-fleet). Authority-gated: commits inline ONLY from a local (owner-only stdio) or roster-verified same-account closed sense; an open sense never commits inline (it downgrades to a confirm-prompt); a bare name with no resolvable handle/DID and no record hit returns needs_handle_or_introduction (never fabricates a target). Writes one control-plane audit record (action:'connect') through the wired sink. Returns { ok:true, status:'connected', record } or { ok:false, status:'downgraded'|'needs_handle_or_introduction', downgrade? }.",
205
+ inputSchema: {
206
+ type: "object",
207
+ properties: {
208
+ agentId: { type: "string", description: "the peer agent's join-key agentId (an owner-supplied handle)" },
209
+ did: { type: "string", description: "the peer's DID (an alternative handle; must resolve to an existing record)" },
210
+ name: { type: "string", description: "the peer's colloquial name (resolves ONLY by matching an existing record — never fabricated)" },
211
+ trustLevel: { type: "string", enum: ["family", "friend", "acquaintance", "stranger"], description: "the trust to link at (default family for own-fleet linked agents)" },
212
+ proof: { type: "string", description: "optional opaque proof slot (reserved; the TOFU path ignores it)" },
213
+ },
214
+ },
215
+ },
198
216
  {
199
217
  name: "whoami",
200
218
  description: "Resolve who the machine owner is and which friend record represents the self.",
@@ -365,6 +383,7 @@ function getToolSchemas() {
365
383
  intent: { type: "string", enum: ["request", "offer", "accept", "decline", "handoff"], description: "the coordination verb: request (will you take this?) / offer (I'll take this) / accept (yes, I'm on it — sets assignee=self) / decline (no) / handoff (it's yours now — you must hold the assignment; proposes a new assignee)" },
366
384
  note: { type: "string", description: "optional free text carried on the message + logged" },
367
385
  proposedAssignee: { type: "object", description: "the proposed new assignee { agentId?, agentName? } — meaningful ONLY on intent=handoff" },
386
+ task: { type: "object", description: "optional delegation task-spec { summary, details?, inputs? } — meaningful ONLY on intent=request (gap-2); the producer mints a requestId, stamps it on the envelope, and records the delegation first-party for the result-return to correlate against" },
368
387
  proof: { type: "string", description: "optional opaque proof to stamp on the envelope (for a non-TOFU recipient verifier)" },
369
388
  },
370
389
  required: ["missionId", "toAgentId", "intent"],
@@ -394,5 +413,33 @@ function getToolSchemas() {
394
413
  required: ["missionId"],
395
414
  },
396
415
  },
416
+ {
417
+ name: "send_result",
418
+ description: "Producer (gap-2 — the result-return): B returns its DELIVERABLE for a delegation, attributed to B (from whoami) + correlated to A's task-spec by requestId, named by the mission's missionKey (never the local uuid). Consent-gated via the identity-tier 'coordinate' scope (a result is B answering A's own delegation — trust ≥ friend suffices; NO new scope). Records the result first-party on B's own mission. Returns { ok, envelope } or { ok:false, status: not_found|no_consent }.",
419
+ inputSchema: {
420
+ type: "object",
421
+ properties: {
422
+ missionId: { type: "string", description: "the local mission B is returning a result for (its local uuid id)" },
423
+ toAgentId: { type: "string", description: "the recipient agent's join-key agentId — A, the delegator" },
424
+ requestId: { type: "string", description: "the delegation correlation key (the task-spec's requestId)" },
425
+ result: { type: "object", description: "B's deliverable { summary, artifact?, outputs? }" },
426
+ proof: { type: "string", description: "optional opaque proof to stamp on the envelope (for a non-TOFU recipient verifier)" },
427
+ },
428
+ required: ["missionId", "toAgentId", "requestId", "result"],
429
+ },
430
+ },
431
+ {
432
+ name: "import_result",
433
+ description: "Consumer (gap-2, non-clobbering merge): A imports B's result-return. Resolves the mission by missionKey; lands B's deliverable QUARANTINED + attributed under importedResults WITHOUT touching first-party; source trust caps acceptance (checked before correlation); a result whose requestId matches no prior first-party delegation is REJECTED (no_delegation — A only accepts results for work it delegated); an unknown mission is no_mission (a result never seeds a mission); never recomputes status/participants. Returns { ok, status, record } or { ok:false, status: untrusted_source|no_mission|no_delegation|invalid }.",
434
+ inputSchema: {
435
+ type: "object",
436
+ properties: {
437
+ envelope: { type: "object", description: "the MissionResultEnvelope to import" },
438
+ fromAgentId: { type: "string", description: "the agent the envelope arrived from (join-key agentId) — B" },
439
+ trustOfSource: { type: "string", enum: ["family", "friend", "acquaintance", "stranger"], description: "this agent's resolved trust in the source agent — the acceptance cap" },
440
+ },
441
+ required: ["envelope", "fromAgentId", "trustOfSource"],
442
+ },
443
+ },
397
444
  ];
398
445
  }
@@ -0,0 +1,82 @@
1
+ import type { MissionStore } from "./mission-store";
2
+ import type { FriendStore } from "./store";
3
+ import type { GrantStore } from "./grant-store";
4
+ import type { MissionRecord, MissionResultEnvelope, TrustLevel } from "./types";
5
+ import type { ConsentPolicy } from "./consent";
6
+ import type { AgentVerifier } from "./verifier";
7
+ export interface PrepareMissionResultInput {
8
+ /** The LOCAL mission B is returning a result for, by its local UUID id. */
9
+ missionId: string;
10
+ /** The recipient agent's join-key agentId — A, the delegator. */
11
+ toAgentId: string;
12
+ /** The delegation correlation key (the gap-1 task-spec's requestId). */
13
+ requestId: string;
14
+ /** B's deliverable. `requestId`/`provenance` are stamped by the producer. */
15
+ result: {
16
+ summary: string;
17
+ artifact?: string;
18
+ outputs?: Record<string, string>;
19
+ };
20
+ /** This agent's own join-key agentId — the attribution (fromAgentId = B). */
21
+ selfAgentId: string;
22
+ /** Optional proof to stamp on the envelope (for a non-TOFU recipient verifier). */
23
+ proof?: string;
24
+ }
25
+ export type PrepareMissionResultStatus = "not_found" | "no_consent";
26
+ export type PrepareMissionResultResult = {
27
+ ok: true;
28
+ envelope: MissionResultEnvelope;
29
+ } | {
30
+ ok: false;
31
+ status: PrepareMissionResultStatus;
32
+ };
33
+ /**
34
+ * Producer half of the result-return. Resolves the local mission by `missionId`; names
35
+ * it by its `missionKey` (NEVER the local UUID); attributes the result to `selfAgentId`
36
+ * (B); correlates by `requestId`. Consent-gated via the `"coordinate"` identity-tier
37
+ * scope (a result is B answering A's own delegation — trust ≥ friend suffices under the
38
+ * tiered default, ZERO new scope). Records the result first-party on B's own record under
39
+ * `results[requestId]`.
40
+ */
41
+ export declare function prepareMissionResult(missions: MissionStore, store: FriendStore, grants: GrantStore, input: PrepareMissionResultInput, consent?: ConsentPolicy): Promise<PrepareMissionResultResult>;
42
+ export interface ImportMissionResultInput {
43
+ envelope: MissionResultEnvelope;
44
+ /** The agent the envelope arrived from (its join-key agentId) — B. */
45
+ fromAgentId: string;
46
+ /** This agent's resolved trust in the source agent — the cap on acceptance. */
47
+ trustOfSource: TrustLevel;
48
+ }
49
+ export type ImportMissionResultStatus = "imported" | "no_mission" | "no_delegation" | "assignee_mismatch" | "untrusted_source";
50
+ export type ImportMissionResultResult = {
51
+ ok: true;
52
+ status: "imported";
53
+ record: MissionRecord;
54
+ } | {
55
+ ok: false;
56
+ status: "no_mission" | "no_delegation" | "assignee_mismatch" | "untrusted_source";
57
+ };
58
+ export interface ImportMissionResultOptions {
59
+ /** Authentication seam. Defaults to TOFU. Authorization (trust) is still applied
60
+ * regardless of what the verifier says. */
61
+ verifier?: AgentVerifier;
62
+ /** Minimum trust a source must hold for its result to be accepted at all.
63
+ * Default `acquaintance`: a stranger source is refused. */
64
+ minTrustToAccept?: TrustLevel;
65
+ }
66
+ /**
67
+ * Consumer half of the result-return — the non-clobbering merge. Order (PINNED):
68
+ * (1) TOFU verifier + trust cap (both must pass, else `untrusted_source`, write nothing);
69
+ * (2) unknown mission (no findByMissionKey hit) → `no_mission` (NO seeding — a result
70
+ * never creates a mission);
71
+ * (3) the result's `requestId` not present in the record's FIRST-PARTY `delegations`
72
+ * (A never delegated this) → `no_delegation` — correlation honesty;
73
+ * (3b) the matched delegation's recorded `assignee` is not the result's source
74
+ * (`delegation.assignee.agentId !== fromAgentId`) → `assignee_mismatch` — assignee
75
+ * honesty (security-review inc-2 finding 1). FAILS CLOSED on a legacy delegation with
76
+ * no recorded assignee. A mismatch writes NOTHING (not even quarantined);
77
+ * (4) otherwise land under `importedResults[agentId][requestId]` (dedupe on replay),
78
+ * stamped imported + attributed + importedAt, NEVER touching first-party
79
+ * `learnings`/`notes`/`status`/`delegations`/`results`, NEVER recomputing
80
+ * status/participants (non-transitive).
81
+ */
82
+ export declare function importMissionResult(missions: MissionStore, input: ImportMissionResultInput, options?: ImportMissionResultOptions): Promise<ImportMissionResultResult>;