@agentdid/langchain 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # @agent-did/langchain
2
+
3
+ Integracion funcional de Agent-DID para LangChain JS 1.x.
4
+
5
+ Esta variante es la referencia original en JavaScript/TypeScript y ahora mantiene parity operativa con la integracion Python en superficies clave: middleware/contexto, tools opt-in, seguridad por defecto, observabilidad callback/logger/LangSmith y ejemplos de operacion.
6
+
7
+ La matriz de parity entre ambas integraciones vive en `../../docs/F1-03-LangChain-TS-Python-Integration-Parity-Matrix.md`.
8
+
9
+ ## Compatibilidad objetivo
10
+
11
+ - `langchain` `^1.2.35`
12
+ - `@langchain/core` `^1.1.34`
13
+ - `@agent-did/sdk` `^0.1.0`
14
+ - Node.js 20+
15
+
16
+ ## Estado
17
+
18
+ - Estado actual: `functional-mvp`
19
+ - Lenguaje objetivo: TypeScript / JavaScript
20
+ - Rol actual: referencia JS de la integracion LangChain de Agent-DID
21
+ - Parity objetivo: alineada con Python en README, ejemplos y observabilidad base
22
+
23
+ ## Instalacion
24
+
25
+ ```bash
26
+ npm install @agent-did/sdk langchain @langchain/core zod
27
+ ```
28
+
29
+ Para habilitar el adaptador opcional de LangSmith:
30
+
31
+ ```bash
32
+ npm install langsmith
33
+ ```
34
+
35
+ Si publicas este paquete por separado:
36
+
37
+ ```bash
38
+ npm install @agent-did/langchain
39
+ ```
40
+
41
+ ## Uso rapido
42
+
43
+ ```ts
44
+ import { ethers } from "ethers";
45
+ import { createAgent } from "langchain";
46
+ import { AgentIdentity } from "@agent-did/sdk";
47
+ import { createAgentDidIntegration } from "@agent-did/langchain";
48
+
49
+ const signer = new ethers.Wallet(process.env.CREATOR_PRIVATE_KEY!);
50
+ const identity = new AgentIdentity({ signer, network: "polygon" });
51
+
52
+ const runtimeIdentity = await identity.create({
53
+ name: "research_assistant",
54
+ description: "Agente de investigacion con identidad verificable",
55
+ coreModel: "gpt-4.1-mini",
56
+ systemPrompt: "Eres un agente de investigacion preciso y trazable.",
57
+ capabilities: ["research:web", "report:write"]
58
+ });
59
+
60
+ const integration = createAgentDidIntegration({
61
+ agentIdentity: identity,
62
+ runtimeIdentity,
63
+ expose: {
64
+ signHttp: true,
65
+ verifySignatures: true,
66
+ signPayload: false,
67
+ rotateKeys: false,
68
+ documentHistory: true
69
+ }
70
+ });
71
+
72
+ const agent = createAgent({
73
+ name: "research_assistant",
74
+ model: "openai:gpt-4.1-mini",
75
+ systemPrompt: "Responde con precision y usa herramientas cuando haga falta.",
76
+ tools: integration.tools,
77
+ middleware: [integration.middleware]
78
+ });
79
+
80
+ const result = await agent.invoke({
81
+ messages: [
82
+ {
83
+ role: "user",
84
+ content: "Muestrame tu DID actual y firma una solicitud POST a https://api.example.com/tasks"
85
+ }
86
+ ]
87
+ });
88
+
89
+ console.log(result.messages.at(-1)?.content);
90
+ ```
91
+
92
+ ## Que agrega la integracion
93
+
94
+ - Middleware que inyecta en el sistema el DID actual, controlador, capacidades y metodo de autenticacion activo.
95
+ - Herramientas de consulta para identidad actual, resolucion DID y verificacion de firmas.
96
+ - Herramientas opcionales para firma de payloads, firma HTTP, historial documental y rotacion de clave.
97
+ - Observabilidad vendor-neutral via callback, logger estructurado y adaptador opcional de LangSmith con redaccion por defecto.
98
+
99
+ ## Compatibilidad de API
100
+
101
+ - `createAgentDidIntegration(...)`: nombre recomendado.
102
+ - `createAgentDidPlugin(...)`: alias mantenido por compatibilidad retroactiva.
103
+
104
+ ## Seguridad por defecto
105
+
106
+ - `signPayload` y `rotateKeys` vienen deshabilitados por defecto.
107
+ - `signHttp` tambien es opt-in y puede habilitarse para que el agente firme solicitudes salientes sin exponer la clave privada al modelo.
108
+ - Los destinos HTTP privados, loopback o con credenciales embebidas se rechazan por defecto.
109
+ - La clave privada nunca se inserta en mensajes, contexto ni estado del agente.
110
+
111
+ ## Observabilidad
112
+
113
+ La factory publica acepta instrumentacion opcional sin acoplar el paquete a un backend especifico:
114
+
115
+ ```js
116
+ const {
117
+ composeEventHandlers,
118
+ createAgentDidIntegration,
119
+ createJsonLoggerEventHandler,
120
+ } = require("@agent-did/langchain");
121
+
122
+ const events = [];
123
+
124
+ const integration = createAgentDidIntegration({
125
+ agentIdentity: identity,
126
+ runtimeIdentity,
127
+ expose: {
128
+ signHttp: true,
129
+ signPayload: true,
130
+ },
131
+ observabilityHandler: composeEventHandlers(
132
+ (event) => events.push(event),
133
+ createJsonLoggerEventHandler(console, {
134
+ extraFields: { service: "agent-gateway" },
135
+ })
136
+ ),
137
+ });
138
+ ```
139
+
140
+ Eventos emitidos:
141
+
142
+ - `agent_did.identity_snapshot.refreshed`
143
+ - `agent_did.tool.started`
144
+ - `agent_did.tool.succeeded`
145
+ - `agent_did.tool.failed`
146
+
147
+ Redaccion por defecto:
148
+
149
+ - `payload`, `body`, `signature` y `agent_private_key` se reemplazan por metadatos de longitud.
150
+ - `Authorization`, `Signature`, `Signature-Input`, `Cookie`, `Set-Cookie` y `X-API-Key` se redactan en headers.
151
+ - Las URLs se serializan sin query string, fragmento ni credenciales embebidas.
152
+
153
+ Helpers publicos disponibles:
154
+
155
+ - `composeEventHandlers(...)`
156
+ - `createJsonLoggerEventHandler(...)`
157
+ - `createLangSmithRunTree(...)`
158
+ - `createLangSmithEventHandler(...)`
159
+ - `serializeObservabilityEvent(...)`
160
+ - `sanitizeObservabilityAttributes(...)`
161
+
162
+ Ejemplo de LangSmith local sin cambiar la factory principal:
163
+
164
+ ```js
165
+ const {
166
+ createAgentDidIntegration,
167
+ createLangSmithEventHandler,
168
+ createLangSmithRunTree,
169
+ } = require("@agent-did/langchain");
170
+
171
+ const rootRun = createLangSmithRunTree({
172
+ name: "agent_did_demo",
173
+ inputs: { scenario: "local" },
174
+ tags: ["agent-did", "demo"],
175
+ });
176
+
177
+ const integration = createAgentDidIntegration({
178
+ agentIdentity: identity,
179
+ runtimeIdentity,
180
+ expose: { signHttp: true, signPayload: true },
181
+ observabilityHandler: createLangSmithEventHandler(rootRun, {
182
+ extraFields: { sink: "langsmith" },
183
+ tags: ["local-demo"],
184
+ }),
185
+ });
186
+ ```
187
+
188
+ ## Archivos relevantes
189
+
190
+ - `src/agentDidLangChain.js`: middleware, herramientas y helpers principales.
191
+ - `src/observability.js`: callbacks, logging JSON y saneamiento de eventos.
192
+ - `examples/agentDidLangChain.example.js`: ejemplo con `createAgent()` de LangChain 1.x.
193
+ - `examples/agentDidLangChain.observability.example.js`: ejemplo de callback + JSON logging saneado.
194
+ - `examples/agentDidLangChain.langsmith.example.js`: tracing local con `RunTree` de LangSmith y child runs saneados.
195
+ - `examples/agentDidLangChain.productionRecipe.example.js`: receta con guardas de entorno para un flujo mas cercano a produccion.
196
+ - `tests/agentDidLangChain.test.js`: pruebas de la integracion.
197
+ - `tests/agentDidLangChain.observability.test.js`: pruebas de observabilidad saneada.
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@agentdid/langchain",
3
+ "version": "0.1.0",
4
+ "description": "LangChain integration for Agent-DID identities with middleware and tools for LangChain JS 1.x agents.",
5
+ "main": "src/index.js",
6
+ "files": [
7
+ "src/",
8
+ "README.md"
9
+ ],
10
+ "scripts": {
11
+ "build": "node -e \"console.log('No build step required for source-first package')\"",
12
+ "test": "jest --runInBand",
13
+ "prepublishOnly": "npm test"
14
+ },
15
+ "keywords": [
16
+ "agent-did",
17
+ "langchain",
18
+ "langchainjs",
19
+ "ai-agent",
20
+ "did",
21
+ "identity",
22
+ "middleware",
23
+ "tool-calling"
24
+ ],
25
+ "author": "Edison Munoz <edison.munoz@agent-did.org>",
26
+ "license": "Apache-2.0",
27
+ "peerDependencies": {
28
+ "@agentdid/sdk": "^0.1.0",
29
+ "@langchain/core": "^1.1.34",
30
+ "langchain": "^1.2.35",
31
+ "langsmith": "^0.5.10"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "langsmith": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "zod": "^3.25.76"
40
+ },
41
+ "devDependencies": {
42
+ "@agentdid/sdk": "file:../../sdk",
43
+ "@langchain/core": "^1.1.34",
44
+ "@types/jest": "^29.5.14",
45
+ "ethers": "^6.11.1",
46
+ "jest": "^29.7.0",
47
+ "langchain": "^1.2.35",
48
+ "langsmith": "^0.5.10",
49
+ "ts-jest": "^29.4.6",
50
+ "typescript": "^5.4.2"
51
+ },
52
+ "engines": {
53
+ "node": ">=20.0.0"
54
+ }
55
+ }
@@ -0,0 +1,518 @@
1
+ const { AgentIdentity } = require("@agentdid/sdk");
2
+ const net = require("node:net");
3
+ const { SystemMessage } = require("@langchain/core/messages");
4
+ const { tool } = require("@langchain/core/tools");
5
+ const { z } = require("zod");
6
+ const { createAgentDidObserver } = require("./observability");
7
+
8
+ const MIDDLEWARE_BRAND = Symbol.for("AgentMiddleware");
9
+ const MAX_PAYLOAD_BYTES = 1048576; // 1 MB
10
+
11
+ const DEFAULT_EXPOSURE = {
12
+ currentIdentity: true,
13
+ resolveDid: true,
14
+ verifySignatures: true,
15
+ signPayload: false,
16
+ signHttp: false,
17
+ documentHistory: false,
18
+ rotateKeys: false,
19
+ };
20
+
21
+ function withPrefix(prefix, name) {
22
+ return `${prefix}_${name}`;
23
+ }
24
+
25
+ function getActiveVerificationMethodId(runtimeIdentity) {
26
+ return runtimeIdentity.verificationMethodId ?? runtimeIdentity.document.authentication[0];
27
+ }
28
+
29
+ function createBrandedMiddleware(config) {
30
+ return {
31
+ [MIDDLEWARE_BRAND]: true,
32
+ name: config.name,
33
+ wrapModelCall: config.wrapModelCall,
34
+ };
35
+ }
36
+
37
+ function buildAgentDidIdentitySnapshot(runtimeIdentity) {
38
+ const { document } = runtimeIdentity;
39
+
40
+ return {
41
+ did: document.id,
42
+ controller: document.controller,
43
+ name: document.agentMetadata.name,
44
+ description: document.agentMetadata.description,
45
+ version: document.agentMetadata.version,
46
+ capabilities: document.agentMetadata.capabilities ?? [],
47
+ memberOf: document.agentMetadata.memberOf,
48
+ authenticationKeyId: getActiveVerificationMethodId(runtimeIdentity),
49
+ created: document.created,
50
+ updated: document.updated,
51
+ };
52
+ }
53
+
54
+ function buildAgentDidSystemPrompt(snapshot, additionalSystemContext) {
55
+ const capabilities = snapshot.capabilities.length > 0 ? snapshot.capabilities.join(", ") : "none";
56
+ const lines = [
57
+ "Agent-DID identity context:",
58
+ `- did: ${snapshot.did}`,
59
+ `- controller: ${snapshot.controller}`,
60
+ `- name: ${snapshot.name}`,
61
+ `- version: ${snapshot.version}`,
62
+ `- capabilities: ${capabilities}`,
63
+ `- member_of: ${snapshot.memberOf ?? "none"}`,
64
+ `- authentication_key_id: ${snapshot.authenticationKeyId ?? "unknown"}`,
65
+ "Rules:",
66
+ "- Treat this DID as the authoritative identity of this agent.",
67
+ "- Never invent or substitute another DID for this agent.",
68
+ "- If an outbound HTTP request must be authenticated with Agent-DID, use the dedicated signing tool instead of fabricating headers.",
69
+ ];
70
+
71
+ if (additionalSystemContext && additionalSystemContext.trim()) {
72
+ lines.push(`Additional identity policy: ${additionalSystemContext.trim()}`);
73
+ }
74
+
75
+ return lines.join("\n");
76
+ }
77
+
78
+ function validateHttpTarget(url, allowPrivateNetworkTargets) {
79
+ const parsedUrl = new URL(url);
80
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
81
+ throw new Error("Only http and https URLs are allowed");
82
+ }
83
+
84
+ if (parsedUrl.username || parsedUrl.password) {
85
+ throw new Error("URLs with embedded credentials are not allowed");
86
+ }
87
+
88
+ const hostname = parsedUrl.hostname;
89
+ if (!hostname) {
90
+ throw new Error("An absolute URL with hostname is required");
91
+ }
92
+
93
+ const normalizedHostname = hostname.toLowerCase();
94
+ if (allowPrivateNetworkTargets) {
95
+ return;
96
+ }
97
+
98
+ if (normalizedHostname === "localhost" || normalizedHostname.endsWith(".localhost")) {
99
+ throw new Error("Private or loopback HTTP targets are not allowed by default");
100
+ }
101
+
102
+ const ipVersion = net.isIP(normalizedHostname);
103
+ if (ipVersion === 4 && isRestrictedIpv4(normalizedHostname)) {
104
+ throw new Error("Private or loopback HTTP targets are not allowed by default");
105
+ }
106
+
107
+ if (ipVersion === 6 && isRestrictedIpv6(normalizedHostname)) {
108
+ throw new Error("Private or loopback HTTP targets are not allowed by default");
109
+ }
110
+ }
111
+
112
+ function isRestrictedIpv4(hostname) {
113
+ const octets = hostname.split(".").map((segment) => Number.parseInt(segment, 10));
114
+ if (octets.length !== 4 || octets.some((segment) => Number.isNaN(segment))) {
115
+ return false;
116
+ }
117
+
118
+ const [first, second] = octets;
119
+ if (first === 10 || first === 127 || first === 0) {
120
+ return true;
121
+ }
122
+ if (first === 169 && second === 254) {
123
+ return true;
124
+ }
125
+ if (first === 172 && second >= 16 && second <= 31) {
126
+ return true;
127
+ }
128
+ if (first === 192 && second === 168) {
129
+ return true;
130
+ }
131
+ if (first >= 224) {
132
+ return true;
133
+ }
134
+
135
+ return false;
136
+ }
137
+
138
+ function isRestrictedIpv6(hostname) {
139
+ const normalizedHostname = hostname.toLowerCase();
140
+ return (
141
+ normalizedHostname === "::1"
142
+ || normalizedHostname === "::"
143
+ || normalizedHostname.startsWith("fc")
144
+ || normalizedHostname.startsWith("fd")
145
+ || normalizedHostname.startsWith("fe8")
146
+ || normalizedHostname.startsWith("fe9")
147
+ || normalizedHostname.startsWith("fea")
148
+ || normalizedHostname.startsWith("feb")
149
+ || normalizedHostname.startsWith("ff")
150
+ );
151
+ }
152
+
153
+ function emitToolStarted(observer, { toolName, did, inputs = {} }) {
154
+ observer.emit("agent_did.tool.started", {
155
+ attributes: {
156
+ tool_name: toolName,
157
+ did,
158
+ inputs,
159
+ },
160
+ });
161
+ }
162
+
163
+ function emitToolSucceeded(observer, { toolName, did, outputs = {} }) {
164
+ observer.emit("agent_did.tool.succeeded", {
165
+ attributes: {
166
+ tool_name: toolName,
167
+ did,
168
+ outputs,
169
+ },
170
+ });
171
+ }
172
+
173
+ function emitToolFailed(observer, { toolName, did, error, inputs = {} }) {
174
+ observer.emit("agent_did.tool.failed", {
175
+ level: "error",
176
+ attributes: {
177
+ tool_name: toolName,
178
+ did,
179
+ inputs,
180
+ error: error instanceof Error ? error.message : String(error),
181
+ },
182
+ });
183
+ }
184
+
185
+ function captureIdentitySnapshot(runtimeIdentity, observer, reason) {
186
+ const snapshot = buildAgentDidIdentitySnapshot(runtimeIdentity);
187
+ observer.emit("agent_did.identity_snapshot.refreshed", {
188
+ attributes: {
189
+ did: snapshot.did,
190
+ authentication_key_id: snapshot.authenticationKeyId,
191
+ reason,
192
+ },
193
+ });
194
+ return snapshot;
195
+ }
196
+
197
+ function createAgentDidMiddleware(options) {
198
+ const middlewareName = options.middlewareName ?? "AgentDidIdentityMiddleware";
199
+ const observer = options.observer ?? createAgentDidObserver();
200
+
201
+ return createBrandedMiddleware({
202
+ name: middlewareName,
203
+ wrapModelCall: async (request, handler) => {
204
+ const snapshot = captureIdentitySnapshot(options.runtimeIdentity, observer, "middleware");
205
+ const identitySection = buildAgentDidSystemPrompt(snapshot, options.additionalSystemContext);
206
+
207
+ return handler({
208
+ ...request,
209
+ systemMessage: request.systemMessage.concat(
210
+ new SystemMessage({
211
+ content: identitySection,
212
+ })
213
+ ),
214
+ });
215
+ },
216
+ });
217
+ }
218
+
219
+ function createAgentDidTools(options) {
220
+ const exposure = { ...DEFAULT_EXPOSURE, ...options.expose };
221
+ const toolPrefix = options.toolPrefix ?? "agent_did";
222
+ const observer = options.observer ?? createAgentDidObserver();
223
+ const tools = [];
224
+
225
+ if (exposure.currentIdentity) {
226
+ tools.push(
227
+ tool(async () => {
228
+ const currentDid = options.runtimeIdentity.document.id;
229
+ const toolName = withPrefix(toolPrefix, "get_current_identity");
230
+ emitToolStarted(observer, { toolName, did: currentDid });
231
+ try {
232
+ const snapshot = captureIdentitySnapshot(options.runtimeIdentity, observer, "tool:get_current_identity");
233
+ emitToolSucceeded(observer, {
234
+ toolName,
235
+ did: currentDid,
236
+ outputs: {
237
+ authentication_key_id: snapshot.authenticationKeyId,
238
+ },
239
+ });
240
+ return snapshot;
241
+ } catch (err) {
242
+ emitToolFailed(observer, { toolName, did: currentDid, error: err });
243
+ return { error: err instanceof Error ? err.message : String(err) };
244
+ }
245
+ }, {
246
+ name: withPrefix(toolPrefix, "get_current_identity"),
247
+ description: "Return the current Agent-DID identity attached to this LangChain agent.",
248
+ schema: z.object({}),
249
+ })
250
+ );
251
+ }
252
+
253
+ if (exposure.resolveDid) {
254
+ tools.push(
255
+ tool(async ({ did }) => {
256
+ const currentDid = options.runtimeIdentity.document.id;
257
+ const toolName = withPrefix(toolPrefix, "resolve_did");
258
+ emitToolStarted(observer, { toolName, did: currentDid, inputs: { did } });
259
+ try {
260
+ const targetDid = did && did.trim() ? did.trim() : options.runtimeIdentity.document.id;
261
+ const resolved = await AgentIdentity.resolve(targetDid);
262
+ emitToolSucceeded(observer, {
263
+ toolName,
264
+ did: currentDid,
265
+ outputs: { resolved_did: resolved.id },
266
+ });
267
+ return resolved;
268
+ } catch (err) {
269
+ emitToolFailed(observer, { toolName, did: currentDid, error: err, inputs: { did } });
270
+ return { error: err instanceof Error ? err.message : String(err) };
271
+ }
272
+ }, {
273
+ name: withPrefix(toolPrefix, "resolve_did"),
274
+ description: "Resolve an Agent-DID document. If no DID is provided, resolves the current agent DID.",
275
+ schema: z.object({
276
+ did: z.string().max(512).optional().describe("Optional DID to resolve"),
277
+ }),
278
+ })
279
+ );
280
+ }
281
+
282
+ if (exposure.verifySignatures) {
283
+ tools.push(
284
+ tool(async ({ did, payload, signature, keyId }) => {
285
+ const currentDid = options.runtimeIdentity.document.id;
286
+ const toolName = withPrefix(toolPrefix, "verify_signature");
287
+ emitToolStarted(observer, {
288
+ toolName,
289
+ did: currentDid,
290
+ inputs: { did, key_id: keyId, payload, signature },
291
+ });
292
+ try {
293
+ const targetDid = did && did.trim() ? did.trim() : options.runtimeIdentity.document.id;
294
+ const isValid = await AgentIdentity.verifySignature(targetDid, payload, signature, keyId);
295
+ emitToolSucceeded(observer, {
296
+ toolName,
297
+ did: currentDid,
298
+ outputs: { target_did: targetDid, is_valid: isValid, key_id: keyId },
299
+ });
300
+ return { did: targetDid, keyId, isValid };
301
+ } catch (err) {
302
+ emitToolFailed(observer, {
303
+ toolName,
304
+ did: currentDid,
305
+ error: err,
306
+ inputs: { did, key_id: keyId, payload, signature },
307
+ });
308
+ return { error: err instanceof Error ? err.message : String(err) };
309
+ }
310
+ }, {
311
+ name: withPrefix(toolPrefix, "verify_signature"),
312
+ description: "Verify an Agent-DID signature against a DID document and active verification methods.",
313
+ schema: z.object({
314
+ did: z.string().max(512).optional().describe("Optional DID. Defaults to the current agent DID."),
315
+ payload: z.string().max(MAX_PAYLOAD_BYTES).describe("The exact payload that was signed."),
316
+ signature: z.string().max(256).describe("Hex-encoded Ed25519 signature."),
317
+ keyId: z.string().max(512).optional().describe("Optional verification method id to pin verification."),
318
+ }),
319
+ })
320
+ );
321
+ }
322
+
323
+ if (exposure.signPayload) {
324
+ tools.push(
325
+ tool(async ({ payload }) => {
326
+ const currentDid = options.runtimeIdentity.document.id;
327
+ const toolName = withPrefix(toolPrefix, "sign_payload");
328
+ emitToolStarted(observer, { toolName, did: currentDid, inputs: { payload } });
329
+ try {
330
+ const signature = await options.agentIdentity.signMessage(payload, options.runtimeIdentity.agentPrivateKey);
331
+ const keyId = getActiveVerificationMethodId(options.runtimeIdentity);
332
+ emitToolSucceeded(observer, {
333
+ toolName,
334
+ did: currentDid,
335
+ outputs: { key_id: keyId, signature_generated: true },
336
+ });
337
+ return {
338
+ did: currentDid,
339
+ keyId,
340
+ payload,
341
+ signature,
342
+ };
343
+ } catch (err) {
344
+ emitToolFailed(observer, { toolName, did: currentDid, error: err, inputs: { payload } });
345
+ return { error: err instanceof Error ? err.message : String(err) };
346
+ }
347
+ }, {
348
+ name: withPrefix(toolPrefix, "sign_payload"),
349
+ description: "Sign a payload with the current agent verification key without exposing the private key.",
350
+ schema: z.object({
351
+ payload: z.string().max(MAX_PAYLOAD_BYTES).describe("Payload to sign exactly as-is."),
352
+ }),
353
+ })
354
+ );
355
+ }
356
+
357
+ if (exposure.signHttp) {
358
+ tools.push(
359
+ tool(async ({ method, url, body }) => {
360
+ const currentDid = options.runtimeIdentity.document.id;
361
+ const toolName = withPrefix(toolPrefix, "sign_http_request");
362
+ emitToolStarted(observer, {
363
+ toolName,
364
+ did: currentDid,
365
+ inputs: { method, url, body },
366
+ });
367
+ try {
368
+ validateHttpTarget(url, options.allowPrivateNetworkTargets === true);
369
+ const keyId = getActiveVerificationMethodId(options.runtimeIdentity);
370
+ const headers = await options.agentIdentity.signHttpRequest({
371
+ method,
372
+ url,
373
+ body,
374
+ agentPrivateKey: options.runtimeIdentity.agentPrivateKey,
375
+ agentDid: options.runtimeIdentity.document.id,
376
+ verificationMethodId: keyId,
377
+ });
378
+
379
+ emitToolSucceeded(observer, {
380
+ toolName,
381
+ did: currentDid,
382
+ outputs: {
383
+ key_id: keyId,
384
+ method,
385
+ url,
386
+ header_names: Object.keys(headers).sort(),
387
+ },
388
+ });
389
+
390
+ return {
391
+ did: currentDid,
392
+ keyId,
393
+ method,
394
+ url,
395
+ headers,
396
+ };
397
+ } catch (err) {
398
+ emitToolFailed(observer, { toolName, did: currentDid, error: err, inputs: { method, url, body } });
399
+ return { error: err instanceof Error ? err.message : String(err) };
400
+ }
401
+ }, {
402
+ name: withPrefix(toolPrefix, "sign_http_request"),
403
+ description: "Create Agent-DID HTTP signature headers for an outbound request.",
404
+ schema: z.object({
405
+ method: z.string().max(16).describe("HTTP method, for example GET or POST."),
406
+ url: z.string().url().max(2048).describe("Target absolute URL."),
407
+ body: z.string().max(MAX_PAYLOAD_BYTES).optional().describe("Optional raw request body."),
408
+ }),
409
+ })
410
+ );
411
+ }
412
+
413
+ if (exposure.documentHistory) {
414
+ tools.push(
415
+ tool(async ({ did }) => {
416
+ const currentDid = options.runtimeIdentity.document.id;
417
+ const toolName = withPrefix(toolPrefix, "get_document_history");
418
+ emitToolStarted(observer, { toolName, did: currentDid, inputs: { did } });
419
+ try {
420
+ const targetDid = did && did.trim() ? did.trim() : options.runtimeIdentity.document.id;
421
+ const history = await AgentIdentity.getDocumentHistory(targetDid);
422
+ emitToolSucceeded(observer, {
423
+ toolName,
424
+ did: currentDid,
425
+ outputs: { target_did: targetDid, entry_count: history.length },
426
+ });
427
+ return history;
428
+ } catch (err) {
429
+ emitToolFailed(observer, { toolName, did: currentDid, error: err, inputs: { did } });
430
+ return { error: err instanceof Error ? err.message : String(err) };
431
+ }
432
+ }, {
433
+ name: withPrefix(toolPrefix, "get_document_history"),
434
+ description: "Return the revision history registered for an Agent-DID document.",
435
+ schema: z.object({
436
+ did: z.string().max(512).optional().describe("Optional DID. Defaults to the current agent DID."),
437
+ }),
438
+ })
439
+ );
440
+ }
441
+
442
+ if (exposure.rotateKeys) {
443
+ tools.push(
444
+ tool(async () => {
445
+ const currentDid = options.runtimeIdentity.document.id;
446
+ const toolName = withPrefix(toolPrefix, "rotate_key");
447
+ emitToolStarted(observer, { toolName, did: currentDid });
448
+ try {
449
+ const rotated = await AgentIdentity.rotateVerificationMethod(options.runtimeIdentity.document.id);
450
+ const updatedIdentity = {
451
+ ...options.runtimeIdentity,
452
+ document: rotated.document,
453
+ agentPrivateKey: rotated.agentPrivateKey,
454
+ verificationMethodId: rotated.verificationMethodId,
455
+ };
456
+ Object.assign(options.runtimeIdentity, updatedIdentity);
457
+ const snapshot = captureIdentitySnapshot(options.runtimeIdentity, observer, "tool:rotate_key");
458
+ emitToolSucceeded(observer, {
459
+ toolName,
460
+ did: currentDid,
461
+ outputs: {
462
+ verification_method_id: rotated.verificationMethodId,
463
+ },
464
+ });
465
+
466
+ return {
467
+ did: rotated.document.id,
468
+ verificationMethodId: rotated.verificationMethodId,
469
+ snapshot,
470
+ };
471
+ } catch (err) {
472
+ emitToolFailed(observer, { toolName, did: currentDid, error: err });
473
+ return { error: err instanceof Error ? err.message : String(err) };
474
+ }
475
+ }, {
476
+ name: withPrefix(toolPrefix, "rotate_key"),
477
+ description: "Rotate the active Agent-DID verification method and update the attached runtime identity.",
478
+ schema: z.object({}),
479
+ })
480
+ );
481
+ }
482
+
483
+ return tools;
484
+ }
485
+
486
+ function createAgentDidIntegration(options) {
487
+ const observer = options.observer ?? createAgentDidObserver({
488
+ eventHandler: options.observabilityHandler,
489
+ logger: options.logger,
490
+ });
491
+ const integrationOptions = {
492
+ ...options,
493
+ observer,
494
+ };
495
+ const middleware = createAgentDidMiddleware(integrationOptions);
496
+ const tools = createAgentDidTools(integrationOptions);
497
+
498
+ return {
499
+ middleware,
500
+ observer,
501
+ tools,
502
+ getCurrentIdentity: () => captureIdentitySnapshot(options.runtimeIdentity, observer, "get_current_identity"),
503
+ getCurrentDocument: () => options.runtimeIdentity.document,
504
+ };
505
+ }
506
+
507
+ function createAgentDidPlugin(options) {
508
+ return createAgentDidIntegration(options);
509
+ }
510
+
511
+ module.exports = {
512
+ buildAgentDidIdentitySnapshot,
513
+ buildAgentDidSystemPrompt,
514
+ createAgentDidIntegration,
515
+ createAgentDidMiddleware,
516
+ createAgentDidPlugin,
517
+ createAgentDidTools,
518
+ };
package/src/index.js ADDED
@@ -0,0 +1,35 @@
1
+ const {
2
+ buildAgentDidIdentitySnapshot,
3
+ buildAgentDidSystemPrompt,
4
+ createAgentDidIntegration,
5
+ createAgentDidMiddleware,
6
+ createAgentDidPlugin,
7
+ createAgentDidTools,
8
+ } = require("./agentDidLangChain");
9
+ const {
10
+ REDACTED_VALUE,
11
+ composeEventHandlers,
12
+ createAgentDidObserver,
13
+ createJsonLoggerEventHandler,
14
+ createLangSmithEventHandler,
15
+ createLangSmithRunTree,
16
+ sanitizeObservabilityAttributes,
17
+ serializeObservabilityEvent,
18
+ } = require("./observability");
19
+
20
+ module.exports = {
21
+ REDACTED_VALUE,
22
+ buildAgentDidIdentitySnapshot,
23
+ buildAgentDidSystemPrompt,
24
+ composeEventHandlers,
25
+ createAgentDidObserver,
26
+ createAgentDidIntegration,
27
+ createAgentDidMiddleware,
28
+ createAgentDidPlugin,
29
+ createAgentDidTools,
30
+ createJsonLoggerEventHandler,
31
+ createLangSmithEventHandler,
32
+ createLangSmithRunTree,
33
+ sanitizeObservabilityAttributes,
34
+ serializeObservabilityEvent,
35
+ };
@@ -0,0 +1,415 @@
1
+ const REDACTED_VALUE = "<redacted>";
2
+ const SENSITIVE_FIELD_NAMES = new Set([
3
+ "agent_private_key",
4
+ "body",
5
+ "payload",
6
+ "signature",
7
+ ]);
8
+ const SENSITIVE_HEADER_NAMES = new Set([
9
+ "authorization",
10
+ "cookie",
11
+ "proxy-authorization",
12
+ "set-cookie",
13
+ "signature",
14
+ "signature-input",
15
+ "x-api-key",
16
+ ]);
17
+
18
+ function composeEventHandlers(...handlers) {
19
+ const activeHandlers = handlers.filter(Boolean);
20
+
21
+ return (event) => {
22
+ for (const handler of activeHandlers) {
23
+ try {
24
+ handler(event);
25
+ } catch {
26
+ continue;
27
+ }
28
+ }
29
+ };
30
+ }
31
+
32
+ function serializeObservabilityEvent(
33
+ event,
34
+ {
35
+ source = "agent_did_langchain",
36
+ includeTimestamp = true,
37
+ extraFields,
38
+ } = {}
39
+ ) {
40
+ const record = {
41
+ source,
42
+ eventType: event.eventType,
43
+ level: event.level,
44
+ attributes: sanitizeObservabilityAttributes(event.attributes ?? {}),
45
+ };
46
+
47
+ if (includeTimestamp) {
48
+ record.timestamp = new Date().toISOString();
49
+ }
50
+
51
+ if (extraFields) {
52
+ Object.assign(record, sanitizeObservabilityAttributes(extraFields));
53
+ }
54
+
55
+ return record;
56
+ }
57
+
58
+ function createJsonLoggerEventHandler(
59
+ logger,
60
+ {
61
+ source = "agent_did_langchain",
62
+ includeTimestamp = true,
63
+ extraFields,
64
+ } = {}
65
+ ) {
66
+ return (event) => {
67
+ const record = serializeObservabilityEvent(event, {
68
+ source,
69
+ includeTimestamp,
70
+ extraFields,
71
+ });
72
+ logWithLogger(logger, event.level, JSON.stringify(record));
73
+ };
74
+ }
75
+
76
+ function createLangSmithRunTree(
77
+ {
78
+ name,
79
+ projectName = "agent-did-langchain",
80
+ runType = "chain",
81
+ inputs,
82
+ tags,
83
+ extra,
84
+ client,
85
+ } = {}
86
+ ) {
87
+ const RunTree = loadLangSmithRunTree();
88
+
89
+ return new RunTree({
90
+ name,
91
+ run_type: runType,
92
+ project_name: projectName,
93
+ inputs: sanitizeObservabilityAttributes(inputs ?? {}),
94
+ tags: [...(tags ?? [])],
95
+ extra: sanitizeObservabilityAttributes(extra ?? {}),
96
+ client,
97
+ serialized: {},
98
+ });
99
+ }
100
+
101
+ function createLangSmithEventHandler(
102
+ runTree,
103
+ {
104
+ source = "agent_did_langchain",
105
+ includeTimestamp = true,
106
+ extraFields,
107
+ tags,
108
+ postImmediately = false,
109
+ } = {}
110
+ ) {
111
+ const activeToolRuns = new Map();
112
+ const normalizedTags = [...(tags ?? [])];
113
+
114
+ return (event) => {
115
+ const record = serializeObservabilityEvent(event, {
116
+ source,
117
+ includeTimestamp,
118
+ extraFields,
119
+ });
120
+ const attributes = record.attributes ?? {};
121
+ const toolName = attributes.tool_name;
122
+ const did = attributes.did;
123
+ const eventType = event.eventType;
124
+
125
+ try {
126
+ runTree.addEvent(toLangSmithEvent(record));
127
+ } catch {
128
+ // no-op defensive boundary
129
+ }
130
+
131
+ if (isLangSmithToolEvent(toolName, did, eventType)) {
132
+ handleLangSmithToolEvent({
133
+ runTree,
134
+ activeToolRuns,
135
+ normalizedTags,
136
+ source,
137
+ postImmediately,
138
+ toolName,
139
+ did,
140
+ eventType,
141
+ attributes,
142
+ record,
143
+ });
144
+ return;
145
+ }
146
+
147
+ handleLangSmithGenericEvent({
148
+ runTree,
149
+ normalizedTags,
150
+ source,
151
+ postImmediately,
152
+ eventType,
153
+ attributes,
154
+ record,
155
+ });
156
+ };
157
+ }
158
+
159
+ function createAgentDidObserver({ eventHandler, logger } = {}) {
160
+ return {
161
+ emit(eventType, { attributes = {}, level = "info" } = {}) {
162
+ const event = {
163
+ eventType,
164
+ level,
165
+ attributes: sanitizeObservabilityAttributes(attributes),
166
+ };
167
+
168
+ if (eventHandler) {
169
+ try {
170
+ eventHandler(event);
171
+ } catch {
172
+ // no-op defensive boundary
173
+ }
174
+ }
175
+
176
+ if (logger) {
177
+ try {
178
+ logWithLogger(
179
+ logger,
180
+ level,
181
+ `agent_did_langchain event=${event.eventType} attributes=${JSON.stringify(event.attributes)}`
182
+ );
183
+ } catch {
184
+ // no-op defensive boundary
185
+ }
186
+ }
187
+ },
188
+ };
189
+ }
190
+
191
+ function sanitizeObservabilityAttributes(attributes) {
192
+ return Object.fromEntries(
193
+ Object.entries(attributes).map(([key, value]) => [String(key), sanitizeValue(String(key), value)])
194
+ );
195
+ }
196
+
197
+ function sanitizeValue(fieldName, value) {
198
+ const normalizedFieldName = fieldName.toLowerCase();
199
+
200
+ if (SENSITIVE_FIELD_NAMES.has(normalizedFieldName) && typeof value === "string") {
201
+ return { redacted: true, length: value.length };
202
+ }
203
+
204
+ if (normalizedFieldName === "url" && typeof value === "string") {
205
+ return sanitizeUrl(value);
206
+ }
207
+
208
+ if (Array.isArray(value)) {
209
+ return value.map((entry) => sanitizeValue(fieldName, entry));
210
+ }
211
+
212
+ if (value && typeof value === "object") {
213
+ if (normalizedFieldName === "headers") {
214
+ return sanitizeHeaders(value);
215
+ }
216
+
217
+ return Object.fromEntries(
218
+ Object.entries(value).map(([key, nestedValue]) => [String(key), sanitizeValue(String(key), nestedValue)])
219
+ );
220
+ }
221
+
222
+ return value;
223
+ }
224
+
225
+ function sanitizeHeaders(headers) {
226
+ return Object.fromEntries(
227
+ Object.entries(headers).map(([headerName, headerValue]) => {
228
+ if (SENSITIVE_HEADER_NAMES.has(String(headerName).toLowerCase())) {
229
+ return [String(headerName), REDACTED_VALUE];
230
+ }
231
+
232
+ return [String(headerName), sanitizeValue(String(headerName), headerValue)];
233
+ })
234
+ );
235
+ }
236
+
237
+ function sanitizeUrl(url) {
238
+ const parsedUrl = new URL(url);
239
+ parsedUrl.username = "";
240
+ parsedUrl.password = "";
241
+ parsedUrl.search = "";
242
+ parsedUrl.hash = "";
243
+ return parsedUrl.toString();
244
+ }
245
+
246
+ function logWithLogger(logger, level, message) {
247
+ const normalizedLevel = normalizeLevel(level);
248
+ if (typeof logger[normalizedLevel] === "function") {
249
+ logger[normalizedLevel](message);
250
+ return;
251
+ }
252
+
253
+ if (typeof logger.log === "function") {
254
+ logger.log(normalizedLevel, message);
255
+ }
256
+ }
257
+
258
+ function normalizeLevel(level) {
259
+ if (level === "debug" || level === "warning" || level === "error") {
260
+ return level === "warning" ? "warn" : level;
261
+ }
262
+
263
+ return "info";
264
+ }
265
+
266
+ function loadLangSmithRunTree() {
267
+ try {
268
+ const { RunTree } = require("langsmith");
269
+ if (typeof RunTree !== "function") {
270
+ throw new Error("RunTree export not found");
271
+ }
272
+ return RunTree;
273
+ } catch (error) {
274
+ const wrappedError = new Error(
275
+ "LangSmith is required for the LangSmith observability adapter. Install the optional 'langsmith' package to enable it."
276
+ );
277
+ wrappedError.cause = error;
278
+ throw wrappedError;
279
+ }
280
+ }
281
+
282
+ function toLangSmithEvent(record) {
283
+ return {
284
+ name: record.eventType ?? "event",
285
+ kwargs: record,
286
+ };
287
+ }
288
+
289
+ function isLangSmithToolEvent(toolName, did, eventType) {
290
+ return typeof toolName === "string" && typeof did === "string" && eventType.startsWith("agent_did.tool.");
291
+ }
292
+
293
+ function createLangSmithChildRun(runTree, { name, runType, inputs, tags, source, eventType }) {
294
+ return runTree.createChild({
295
+ name,
296
+ run_type: runType,
297
+ inputs,
298
+ tags,
299
+ extra: { source, agent_did_event_type: eventType },
300
+ serialized: {},
301
+ });
302
+ }
303
+
304
+ function handleLangSmithToolEvent({
305
+ runTree,
306
+ activeToolRuns,
307
+ normalizedTags,
308
+ source,
309
+ postImmediately,
310
+ toolName,
311
+ did,
312
+ eventType,
313
+ attributes,
314
+ record,
315
+ }) {
316
+ const runKey = `${toolName}:${did}`;
317
+ let childRun = activeToolRuns.get(runKey);
318
+
319
+ if (eventType.endsWith(".started")) {
320
+ childRun = createLangSmithChildRun(runTree, {
321
+ name: toolName,
322
+ runType: "tool",
323
+ inputs: { did, inputs: attributes.inputs ?? {}, event_type: eventType },
324
+ tags: [...normalizedTags, "agent-did", "tool"],
325
+ source,
326
+ eventType,
327
+ });
328
+ childRun.addEvent(toLangSmithEvent(record));
329
+ activeToolRuns.set(runKey, childRun);
330
+ maybePostLangSmithRun(childRun, { postImmediately, finalized: false });
331
+ return;
332
+ }
333
+
334
+ if (!childRun) {
335
+ childRun = createLangSmithChildRun(runTree, {
336
+ name: toolName,
337
+ runType: "tool",
338
+ inputs: { did, event_type: eventType },
339
+ tags: [...normalizedTags, "agent-did", "tool"],
340
+ source,
341
+ eventType,
342
+ });
343
+ }
344
+
345
+ childRun.addEvent(toLangSmithEvent(record));
346
+
347
+ if (eventType.endsWith(".failed")) {
348
+ void childRun.end(
349
+ { did, event_type: eventType, attributes },
350
+ String(attributes.error ?? "unknown error")
351
+ );
352
+ } else {
353
+ void childRun.end({ did, event_type: eventType, attributes });
354
+ }
355
+
356
+ activeToolRuns.delete(runKey);
357
+ maybePostLangSmithRun(childRun, { postImmediately, finalized: true });
358
+ }
359
+
360
+ function handleLangSmithGenericEvent({
361
+ runTree,
362
+ normalizedTags,
363
+ source,
364
+ postImmediately,
365
+ eventType,
366
+ attributes,
367
+ record,
368
+ }) {
369
+ const childRun = createLangSmithChildRun(runTree, {
370
+ name: eventType,
371
+ runType: "chain",
372
+ inputs: { event_type: eventType, attributes },
373
+ tags: [...normalizedTags, "agent-did", "event"],
374
+ source,
375
+ eventType,
376
+ });
377
+
378
+ childRun.addEvent(toLangSmithEvent(record));
379
+ void childRun.end({ event_type: eventType, attributes });
380
+ maybePostLangSmithRun(childRun, { postImmediately, finalized: true });
381
+ }
382
+
383
+ const postedRuns = new WeakSet();
384
+
385
+ function maybePostLangSmithRun(runTree, { postImmediately, finalized }) {
386
+ if (!postImmediately) {
387
+ return;
388
+ }
389
+
390
+ void Promise.resolve().then(async () => {
391
+ try {
392
+ if (!postedRuns.has(runTree) && typeof runTree.postRun === "function") {
393
+ postedRuns.add(runTree);
394
+ await runTree.postRun();
395
+ }
396
+
397
+ if (finalized && typeof runTree.patchRun === "function") {
398
+ await runTree.patchRun();
399
+ }
400
+ } catch {
401
+ // no-op defensive boundary
402
+ }
403
+ });
404
+ }
405
+
406
+ module.exports = {
407
+ REDACTED_VALUE,
408
+ composeEventHandlers,
409
+ createAgentDidObserver,
410
+ createJsonLoggerEventHandler,
411
+ createLangSmithEventHandler,
412
+ createLangSmithRunTree,
413
+ sanitizeObservabilityAttributes,
414
+ serializeObservabilityEvent,
415
+ };