@openbkn/dsh-business-context 0.1.5-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +42 -0
- package/README.zh.md +42 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +7553 -0
- package/lib/index.js +1788 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +624 -0
- package/lib/typert.remote-client.d.ts +40 -0
- package/lib/typert.remote-client.js +435 -0
- package/lib/types/auth.d.ts +42 -0
- package/lib/types/business-context-service.d.ts +126 -0
- package/lib/types/business-graph-model.d.ts +34 -0
- package/lib/types/business-network-catalog.d.ts +8 -0
- package/lib/types/client/BoundNetworkBadge.d.ts +37 -0
- package/lib/types/client/OpenBknContextToolView.d.ts +4 -0
- package/lib/types/client/OpenBknEntry.d.ts +9 -0
- package/lib/types/client/OpenBknOverlay.d.ts +16 -0
- package/lib/types/client/ProvenanceOverlay.d.ts +33 -0
- package/lib/types/client/SuggestionDock.d.ts +12 -0
- package/lib/types/client/TurnProvenanceActions.d.ts +13 -0
- package/lib/types/client/index.d.ts +11 -0
- package/lib/types/client/network-directory.d.ts +13 -0
- package/lib/types/client/openbkn-ui-controller.d.ts +51 -0
- package/lib/types/client/suggestion-dock-controller.d.ts +22 -0
- package/lib/types/client/turn-provenance-controller.d.ts +14 -0
- package/lib/types/config.d.ts +30 -0
- package/lib/types/dsh-session-binding.d.ts +26 -0
- package/lib/types/dsh-session-provenance.d.ts +27 -0
- package/lib/types/index.d.ts +33 -0
- package/lib/types/interaction-references.d.ts +15 -0
- package/lib/types/managed-session-policy.d.ts +18 -0
- package/lib/types/native-mcp-provenance.d.ts +17 -0
- package/lib/types/network-capability-profile.d.ts +26 -0
- package/lib/types/openbkn-cli-subprocess.d.ts +50 -0
- package/lib/types/openbkn-mcp-manager.d.ts +32 -0
- package/lib/types/platform-reader.d.ts +51 -0
- package/lib/types/provenance-handle.d.ts +5 -0
- package/lib/types/provenance-view.d.ts +11 -0
- package/lib/types/scoped-business-context.d.ts +10 -0
- package/lib/types/session-binding.d.ts +35 -0
- package/lib/types/suggested-prompts.d.ts +3 -0
- package/lib/types/turn-provenance.d.ts +25 -0
- package/lib/types/types.d.ts +111 -0
- package/lib/types/workspace-binding-registry.d.ts +40 -0
- package/package.json +131 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1788 @@
|
|
|
1
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
2
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
3
|
+
import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
|
+
import * as McpClient from "@deepseek-ai/dsh-mcp-client";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { realpath, stat } from "node:fs/promises";
|
|
7
|
+
import { isAbsolute, resolve } from "node:path";
|
|
8
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
9
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
//#region src/config.ts
|
|
12
|
+
/** Runtime schema and conservative defaults for the host plugin row. */
|
|
13
|
+
const Config = Schema.object({
|
|
14
|
+
baseUrl: Schema.string().required(),
|
|
15
|
+
mcpUrl: Schema.string(),
|
|
16
|
+
businessDomain: Schema.string().pattern(/^[A-Za-z0-9_-]{1,64}$/),
|
|
17
|
+
cliPath: Schema.string().default("openbkn"),
|
|
18
|
+
requestTimeoutMs: Schema.natural().min(1).default(3e4),
|
|
19
|
+
maxResultBytes: Schema.natural().min(1).default(1e6),
|
|
20
|
+
maxGraphNodes: Schema.natural().min(1).default(200),
|
|
21
|
+
maxGraphEdges: Schema.natural().min(1).default(400),
|
|
22
|
+
allowInsecureTls: Schema.boolean().default(false)
|
|
23
|
+
});
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/business-network-catalog.ts
|
|
26
|
+
/**
|
|
27
|
+
* Convert one platform response into the bounded data shape consumed by the
|
|
28
|
+
* network selector. The platform remains the authorization authority; this
|
|
29
|
+
* function only removes unneeded fields and rejects malformed rows.
|
|
30
|
+
*/
|
|
31
|
+
function parseVisibleBusinessNetworks(value) {
|
|
32
|
+
const entries = entriesOf(value);
|
|
33
|
+
if (entries === void 0) throw new Error("OpenBKN returned an invalid network list.");
|
|
34
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35
|
+
const networks = [];
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const network = networkOf(entry);
|
|
38
|
+
if (network === void 0 || seen.has(network.id)) continue;
|
|
39
|
+
seen.add(network.id);
|
|
40
|
+
networks.push(network);
|
|
41
|
+
}
|
|
42
|
+
return networks;
|
|
43
|
+
}
|
|
44
|
+
function entriesOf(value) {
|
|
45
|
+
if (!isRecord$3(value)) return void 0;
|
|
46
|
+
if (Array.isArray(value.entries)) return value.entries;
|
|
47
|
+
if (isRecord$3(value.result) && Array.isArray(value.result.entries)) return value.result.entries;
|
|
48
|
+
}
|
|
49
|
+
function networkOf(value) {
|
|
50
|
+
if (!isRecord$3(value) || typeof value.id !== "string" || typeof value.name !== "string") return void 0;
|
|
51
|
+
const id = value.id.trim();
|
|
52
|
+
const displayName = value.name.trim();
|
|
53
|
+
if (id.length === 0 || displayName.length === 0) return void 0;
|
|
54
|
+
const description = stringOrUndefined(value.description) ?? stringOrUndefined(value.comment);
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
displayName,
|
|
58
|
+
...description === void 0 ? {} : { description }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function stringOrUndefined(value) {
|
|
62
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
63
|
+
}
|
|
64
|
+
function isRecord$3(value) {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/session-binding.ts
|
|
69
|
+
/** Durable DSH event type for the business network selected by a session. */
|
|
70
|
+
const BUSINESS_NETWORK_BOUND_EVENT = "openbkn/business-network-bound";
|
|
71
|
+
/** A durable session already names a different OpenBKN network or platform. */
|
|
72
|
+
var BusinessNetworkBindingConflictError = class extends Error {
|
|
73
|
+
existing;
|
|
74
|
+
requested;
|
|
75
|
+
constructor(existing, requested) {
|
|
76
|
+
super(`This DSH session is already bound to ${existing.platformBaseUrl}/${existing.knowledgeNetworkId}; cannot bind ${requested.platformBaseUrl}/${requested.knowledgeNetworkId}`);
|
|
77
|
+
this.existing = existing;
|
|
78
|
+
this.requested = requested;
|
|
79
|
+
this.name = "BusinessNetworkBindingConflictError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/** Reconstruct the one permitted business-network binding from a DSH event log. */
|
|
83
|
+
function readBusinessNetworkBinding(events) {
|
|
84
|
+
let binding;
|
|
85
|
+
for (const event of events) {
|
|
86
|
+
if (event.type !== "openbkn/business-network-bound") continue;
|
|
87
|
+
const next = parseBinding(event.data);
|
|
88
|
+
if (binding === void 0) {
|
|
89
|
+
binding = next;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!sameIdentity(binding, next)) throw new BusinessNetworkBindingConflictError(binding, next);
|
|
93
|
+
}
|
|
94
|
+
return binding;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Decide whether selection emits the one durable event. The caller appends the
|
|
98
|
+
* returned event through DSH's session log; no in-memory side store exists.
|
|
99
|
+
*/
|
|
100
|
+
function bindBusinessNetwork(events, requested) {
|
|
101
|
+
const next = normalizeBinding(requested);
|
|
102
|
+
const existing = readBusinessNetworkBinding(events);
|
|
103
|
+
if (existing === void 0) return {
|
|
104
|
+
kind: "bound",
|
|
105
|
+
event: {
|
|
106
|
+
type: BUSINESS_NETWORK_BOUND_EVENT,
|
|
107
|
+
data: next
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
if (!sameIdentity(existing, next)) throw new BusinessNetworkBindingConflictError(existing, next);
|
|
111
|
+
return {
|
|
112
|
+
kind: "already-bound",
|
|
113
|
+
binding: existing
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function parseBinding(value) {
|
|
117
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("OpenBKN business-network binding event is malformed");
|
|
118
|
+
const candidate = value;
|
|
119
|
+
if (typeof candidate.platformBaseUrl !== "string" || typeof candidate.knowledgeNetworkId !== "string" || typeof candidate.displayName !== "string") throw new TypeError("OpenBKN business-network binding event is missing required fields");
|
|
120
|
+
return normalizeBinding({
|
|
121
|
+
platformBaseUrl: candidate.platformBaseUrl,
|
|
122
|
+
knowledgeNetworkId: candidate.knowledgeNetworkId,
|
|
123
|
+
displayName: candidate.displayName
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function normalizeBinding(binding) {
|
|
127
|
+
const platformBaseUrl = binding.platformBaseUrl.replace(/\/+$/, "");
|
|
128
|
+
const knowledgeNetworkId = binding.knowledgeNetworkId.trim();
|
|
129
|
+
const displayName = binding.displayName.trim();
|
|
130
|
+
if (platformBaseUrl.length === 0 || knowledgeNetworkId.length === 0 || displayName.length === 0) throw new TypeError("OpenBKN business-network binding fields must not be empty");
|
|
131
|
+
return {
|
|
132
|
+
platformBaseUrl,
|
|
133
|
+
knowledgeNetworkId,
|
|
134
|
+
displayName
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function sameIdentity(left, right) {
|
|
138
|
+
return left.platformBaseUrl === right.platformBaseUrl && left.knowledgeNetworkId === right.knowledgeNetworkId;
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/dsh-session-binding.ts
|
|
142
|
+
/** Recover the binding directly from DSH's append-only session log. */
|
|
143
|
+
function readDshSessionBusinessNetwork(session) {
|
|
144
|
+
return readBusinessNetworkBinding(session.snapshotEvents());
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Persist the selected business network as the one DSH session event. No
|
|
148
|
+
* side-store is created; reselecting the same network is idempotent.
|
|
149
|
+
*/
|
|
150
|
+
function bindDshSessionBusinessNetwork(session, requested) {
|
|
151
|
+
const result = bindBusinessNetwork(session.snapshotEvents(), requested);
|
|
152
|
+
if (result.kind === "bound") session.append(result.event.type, result.event.data, { ignorable: true });
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/provenance-handle.ts
|
|
157
|
+
const MAX_IDENTIFIERS = 32;
|
|
158
|
+
/** Validate untrusted platform metadata before it becomes a Client-visible provenance reference. */
|
|
159
|
+
function normalizeProvenanceHandle(value) {
|
|
160
|
+
if (!isRecord$2(value)) throw new TypeError("Provenance handle must be an object.");
|
|
161
|
+
if (value.schemaVersion !== 1) throw new TypeError("Unsupported provenance handle schema version.");
|
|
162
|
+
if (value.status !== "completed" && value.status !== "failed") throw new TypeError("Provenance handle must describe a completed turn.");
|
|
163
|
+
if (typeof value.partial !== "boolean") throw new TypeError("Provenance handle partial state is invalid.");
|
|
164
|
+
return {
|
|
165
|
+
schemaVersion: 1,
|
|
166
|
+
interactionId: identifier$2(value.interactionId, "interaction id"),
|
|
167
|
+
requestIds: identifiers(value.requestIds, "request ids"),
|
|
168
|
+
traceIds: identifiers(value.traceIds, "trace ids"),
|
|
169
|
+
receiptIds: identifiers(value.receiptIds, "receipt ids"),
|
|
170
|
+
status: value.status,
|
|
171
|
+
partial: value.partial
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function identifiers(value, label) {
|
|
175
|
+
if (!Array.isArray(value)) throw new TypeError(`Provenance handle ${label} are invalid.`);
|
|
176
|
+
if (value.length > MAX_IDENTIFIERS) throw new RangeError(`Provenance handle ${label} exceed the limit.`);
|
|
177
|
+
return [...new Set(value.map((item) => identifier$2(item, label)))];
|
|
178
|
+
}
|
|
179
|
+
function identifier$2(value, label) {
|
|
180
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.length > 256) throw new TypeError(`Provenance handle ${label} is invalid.`);
|
|
181
|
+
return value.trim();
|
|
182
|
+
}
|
|
183
|
+
function isRecord$2(value) {
|
|
184
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/turn-provenance.ts
|
|
188
|
+
/** Durable session event binding one finalized assistant message to one OpenBKN interaction. */
|
|
189
|
+
const TURN_PROVENANCE_EVENT = "openbkn/turn-provenance";
|
|
190
|
+
/** A completed message cannot be rewritten to point at a different interaction. */
|
|
191
|
+
var TurnProvenanceConflictError = class extends Error {
|
|
192
|
+
messageId;
|
|
193
|
+
constructor(messageId) {
|
|
194
|
+
super(`Assistant message ${messageId} already has a different OpenBKN provenance handle.`);
|
|
195
|
+
this.messageId = messageId;
|
|
196
|
+
this.name = "TurnProvenanceConflictError";
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
/** Return the one append-only provenance event to write, or undefined if already recorded identically. */
|
|
200
|
+
function appendTurnProvenance(events, messageId, handle) {
|
|
201
|
+
const normalizedMessageId = normalizeMessageId(messageId);
|
|
202
|
+
const normalizedHandle = normalizeProvenanceHandle(handle);
|
|
203
|
+
const existing = readTurnProvenance(events, normalizedMessageId);
|
|
204
|
+
if (existing === void 0) return {
|
|
205
|
+
type: TURN_PROVENANCE_EVENT,
|
|
206
|
+
data: {
|
|
207
|
+
messageId: normalizedMessageId,
|
|
208
|
+
handle: normalizedHandle
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
if (!sameHandle(existing, normalizedHandle)) throw new TurnProvenanceConflictError(normalizedMessageId);
|
|
212
|
+
}
|
|
213
|
+
/** Recover the committed handle for exactly one finalized assistant message. */
|
|
214
|
+
function readTurnProvenance(events, messageId) {
|
|
215
|
+
const normalizedMessageId = normalizeMessageId(messageId);
|
|
216
|
+
let handle;
|
|
217
|
+
for (const event of events) {
|
|
218
|
+
if (event.type !== "openbkn/turn-provenance") continue;
|
|
219
|
+
const candidate = parseTurnProvenanceEvent(event.data);
|
|
220
|
+
if (candidate.messageId !== normalizedMessageId) continue;
|
|
221
|
+
if (handle === void 0) handle = candidate.handle;
|
|
222
|
+
else if (!sameHandle(handle, candidate.handle)) throw new TurnProvenanceConflictError(normalizedMessageId);
|
|
223
|
+
}
|
|
224
|
+
return handle;
|
|
225
|
+
}
|
|
226
|
+
function parseTurnProvenanceEvent(value) {
|
|
227
|
+
if (!isRecord$1(value)) throw new TypeError("OpenBKN turn provenance event is malformed.");
|
|
228
|
+
return {
|
|
229
|
+
messageId: normalizeMessageId(value.messageId),
|
|
230
|
+
handle: normalizeProvenanceHandle(value.handle)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function normalizeMessageId(value) {
|
|
234
|
+
if (typeof value !== "string") throw new TypeError("OpenBKN turn provenance message id is invalid.");
|
|
235
|
+
const normalized = value.trim();
|
|
236
|
+
if (normalized.length === 0 || normalized.length > 256) throw new TypeError("OpenBKN turn provenance message id is invalid.");
|
|
237
|
+
return normalized;
|
|
238
|
+
}
|
|
239
|
+
function sameHandle(left, right) {
|
|
240
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
241
|
+
}
|
|
242
|
+
function isRecord$1(value) {
|
|
243
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/dsh-session-provenance.ts
|
|
247
|
+
/** Read one message's committed provenance directly from the append-only DSH log. */
|
|
248
|
+
function readDshSessionTurnProvenance(session, messageId) {
|
|
249
|
+
return readTurnProvenance(session.snapshotEvents(), messageId);
|
|
250
|
+
}
|
|
251
|
+
/** Append a completed-turn handle once; identical retries are intentionally idempotent. */
|
|
252
|
+
function appendDshSessionTurnProvenance(session, messageId, handle) {
|
|
253
|
+
const event = appendTurnProvenance(session.snapshotEvents(), messageId, handle);
|
|
254
|
+
if (event !== void 0) session.append(event.type, event.data, { ignorable: true });
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/native-mcp-provenance.ts
|
|
258
|
+
const OPENBKN_FINISH_INTERACTION_TOOL = "mcp__openbkn__bkn_finish_interaction";
|
|
259
|
+
/**
|
|
260
|
+
* Recover provenance only from a completed interaction explicitly returned by
|
|
261
|
+
* the OpenBKN MCP finish call in the same DSH turn. No model text, tool args,
|
|
262
|
+
* or unrelated MCP output participates in this association.
|
|
263
|
+
*/
|
|
264
|
+
function findCompletedNativeMcpProvenance(events, turn) {
|
|
265
|
+
const calls = /* @__PURE__ */ new Map();
|
|
266
|
+
for (const event of events) {
|
|
267
|
+
if (event.type !== "tool/call") continue;
|
|
268
|
+
const data = record$2(event.data);
|
|
269
|
+
if (data?.turn !== turn || typeof data.callId !== "string" || typeof data.name !== "string") continue;
|
|
270
|
+
calls.set(data.callId, data.name);
|
|
271
|
+
}
|
|
272
|
+
const completed = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const [index, event] of events.entries()) {
|
|
274
|
+
if (event.type !== "tool/result") continue;
|
|
275
|
+
const data = record$2(event.data);
|
|
276
|
+
if (data?.turn !== turn) continue;
|
|
277
|
+
const message = record$2(data.message);
|
|
278
|
+
const source = message === void 0 ? void 0 : record$2(message.source);
|
|
279
|
+
const callId = source?.kind === "tool" && typeof source.callId === "string" ? source.callId : void 0;
|
|
280
|
+
if (callId === void 0 || calls.get(callId) !== OPENBKN_FINISH_INTERACTION_TOOL) continue;
|
|
281
|
+
const interactionId = completedInteractionId(message);
|
|
282
|
+
if (interactionId !== void 0) completed.set(callId, {
|
|
283
|
+
interactionId,
|
|
284
|
+
resultIndex: index
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (completed.size !== 1) return void 0;
|
|
288
|
+
const [, completedInteraction] = [...completed][0];
|
|
289
|
+
const answer = finalAssistantMessageAfter(events, turn, completedInteraction.resultIndex);
|
|
290
|
+
if (answer === void 0) return void 0;
|
|
291
|
+
return {
|
|
292
|
+
messageId: answer,
|
|
293
|
+
handle: {
|
|
294
|
+
schemaVersion: 1,
|
|
295
|
+
interactionId: completedInteraction.interactionId,
|
|
296
|
+
requestIds: [],
|
|
297
|
+
traceIds: [],
|
|
298
|
+
receiptIds: [],
|
|
299
|
+
status: "completed",
|
|
300
|
+
partial: true
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function finalAssistantMessageAfter(events, turn, afterIndex) {
|
|
305
|
+
for (let index = events.length - 1; index > afterIndex; index -= 1) {
|
|
306
|
+
const event = events[index];
|
|
307
|
+
if (event?.type !== "assistant/message") continue;
|
|
308
|
+
const data = record$2(event.data);
|
|
309
|
+
if (data?.turn !== turn) continue;
|
|
310
|
+
const message = record$2(data.message);
|
|
311
|
+
if (message === void 0 || typeof message.id !== "string" || !Array.isArray(message.content)) continue;
|
|
312
|
+
if (!message.content.some((block) => record$2(block)?.type === "tool-call")) return message.id;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function completedInteractionId(message) {
|
|
316
|
+
if (message === void 0 || !Array.isArray(message.content)) return void 0;
|
|
317
|
+
for (const block of message.content) {
|
|
318
|
+
const toolResult = record$2(block);
|
|
319
|
+
if (toolResult?.type !== "tool-result" || !Array.isArray(toolResult.content)) continue;
|
|
320
|
+
for (const content of toolResult.content) {
|
|
321
|
+
const text = record$2(content);
|
|
322
|
+
if (text?.type !== "text" || typeof text.text !== "string") continue;
|
|
323
|
+
const parsed = jsonRecord(text.text);
|
|
324
|
+
if (parsed?.execution_status !== "completed") continue;
|
|
325
|
+
const interactionId = identifier$1(parsed.interaction_id);
|
|
326
|
+
if (interactionId !== void 0) return interactionId;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function jsonRecord(value) {
|
|
331
|
+
try {
|
|
332
|
+
return record$2(JSON.parse(value));
|
|
333
|
+
} catch {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function identifier$1(value) {
|
|
338
|
+
if (typeof value !== "string") return void 0;
|
|
339
|
+
const normalized = value.trim();
|
|
340
|
+
return normalized.length > 0 && normalized.length <= 256 ? normalized : void 0;
|
|
341
|
+
}
|
|
342
|
+
function record$2(value) {
|
|
343
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
344
|
+
}
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/platform-reader.ts
|
|
347
|
+
/** A bounded Host-side error. It never includes platform response bodies. */
|
|
348
|
+
var PlatformReaderError = class extends Error {
|
|
349
|
+
code;
|
|
350
|
+
constructor(code, message, options) {
|
|
351
|
+
super(message, options);
|
|
352
|
+
this.code = code;
|
|
353
|
+
this.name = "PlatformReaderError";
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const MAX_PLATFORM_RESPONSE_BYTES = 8388608;
|
|
357
|
+
/**
|
|
358
|
+
* A fixed-route Host reader for plugin control-plane data. It is deliberately
|
|
359
|
+
* separate from the model path: agents use Context Loader MCP; this reader
|
|
360
|
+
* only obtains the catalog, schema profile, and safe Trace projections.
|
|
361
|
+
*/
|
|
362
|
+
var OpenBknPlatformReader = class {
|
|
363
|
+
config;
|
|
364
|
+
fetcher;
|
|
365
|
+
constructor(config, fetcher = fetch) {
|
|
366
|
+
this.config = config;
|
|
367
|
+
this.fetcher = fetcher;
|
|
368
|
+
}
|
|
369
|
+
async listKnowledgeNetworks(signal, _cwd) {
|
|
370
|
+
return this.admit(await this.get("/api/bkn-backend/v1/knowledge-networks?limit=100", signal));
|
|
371
|
+
}
|
|
372
|
+
async getKnowledgeNetworkDetail(binding, signal, _cwd) {
|
|
373
|
+
if (normalizeBaseUrl$5(binding.platformBaseUrl) !== normalizeBaseUrl$5(this.config.baseUrl)) throw new PlatformReaderError("PLATFORM_MISMATCH", "The selected business network belongs to another OpenBKN platform.");
|
|
374
|
+
return this.admit(await this.post("/api/agent-retrieval/v1/kn/get_kn_detail", {
|
|
375
|
+
kn_id: binding.knowledgeNetworkId,
|
|
376
|
+
detail_level: "summary",
|
|
377
|
+
response_format: "json"
|
|
378
|
+
}, signal));
|
|
379
|
+
}
|
|
380
|
+
async getInteractionOperations(interactionId, signal, _cwd) {
|
|
381
|
+
const value = await this.get(`/api/agent-observability/v1/interactions/${encodeURIComponent(interactionId)}/operations`, signal, { licenseGated: true });
|
|
382
|
+
return this.admit(projectOperations$1(value));
|
|
383
|
+
}
|
|
384
|
+
async getInteractionBusinessGraph(interactionId, signal, _cwd) {
|
|
385
|
+
const value = await this.get(`/api/agent-observability/v1/interactions/${encodeURIComponent(interactionId)}/business-graph`, signal, { licenseGated: true });
|
|
386
|
+
return this.admit(projectBusinessGraph(value));
|
|
387
|
+
}
|
|
388
|
+
/** Resolve the deployment's license edition so license-gated failures can explain themselves. */
|
|
389
|
+
async getLicenseEdition(signal) {
|
|
390
|
+
const source = record$1(await this.get("/api/safe/v1/capabilities", signal));
|
|
391
|
+
const edition = string(source?.edition);
|
|
392
|
+
return {
|
|
393
|
+
...edition === void 0 ? {} : { edition },
|
|
394
|
+
...typeof source?.licensed === "boolean" ? { licensed: source.licensed } : {}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async get(path, signal, options) {
|
|
398
|
+
return await this.request(path, { method: "GET" }, signal, options);
|
|
399
|
+
}
|
|
400
|
+
async post(path, body, signal) {
|
|
401
|
+
return await this.request(path, {
|
|
402
|
+
method: "POST",
|
|
403
|
+
headers: { "content-type": "application/json" },
|
|
404
|
+
body: JSON.stringify(body)
|
|
405
|
+
}, signal);
|
|
406
|
+
}
|
|
407
|
+
async request(path, init, signal, options) {
|
|
408
|
+
if (signal.aborted) throw new PlatformReaderError("REQUEST_ABORTED", "OpenBKN context request was cancelled.");
|
|
409
|
+
const url = fixedUrl(this.config.baseUrl, path, this.config.allowInsecureTls);
|
|
410
|
+
const token = await this.config.resolveToken?.();
|
|
411
|
+
if (token === void 0 || token.trim().length === 0) throw new PlatformReaderError("AUTHENTICATION_REQUIRED", "OpenBKN authentication is required.");
|
|
412
|
+
const timeout = AbortSignal.timeout(this.config.requestTimeoutMs);
|
|
413
|
+
const requestSignal = AbortSignal.any([signal, timeout]);
|
|
414
|
+
let response;
|
|
415
|
+
try {
|
|
416
|
+
response = await this.fetcher(url, {
|
|
417
|
+
...init,
|
|
418
|
+
headers: {
|
|
419
|
+
...init.headers,
|
|
420
|
+
"x-business-domain": this.config.businessDomain?.trim() || "bd_public",
|
|
421
|
+
authorization: `Bearer ${token}`
|
|
422
|
+
},
|
|
423
|
+
redirect: "error",
|
|
424
|
+
signal: requestSignal
|
|
425
|
+
});
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (signal.aborted) throw new PlatformReaderError("REQUEST_ABORTED", "OpenBKN context request was cancelled.", { cause: error });
|
|
428
|
+
throw new PlatformReaderError("PLATFORM_UNAVAILABLE", "OpenBKN platform data is temporarily unavailable.", { cause: error });
|
|
429
|
+
}
|
|
430
|
+
if (response.status === 401) throw new PlatformReaderError("AUTHENTICATION_REQUIRED", "OpenBKN authentication is required.");
|
|
431
|
+
if (response.status === 403) {
|
|
432
|
+
if (options?.licenseGated === true) {
|
|
433
|
+
const body = await response.text().catch(() => "");
|
|
434
|
+
if (string(record$1(body.length <= 4096 ? record$1(safeParse(body))?.error : void 0)?.code) === "permission_denied") throw new PlatformReaderError("LICENSE_REQUIRED", "The requested OpenBKN capability requires an enterprise license for this business domain.");
|
|
435
|
+
}
|
|
436
|
+
throw new PlatformReaderError("AUTHENTICATION_REQUIRED", "OpenBKN authentication is required.");
|
|
437
|
+
}
|
|
438
|
+
if (!response.ok) throw new PlatformReaderError("PLATFORM_UNAVAILABLE", "OpenBKN platform data is temporarily unavailable.");
|
|
439
|
+
const contentLength = response.headers.get("content-length");
|
|
440
|
+
if (contentLength !== null && Number(contentLength) > MAX_PLATFORM_RESPONSE_BYTES) throw new PlatformReaderError("OUTPUT_OVERFLOW", "OpenBKN platform response exceeded the safe Host limit.");
|
|
441
|
+
let text;
|
|
442
|
+
try {
|
|
443
|
+
text = await readCappedBody(response, MAX_PLATFORM_RESPONSE_BYTES);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (error instanceof PlatformReaderError) throw error;
|
|
446
|
+
throw new PlatformReaderError("PLATFORM_UNAVAILABLE", "OpenBKN platform data is temporarily unavailable.", { cause: error });
|
|
447
|
+
}
|
|
448
|
+
let value;
|
|
449
|
+
try {
|
|
450
|
+
value = JSON.parse(text);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
throw new PlatformReaderError("INVALID_RESPONSE", "OpenBKN platform returned an invalid response.", { cause: error });
|
|
453
|
+
}
|
|
454
|
+
return value;
|
|
455
|
+
}
|
|
456
|
+
admit(value) {
|
|
457
|
+
if (new TextEncoder().encode(JSON.stringify(value)).byteLength > this.config.maxResultBytes) throw new PlatformReaderError("OUTPUT_OVERFLOW", "OpenBKN context result exceeded the configured size limit.");
|
|
458
|
+
return value;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
function projectOperations$1(value) {
|
|
462
|
+
const source = record$1(value);
|
|
463
|
+
const entries = array(source?.entries ?? source?.operations).flatMap((entry) => {
|
|
464
|
+
const value = record$1(entry);
|
|
465
|
+
const operationId = string(value?.operation_id);
|
|
466
|
+
if (operationId === void 0) return [];
|
|
467
|
+
return [{
|
|
468
|
+
operation_id: operationId,
|
|
469
|
+
tool_name: string(value?.tool_name),
|
|
470
|
+
source_module: string(value?.source_module),
|
|
471
|
+
protocol: string(value?.protocol),
|
|
472
|
+
status: string(value?.status),
|
|
473
|
+
started_at: string(value?.started_at),
|
|
474
|
+
finished_at: string(value?.finished_at),
|
|
475
|
+
request_id: string(value?.request_id),
|
|
476
|
+
trace_id: string(value?.trace_id),
|
|
477
|
+
receipt_id: string(value?.receipt_id)
|
|
478
|
+
}];
|
|
479
|
+
});
|
|
480
|
+
return json({
|
|
481
|
+
entries,
|
|
482
|
+
total: number(source?.total) ?? entries.length
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function projectBusinessGraph(value) {
|
|
486
|
+
const source = record$1(value);
|
|
487
|
+
const assembly = record$1(source?.assembly);
|
|
488
|
+
const projectRef = (candidate) => {
|
|
489
|
+
const ref = record$1(candidate);
|
|
490
|
+
const technical = record$1(ref?.technical_ref);
|
|
491
|
+
const display = record$1(ref?.display);
|
|
492
|
+
const id = string(technical?.ref_id);
|
|
493
|
+
const type = string(technical?.ref_type);
|
|
494
|
+
const name = string(display?.name);
|
|
495
|
+
return id === void 0 || type === void 0 || name === void 0 ? void 0 : {
|
|
496
|
+
technical_ref: {
|
|
497
|
+
ref_id: id,
|
|
498
|
+
ref_type: type,
|
|
499
|
+
visibility: string(technical?.visibility),
|
|
500
|
+
version_status: string(technical?.version_status)
|
|
501
|
+
},
|
|
502
|
+
display: {
|
|
503
|
+
name,
|
|
504
|
+
resolution_status: string(display?.resolution_status),
|
|
505
|
+
source_version: string(display?.source_version)
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
const refs = array(assembly?.business_refs).flatMap((candidate) => {
|
|
510
|
+
const projected = projectRef(candidate);
|
|
511
|
+
return projected === void 0 ? [] : [projected];
|
|
512
|
+
});
|
|
513
|
+
const edges = array(assembly?.operation_business_edges).flatMap((candidate) => {
|
|
514
|
+
const edge = record$1(candidate);
|
|
515
|
+
const operationId = string(edge?.operation_id);
|
|
516
|
+
const ref = projectRef(edge?.business_ref);
|
|
517
|
+
return operationId === void 0 || ref === void 0 ? [] : [{
|
|
518
|
+
operation_id: operationId,
|
|
519
|
+
role: string(edge?.role),
|
|
520
|
+
observed_at: string(edge?.observed_at),
|
|
521
|
+
business_ref: ref
|
|
522
|
+
}];
|
|
523
|
+
});
|
|
524
|
+
const events = array(source?.events).flatMap((candidate) => {
|
|
525
|
+
const event = record$1(candidate);
|
|
526
|
+
const eventId = string(event?.event_id);
|
|
527
|
+
const eventType = string(event?.event_type);
|
|
528
|
+
return eventId === void 0 || eventType === void 0 ? [] : [{
|
|
529
|
+
event_id: eventId,
|
|
530
|
+
event_type: eventType,
|
|
531
|
+
layer: string(event?.layer),
|
|
532
|
+
operation_id: string(event?.operation_id)
|
|
533
|
+
}];
|
|
534
|
+
});
|
|
535
|
+
return json({
|
|
536
|
+
interaction_id: string(source?.interaction_id),
|
|
537
|
+
conversation_id: string(source?.conversation_id),
|
|
538
|
+
execution_status: string(source?.execution_status),
|
|
539
|
+
evidence_status: string(source?.evidence_status),
|
|
540
|
+
assembly: {
|
|
541
|
+
business_refs: refs,
|
|
542
|
+
operation_business_edges: edges
|
|
543
|
+
},
|
|
544
|
+
events
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
/** Hosts for which plaintext http is tolerated regardless of the insecure switch. */
|
|
548
|
+
function isLoopbackHost(hostname) {
|
|
549
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Scheme fence shared by every outbound OpenBKN path so the Bearer token can
|
|
553
|
+
* never be configured onto a plaintext or unexpected endpoint: https is
|
|
554
|
+
* required outside loopback unless plaintext was explicitly allowed.
|
|
555
|
+
*/
|
|
556
|
+
function assertHttpsEndpoint(url, allowPlaintext) {
|
|
557
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && (isLoopbackHost(url.hostname) || allowPlaintext))) throw new PlatformReaderError("PLATFORM_MISMATCH", "OpenBKN platform URL must use HTTPS outside loopback.");
|
|
558
|
+
}
|
|
559
|
+
function fixedUrl(baseUrl, path, allowInsecureTls) {
|
|
560
|
+
let url;
|
|
561
|
+
try {
|
|
562
|
+
url = new URL(path, `${normalizeBaseUrl$5(baseUrl)}/`);
|
|
563
|
+
} catch {
|
|
564
|
+
throw new PlatformReaderError("PLATFORM_MISMATCH", "OpenBKN platform URL is not configured.");
|
|
565
|
+
}
|
|
566
|
+
assertHttpsEndpoint(url, allowInsecureTls);
|
|
567
|
+
return url;
|
|
568
|
+
}
|
|
569
|
+
function normalizeBaseUrl$5(value) {
|
|
570
|
+
return value.trim().replace(/\/+$/, "");
|
|
571
|
+
}
|
|
572
|
+
function record$1(value) {
|
|
573
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
574
|
+
}
|
|
575
|
+
function safeParse(text) {
|
|
576
|
+
try {
|
|
577
|
+
return JSON.parse(text);
|
|
578
|
+
} catch {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/** Read a response body as text, aborting the stream once the byte cap is exceeded. */
|
|
583
|
+
async function readCappedBody(response, capBytes) {
|
|
584
|
+
if (response.body === null) return response.text();
|
|
585
|
+
const reader = response.body.getReader();
|
|
586
|
+
const chunks = [];
|
|
587
|
+
let received = 0;
|
|
588
|
+
for (;;) {
|
|
589
|
+
const { done, value } = await reader.read();
|
|
590
|
+
if (done === true) break;
|
|
591
|
+
if (value === void 0) continue;
|
|
592
|
+
received += value.byteLength;
|
|
593
|
+
if (received > capBytes) {
|
|
594
|
+
await reader.cancel().catch(() => void 0);
|
|
595
|
+
throw new PlatformReaderError("OUTPUT_OVERFLOW", "OpenBKN platform response exceeded the safe Host limit.");
|
|
596
|
+
}
|
|
597
|
+
chunks.push(value);
|
|
598
|
+
}
|
|
599
|
+
const merged = new Uint8Array(received);
|
|
600
|
+
let offset = 0;
|
|
601
|
+
for (const chunk of chunks) {
|
|
602
|
+
merged.set(chunk, offset);
|
|
603
|
+
offset += chunk.byteLength;
|
|
604
|
+
}
|
|
605
|
+
return new TextDecoder().decode(merged);
|
|
606
|
+
}
|
|
607
|
+
function array(value) {
|
|
608
|
+
return Array.isArray(value) ? value : [];
|
|
609
|
+
}
|
|
610
|
+
function string(value) {
|
|
611
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, 512) : void 0;
|
|
612
|
+
}
|
|
613
|
+
function number(value) {
|
|
614
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
615
|
+
}
|
|
616
|
+
/** Removes absent optional fields before the value crosses the Host boundary. */
|
|
617
|
+
function json(value) {
|
|
618
|
+
return JSON.parse(JSON.stringify(value));
|
|
619
|
+
}
|
|
620
|
+
//#endregion
|
|
621
|
+
//#region src/auth.ts
|
|
622
|
+
/** Thrown when the CLI cannot provide a trustworthy authentication status. */
|
|
623
|
+
var OpenBknCliError = class extends Error {
|
|
624
|
+
constructor(message) {
|
|
625
|
+
super(message);
|
|
626
|
+
this.name = "OpenBknCliError";
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
/**
|
|
630
|
+
* Owns authentication lifecycle decisions. OpenBKN CLI remains the credential
|
|
631
|
+
* store and refresh authority. Its token value is read only after an exact
|
|
632
|
+
* platform fence and is returned only to Host-side credential synchronization.
|
|
633
|
+
*/
|
|
634
|
+
var AuthCoordinator = class {
|
|
635
|
+
cli;
|
|
636
|
+
baseUrl;
|
|
637
|
+
constructor(cli, baseUrl) {
|
|
638
|
+
this.cli = cli;
|
|
639
|
+
this.baseUrl = normalizeBaseUrl$4(baseUrl);
|
|
640
|
+
}
|
|
641
|
+
/** Read the active CLI session and fence it to this plugin's configured platform. */
|
|
642
|
+
async status(signal) {
|
|
643
|
+
const result = await this.cli.run([
|
|
644
|
+
"auth",
|
|
645
|
+
"status",
|
|
646
|
+
"--json"
|
|
647
|
+
], signal);
|
|
648
|
+
if (result.code !== 0) throw cliFailure("read authentication status", result);
|
|
649
|
+
const status = parseStatus(result.stdout);
|
|
650
|
+
const actualBaseUrl = normalizeBaseUrl$4(status.baseUrl);
|
|
651
|
+
if (actualBaseUrl !== this.baseUrl) return {
|
|
652
|
+
kind: "platform-mismatch",
|
|
653
|
+
expectedBaseUrl: this.baseUrl,
|
|
654
|
+
actualBaseUrl
|
|
655
|
+
};
|
|
656
|
+
if (!status.hasToken || status.expired) return {
|
|
657
|
+
kind: "authentication-required",
|
|
658
|
+
baseUrl: this.baseUrl
|
|
659
|
+
};
|
|
660
|
+
return {
|
|
661
|
+
kind: "authenticated",
|
|
662
|
+
baseUrl: this.baseUrl,
|
|
663
|
+
...status.userId === void 0 ? {} : { userId: status.userId },
|
|
664
|
+
...status.username === void 0 ? {} : { username: status.username }
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Ask the CLI to initiate its normal local-browser OAuth flow. The plugin
|
|
669
|
+
* deliberately does not use `--no-browser`: that variant requires a human
|
|
670
|
+
* to paste an OAuth callback into stdin, while DSH's managed subprocess has
|
|
671
|
+
* no terminal input surface. Its human-facing output is not persisted here.
|
|
672
|
+
*/
|
|
673
|
+
async beginLogin() {
|
|
674
|
+
const result = await this.cli.run([
|
|
675
|
+
"auth",
|
|
676
|
+
"login",
|
|
677
|
+
this.baseUrl
|
|
678
|
+
]);
|
|
679
|
+
if (result.code !== 0) throw cliFailure("start login", result);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Read one refreshed token for the configured, authenticated platform.
|
|
683
|
+
* Callers must immediately write it to DSH credentials and must never expose
|
|
684
|
+
* this value through a Remote result, UI state, diagnostics, or logs.
|
|
685
|
+
*/
|
|
686
|
+
async readToken(signal) {
|
|
687
|
+
if ((await this.status(signal)).kind !== "authenticated") throw new OpenBknCliError("OpenBKN CLI is not authenticated for the configured platform");
|
|
688
|
+
const result = await this.cli.run(["auth", "token"], signal);
|
|
689
|
+
if (result.code !== 0) throw cliFailure("read authentication token", result);
|
|
690
|
+
const token = result.stdout.trim();
|
|
691
|
+
if (token.length === 0 || token.length > 16384 || /[\r\n]/.test(token)) throw new OpenBknCliError("OpenBKN CLI returned an invalid authentication token");
|
|
692
|
+
return token;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
function parseStatus(stdout) {
|
|
696
|
+
let value;
|
|
697
|
+
try {
|
|
698
|
+
value = JSON.parse(stdout);
|
|
699
|
+
} catch {
|
|
700
|
+
throw new OpenBknCliError("OpenBKN CLI returned invalid JSON for auth status");
|
|
701
|
+
}
|
|
702
|
+
if (typeof value !== "object" || value === null) throw new OpenBknCliError("OpenBKN CLI returned an invalid auth status payload");
|
|
703
|
+
const candidate = value;
|
|
704
|
+
if (typeof candidate.baseUrl !== "string" || typeof candidate.hasToken !== "boolean" || typeof candidate.expired !== "boolean") throw new OpenBknCliError("OpenBKN CLI auth status is missing required fields");
|
|
705
|
+
if (candidate.userId !== void 0 && typeof candidate.userId !== "string") throw new OpenBknCliError("OpenBKN CLI auth status contains an invalid user id");
|
|
706
|
+
if (candidate.username !== void 0 && typeof candidate.username !== "string") throw new OpenBknCliError("OpenBKN CLI auth status contains an invalid username");
|
|
707
|
+
return {
|
|
708
|
+
baseUrl: candidate.baseUrl,
|
|
709
|
+
hasToken: candidate.hasToken,
|
|
710
|
+
expired: candidate.expired,
|
|
711
|
+
...candidate.userId === void 0 ? {} : { userId: candidate.userId },
|
|
712
|
+
...candidate.username === void 0 ? {} : { username: candidate.username }
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function normalizeBaseUrl$4(value) {
|
|
716
|
+
return value.replace(/\/+$/, "");
|
|
717
|
+
}
|
|
718
|
+
function cliFailure(action, result) {
|
|
719
|
+
return new OpenBknCliError(`OpenBKN CLI could not ${action}: ${result.stderr.trim() || "no diagnostic output"}`);
|
|
720
|
+
}
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region src/openbkn-cli-subprocess.ts
|
|
723
|
+
const OUTPUT_LIMIT = 65536;
|
|
724
|
+
const GRACE_MS = 3e3;
|
|
725
|
+
/** DSH-managed invocation of the OpenBKN CLI with a fixed authentication contract. */
|
|
726
|
+
var OpenBknCliSubprocess = class {
|
|
727
|
+
subprocess;
|
|
728
|
+
cwd;
|
|
729
|
+
cliPath;
|
|
730
|
+
baseUrl;
|
|
731
|
+
constructor(subprocess, cwd, baseUrl, cliPath = "openbkn") {
|
|
732
|
+
this.subprocess = subprocess;
|
|
733
|
+
this.cwd = cwd;
|
|
734
|
+
this.cliPath = cliPath;
|
|
735
|
+
this.baseUrl = normalizeBaseUrl$3(baseUrl);
|
|
736
|
+
}
|
|
737
|
+
async run(args, signal) {
|
|
738
|
+
const argv = this.resolveArgv(args);
|
|
739
|
+
const child = this.subprocess.spawn({
|
|
740
|
+
argv,
|
|
741
|
+
cwd: this.cwd,
|
|
742
|
+
stdio: {
|
|
743
|
+
stdin: { data: "" },
|
|
744
|
+
stdout: { maxBytes: OUTPUT_LIMIT },
|
|
745
|
+
stderr: { maxBytes: OUTPUT_LIMIT }
|
|
746
|
+
},
|
|
747
|
+
graceMs: GRACE_MS,
|
|
748
|
+
...signal === void 0 ? {} : { signal }
|
|
749
|
+
});
|
|
750
|
+
const outcome = await child.done;
|
|
751
|
+
const stdout = child.collected.stdout?.readFrom(0);
|
|
752
|
+
const stderr = child.collected.stderr?.readFrom(0);
|
|
753
|
+
if (stdout?.lossy || stderr?.lossy) throw new Error("OpenBKN CLI output exceeded the safe size limit.");
|
|
754
|
+
return {
|
|
755
|
+
code: outcome.exitCode ?? 1,
|
|
756
|
+
stdout: stdout?.text ?? "",
|
|
757
|
+
stderr: stderr?.text ?? ""
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
resolveArgv(args) {
|
|
761
|
+
if (isAuthStatusCommand(args)) return [
|
|
762
|
+
this.cliPath,
|
|
763
|
+
"auth",
|
|
764
|
+
"status",
|
|
765
|
+
"--json"
|
|
766
|
+
];
|
|
767
|
+
if (isAuthTokenCommand(args)) return [
|
|
768
|
+
this.cliPath,
|
|
769
|
+
"auth",
|
|
770
|
+
"token"
|
|
771
|
+
];
|
|
772
|
+
if (isAuthLoginCommand(args, this.baseUrl)) return [
|
|
773
|
+
this.cliPath,
|
|
774
|
+
"auth",
|
|
775
|
+
"login",
|
|
776
|
+
this.baseUrl
|
|
777
|
+
];
|
|
778
|
+
throw new Error("OpenBKN CLI command is not allowed by this plugin.");
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
function isAuthStatusCommand(args) {
|
|
782
|
+
return args.length === 3 && args[0] === "auth" && args[1] === "status" && args[2] === "--json";
|
|
783
|
+
}
|
|
784
|
+
function isAuthTokenCommand(args) {
|
|
785
|
+
return args.length === 2 && args[0] === "auth" && args[1] === "token";
|
|
786
|
+
}
|
|
787
|
+
function isAuthLoginCommand(args, baseUrl) {
|
|
788
|
+
return args.length === 3 && args[0] === "auth" && args[1] === "login" && normalizeBaseUrl$3(args[2] ?? "") === baseUrl;
|
|
789
|
+
}
|
|
790
|
+
function normalizeBaseUrl$3(value) {
|
|
791
|
+
return value.trim().replace(/\/+$/, "");
|
|
792
|
+
}
|
|
793
|
+
//#endregion
|
|
794
|
+
//#region src/openbkn-mcp-manager.ts
|
|
795
|
+
const OPENBKN_MCP_TOKEN_REF = "OPENBKN_MCP_TOKEN";
|
|
796
|
+
const REQUIRED_TOOL = "mcp__openbkn__bkn_start_interaction";
|
|
797
|
+
/**
|
|
798
|
+
* Mounts the standard DSH MCP client once the user has stored an OpenBKN
|
|
799
|
+
* token. It resolves the credential immediately before mounting, and passes it
|
|
800
|
+
* only to the in-memory standard MCP client configuration. An explicitly configured client with the same public tool
|
|
801
|
+
* namespace is treated as the deployment-owned instance.
|
|
802
|
+
*/
|
|
803
|
+
var OpenBknMcpManager = class {
|
|
804
|
+
ctx;
|
|
805
|
+
config;
|
|
806
|
+
resolveToken;
|
|
807
|
+
starting;
|
|
808
|
+
fiber;
|
|
809
|
+
constructor(ctx, config, resolveToken) {
|
|
810
|
+
this.ctx = ctx;
|
|
811
|
+
this.config = config;
|
|
812
|
+
this.resolveToken = resolveToken;
|
|
813
|
+
}
|
|
814
|
+
reloading;
|
|
815
|
+
async ensure() {
|
|
816
|
+
if (this.reloading !== void 0) await this.reloading.catch(() => void 0);
|
|
817
|
+
await this.ensureMounted();
|
|
818
|
+
}
|
|
819
|
+
async ensureMounted() {
|
|
820
|
+
if (this.ctx.tools.get(REQUIRED_TOOL) !== void 0) return;
|
|
821
|
+
if (this.starting === void 0) this.starting = this.start().finally(() => {
|
|
822
|
+
this.starting = void 0;
|
|
823
|
+
});
|
|
824
|
+
await this.starting;
|
|
825
|
+
}
|
|
826
|
+
/** Reconnect the client after DSH rotates the managed credential. */
|
|
827
|
+
async refresh() {
|
|
828
|
+
await this.starting;
|
|
829
|
+
if (this.reloading !== void 0) return this.reloading;
|
|
830
|
+
this.reloading = (async () => {
|
|
831
|
+
const fiber = this.fiber;
|
|
832
|
+
this.fiber = void 0;
|
|
833
|
+
await fiber?.dispose();
|
|
834
|
+
await this.ensureMounted();
|
|
835
|
+
})().finally(() => {
|
|
836
|
+
this.reloading = void 0;
|
|
837
|
+
});
|
|
838
|
+
return this.reloading;
|
|
839
|
+
}
|
|
840
|
+
async start() {
|
|
841
|
+
const token = await this.resolveToken();
|
|
842
|
+
if (token === void 0 || token.length === 0) throw new Error("OpenBKN Context Loader MCP requires a configured token.");
|
|
843
|
+
const fiber = await this.ctx.plugin(McpClient, McpClient.Config({
|
|
844
|
+
transport: "streamable-http",
|
|
845
|
+
serverName: "openbkn",
|
|
846
|
+
url: resolveMcpUrl(this.config),
|
|
847
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
848
|
+
toolCallTimeoutMs: 2e4,
|
|
849
|
+
failOnStartupError: true,
|
|
850
|
+
reconnect: {
|
|
851
|
+
enabled: true,
|
|
852
|
+
initialDelayMs: 500,
|
|
853
|
+
maxDelayMs: 3e4,
|
|
854
|
+
maxAttempts: 10
|
|
855
|
+
}
|
|
856
|
+
}));
|
|
857
|
+
this.fiber = fiber;
|
|
858
|
+
if (this.ctx.tools.get(REQUIRED_TOOL) === void 0) {
|
|
859
|
+
this.fiber = void 0;
|
|
860
|
+
await fiber.dispose();
|
|
861
|
+
throw new Error("OpenBKN Context Loader MCP did not publish its managed interaction tools.");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
/**
|
|
866
|
+
* Resolve the stable Context Loader endpoint from the deployment platform URL.
|
|
867
|
+
* An explicitly configured mcpUrl gets the same scheme fence as the control
|
|
868
|
+
* plane plus an origin fence: the Bearer token mounted on this client may
|
|
869
|
+
* only ever be sent to the configured platform (or an explicitly allowed
|
|
870
|
+
* plaintext loopback), never to an arbitrary host.
|
|
871
|
+
*/
|
|
872
|
+
function resolveMcpUrl(config) {
|
|
873
|
+
const base = new URL(config.baseUrl);
|
|
874
|
+
let url;
|
|
875
|
+
if (config.mcpUrl !== void 0 && config.mcpUrl.trim().length > 0) {
|
|
876
|
+
try {
|
|
877
|
+
url = new URL(config.mcpUrl);
|
|
878
|
+
} catch {
|
|
879
|
+
throw new Error("OpenBKN mcpUrl is not a valid URL.");
|
|
880
|
+
}
|
|
881
|
+
assertHttpsEndpoint(url, config.allowInsecureTls);
|
|
882
|
+
const sameOrigin = url.origin === base.origin;
|
|
883
|
+
const bothLoopback = isLoopbackHost(url.hostname) && isLoopbackHost(base.hostname);
|
|
884
|
+
if (!sameOrigin && !bothLoopback) throw new Error(`OpenBKN mcpUrl must stay on the configured platform (${base.origin}); refusing to send credentials to ${url.origin}.`);
|
|
885
|
+
} else url = new URL("/api/agent-retrieval/v1/mcp/", base);
|
|
886
|
+
return url.toString();
|
|
887
|
+
}
|
|
888
|
+
//#endregion
|
|
889
|
+
//#region src/provenance-view.ts
|
|
890
|
+
/**
|
|
891
|
+
* Projects Community execution facts plus the exact EE-owned interaction
|
|
892
|
+
* projection. It never derives BKN mappings from raw Core or MCP payloads.
|
|
893
|
+
*/
|
|
894
|
+
function buildProvenanceView(handle, operationsResponse, enterpriseProjection, limits) {
|
|
895
|
+
return {
|
|
896
|
+
interactionId: handle.interactionId,
|
|
897
|
+
execution: {
|
|
898
|
+
status: handle.status,
|
|
899
|
+
operations: projectOperations(operationRecords(operationsResponse))
|
|
900
|
+
},
|
|
901
|
+
business: projectBusiness(enterpriseProjection, operationsResponse, limits),
|
|
902
|
+
evidence: { kind: "unavailable" }
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function projectOperations(value) {
|
|
906
|
+
return records(value).flatMap((entry) => {
|
|
907
|
+
const id = stringValue(entry.operation_id);
|
|
908
|
+
if (id === void 0) return [];
|
|
909
|
+
return [{
|
|
910
|
+
id,
|
|
911
|
+
label: stringValue(entry.tool_name) ?? stringValue(entry.source_module) ?? stringValue(entry.protocol) ?? "OpenBKN operation",
|
|
912
|
+
protocol: stringValue(entry.protocol),
|
|
913
|
+
status: stringValue(entry.call_status) ?? stringValue(entry.execution_status) ?? stringValue(entry.status),
|
|
914
|
+
startedAt: stringValue(entry.started_at),
|
|
915
|
+
finishedAt: stringValue(entry.finished_at),
|
|
916
|
+
requestId: stringValue(entry.request_id),
|
|
917
|
+
traceId: stringValue(entry.trace_id),
|
|
918
|
+
receiptId: stringValue(entry.receipt_id)
|
|
919
|
+
}];
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
function projectBusiness(value, operationsResponse, limits) {
|
|
923
|
+
const graph = record(value);
|
|
924
|
+
const assembly = graph === void 0 ? void 0 : record(graph.assembly);
|
|
925
|
+
if (assembly === void 0) return { kind: "unavailable" };
|
|
926
|
+
const elementsRemaining = { value: limits.maxGraphNodes };
|
|
927
|
+
const edgesRemaining = { value: limits.maxGraphEdges };
|
|
928
|
+
const coreLabels = new Map(projectOperations(operationRecords(operationsResponse)).map((operation) => [operation.id, operation.label]));
|
|
929
|
+
const byOperation = /* @__PURE__ */ new Map();
|
|
930
|
+
for (const edge of records(assembly.operation_business_edges)) {
|
|
931
|
+
if (edgesRemaining.value < 1) break;
|
|
932
|
+
const operationId = stringValue(edge.operation_id);
|
|
933
|
+
const element = projectTraceBusinessRef(record(edge.business_ref), elementsRemaining);
|
|
934
|
+
if (operationId === void 0 || element === void 0) continue;
|
|
935
|
+
edgesRemaining.value -= 1;
|
|
936
|
+
const existing = byOperation.get(operationId);
|
|
937
|
+
if (existing !== void 0) {
|
|
938
|
+
byOperation.set(operationId, {
|
|
939
|
+
...existing,
|
|
940
|
+
elements: [...existing.elements, element]
|
|
941
|
+
});
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
byOperation.set(operationId, {
|
|
945
|
+
id: operationId,
|
|
946
|
+
attempt: 0,
|
|
947
|
+
toolName: coreLabels.get(operationId) ?? "OpenBKN operation",
|
|
948
|
+
status: "resolved",
|
|
949
|
+
elements: [element],
|
|
950
|
+
missingFacts: []
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
kind: "ready",
|
|
955
|
+
operations: [...byOperation.values()],
|
|
956
|
+
conversationContext: [],
|
|
957
|
+
contextRelations: [],
|
|
958
|
+
derivedFacts: []
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function projectTraceBusinessRef(entry, remaining) {
|
|
962
|
+
if (entry === void 0 || remaining.value < 1) return void 0;
|
|
963
|
+
const technicalRef = record(entry.technical_ref);
|
|
964
|
+
const display = record(entry.display);
|
|
965
|
+
const id = technicalRef === void 0 ? void 0 : stringValue(technicalRef.ref_id);
|
|
966
|
+
const kind = technicalRef === void 0 ? void 0 : traceRefKind(technicalRef.ref_type);
|
|
967
|
+
const name = display === void 0 ? void 0 : stringValue(display.name);
|
|
968
|
+
if (id === void 0 || kind === void 0 || name === void 0) return void 0;
|
|
969
|
+
remaining.value -= 1;
|
|
970
|
+
return {
|
|
971
|
+
id,
|
|
972
|
+
kind,
|
|
973
|
+
name
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function record(value) {
|
|
977
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
978
|
+
}
|
|
979
|
+
function records(value) {
|
|
980
|
+
return Array.isArray(value) ? value.flatMap((item) => record(item) === void 0 ? [] : [record(item)]) : [];
|
|
981
|
+
}
|
|
982
|
+
/** Community deployments expose either `entries` or the documented `operations` list. */
|
|
983
|
+
function operationRecords(value) {
|
|
984
|
+
const response = record(value);
|
|
985
|
+
const candidates = response?.entries ?? response?.operations;
|
|
986
|
+
return Array.isArray(candidates) ? candidates : [];
|
|
987
|
+
}
|
|
988
|
+
function stringValue(value) {
|
|
989
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, 512) : void 0;
|
|
990
|
+
}
|
|
991
|
+
function traceRefKind(value) {
|
|
992
|
+
switch (value) {
|
|
993
|
+
case "object":
|
|
994
|
+
case "object_type":
|
|
995
|
+
case "object_instance":
|
|
996
|
+
case "knowledge_network": return "object";
|
|
997
|
+
case "relation":
|
|
998
|
+
case "relation_type": return "relation";
|
|
999
|
+
case "action":
|
|
1000
|
+
case "action_type": return "action";
|
|
1001
|
+
case "property":
|
|
1002
|
+
case "property_type": return "property";
|
|
1003
|
+
case "logic":
|
|
1004
|
+
case "logic_property": return "logic";
|
|
1005
|
+
case "metric": return "metric";
|
|
1006
|
+
default: return;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
//#endregion
|
|
1010
|
+
//#region src/network-capability-profile.ts
|
|
1011
|
+
const MAX_ENTRIES = 48;
|
|
1012
|
+
const MAX_IDENTIFIER_LENGTH = 160;
|
|
1013
|
+
/**
|
|
1014
|
+
* Project the platform summary into the small, non-instructional index the
|
|
1015
|
+
* managed-session policy may expose to an Agent. The original platform payload
|
|
1016
|
+
* is deliberately not retained.
|
|
1017
|
+
*/
|
|
1018
|
+
function buildNetworkCapabilityProfile(binding, payload) {
|
|
1019
|
+
const detail = detailOf(payload);
|
|
1020
|
+
const networkId = identifier(detail.id);
|
|
1021
|
+
if (networkId === void 0 || networkId !== binding.knowledgeNetworkId) throw new Error("OpenBKN knowledge-network detail does not match the selected network.");
|
|
1022
|
+
if (!Array.isArray(detail.object_types) || !Array.isArray(detail.concept_groups) || !Array.isArray(detail.relation_types)) throw new Error("OpenBKN knowledge-network detail is not a summary response.");
|
|
1023
|
+
const profile = {
|
|
1024
|
+
schemaVersion: 1,
|
|
1025
|
+
knowledgeNetworkId: networkId,
|
|
1026
|
+
conceptGroups: named(detail.concept_groups),
|
|
1027
|
+
objectTypes: named(detail.object_types),
|
|
1028
|
+
relationTypes: relations(detail.relation_types),
|
|
1029
|
+
actionTypes: named(Array.isArray(detail.action_types) ? detail.action_types : [])
|
|
1030
|
+
};
|
|
1031
|
+
return {
|
|
1032
|
+
...profile,
|
|
1033
|
+
profileVersion: createHash("sha256").update(JSON.stringify(profile)).digest("hex").slice(0, 16)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function detailOf(payload) {
|
|
1037
|
+
if (!isRecord(payload)) throw new Error("OpenBKN knowledge-network detail is not a summary response.");
|
|
1038
|
+
return isRecord(payload.result) ? payload.result : payload;
|
|
1039
|
+
}
|
|
1040
|
+
function named(values) {
|
|
1041
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1042
|
+
const result = [];
|
|
1043
|
+
for (const value of values) {
|
|
1044
|
+
if (result.length === MAX_ENTRIES || !isRecord(value)) continue;
|
|
1045
|
+
const id = identifier(value.id);
|
|
1046
|
+
if (id === void 0 || seen.has(id)) continue;
|
|
1047
|
+
seen.add(id);
|
|
1048
|
+
const name = identifier(value.name);
|
|
1049
|
+
result.push({
|
|
1050
|
+
id,
|
|
1051
|
+
...name === void 0 ? {} : { name }
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
return result;
|
|
1055
|
+
}
|
|
1056
|
+
function relations(values) {
|
|
1057
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1058
|
+
const result = [];
|
|
1059
|
+
for (const value of values) {
|
|
1060
|
+
if (result.length === MAX_ENTRIES || !isRecord(value)) continue;
|
|
1061
|
+
const id = identifier(value.id) ?? identifier(value.relation_type_id);
|
|
1062
|
+
const sourceObjectTypeId = identifier(value.source_object_type_id) ?? nestedId(value.source_object_type);
|
|
1063
|
+
const targetObjectTypeId = identifier(value.target_object_type_id) ?? nestedId(value.target_object_type);
|
|
1064
|
+
if (id === void 0 || sourceObjectTypeId === void 0 || targetObjectTypeId === void 0 || seen.has(id)) continue;
|
|
1065
|
+
seen.add(id);
|
|
1066
|
+
result.push({
|
|
1067
|
+
id,
|
|
1068
|
+
sourceObjectTypeId,
|
|
1069
|
+
targetObjectTypeId
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
return result;
|
|
1073
|
+
}
|
|
1074
|
+
function nestedId(value) {
|
|
1075
|
+
return isRecord(value) ? identifier(value.id) : void 0;
|
|
1076
|
+
}
|
|
1077
|
+
function identifier(value) {
|
|
1078
|
+
if (typeof value !== "string") return void 0;
|
|
1079
|
+
const normalized = value.trim();
|
|
1080
|
+
return normalized.length > 0 && normalized.length <= MAX_IDENTIFIER_LENGTH ? normalized : void 0;
|
|
1081
|
+
}
|
|
1082
|
+
function isRecord(value) {
|
|
1083
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1084
|
+
}
|
|
1085
|
+
//#endregion
|
|
1086
|
+
//#region src/managed-session-policy.ts
|
|
1087
|
+
/**
|
|
1088
|
+
* Stable lifecycle identity written by OpenBKN Trace for this plugin surface.
|
|
1089
|
+
* It is not a user, workspace, model, or knowledge-network identifier.
|
|
1090
|
+
*/
|
|
1091
|
+
const OPENBKN_DSH_INTERACTION_AGENT_NAME = "bkn-agent-dsh-business-context";
|
|
1092
|
+
/**
|
|
1093
|
+
* Produce the two scoped system-prompt sections for one bound network. The
|
|
1094
|
+
* fixed section renders the Host-verified identity as quoted data; the optional
|
|
1095
|
+
* profile remains a projection rather than a copy of platform metadata.
|
|
1096
|
+
*/
|
|
1097
|
+
function buildManagedSessionPolicy(binding, profile) {
|
|
1098
|
+
return {
|
|
1099
|
+
governance: [
|
|
1100
|
+
"Bound OpenBKN knowledge network:",
|
|
1101
|
+
`- kn_id: ${JSON.stringify(binding.knowledgeNetworkId)}`,
|
|
1102
|
+
`- kn_name: ${JSON.stringify(binding.displayName)}`,
|
|
1103
|
+
"The binding above is authoritative for this DSH session.",
|
|
1104
|
+
`For every OpenBKN MCP tool that accepts \`kn_id\`, pass exactly ${JSON.stringify(binding.knowledgeNetworkId)}. Do not omit, discover, or infer \`kn_id\` from skills, schema results, or tool output.`,
|
|
1105
|
+
"bkn_start_interaction manages only the conversation and interaction lifecycle; its response does not replace or unset the bound knowledge network.",
|
|
1106
|
+
"bkn_start_interaction only accepts its documented lifecycle fields: conversation_mode, question, and agent_name; never pass kn_id or query to it.",
|
|
1107
|
+
`For every bkn_start_interaction call, pass agent_name exactly ${JSON.stringify(OPENBKN_DSH_INTERACTION_AGENT_NAME)}. Keep this stable when continuing the Conversation.`,
|
|
1108
|
+
"For the first question use conversation_mode \"new\" without a conversation_id. For later questions use conversation_mode \"continue\" with the conversation_id returned by the prior interaction.",
|
|
1109
|
+
"For each user question, start exactly one mcp__openbkn__bkn_start_interaction before business retrieval and finish it with mcp__openbkn__bkn_finish_interaction using the final outcome.",
|
|
1110
|
+
"Do not probe Bash or a tool list to test OpenBKN availability. Even if managed MCP tools are not listed in the initial tool catalog, directly call mcp__openbkn__bkn_start_interaction.",
|
|
1111
|
+
"If bkn_start_interaction returns a retryable error, retry at most once. If that retry fails, do not retry again or perform business retrieval; report the platform condition briefly.",
|
|
1112
|
+
"Use only mcp__openbkn__ tools for business data. Do not invent facts, identifiers, metrics, tool results, or provenance. State limits and missing data plainly.",
|
|
1113
|
+
"Route a single fact to query_object_instance; a defined aggregate to query_metric; and BOM expansion, availability, substitution, common-material, reverse lookup, or delivery calculations to a matching published tool via search_tools then execute_tool. Pass only the documented business parameters.",
|
|
1114
|
+
"Use search_schema then targeted get_object_types or get_relation_types when the needed schema is unknown. Schema fallback rule: if get_kn_detail returns a rendering, structured-output, or validation error, do not retry get_kn_detail with another format or detail_level. Preserve the bound kn_id. Use search_schema at most once, only when it can directly answer the requested schema question; do not use it to derive an exact exhaustive count or list. If it cannot directly answer, finish the Interaction as failed and state that the schema detail is unavailable.",
|
|
1115
|
+
"Use run_code only as a read-only fallback for a business calculation when no matching published tool is available or its result cannot be obtained. Keep the bound kn_id and managed interaction context in every OpenBKN call made by the script. Do not use run_shell, run_sql, resources, or action execution unless the deployment explicitly enables them."
|
|
1116
|
+
].join("\n"),
|
|
1117
|
+
capabilities: profile === void 0 ? "" : capabilitySection(profile)
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
function capabilitySection(profile) {
|
|
1121
|
+
const named = (values) => values.map((value) => value.name === void 0 ? value.id : `${value.id} (${value.name})`).join(", ") || "none declared";
|
|
1122
|
+
const relations = profile.relationTypes.map((value) => `${value.id}: ${value.sourceObjectTypeId} → ${value.targetObjectTypeId}`).join(", ") || "none declared";
|
|
1123
|
+
return [
|
|
1124
|
+
`OpenBKN network capability index for ${profile.knowledgeNetworkId} (profile ${profile.profileVersion}; authoritative schema remains MCP):`,
|
|
1125
|
+
`Concept groups: ${named(profile.conceptGroups)}.`,
|
|
1126
|
+
`Object types: ${named(profile.objectTypes)}.`,
|
|
1127
|
+
`Relations: ${relations}.`,
|
|
1128
|
+
`Action types: ${named(profile.actionTypes)}.`,
|
|
1129
|
+
"For a business function, search_tools first and obey the returned use_rule and input schema before execute_tool."
|
|
1130
|
+
].join("\n");
|
|
1131
|
+
}
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region src/scoped-business-context.ts
|
|
1134
|
+
const MANAGED_OPENBKN_TOOLS = [
|
|
1135
|
+
"mcp__openbkn__bkn_start_interaction",
|
|
1136
|
+
"mcp__openbkn__bkn_finish_interaction",
|
|
1137
|
+
"mcp__openbkn__get_kn_detail",
|
|
1138
|
+
"mcp__openbkn__search_schema",
|
|
1139
|
+
"mcp__openbkn__get_object_types",
|
|
1140
|
+
"mcp__openbkn__get_relation_types",
|
|
1141
|
+
"mcp__openbkn__query_object_instance",
|
|
1142
|
+
"mcp__openbkn__query_instance_subgraph",
|
|
1143
|
+
"mcp__openbkn__explore_subgraph",
|
|
1144
|
+
"mcp__openbkn__search_instance",
|
|
1145
|
+
"mcp__openbkn__query_metric",
|
|
1146
|
+
"mcp__openbkn__get_logic_properties_values",
|
|
1147
|
+
"mcp__openbkn__run_code",
|
|
1148
|
+
"mcp__openbkn__list_skills",
|
|
1149
|
+
"mcp__openbkn__find_skills",
|
|
1150
|
+
"mcp__openbkn__get_skill_content",
|
|
1151
|
+
"mcp__openbkn__read_skill_file",
|
|
1152
|
+
"mcp__openbkn__search_tools",
|
|
1153
|
+
"mcp__openbkn__execute_tool"
|
|
1154
|
+
];
|
|
1155
|
+
/** Agent-scoped prompt rows: only mounted after the session has a compatible binding. */
|
|
1156
|
+
const scopedPolicyPlugin = (binding, profile) => ({
|
|
1157
|
+
name: "openbkn-business-context-policy",
|
|
1158
|
+
inject: ["systemPrompt", "tools"],
|
|
1159
|
+
apply(ctx) {
|
|
1160
|
+
if (binding === void 0) return;
|
|
1161
|
+
const policy = buildManagedSessionPolicy(binding, profile);
|
|
1162
|
+
const systemPrompt = ctx.systemPrompt;
|
|
1163
|
+
ctx.tools.guard((execution) => MANAGED_OPENBKN_TOOLS.includes(execution.name) ? void 0 : "This OpenBKN business session only permits managed OpenBKN tools.");
|
|
1164
|
+
systemPrompt.section({
|
|
1165
|
+
name: "openbkn:managed-session",
|
|
1166
|
+
order: 520,
|
|
1167
|
+
text: policy.governance
|
|
1168
|
+
});
|
|
1169
|
+
if (policy.capabilities.length > 0) systemPrompt.section({
|
|
1170
|
+
name: "openbkn:network-capabilities",
|
|
1171
|
+
order: 521,
|
|
1172
|
+
text: policy.capabilities
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
/**
|
|
1177
|
+
* Mount the model-visible OpenBKN tool in one Agent's Cordis scope. An
|
|
1178
|
+
* unbound or differently configured session gets no registration at all, so
|
|
1179
|
+
* native DSH conversations keep their original tool catalogue.
|
|
1180
|
+
*/
|
|
1181
|
+
function mountBoundBusinessNetworkTool(agent, config, profile) {
|
|
1182
|
+
const binding = readDshSessionBusinessNetwork(agent.session);
|
|
1183
|
+
if (binding === void 0 || normalizeBaseUrl$2(binding.platformBaseUrl) !== normalizeBaseUrl$2(config.baseUrl)) return false;
|
|
1184
|
+
scopedPolicyPlugin(binding, profile).apply(agent.ctx);
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
function normalizeBaseUrl$2(value) {
|
|
1188
|
+
return value.trim().replace(/\/+$/, "");
|
|
1189
|
+
}
|
|
1190
|
+
//#endregion
|
|
1191
|
+
//#region src/suggested-prompts.ts
|
|
1192
|
+
const MAX_NETWORK_NAME_LENGTH = 400;
|
|
1193
|
+
/** The only prompt shown before a bound business session has a completed answer. */
|
|
1194
|
+
function emptyBusinessSessionPrompt(displayName) {
|
|
1195
|
+
const name = displayName.trim();
|
|
1196
|
+
if (name.length === 0 || name.length > MAX_NETWORK_NAME_LENGTH || /[\u0000-\u001F\u007F]/.test(name)) throw new TypeError("OpenBKN business network name is invalid.");
|
|
1197
|
+
return `了解「${name}」知识网络。`;
|
|
1198
|
+
}
|
|
1199
|
+
//#endregion
|
|
1200
|
+
//#region src/business-context-service.ts
|
|
1201
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
1202
|
+
var useValue = arguments.length > 2;
|
|
1203
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
1204
|
+
return useValue ? value : void 0;
|
|
1205
|
+
};
|
|
1206
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
1207
|
+
function accept(f) {
|
|
1208
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
1209
|
+
return f;
|
|
1210
|
+
}
|
|
1211
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
1212
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
1213
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
1214
|
+
var _, done = false;
|
|
1215
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
1216
|
+
var context = {};
|
|
1217
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
1218
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
1219
|
+
context.addInitializer = function(f) {
|
|
1220
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
1221
|
+
extraInitializers.push(accept(f || null));
|
|
1222
|
+
};
|
|
1223
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
1224
|
+
get: descriptor.get,
|
|
1225
|
+
set: descriptor.set
|
|
1226
|
+
} : descriptor[key], context);
|
|
1227
|
+
if (kind === "accessor") {
|
|
1228
|
+
if (result === void 0) continue;
|
|
1229
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
1230
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
1231
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
1232
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
1233
|
+
} else if (_ = accept(result)) {
|
|
1234
|
+
if (kind === "field") initializers.unshift(_);
|
|
1235
|
+
else descriptor[key] = _;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
1239
|
+
done = true;
|
|
1240
|
+
};
|
|
1241
|
+
/**
|
|
1242
|
+
* Owns the only selection transition: append the immutable DSH session event,
|
|
1243
|
+
* then activate the model tool only inside that Agent scope. The browser UI
|
|
1244
|
+
* will call this service through its host bridge in the next slice.
|
|
1245
|
+
*/
|
|
1246
|
+
let OpenBknBusinessContextService = (() => {
|
|
1247
|
+
let _classSuper = TypertRemoteService;
|
|
1248
|
+
let _instanceExtraInitializers = [];
|
|
1249
|
+
let _remoteStatus_decorators;
|
|
1250
|
+
let _remoteBeginLogin_decorators;
|
|
1251
|
+
let _remoteConfigureToken_decorators;
|
|
1252
|
+
let _remoteGetNetworkBinding_decorators;
|
|
1253
|
+
let _remoteGetTurnProvenance_decorators;
|
|
1254
|
+
let _remoteGetTurnProvenanceView_decorators;
|
|
1255
|
+
let _remoteGetSessionSuggestions_decorators;
|
|
1256
|
+
let _remoteListNetworks_decorators;
|
|
1257
|
+
let _remoteBindNetworkWorkspace_decorators;
|
|
1258
|
+
let _remoteBindNetwork_decorators;
|
|
1259
|
+
return class OpenBknBusinessContextService extends _classSuper {
|
|
1260
|
+
static {
|
|
1261
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
1262
|
+
_remoteStatus_decorators = [Remote("status")];
|
|
1263
|
+
_remoteBeginLogin_decorators = [Remote("beginLogin")];
|
|
1264
|
+
_remoteConfigureToken_decorators = [Remote("configureToken")];
|
|
1265
|
+
_remoteGetNetworkBinding_decorators = [Remote("getNetworkBinding")];
|
|
1266
|
+
_remoteGetTurnProvenance_decorators = [Remote("getTurnProvenance")];
|
|
1267
|
+
_remoteGetTurnProvenanceView_decorators = [Remote("getTurnProvenanceView")];
|
|
1268
|
+
_remoteGetSessionSuggestions_decorators = [Remote("getSessionSuggestions")];
|
|
1269
|
+
_remoteListNetworks_decorators = [Remote("listNetworks")];
|
|
1270
|
+
_remoteBindNetworkWorkspace_decorators = [Remote("bindNetworkWorkspace")];
|
|
1271
|
+
_remoteBindNetwork_decorators = [Remote("bindNetwork")];
|
|
1272
|
+
__esDecorate(this, null, _remoteStatus_decorators, {
|
|
1273
|
+
kind: "method",
|
|
1274
|
+
name: "remoteStatus",
|
|
1275
|
+
static: false,
|
|
1276
|
+
private: false,
|
|
1277
|
+
access: {
|
|
1278
|
+
has: (obj) => "remoteStatus" in obj,
|
|
1279
|
+
get: (obj) => obj.remoteStatus
|
|
1280
|
+
},
|
|
1281
|
+
metadata: _metadata
|
|
1282
|
+
}, null, _instanceExtraInitializers);
|
|
1283
|
+
__esDecorate(this, null, _remoteBeginLogin_decorators, {
|
|
1284
|
+
kind: "method",
|
|
1285
|
+
name: "remoteBeginLogin",
|
|
1286
|
+
static: false,
|
|
1287
|
+
private: false,
|
|
1288
|
+
access: {
|
|
1289
|
+
has: (obj) => "remoteBeginLogin" in obj,
|
|
1290
|
+
get: (obj) => obj.remoteBeginLogin
|
|
1291
|
+
},
|
|
1292
|
+
metadata: _metadata
|
|
1293
|
+
}, null, _instanceExtraInitializers);
|
|
1294
|
+
__esDecorate(this, null, _remoteConfigureToken_decorators, {
|
|
1295
|
+
kind: "method",
|
|
1296
|
+
name: "remoteConfigureToken",
|
|
1297
|
+
static: false,
|
|
1298
|
+
private: false,
|
|
1299
|
+
access: {
|
|
1300
|
+
has: (obj) => "remoteConfigureToken" in obj,
|
|
1301
|
+
get: (obj) => obj.remoteConfigureToken
|
|
1302
|
+
},
|
|
1303
|
+
metadata: _metadata
|
|
1304
|
+
}, null, _instanceExtraInitializers);
|
|
1305
|
+
__esDecorate(this, null, _remoteGetNetworkBinding_decorators, {
|
|
1306
|
+
kind: "method",
|
|
1307
|
+
name: "remoteGetNetworkBinding",
|
|
1308
|
+
static: false,
|
|
1309
|
+
private: false,
|
|
1310
|
+
access: {
|
|
1311
|
+
has: (obj) => "remoteGetNetworkBinding" in obj,
|
|
1312
|
+
get: (obj) => obj.remoteGetNetworkBinding
|
|
1313
|
+
},
|
|
1314
|
+
metadata: _metadata
|
|
1315
|
+
}, null, _instanceExtraInitializers);
|
|
1316
|
+
__esDecorate(this, null, _remoteGetTurnProvenance_decorators, {
|
|
1317
|
+
kind: "method",
|
|
1318
|
+
name: "remoteGetTurnProvenance",
|
|
1319
|
+
static: false,
|
|
1320
|
+
private: false,
|
|
1321
|
+
access: {
|
|
1322
|
+
has: (obj) => "remoteGetTurnProvenance" in obj,
|
|
1323
|
+
get: (obj) => obj.remoteGetTurnProvenance
|
|
1324
|
+
},
|
|
1325
|
+
metadata: _metadata
|
|
1326
|
+
}, null, _instanceExtraInitializers);
|
|
1327
|
+
__esDecorate(this, null, _remoteGetTurnProvenanceView_decorators, {
|
|
1328
|
+
kind: "method",
|
|
1329
|
+
name: "remoteGetTurnProvenanceView",
|
|
1330
|
+
static: false,
|
|
1331
|
+
private: false,
|
|
1332
|
+
access: {
|
|
1333
|
+
has: (obj) => "remoteGetTurnProvenanceView" in obj,
|
|
1334
|
+
get: (obj) => obj.remoteGetTurnProvenanceView
|
|
1335
|
+
},
|
|
1336
|
+
metadata: _metadata
|
|
1337
|
+
}, null, _instanceExtraInitializers);
|
|
1338
|
+
__esDecorate(this, null, _remoteGetSessionSuggestions_decorators, {
|
|
1339
|
+
kind: "method",
|
|
1340
|
+
name: "remoteGetSessionSuggestions",
|
|
1341
|
+
static: false,
|
|
1342
|
+
private: false,
|
|
1343
|
+
access: {
|
|
1344
|
+
has: (obj) => "remoteGetSessionSuggestions" in obj,
|
|
1345
|
+
get: (obj) => obj.remoteGetSessionSuggestions
|
|
1346
|
+
},
|
|
1347
|
+
metadata: _metadata
|
|
1348
|
+
}, null, _instanceExtraInitializers);
|
|
1349
|
+
__esDecorate(this, null, _remoteListNetworks_decorators, {
|
|
1350
|
+
kind: "method",
|
|
1351
|
+
name: "remoteListNetworks",
|
|
1352
|
+
static: false,
|
|
1353
|
+
private: false,
|
|
1354
|
+
access: {
|
|
1355
|
+
has: (obj) => "remoteListNetworks" in obj,
|
|
1356
|
+
get: (obj) => obj.remoteListNetworks
|
|
1357
|
+
},
|
|
1358
|
+
metadata: _metadata
|
|
1359
|
+
}, null, _instanceExtraInitializers);
|
|
1360
|
+
__esDecorate(this, null, _remoteBindNetworkWorkspace_decorators, {
|
|
1361
|
+
kind: "method",
|
|
1362
|
+
name: "remoteBindNetworkWorkspace",
|
|
1363
|
+
static: false,
|
|
1364
|
+
private: false,
|
|
1365
|
+
access: {
|
|
1366
|
+
has: (obj) => "remoteBindNetworkWorkspace" in obj,
|
|
1367
|
+
get: (obj) => obj.remoteBindNetworkWorkspace
|
|
1368
|
+
},
|
|
1369
|
+
metadata: _metadata
|
|
1370
|
+
}, null, _instanceExtraInitializers);
|
|
1371
|
+
__esDecorate(this, null, _remoteBindNetwork_decorators, {
|
|
1372
|
+
kind: "method",
|
|
1373
|
+
name: "remoteBindNetwork",
|
|
1374
|
+
static: false,
|
|
1375
|
+
private: false,
|
|
1376
|
+
access: {
|
|
1377
|
+
has: (obj) => "remoteBindNetwork" in obj,
|
|
1378
|
+
get: (obj) => obj.remoteBindNetwork
|
|
1379
|
+
},
|
|
1380
|
+
metadata: _metadata
|
|
1381
|
+
}, null, _instanceExtraInitializers);
|
|
1382
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
1383
|
+
enumerable: true,
|
|
1384
|
+
configurable: true,
|
|
1385
|
+
writable: true,
|
|
1386
|
+
value: _metadata
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
config = __runInitializers(this, _instanceExtraInitializers);
|
|
1390
|
+
static inject = [
|
|
1391
|
+
"agents",
|
|
1392
|
+
"credentials",
|
|
1393
|
+
"subprocess",
|
|
1394
|
+
"tools",
|
|
1395
|
+
"openbknWorkspaceBindingRegistry"
|
|
1396
|
+
];
|
|
1397
|
+
static Config = Config;
|
|
1398
|
+
mounted = /* @__PURE__ */ new WeakSet();
|
|
1399
|
+
capabilityProfiles = /* @__PURE__ */ new WeakMap();
|
|
1400
|
+
mcpManagerInstance;
|
|
1401
|
+
constructor(ctx, config) {
|
|
1402
|
+
super(ctx, "openbknBusinessContext");
|
|
1403
|
+
this.config = config;
|
|
1404
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
1405
|
+
this.bindWorkspaceNetworkIfUnique(agent);
|
|
1406
|
+
});
|
|
1407
|
+
ctx.on("agent/pre-step", async ({ agent, step, signal }, next) => await this.refreshManagedMcpAtTurnStart(agent, step, signal, next));
|
|
1408
|
+
ctx.on("agent/turn-stopping", ({ agent, turn }) => {
|
|
1409
|
+
this.captureTurnProvenance(agent, turn);
|
|
1410
|
+
});
|
|
1411
|
+
for (const agent of ctx.agents.list()) this.bindWorkspaceNetworkIfUnique(agent);
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Return the CLI-authenticated state and synchronize its token into the
|
|
1415
|
+
* DSH credential vault when needed. The token stays Host-only throughout.
|
|
1416
|
+
* A pre-existing manually configured DSH credential remains a fallback for
|
|
1417
|
+
* deployments without a usable CLI login.
|
|
1418
|
+
*/
|
|
1419
|
+
async remoteStatus(signal) {
|
|
1420
|
+
if (signal.aborted) throw signal.reason;
|
|
1421
|
+
try {
|
|
1422
|
+
const auth = await this.authCoordinator().status(signal);
|
|
1423
|
+
if (auth.kind === "authenticated") await this.synchronizeCliCredential(signal);
|
|
1424
|
+
return auth;
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
if (!(await this.ctx.credentials.describe(credentialRef("OPENBKN_MCP_TOKEN"))).configured) throw error;
|
|
1427
|
+
if (error instanceof OpenBknCliError) return {
|
|
1428
|
+
kind: "authentication-required",
|
|
1429
|
+
baseUrl: this.config.baseUrl
|
|
1430
|
+
};
|
|
1431
|
+
throw error;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
/** Start the CLI's configured-platform browser login, then synchronize and verify it. */
|
|
1435
|
+
async remoteBeginLogin(signal) {
|
|
1436
|
+
if (signal.aborted) throw signal.reason;
|
|
1437
|
+
await this.authCoordinator().beginLogin();
|
|
1438
|
+
await this.synchronizeCliCredential(signal);
|
|
1439
|
+
return await this.listNetworksAfterAuthentication(signal);
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Save one user-entered token to DSH's credential provider, connect the
|
|
1443
|
+
* standard MCP client by reference, then return only visible network DTOs.
|
|
1444
|
+
*/
|
|
1445
|
+
async remoteConfigureToken(token, signal) {
|
|
1446
|
+
const value = token.trim();
|
|
1447
|
+
if (value.length === 0) throw new Error("An OpenBKN token is required.");
|
|
1448
|
+
await this.ctx.credentials.set(credentialRef(OPENBKN_MCP_TOKEN_REF), value);
|
|
1449
|
+
try {
|
|
1450
|
+
await this.refreshMcpConnection();
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
throw new RemoteError("openbkn/connection-failed", "The OpenBKN Context Loader MCP could not be connected.", {
|
|
1453
|
+
baseUrl: this.config.baseUrl,
|
|
1454
|
+
layer: "context-loader-mcp"
|
|
1455
|
+
}, { cause: error });
|
|
1456
|
+
}
|
|
1457
|
+
return await this.listNetworksAfterAuthentication(signal);
|
|
1458
|
+
}
|
|
1459
|
+
/** Read the immutable network identity already recorded on one live DSH session. */
|
|
1460
|
+
remoteGetNetworkBinding(sessionId) {
|
|
1461
|
+
const agent = this.ctx.agents.get(sessionId);
|
|
1462
|
+
if (agent === void 0) throw new Error("OpenBKN business-network binding target is not a live DSH session.");
|
|
1463
|
+
return readDshSessionBusinessNetwork(agent.session);
|
|
1464
|
+
}
|
|
1465
|
+
/** Read only the provenance already committed for one finalized assistant message. */
|
|
1466
|
+
remoteGetTurnProvenance(sessionId, messageId) {
|
|
1467
|
+
const agent = this.ctx.agents.get(sessionId);
|
|
1468
|
+
if (agent === void 0) throw new Error("OpenBKN turn provenance target is not a live DSH session.");
|
|
1469
|
+
return readDshSessionTurnProvenance(agent.session, messageId);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Resolve the selected turn's provenance on demand through the fixed
|
|
1473
|
+
* platform OSDK catalogue. The browser receives a bounded presentation DTO,
|
|
1474
|
+
* never raw MCP output, OSDK routes, or credentials.
|
|
1475
|
+
*/
|
|
1476
|
+
async remoteGetTurnProvenanceView(sessionId, messageId, signal) {
|
|
1477
|
+
const agent = this.ctx.agents.get(sessionId);
|
|
1478
|
+
if (agent === void 0) throw new Error("OpenBKN turn provenance target is not a live DSH session.");
|
|
1479
|
+
const handle = readDshSessionTurnProvenance(agent.session, messageId);
|
|
1480
|
+
if (handle === void 0) return void 0;
|
|
1481
|
+
const cwd = agent.session.header.cwd ?? ".";
|
|
1482
|
+
const reader = this.platformReader();
|
|
1483
|
+
const [core, graph] = await Promise.allSettled([reader.getInteractionOperations(handle.interactionId, signal, cwd), reader.getInteractionBusinessGraph(handle.interactionId, signal, cwd)]);
|
|
1484
|
+
if (signal.aborted) throw signal.reason;
|
|
1485
|
+
if (core.status === "rejected" && graph.status === "rejected") {
|
|
1486
|
+
this.ctx.logger.warn("openbkn-business-context: provenance reads failed (core=%s, graph=%s)", safeRunnerFailureCode(core.reason), safeRunnerFailureCode(graph.reason));
|
|
1487
|
+
if (safeRunnerFailureCode(core.reason) === "LICENSE_REQUIRED" || safeRunnerFailureCode(graph.reason) === "LICENSE_REQUIRED") {
|
|
1488
|
+
const decision = provenanceLicenseDecision(await this.platformReader().getLicenseEdition(signal).then((value) => value, () => void 0));
|
|
1489
|
+
if (decision.kind === "unavailable") throw new Error("OpenBKN provenance records are unavailable for this interaction.");
|
|
1490
|
+
throw new RemoteError("openbkn/provenance-license-required", "Business provenance is an enterprise capability; the current deployment license does not include it.", { edition: decision.edition });
|
|
1491
|
+
}
|
|
1492
|
+
throw new Error("OpenBKN provenance records are unavailable for this interaction.");
|
|
1493
|
+
}
|
|
1494
|
+
return buildProvenanceView(handle, core.status === "fulfilled" ? core.value : void 0, graph.status === "fulfilled" ? graph.value : void 0, {
|
|
1495
|
+
maxGraphNodes: this.config.maxGraphNodes,
|
|
1496
|
+
maxGraphEdges: this.config.maxGraphEdges
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
/** Return the one optional, draft-only introduction for an empty bound session. */
|
|
1500
|
+
remoteGetSessionSuggestions(sessionId) {
|
|
1501
|
+
const agent = this.ctx.agents.get(sessionId);
|
|
1502
|
+
if (agent === void 0) throw new Error("OpenBKN suggestion target is not a live DSH session.");
|
|
1503
|
+
const binding = readDshSessionBusinessNetwork(agent.session);
|
|
1504
|
+
if (binding === void 0) return [];
|
|
1505
|
+
if (agent.session.snapshotEvents().some((event) => event.type === "assistant/message")) return [];
|
|
1506
|
+
return [emptyBusinessSessionPrompt(binding.displayName)];
|
|
1507
|
+
}
|
|
1508
|
+
/** List only the business networks authorized by the managed OpenBKN token. */
|
|
1509
|
+
async remoteListNetworks(signal) {
|
|
1510
|
+
if ((await this.remoteStatus(signal)).kind !== "authenticated") throw new Error("OpenBKN authentication is required before listing business networks.");
|
|
1511
|
+
await this.ensureMcpConnection();
|
|
1512
|
+
return await this.listNetworksAfterAuthentication(signal);
|
|
1513
|
+
}
|
|
1514
|
+
/** A successful MCP initial handshake plus OSDK catalogue read is the connection test. */
|
|
1515
|
+
async listNetworksAfterAuthentication(signal) {
|
|
1516
|
+
let payload;
|
|
1517
|
+
try {
|
|
1518
|
+
payload = await this.platformReader().listKnowledgeNetworks(signal, ".");
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
if (error instanceof PlatformReaderError && error.code === "AUTHENTICATION_REQUIRED") throw new RemoteError("openbkn/authentication-required", "OpenBKN authentication is required.", { baseUrl: this.config.baseUrl });
|
|
1521
|
+
if (error instanceof PlatformReaderError && error.code === "PLATFORM_UNAVAILABLE") throw new RemoteError("openbkn/platform-unavailable", "The OpenBKN platform knowledge-network catalogue is temporarily unavailable.", { baseUrl: this.config.baseUrl }, { cause: error });
|
|
1522
|
+
throw new RemoteError("openbkn/connection-failed", "The OpenBKN platform API could not be queried.", {
|
|
1523
|
+
baseUrl: this.config.baseUrl,
|
|
1524
|
+
layer: "platform-api"
|
|
1525
|
+
}, { cause: error });
|
|
1526
|
+
}
|
|
1527
|
+
return parseVisibleBusinessNetworks(payload).map((network) => {
|
|
1528
|
+
const workspacePath = this.ctx.openbknWorkspaceBindingRegistry.get(this.config.baseUrl, network.id)?.workspacePath;
|
|
1529
|
+
return workspacePath === void 0 ? network : {
|
|
1530
|
+
...network,
|
|
1531
|
+
workspacePath
|
|
1532
|
+
};
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
/** Persist a selected local DSH workspace only after the network is authorized. */
|
|
1536
|
+
async remoteBindNetworkWorkspace(networkId, workspacePath, signal) {
|
|
1537
|
+
if ((await this.remoteStatus(signal)).kind !== "authenticated") throw new Error("OpenBKN authentication is required before selecting a workspace.");
|
|
1538
|
+
const network = parseVisibleBusinessNetworks(await this.platformReader().listKnowledgeNetworks(signal, ".")).find((candidate) => candidate.id === networkId.trim());
|
|
1539
|
+
if (network === void 0) throw new Error("The requested OpenBKN business network is not visible to the current identity.");
|
|
1540
|
+
const canonicalPath = await canonicalDirectory(workspacePath);
|
|
1541
|
+
await this.ctx.openbknWorkspaceBindingRegistry.put({
|
|
1542
|
+
platformBaseUrl: this.config.baseUrl,
|
|
1543
|
+
knowledgeNetworkId: network.id,
|
|
1544
|
+
displayName: network.displayName,
|
|
1545
|
+
workspacePath: canonicalPath
|
|
1546
|
+
});
|
|
1547
|
+
return {
|
|
1548
|
+
...network,
|
|
1549
|
+
workspacePath: canonicalPath
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Bind this exact live Agent to one network already confirmed visible by the
|
|
1554
|
+
* configured CLI identity. The Client provides only the opaque network id;
|
|
1555
|
+
* display data and platform routing are re-derived on the Host.
|
|
1556
|
+
*/
|
|
1557
|
+
async remoteBindNetwork(sessionId, networkId, signal) {
|
|
1558
|
+
if ((await this.remoteStatus(signal)).kind !== "authenticated") throw new Error("OpenBKN authentication is required before binding a business network.");
|
|
1559
|
+
const network = parseVisibleBusinessNetworks(await this.platformReader().listKnowledgeNetworks(signal, ".")).find((candidate) => candidate.id === networkId.trim());
|
|
1560
|
+
if (network === void 0) throw new Error("The requested OpenBKN business network is not visible to the current identity.");
|
|
1561
|
+
const agent = this.ctx.agents.get(sessionId);
|
|
1562
|
+
if (agent === void 0) throw new Error("OpenBKN business-network selection target is not a live DSH session.");
|
|
1563
|
+
const binding = {
|
|
1564
|
+
platformBaseUrl: this.config.baseUrl,
|
|
1565
|
+
knowledgeNetworkId: network.id,
|
|
1566
|
+
displayName: network.displayName
|
|
1567
|
+
};
|
|
1568
|
+
try {
|
|
1569
|
+
this.capabilityProfiles.set(agent, buildNetworkCapabilityProfile(binding, await this.platformReader().getKnowledgeNetworkDetail(binding, signal, agent.session.header.cwd ?? ".")));
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
this.ctx.logger.warn("openbkn-business-context: capability profile unavailable; continuing with the verified network binding and fixed managed-session policy (code=%s)", safeCapabilityProfileFailureCode(error));
|
|
1572
|
+
}
|
|
1573
|
+
const result = this.bind(agent, binding);
|
|
1574
|
+
return result.kind === "bound" ? result.event.data : result.binding;
|
|
1575
|
+
}
|
|
1576
|
+
/** Persist one network selection, reject conflicts, then scope the capability to this Agent. */
|
|
1577
|
+
bind(agent, requested) {
|
|
1578
|
+
if (this.ctx.agents.get(agent.id) !== agent) throw new Error("OpenBKN business-network selection target is not a live DSH agent.");
|
|
1579
|
+
if (normalizeBaseUrl$1(requested.platformBaseUrl) !== normalizeBaseUrl$1(this.config.baseUrl)) throw new Error("OpenBKN business-network selection must use the configured OpenBKN platform.");
|
|
1580
|
+
const result = bindDshSessionBusinessNetwork(agent.session, requested);
|
|
1581
|
+
this.mountIfBound(agent);
|
|
1582
|
+
return result;
|
|
1583
|
+
}
|
|
1584
|
+
mountIfBound(agent) {
|
|
1585
|
+
if (this.mounted.has(agent)) return;
|
|
1586
|
+
if (!mountBoundBusinessNetworkTool(agent, this.config, this.capabilityProfiles.get(agent))) return;
|
|
1587
|
+
this.mounted.add(agent);
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Project one user-approved workspace association into a newly live native
|
|
1591
|
+
* DSH session. A missing or ambiguous association deliberately leaves the
|
|
1592
|
+
* native session untouched.
|
|
1593
|
+
*/
|
|
1594
|
+
bindWorkspaceNetworkIfUnique(agent) {
|
|
1595
|
+
if (readDshSessionBusinessNetwork(agent.session) !== void 0) {
|
|
1596
|
+
this.mountIfBound(agent);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const workspacePath = agent.session.header.cwd;
|
|
1600
|
+
if (workspacePath === void 0) {
|
|
1601
|
+
this.mountIfBound(agent);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const record = this.ctx.openbknWorkspaceBindingRegistry.findUniqueByWorkspace(this.config.baseUrl, workspacePath);
|
|
1605
|
+
if (record === void 0) {
|
|
1606
|
+
this.mountIfBound(agent);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
this.bind(agent, {
|
|
1610
|
+
platformBaseUrl: this.config.baseUrl,
|
|
1611
|
+
knowledgeNetworkId: record.knowledgeNetworkId,
|
|
1612
|
+
displayName: record.displayName ?? record.knowledgeNetworkId
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Reconcile the CLI-owned credential before a bound business turn reaches
|
|
1617
|
+
* its first model step. This is the only per-turn lifecycle point: later
|
|
1618
|
+
* tool-planning steps retain the same connection and do not re-authenticate.
|
|
1619
|
+
*/
|
|
1620
|
+
async refreshManagedMcpAtTurnStart(agent, step, signal, next) {
|
|
1621
|
+
if (step === 1 && readDshSessionBusinessNetwork(agent.session) !== void 0) await this.remoteStatus(signal);
|
|
1622
|
+
return await next();
|
|
1623
|
+
}
|
|
1624
|
+
/** Persist only an explicitly completed OpenBKN interaction when its turn ends. */
|
|
1625
|
+
captureTurnProvenance(agent, turn) {
|
|
1626
|
+
if (readDshSessionBusinessNetwork(agent.session) === void 0) return;
|
|
1627
|
+
const provenance = findCompletedNativeMcpProvenance(agent.session.snapshotEvents(), turn);
|
|
1628
|
+
if (provenance === void 0) return;
|
|
1629
|
+
appendDshSessionTurnProvenance(agent.session, provenance.messageId, provenance.handle);
|
|
1630
|
+
}
|
|
1631
|
+
async ensureMcpConnection() {
|
|
1632
|
+
if (this.ctx.tools === void 0) return;
|
|
1633
|
+
await this.mcpManager().ensure();
|
|
1634
|
+
}
|
|
1635
|
+
async refreshMcpConnection() {
|
|
1636
|
+
if (this.ctx.tools === void 0) return;
|
|
1637
|
+
await this.mcpManager().refresh();
|
|
1638
|
+
}
|
|
1639
|
+
mcpManager() {
|
|
1640
|
+
return this.mcpManagerInstance ??= new OpenBknMcpManager(this.ctx, this.config, () => this.resolveOpenBknToken());
|
|
1641
|
+
}
|
|
1642
|
+
authCoordinator() {
|
|
1643
|
+
return new AuthCoordinator(new OpenBknCliSubprocess(this.ctx.subprocess, ".", this.config.baseUrl, this.config.cliPath), this.config.baseUrl);
|
|
1644
|
+
}
|
|
1645
|
+
/** Synchronize exactly one configured-platform CLI token into DSH credentials. */
|
|
1646
|
+
async synchronizeCliCredential(signal) {
|
|
1647
|
+
const token = await this.authCoordinator().readToken(signal);
|
|
1648
|
+
await this.ctx.credentials.set(credentialRef(OPENBKN_MCP_TOKEN_REF), token);
|
|
1649
|
+
await this.refreshMcpConnection();
|
|
1650
|
+
}
|
|
1651
|
+
/** Historical private seam retained for unit fixtures; it now returns the Host HTTP reader, never a Python OSDK process. */
|
|
1652
|
+
platformReader() {
|
|
1653
|
+
return new OpenBknPlatformReader({
|
|
1654
|
+
...this.config,
|
|
1655
|
+
resolveToken: () => this.resolveOpenBknToken()
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
/** Resolve only the DSH-managed token; legacy environment variables are not silently migrated. */
|
|
1659
|
+
async resolveOpenBknToken() {
|
|
1660
|
+
const ref = credentialRef(OPENBKN_MCP_TOKEN_REF);
|
|
1661
|
+
return (await this.ctx.credentials.resolve(ref))?.value;
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
})();
|
|
1665
|
+
/**
|
|
1666
|
+
* A permission_denied gate is only a license statement when the deployment is
|
|
1667
|
+
* verifiably unlicensed; an enterprise deployment that still denies the read
|
|
1668
|
+
* has a domain-authorization problem instead, and must not be told to upgrade.
|
|
1669
|
+
* Capabilities being unreachable leaves the cause undetermined: report the
|
|
1670
|
+
* generic unavailable failure rather than guessing an upgrade path.
|
|
1671
|
+
*/
|
|
1672
|
+
function provenanceLicenseDecision(license) {
|
|
1673
|
+
if (license === void 0 || license.licensed !== false) return { kind: "unavailable" };
|
|
1674
|
+
return {
|
|
1675
|
+
kind: "license-required",
|
|
1676
|
+
edition: license.edition ?? ""
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function safeRunnerFailureCode(error) {
|
|
1680
|
+
return error instanceof PlatformReaderError ? error.code : "UNCLASSIFIED";
|
|
1681
|
+
}
|
|
1682
|
+
function safeCapabilityProfileFailureCode(error) {
|
|
1683
|
+
return error instanceof PlatformReaderError ? error.code : "PROFILE_UNAVAILABLE";
|
|
1684
|
+
}
|
|
1685
|
+
function normalizeBaseUrl$1(value) {
|
|
1686
|
+
return value.trim().replace(/\/+$/, "");
|
|
1687
|
+
}
|
|
1688
|
+
async function canonicalDirectory(path) {
|
|
1689
|
+
const canonicalPath = await realpath(path);
|
|
1690
|
+
if (!(await stat(canonicalPath)).isDirectory()) throw new Error("OpenBKN workspace must be an existing local directory.");
|
|
1691
|
+
return canonicalPath;
|
|
1692
|
+
}
|
|
1693
|
+
//#endregion
|
|
1694
|
+
//#region src/workspace-binding-registry.ts
|
|
1695
|
+
/** A local DSH workspace chosen for one OpenBKN business knowledge network. */
|
|
1696
|
+
const workspaceBindingRecord = z.object({
|
|
1697
|
+
platformBaseUrl: z.string().url(),
|
|
1698
|
+
knowledgeNetworkId: z.string().trim().min(1),
|
|
1699
|
+
/** Optional only for records written before business display metadata existed. */
|
|
1700
|
+
displayName: z.string().trim().min(1).max(512).optional(),
|
|
1701
|
+
workspacePath: z.string().trim().refine(isAbsolute, "workspacePath must be absolute"),
|
|
1702
|
+
updatedAt: z.string().datetime()
|
|
1703
|
+
});
|
|
1704
|
+
const workspaceBindingDomain = defineDomain({
|
|
1705
|
+
name: "openbkn_workspace_bindings",
|
|
1706
|
+
version: 1,
|
|
1707
|
+
tables: { bindings: domainTable(workspaceBindingRecord) }
|
|
1708
|
+
});
|
|
1709
|
+
/** Stable storage key: one configurable OpenBKN platform and network identity. */
|
|
1710
|
+
function workspaceBindingKey(platformBaseUrl, knowledgeNetworkId) {
|
|
1711
|
+
return `${normalizeBaseUrl(platformBaseUrl)}::${knowledgeNetworkId.trim()}`;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Host-owned durable index for the small, non-secret network-to-workspace
|
|
1715
|
+
* association. It deliberately stores a path rather than a DSH workspace id:
|
|
1716
|
+
* DSH resolves a registered workspace id from the path at each Client run.
|
|
1717
|
+
*/
|
|
1718
|
+
var OpenBknWorkspaceBindingRegistry = class extends Service {
|
|
1719
|
+
static inject = ["storageDomain"];
|
|
1720
|
+
table;
|
|
1721
|
+
constructor(ctx) {
|
|
1722
|
+
super(ctx, "openbknWorkspaceBindingRegistry");
|
|
1723
|
+
}
|
|
1724
|
+
async [Service.init]() {
|
|
1725
|
+
const domain = await this.ctx.storageDomain.open(workspaceBindingDomain);
|
|
1726
|
+
this.ctx.effect(() => () => domain.close(), "openbknWorkspaceBindingRegistry.domainClose");
|
|
1727
|
+
this.table = domain.table("bindings");
|
|
1728
|
+
}
|
|
1729
|
+
get(platformBaseUrl, knowledgeNetworkId) {
|
|
1730
|
+
return this.requireTable().get(workspaceBindingKey(platformBaseUrl, knowledgeNetworkId));
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Return the one network associated with a native DSH workspace. Ambiguous
|
|
1734
|
+
* workspace associations deliberately produce no result so a session is
|
|
1735
|
+
* never bound to a network by guesswork.
|
|
1736
|
+
*/
|
|
1737
|
+
findUniqueByWorkspace(platformBaseUrl, workspacePath) {
|
|
1738
|
+
const expectedPlatform = normalizeBaseUrl(platformBaseUrl);
|
|
1739
|
+
const expectedPath = normalizeWorkspacePath(workspacePath);
|
|
1740
|
+
let match;
|
|
1741
|
+
for (const [, candidate] of this.requireTable().entries()) {
|
|
1742
|
+
if (normalizeBaseUrl(candidate.platformBaseUrl) !== expectedPlatform) continue;
|
|
1743
|
+
if (normalizeWorkspacePath(candidate.workspacePath) !== expectedPath) continue;
|
|
1744
|
+
if (match !== void 0) return void 0;
|
|
1745
|
+
match = candidate;
|
|
1746
|
+
}
|
|
1747
|
+
return match;
|
|
1748
|
+
}
|
|
1749
|
+
async put(input) {
|
|
1750
|
+
const record = workspaceBindingRecord.parse({
|
|
1751
|
+
...input,
|
|
1752
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1753
|
+
});
|
|
1754
|
+
for (const [, candidate] of this.requireTable().entries()) {
|
|
1755
|
+
if (normalizeBaseUrl(candidate.platformBaseUrl) !== normalizeBaseUrl(record.platformBaseUrl)) continue;
|
|
1756
|
+
if (normalizeWorkspacePath(candidate.workspacePath) !== normalizeWorkspacePath(record.workspacePath)) continue;
|
|
1757
|
+
if (candidate.knowledgeNetworkId !== record.knowledgeNetworkId) throw new Error("This local DSH workspace is already associated with another OpenBKN knowledge network.");
|
|
1758
|
+
}
|
|
1759
|
+
await this.requireTable().put(workspaceBindingKey(record.platformBaseUrl, record.knowledgeNetworkId), record);
|
|
1760
|
+
return record;
|
|
1761
|
+
}
|
|
1762
|
+
requireTable() {
|
|
1763
|
+
if (this.table === void 0) throw new Error("OpenBKN workspace binding registry is not ready.");
|
|
1764
|
+
return this.table;
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
function normalizeBaseUrl(value) {
|
|
1768
|
+
return value.trim().replace(/\/+$/, "");
|
|
1769
|
+
}
|
|
1770
|
+
function normalizeWorkspacePath(value) {
|
|
1771
|
+
return resolve(value);
|
|
1772
|
+
}
|
|
1773
|
+
//#endregion
|
|
1774
|
+
//#region src/index.ts
|
|
1775
|
+
/** Cordis identity used by the bundle's Host row. */
|
|
1776
|
+
const name = "openbkn-business-context";
|
|
1777
|
+
const inject = [
|
|
1778
|
+
"agents",
|
|
1779
|
+
"subprocess",
|
|
1780
|
+
"storageDomain"
|
|
1781
|
+
];
|
|
1782
|
+
/** Register the host service; it contributes no model-visible tool globally. */
|
|
1783
|
+
async function apply(ctx, config) {
|
|
1784
|
+
await ctx.plugin(OpenBknWorkspaceBindingRegistry);
|
|
1785
|
+
await ctx.plugin(OpenBknBusinessContextService, config);
|
|
1786
|
+
}
|
|
1787
|
+
//#endregion
|
|
1788
|
+
export { AuthCoordinator, BUSINESS_NETWORK_BOUND_EVENT, BusinessNetworkBindingConflictError, Config, OPENBKN_DSH_INTERACTION_AGENT_NAME, OpenBknBusinessContextService, OpenBknCliError, OpenBknPlatformReader, OpenBknWorkspaceBindingRegistry, PlatformReaderError, TURN_PROVENANCE_EVENT, TurnProvenanceConflictError, appendDshSessionTurnProvenance, appendTurnProvenance, apply, bindBusinessNetwork, bindDshSessionBusinessNetwork, buildManagedSessionPolicy, buildNetworkCapabilityProfile, emptyBusinessSessionPrompt, inject, mountBoundBusinessNetworkTool, name, normalizeProvenanceHandle, readBusinessNetworkBinding, readDshSessionBusinessNetwork, readDshSessionTurnProvenance, readTurnProvenance, workspaceBindingKey, workspaceBindingRecord };
|