@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
package/dist/coordination.js
CHANGED
|
@@ -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`.
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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; } });
|
package/dist/mailbox/index.d.ts
CHANGED
|
@@ -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
|
|
18
|
-
* payload-agnostic — this union grows by one leaf per brick (additive,
|
|
19
|
-
* compatible); buildOutgoing/readIncoming carry any of them unchanged.
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/mailbox/index.js
CHANGED
|
@@ -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));
|
package/dist/mcp/dispatch.d.ts
CHANGED
|
@@ -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).
|
package/dist/mcp/dispatch.js
CHANGED
|
@@ -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
|
}
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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>;
|