@datacules/agent-identity-langchain 0.10.0 → 0.11.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 +109 -0
- package/dist/cjs/index.js +154 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +115 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/index.d.ts +80 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +30 -3
- package/src/index.ts +0 -175
- package/src/langchain.test.ts +0 -179
package/LICENSE
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Datacules Agent Identity License — Version 1.0
|
|
2
|
+
Copyright (c) 2026 Datacules LLC. All rights reserved.
|
|
3
|
+
|
|
4
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
PREAMBLE
|
|
6
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
This software — Agent Identity & Auth Patterns — is developed and owned by
|
|
9
|
+
Datacules LLC. It is made available to the public as open-source software
|
|
10
|
+
under the permissive terms below.
|
|
11
|
+
|
|
12
|
+
Datacules LLC retains ownership and authorship of this software while
|
|
13
|
+
granting broad, royalty-free rights for anyone to use, copy, modify, and
|
|
14
|
+
distribute it — in commercial or non-commercial contexts — without requiring
|
|
15
|
+
that derivative works also become open source.
|
|
16
|
+
|
|
17
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
TERMS AND CONDITIONS
|
|
19
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
1. PERMISSION TO USE
|
|
22
|
+
|
|
23
|
+
Permission is hereby granted, free of charge, to any person or
|
|
24
|
+
organization obtaining a copy of this software and associated
|
|
25
|
+
documentation files (the "Software"), to use, copy, modify, merge,
|
|
26
|
+
publish, distribute, sublicense, and/or sell copies of the Software,
|
|
27
|
+
and to permit persons to whom the Software is furnished to do so,
|
|
28
|
+
subject to the conditions below.
|
|
29
|
+
|
|
30
|
+
2. ATTRIBUTION
|
|
31
|
+
|
|
32
|
+
a. Redistributions of source code must retain this copyright notice,
|
|
33
|
+
this list of conditions, and the disclaimer below.
|
|
34
|
+
|
|
35
|
+
b. Redistributions in binary form or as a product must reproduce this
|
|
36
|
+
copyright notice, this list of conditions, and the disclaimer in the
|
|
37
|
+
documentation and/or other materials provided with the distribution.
|
|
38
|
+
|
|
39
|
+
c. Neither the name "Datacules LLC" nor the names of its contributors
|
|
40
|
+
may be used to endorse or promote products derived from this Software
|
|
41
|
+
without prior written permission from Datacules LLC.
|
|
42
|
+
|
|
43
|
+
3. COMMERCIAL USE
|
|
44
|
+
|
|
45
|
+
Use of this Software in commercial products, SaaS platforms, internal
|
|
46
|
+
enterprise tools, or any revenue-generating context is explicitly
|
|
47
|
+
permitted without royalty, fee, or additional licensing agreement,
|
|
48
|
+
provided that the conditions in Section 2 (Attribution) are met.
|
|
49
|
+
|
|
50
|
+
4. NO COPYLEFT / NO VIRAL REQUIREMENT
|
|
51
|
+
|
|
52
|
+
This license does NOT require that derivative works, modifications,
|
|
53
|
+
or software that uses or embeds this Software be made open source.
|
|
54
|
+
You may incorporate this Software into proprietary or closed-source
|
|
55
|
+
products under your own license terms.
|
|
56
|
+
|
|
57
|
+
5. MODIFICATIONS
|
|
58
|
+
|
|
59
|
+
Modified versions of the Software may be distributed under the same
|
|
60
|
+
terms as this license or under any other permissive open-source
|
|
61
|
+
license (e.g. MIT, Apache 2.0, BSD), provided that:
|
|
62
|
+
|
|
63
|
+
a. The original copyright notice of Datacules LLC is preserved.
|
|
64
|
+
b. Modifications are clearly documented and distinguished from the
|
|
65
|
+
original work.
|
|
66
|
+
|
|
67
|
+
6. COMPATIBILITY
|
|
68
|
+
|
|
69
|
+
This license is compatible with other permissive open-source licenses
|
|
70
|
+
such as MIT, BSD 2-Clause, BSD 3-Clause, and Apache License 2.0. It
|
|
71
|
+
is also GPL-compatible — this Software may coexist with GPL-licensed
|
|
72
|
+
code, though this Software itself is not distributed under the GPL.
|
|
73
|
+
|
|
74
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
DISCLAIMER
|
|
76
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
THIS SOFTWARE IS PROVIDED BY DATACULES LLC AND CONTRIBUTORS "AS IS" AND
|
|
79
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
80
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
81
|
+
AND NON-INFRINGEMENT ARE DISCLAIMED.
|
|
82
|
+
|
|
83
|
+
IN NO EVENT SHALL DATACULES LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
84
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
85
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
86
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
87
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
88
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
89
|
+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
90
|
+
|
|
91
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
SUMMARY (non-binding)
|
|
93
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
✔ Use freely — commercial, proprietary, or open-source projects
|
|
96
|
+
✔ Modify and distribute with or without changes
|
|
97
|
+
✔ Sell products built on this Software
|
|
98
|
+
✔ No royalties or fees
|
|
99
|
+
✔ No requirement to open-source your own code
|
|
100
|
+
✔ Attribution to Datacules LLC required in source and binary distributions
|
|
101
|
+
✗ Do not use "Datacules LLC" to endorse derived products without permission
|
|
102
|
+
|
|
103
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
CONTACT
|
|
105
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
Datacules LLC
|
|
108
|
+
For licensing enquiries: legal@datacules.com
|
|
109
|
+
Product: https://github.com/hvrcharon1/agent-identity
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LangChain integration for @datacules/agent-identity.
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* createAgentIdentityModel() — wraps ChatOpenAI/ChatAnthropic with credential resolution
|
|
7
|
+
* AgentIdentityCallbackHandler — LangChain callback handler for audit logging
|
|
8
|
+
* createAgentIdentityNode() — LangGraph StateGraph node for credential resolution
|
|
9
|
+
*
|
|
10
|
+
* Usage (LangChain):
|
|
11
|
+
* const { getModel } = createAgentIdentityModel(ctx, credentials, rules, fetchSecret);
|
|
12
|
+
* const model = await getModel();
|
|
13
|
+
* const result = await model.invoke([{ role: 'user', content: 'Hello' }]);
|
|
14
|
+
*
|
|
15
|
+
* Usage (LangGraph):
|
|
16
|
+
* const graph = new StateGraph(...);
|
|
17
|
+
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules, logger));
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.AgentIdentityCallbackHandler = void 0;
|
|
54
|
+
exports.createAgentIdentityModel = createAgentIdentityModel;
|
|
55
|
+
exports.createAgentIdentityNode = createAgentIdentityNode;
|
|
56
|
+
const agent_identity_1 = require("@datacules/agent-identity");
|
|
57
|
+
const base_1 = require("@langchain/core/callbacks/base");
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a credential and return a factory that builds the correct
|
|
60
|
+
* LangChain chat model with the API key injected server-side.
|
|
61
|
+
*
|
|
62
|
+
* The raw secret is fetched via fetchSecret(ref) and passed directly to the
|
|
63
|
+
* model constructor. It never appears in a log, a response body, or the
|
|
64
|
+
* browser — the model is built on the server and used there.
|
|
65
|
+
*/
|
|
66
|
+
function createAgentIdentityModel(ctx, options) {
|
|
67
|
+
const router = (0, agent_identity_1.createRouter)(options.credentials, options.rules, options.logger);
|
|
68
|
+
const resolved = router.resolve(ctx);
|
|
69
|
+
if (!resolved)
|
|
70
|
+
throw new Error(`[agent-identity] No credential resolved for context: ${JSON.stringify(ctx)}`);
|
|
71
|
+
const getModel = async () => {
|
|
72
|
+
const apiKey = await options.fetchSecret(resolved.ref);
|
|
73
|
+
const meta = {
|
|
74
|
+
agentIdentityCredentialId: resolved.credentialId,
|
|
75
|
+
agentIdentityResolvedFor: resolved.resolvedFor,
|
|
76
|
+
traceId: ctx.traceId,
|
|
77
|
+
};
|
|
78
|
+
if (ctx.provider === 'openai') {
|
|
79
|
+
const { ChatOpenAI } = await Promise.resolve(`${'@langchain/openai'}`).then(s => __importStar(require(s)));
|
|
80
|
+
return new ChatOpenAI({ modelName: ctx.model, openAIApiKey: apiKey, metadata: meta });
|
|
81
|
+
}
|
|
82
|
+
if (ctx.provider === 'anthropic') {
|
|
83
|
+
const { ChatAnthropic } = await Promise.resolve(`${'@langchain/anthropic'}`).then(s => __importStar(require(s)));
|
|
84
|
+
return new ChatAnthropic({ modelName: ctx.model, anthropicApiKey: apiKey, metadata: meta });
|
|
85
|
+
}
|
|
86
|
+
if (ctx.provider === 'gemini') {
|
|
87
|
+
const { ChatGoogleGenerativeAI } = await Promise.resolve(`${'@langchain/google-genai'}`).then(s => __importStar(require(s)));
|
|
88
|
+
return new ChatGoogleGenerativeAI({ modelName: ctx.model, apiKey, metadata: meta });
|
|
89
|
+
}
|
|
90
|
+
if (ctx.provider === 'mistral') {
|
|
91
|
+
const { ChatMistralAI } = await Promise.resolve(`${'@langchain/mistralai'}`).then(s => __importStar(require(s)));
|
|
92
|
+
return new ChatMistralAI({ modelName: ctx.model, apiKey, metadata: meta });
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`[agent-identity/langchain] Provider "${ctx.provider}" not yet supported in LangChain adapter.`);
|
|
95
|
+
};
|
|
96
|
+
return { resolved, getModel };
|
|
97
|
+
}
|
|
98
|
+
// ─── AgentIdentityCallbackHandler ────────────────────────────────────────────────
|
|
99
|
+
/**
|
|
100
|
+
* LangChain callback handler that records agent-identity resolution metadata
|
|
101
|
+
* into each LLM run's metadata. Drop this into any LangChain chain or agent
|
|
102
|
+
* to get automatic credential tracing without modifying the chain itself.
|
|
103
|
+
*
|
|
104
|
+
* Example:
|
|
105
|
+
* const handler = new AgentIdentityCallbackHandler(resolved, logger);
|
|
106
|
+
* await model.invoke(messages, { callbacks: [handler] });
|
|
107
|
+
*/
|
|
108
|
+
class AgentIdentityCallbackHandler extends base_1.BaseCallbackHandler {
|
|
109
|
+
constructor(resolved, logger) {
|
|
110
|
+
super();
|
|
111
|
+
this.resolved = resolved;
|
|
112
|
+
this.logger = logger;
|
|
113
|
+
this.name = 'AgentIdentityCallbackHandler';
|
|
114
|
+
}
|
|
115
|
+
async handleLLMStart(_llm, _prompts, runId, _parentRunId, extraParams) {
|
|
116
|
+
if (extraParams) {
|
|
117
|
+
extraParams['agentIdentityCredentialId'] = this.resolved.credentialId;
|
|
118
|
+
extraParams['agentIdentityResolvedFor'] = this.resolved.resolvedFor;
|
|
119
|
+
}
|
|
120
|
+
// logger.log() is called by the router at resolve time; no duplicate log here
|
|
121
|
+
void runId;
|
|
122
|
+
}
|
|
123
|
+
async handleLLMEnd(_output, _runId) {
|
|
124
|
+
// Extension point: log token usage, latency, etc.
|
|
125
|
+
}
|
|
126
|
+
async handleLLMError(error, _runId) {
|
|
127
|
+
console.warn('[agent-identity] LLM error with credential', this.resolved.ref, error.message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.AgentIdentityCallbackHandler = AgentIdentityCallbackHandler;
|
|
131
|
+
// ─── createAgentIdentityNode (LangGraph) ────────────────────────────────────────
|
|
132
|
+
/**
|
|
133
|
+
* Drop-in LangGraph StateGraph node that resolves credentials before any
|
|
134
|
+
* LLM call in the graph. Attaches resolvedCredential to the state so
|
|
135
|
+
* downstream nodes can use it without repeating resolution.
|
|
136
|
+
*
|
|
137
|
+
* Example:
|
|
138
|
+
* const graph = new StateGraph(...);
|
|
139
|
+
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules));
|
|
140
|
+
* graph.addEdge('resolve-creds', 'llm-call');
|
|
141
|
+
*/
|
|
142
|
+
function createAgentIdentityNode(credentials, rules, logger) {
|
|
143
|
+
const router = (0, agent_identity_1.createRouter)(credentials, rules, logger);
|
|
144
|
+
return async (state) => {
|
|
145
|
+
const ctx = state['agentContext'];
|
|
146
|
+
if (!ctx)
|
|
147
|
+
throw new Error('[agent-identity/langchain] state.agentContext is required');
|
|
148
|
+
const resolved = router.resolve(ctx);
|
|
149
|
+
if (!resolved)
|
|
150
|
+
throw new Error(`[agent-identity/langchain] No credential resolved for context: ${JSON.stringify(ctx)}`);
|
|
151
|
+
return { ...state, resolvedCredential: resolved };
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain integration for @datacules/agent-identity.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* createAgentIdentityModel() — wraps ChatOpenAI/ChatAnthropic with credential resolution
|
|
6
|
+
* AgentIdentityCallbackHandler — LangChain callback handler for audit logging
|
|
7
|
+
* createAgentIdentityNode() — LangGraph StateGraph node for credential resolution
|
|
8
|
+
*
|
|
9
|
+
* Usage (LangChain):
|
|
10
|
+
* const { getModel } = createAgentIdentityModel(ctx, credentials, rules, fetchSecret);
|
|
11
|
+
* const model = await getModel();
|
|
12
|
+
* const result = await model.invoke([{ role: 'user', content: 'Hello' }]);
|
|
13
|
+
*
|
|
14
|
+
* Usage (LangGraph):
|
|
15
|
+
* const graph = new StateGraph(...);
|
|
16
|
+
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules, logger));
|
|
17
|
+
*/
|
|
18
|
+
import { createRouter } from '@datacules/agent-identity';
|
|
19
|
+
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a credential and return a factory that builds the correct
|
|
22
|
+
* LangChain chat model with the API key injected server-side.
|
|
23
|
+
*
|
|
24
|
+
* The raw secret is fetched via fetchSecret(ref) and passed directly to the
|
|
25
|
+
* model constructor. It never appears in a log, a response body, or the
|
|
26
|
+
* browser — the model is built on the server and used there.
|
|
27
|
+
*/
|
|
28
|
+
export function createAgentIdentityModel(ctx, options) {
|
|
29
|
+
const router = createRouter(options.credentials, options.rules, options.logger);
|
|
30
|
+
const resolved = router.resolve(ctx);
|
|
31
|
+
if (!resolved)
|
|
32
|
+
throw new Error(`[agent-identity] No credential resolved for context: ${JSON.stringify(ctx)}`);
|
|
33
|
+
const getModel = async () => {
|
|
34
|
+
const apiKey = await options.fetchSecret(resolved.ref);
|
|
35
|
+
const meta = {
|
|
36
|
+
agentIdentityCredentialId: resolved.credentialId,
|
|
37
|
+
agentIdentityResolvedFor: resolved.resolvedFor,
|
|
38
|
+
traceId: ctx.traceId,
|
|
39
|
+
};
|
|
40
|
+
if (ctx.provider === 'openai') {
|
|
41
|
+
const { ChatOpenAI } = await import('@langchain/openai');
|
|
42
|
+
return new ChatOpenAI({ modelName: ctx.model, openAIApiKey: apiKey, metadata: meta });
|
|
43
|
+
}
|
|
44
|
+
if (ctx.provider === 'anthropic') {
|
|
45
|
+
const { ChatAnthropic } = await import('@langchain/anthropic');
|
|
46
|
+
return new ChatAnthropic({ modelName: ctx.model, anthropicApiKey: apiKey, metadata: meta });
|
|
47
|
+
}
|
|
48
|
+
if (ctx.provider === 'gemini') {
|
|
49
|
+
const { ChatGoogleGenerativeAI } = await import('@langchain/google-genai');
|
|
50
|
+
return new ChatGoogleGenerativeAI({ modelName: ctx.model, apiKey, metadata: meta });
|
|
51
|
+
}
|
|
52
|
+
if (ctx.provider === 'mistral') {
|
|
53
|
+
const { ChatMistralAI } = await import('@langchain/mistralai');
|
|
54
|
+
return new ChatMistralAI({ modelName: ctx.model, apiKey, metadata: meta });
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`[agent-identity/langchain] Provider "${ctx.provider}" not yet supported in LangChain adapter.`);
|
|
57
|
+
};
|
|
58
|
+
return { resolved, getModel };
|
|
59
|
+
}
|
|
60
|
+
// ─── AgentIdentityCallbackHandler ────────────────────────────────────────────────
|
|
61
|
+
/**
|
|
62
|
+
* LangChain callback handler that records agent-identity resolution metadata
|
|
63
|
+
* into each LLM run's metadata. Drop this into any LangChain chain or agent
|
|
64
|
+
* to get automatic credential tracing without modifying the chain itself.
|
|
65
|
+
*
|
|
66
|
+
* Example:
|
|
67
|
+
* const handler = new AgentIdentityCallbackHandler(resolved, logger);
|
|
68
|
+
* await model.invoke(messages, { callbacks: [handler] });
|
|
69
|
+
*/
|
|
70
|
+
export class AgentIdentityCallbackHandler extends BaseCallbackHandler {
|
|
71
|
+
constructor(resolved, logger) {
|
|
72
|
+
super();
|
|
73
|
+
this.resolved = resolved;
|
|
74
|
+
this.logger = logger;
|
|
75
|
+
this.name = 'AgentIdentityCallbackHandler';
|
|
76
|
+
}
|
|
77
|
+
async handleLLMStart(_llm, _prompts, runId, _parentRunId, extraParams) {
|
|
78
|
+
if (extraParams) {
|
|
79
|
+
extraParams['agentIdentityCredentialId'] = this.resolved.credentialId;
|
|
80
|
+
extraParams['agentIdentityResolvedFor'] = this.resolved.resolvedFor;
|
|
81
|
+
}
|
|
82
|
+
// logger.log() is called by the router at resolve time; no duplicate log here
|
|
83
|
+
void runId;
|
|
84
|
+
}
|
|
85
|
+
async handleLLMEnd(_output, _runId) {
|
|
86
|
+
// Extension point: log token usage, latency, etc.
|
|
87
|
+
}
|
|
88
|
+
async handleLLMError(error, _runId) {
|
|
89
|
+
console.warn('[agent-identity] LLM error with credential', this.resolved.ref, error.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ─── createAgentIdentityNode (LangGraph) ────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Drop-in LangGraph StateGraph node that resolves credentials before any
|
|
95
|
+
* LLM call in the graph. Attaches resolvedCredential to the state so
|
|
96
|
+
* downstream nodes can use it without repeating resolution.
|
|
97
|
+
*
|
|
98
|
+
* Example:
|
|
99
|
+
* const graph = new StateGraph(...);
|
|
100
|
+
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules));
|
|
101
|
+
* graph.addEdge('resolve-creds', 'llm-call');
|
|
102
|
+
*/
|
|
103
|
+
export function createAgentIdentityNode(credentials, rules, logger) {
|
|
104
|
+
const router = createRouter(credentials, rules, logger);
|
|
105
|
+
return async (state) => {
|
|
106
|
+
const ctx = state['agentContext'];
|
|
107
|
+
if (!ctx)
|
|
108
|
+
throw new Error('[agent-identity/langchain] state.agentContext is required');
|
|
109
|
+
const resolved = router.resolve(ctx);
|
|
110
|
+
if (!resolved)
|
|
111
|
+
throw new Error(`[agent-identity/langchain] No credential resolved for context: ${JSON.stringify(ctx)}`);
|
|
112
|
+
return { ...state, resolvedCredential: resolved };
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain integration for @datacules/agent-identity.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* createAgentIdentityModel() — wraps ChatOpenAI/ChatAnthropic with credential resolution
|
|
6
|
+
* AgentIdentityCallbackHandler — LangChain callback handler for audit logging
|
|
7
|
+
* createAgentIdentityNode() — LangGraph StateGraph node for credential resolution
|
|
8
|
+
*
|
|
9
|
+
* Usage (LangChain):
|
|
10
|
+
* const { getModel } = createAgentIdentityModel(ctx, credentials, rules, fetchSecret);
|
|
11
|
+
* const model = await getModel();
|
|
12
|
+
* const result = await model.invoke([{ role: 'user', content: 'Hello' }]);
|
|
13
|
+
*
|
|
14
|
+
* Usage (LangGraph):
|
|
15
|
+
* const graph = new StateGraph(...);
|
|
16
|
+
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules, logger));
|
|
17
|
+
*/
|
|
18
|
+
import type { AgentRequestContext, AuditLogger, Credential, ResolvedCredential, RoutingRule } from '@datacules/agent-identity';
|
|
19
|
+
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
20
|
+
import type { Serialized } from '@langchain/core/load/serializable';
|
|
21
|
+
import type { LLMResult } from '@langchain/core/outputs';
|
|
22
|
+
export interface AgentIdentityModelOptions {
|
|
23
|
+
credentials: Credential[];
|
|
24
|
+
rules: RoutingRule[];
|
|
25
|
+
/**
|
|
26
|
+
* Server-side secret fetcher. Receives the credential ref and returns
|
|
27
|
+
* the raw API key. NEVER call this client-side — use in a server route only.
|
|
28
|
+
*/
|
|
29
|
+
fetchSecret: (ref: string) => Promise<string>;
|
|
30
|
+
logger?: AuditLogger;
|
|
31
|
+
}
|
|
32
|
+
export interface AgentIdentityModelResult {
|
|
33
|
+
/** Resolved credential metadata (safe to log, never contains raw secret) */
|
|
34
|
+
resolved: ResolvedCredential;
|
|
35
|
+
/**
|
|
36
|
+
* Returns the LangChain model instance with the API key injected.
|
|
37
|
+
* Dynamic import keeps @langchain/openai and @langchain/anthropic as
|
|
38
|
+
* optional peer deps — only the provider actually used is imported.
|
|
39
|
+
*/
|
|
40
|
+
getModel: () => Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a credential and return a factory that builds the correct
|
|
44
|
+
* LangChain chat model with the API key injected server-side.
|
|
45
|
+
*
|
|
46
|
+
* The raw secret is fetched via fetchSecret(ref) and passed directly to the
|
|
47
|
+
* model constructor. It never appears in a log, a response body, or the
|
|
48
|
+
* browser — the model is built on the server and used there.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createAgentIdentityModel(ctx: AgentRequestContext, options: AgentIdentityModelOptions): AgentIdentityModelResult;
|
|
51
|
+
/**
|
|
52
|
+
* LangChain callback handler that records agent-identity resolution metadata
|
|
53
|
+
* into each LLM run's metadata. Drop this into any LangChain chain or agent
|
|
54
|
+
* to get automatic credential tracing without modifying the chain itself.
|
|
55
|
+
*
|
|
56
|
+
* Example:
|
|
57
|
+
* const handler = new AgentIdentityCallbackHandler(resolved, logger);
|
|
58
|
+
* await model.invoke(messages, { callbacks: [handler] });
|
|
59
|
+
*/
|
|
60
|
+
export declare class AgentIdentityCallbackHandler extends BaseCallbackHandler {
|
|
61
|
+
private readonly resolved;
|
|
62
|
+
private readonly logger?;
|
|
63
|
+
readonly name = "AgentIdentityCallbackHandler";
|
|
64
|
+
constructor(resolved: ResolvedCredential, logger?: AuditLogger | undefined);
|
|
65
|
+
handleLLMStart(_llm: Serialized, _prompts: string[], runId: string, _parentRunId?: string, extraParams?: Record<string, unknown>): Promise<void>;
|
|
66
|
+
handleLLMEnd(_output: LLMResult, _runId: string): Promise<void>;
|
|
67
|
+
handleLLMError(error: Error, _runId: string): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Drop-in LangGraph StateGraph node that resolves credentials before any
|
|
71
|
+
* LLM call in the graph. Attaches resolvedCredential to the state so
|
|
72
|
+
* downstream nodes can use it without repeating resolution.
|
|
73
|
+
*
|
|
74
|
+
* Example:
|
|
75
|
+
* const graph = new StateGraph(...);
|
|
76
|
+
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules));
|
|
77
|
+
* graph.addEdge('resolve-creds', 'llm-call');
|
|
78
|
+
*/
|
|
79
|
+
export declare function createAgentIdentityNode(credentials: Credential[], rules: RoutingRule[], logger?: AuditLogger): (state: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
80
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EACV,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,WAAW,EACZ,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAIzD,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB;;;OAGG;IACH,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,mBAAmB,EACxB,OAAO,EAAE,yBAAyB,GACjC,wBAAwB,CAkC1B;AAID;;;;;;;;GAQG;AACH,qBAAa,4BAA6B,SAAQ,mBAAmB;IAIjE,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAJ1B,QAAQ,CAAC,IAAI,kCAAkC;gBAG5B,QAAQ,EAAE,kBAAkB,EAC5B,MAAM,CAAC,EAAE,WAAW,YAAA;IAKjC,cAAc,CAClB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,MAAM,EAAE,EAClB,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC;IASV,YAAY,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/D,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlE;AAID;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,UAAU,EAAE,EACzB,KAAK,EAAE,WAAW,EAAE,EACpB,MAAM,CAAC,EAAE,WAAW,IAIN,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAShF"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datacules/agent-identity-langchain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "LangChain and LangGraph integration for @datacules/agent-identity",
|
|
6
|
+
"author": "Datacules LLC",
|
|
7
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/hvrcharon1/agent-identity.git",
|
|
11
|
+
"directory": "packages/integrations/langchain"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agent-identity",
|
|
15
|
+
"langchain",
|
|
16
|
+
"langgraph",
|
|
17
|
+
"ai-agents",
|
|
18
|
+
"credential-routing",
|
|
19
|
+
"datacules"
|
|
20
|
+
],
|
|
6
21
|
"main": "./dist/cjs/index.js",
|
|
7
22
|
"module": "./dist/esm/index.js",
|
|
8
23
|
"types": "./dist/types/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./dist/esm/index.js",
|
|
27
|
+
"require": "./dist/cjs/index.js",
|
|
28
|
+
"types": "./dist/types/index.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
9
36
|
"scripts": {
|
|
10
|
-
"build": "tsc -p tsconfig.build.json",
|
|
37
|
+
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.cjs.json",
|
|
11
38
|
"type-check": "tsc --noEmit"
|
|
12
39
|
},
|
|
13
40
|
"peerDependencies": {
|
|
14
|
-
"@datacules/agent-identity": "^0.
|
|
41
|
+
"@datacules/agent-identity": "^0.11.1",
|
|
15
42
|
"@langchain/core": ">=0.2.0"
|
|
16
43
|
},
|
|
17
44
|
"peerDependenciesMeta": {
|
package/src/index.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LangChain integration for @datacules/agent-identity.
|
|
3
|
-
*
|
|
4
|
-
* Provides:
|
|
5
|
-
* createAgentIdentityModel() — wraps ChatOpenAI/ChatAnthropic with credential resolution
|
|
6
|
-
* AgentIdentityCallbackHandler — LangChain callback handler for audit logging
|
|
7
|
-
* createAgentIdentityNode() — LangGraph StateGraph node for credential resolution
|
|
8
|
-
*
|
|
9
|
-
* Usage (LangChain):
|
|
10
|
-
* const { getModel } = createAgentIdentityModel(ctx, credentials, rules, fetchSecret);
|
|
11
|
-
* const model = await getModel();
|
|
12
|
-
* const result = await model.invoke([{ role: 'user', content: 'Hello' }]);
|
|
13
|
-
*
|
|
14
|
-
* Usage (LangGraph):
|
|
15
|
-
* const graph = new StateGraph(...);
|
|
16
|
-
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules, logger));
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { createRouter } from '@datacules/agent-identity';
|
|
20
|
-
import type {
|
|
21
|
-
AgentRequestContext,
|
|
22
|
-
AuditLogger,
|
|
23
|
-
Credential,
|
|
24
|
-
ResolvedCredential,
|
|
25
|
-
RoutingRule,
|
|
26
|
-
} from '@datacules/agent-identity';
|
|
27
|
-
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
28
|
-
import type { Serialized } from '@langchain/core/load/serializable';
|
|
29
|
-
import type { LLMResult } from '@langchain/core/outputs';
|
|
30
|
-
|
|
31
|
-
// ─── createAgentIdentityModel ──────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
export interface AgentIdentityModelOptions {
|
|
34
|
-
credentials: Credential[];
|
|
35
|
-
rules: RoutingRule[];
|
|
36
|
-
/**
|
|
37
|
-
* Server-side secret fetcher. Receives the credential ref and returns
|
|
38
|
-
* the raw API key. NEVER call this client-side — use in a server route only.
|
|
39
|
-
*/
|
|
40
|
-
fetchSecret: (ref: string) => Promise<string>;
|
|
41
|
-
logger?: AuditLogger;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface AgentIdentityModelResult {
|
|
45
|
-
/** Resolved credential metadata (safe to log, never contains raw secret) */
|
|
46
|
-
resolved: ResolvedCredential;
|
|
47
|
-
/**
|
|
48
|
-
* Returns the LangChain model instance with the API key injected.
|
|
49
|
-
* Dynamic import keeps @langchain/openai and @langchain/anthropic as
|
|
50
|
-
* optional peer deps — only the provider actually used is imported.
|
|
51
|
-
*/
|
|
52
|
-
getModel: () => Promise<unknown>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Resolve a credential and return a factory that builds the correct
|
|
57
|
-
* LangChain chat model with the API key injected server-side.
|
|
58
|
-
*
|
|
59
|
-
* The raw secret is fetched via fetchSecret(ref) and passed directly to the
|
|
60
|
-
* model constructor. It never appears in a log, a response body, or the
|
|
61
|
-
* browser — the model is built on the server and used there.
|
|
62
|
-
*/
|
|
63
|
-
export function createAgentIdentityModel(
|
|
64
|
-
ctx: AgentRequestContext,
|
|
65
|
-
options: AgentIdentityModelOptions
|
|
66
|
-
): AgentIdentityModelResult {
|
|
67
|
-
const router = createRouter(options.credentials, options.rules, options.logger);
|
|
68
|
-
const resolved = router.resolve(ctx);
|
|
69
|
-
if (!resolved) throw new Error(`[agent-identity] No credential resolved for context: ${JSON.stringify(ctx)}`);
|
|
70
|
-
|
|
71
|
-
const getModel = async (): Promise<unknown> => {
|
|
72
|
-
const apiKey = await options.fetchSecret(resolved.ref);
|
|
73
|
-
|
|
74
|
-
const meta = {
|
|
75
|
-
agentIdentityCredentialId: resolved.credentialId,
|
|
76
|
-
agentIdentityResolvedFor: resolved.resolvedFor,
|
|
77
|
-
traceId: ctx.traceId,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
if (ctx.provider === 'openai') {
|
|
81
|
-
const { ChatOpenAI } = await import('@langchain/openai' as string);
|
|
82
|
-
return new (ChatOpenAI as any)({ modelName: ctx.model, openAIApiKey: apiKey, metadata: meta });
|
|
83
|
-
}
|
|
84
|
-
if (ctx.provider === 'anthropic') {
|
|
85
|
-
const { ChatAnthropic } = await import('@langchain/anthropic' as string);
|
|
86
|
-
return new (ChatAnthropic as any)({ modelName: ctx.model, anthropicApiKey: apiKey, metadata: meta });
|
|
87
|
-
}
|
|
88
|
-
if (ctx.provider === 'gemini') {
|
|
89
|
-
const { ChatGoogleGenerativeAI } = await import('@langchain/google-genai' as string);
|
|
90
|
-
return new (ChatGoogleGenerativeAI as any)({ modelName: ctx.model, apiKey, metadata: meta });
|
|
91
|
-
}
|
|
92
|
-
if (ctx.provider === 'mistral') {
|
|
93
|
-
const { ChatMistralAI } = await import('@langchain/mistralai' as string);
|
|
94
|
-
return new (ChatMistralAI as any)({ modelName: ctx.model, apiKey, metadata: meta });
|
|
95
|
-
}
|
|
96
|
-
throw new Error(`[agent-identity/langchain] Provider "${ctx.provider}" not yet supported in LangChain adapter.`);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
return { resolved, getModel };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ─── AgentIdentityCallbackHandler ────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* LangChain callback handler that records agent-identity resolution metadata
|
|
106
|
-
* into each LLM run's metadata. Drop this into any LangChain chain or agent
|
|
107
|
-
* to get automatic credential tracing without modifying the chain itself.
|
|
108
|
-
*
|
|
109
|
-
* Example:
|
|
110
|
-
* const handler = new AgentIdentityCallbackHandler(resolved, logger);
|
|
111
|
-
* await model.invoke(messages, { callbacks: [handler] });
|
|
112
|
-
*/
|
|
113
|
-
export class AgentIdentityCallbackHandler extends BaseCallbackHandler {
|
|
114
|
-
readonly name = 'AgentIdentityCallbackHandler';
|
|
115
|
-
|
|
116
|
-
constructor(
|
|
117
|
-
private readonly resolved: ResolvedCredential,
|
|
118
|
-
private readonly logger?: AuditLogger
|
|
119
|
-
) {
|
|
120
|
-
super();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async handleLLMStart(
|
|
124
|
-
_llm: Serialized,
|
|
125
|
-
_prompts: string[],
|
|
126
|
-
runId: string,
|
|
127
|
-
_parentRunId?: string,
|
|
128
|
-
extraParams?: Record<string, unknown>
|
|
129
|
-
): Promise<void> {
|
|
130
|
-
if (extraParams) {
|
|
131
|
-
extraParams['agentIdentityCredentialId'] = this.resolved.credentialId;
|
|
132
|
-
extraParams['agentIdentityResolvedFor'] = this.resolved.resolvedFor;
|
|
133
|
-
}
|
|
134
|
-
// logger.log() is called by the router at resolve time; no duplicate log here
|
|
135
|
-
void runId;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async handleLLMEnd(_output: LLMResult, _runId: string): Promise<void> {
|
|
139
|
-
// Extension point: log token usage, latency, etc.
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async handleLLMError(error: Error, _runId: string): Promise<void> {
|
|
143
|
-
console.warn('[agent-identity] LLM error with credential', this.resolved.ref, error.message);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ─── createAgentIdentityNode (LangGraph) ────────────────────────────────────────
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Drop-in LangGraph StateGraph node that resolves credentials before any
|
|
151
|
-
* LLM call in the graph. Attaches resolvedCredential to the state so
|
|
152
|
-
* downstream nodes can use it without repeating resolution.
|
|
153
|
-
*
|
|
154
|
-
* Example:
|
|
155
|
-
* const graph = new StateGraph(...);
|
|
156
|
-
* graph.addNode('resolve-creds', createAgentIdentityNode(credentials, rules));
|
|
157
|
-
* graph.addEdge('resolve-creds', 'llm-call');
|
|
158
|
-
*/
|
|
159
|
-
export function createAgentIdentityNode(
|
|
160
|
-
credentials: Credential[],
|
|
161
|
-
rules: RoutingRule[],
|
|
162
|
-
logger?: AuditLogger
|
|
163
|
-
) {
|
|
164
|
-
const router = createRouter(credentials, rules, logger);
|
|
165
|
-
|
|
166
|
-
return async (state: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
|
167
|
-
const ctx = state['agentContext'] as AgentRequestContext | undefined;
|
|
168
|
-
if (!ctx) throw new Error('[agent-identity/langchain] state.agentContext is required');
|
|
169
|
-
|
|
170
|
-
const resolved = router.resolve(ctx);
|
|
171
|
-
if (!resolved) throw new Error(`[agent-identity/langchain] No credential resolved for context: ${JSON.stringify(ctx)}`);
|
|
172
|
-
|
|
173
|
-
return { ...state, resolvedCredential: resolved };
|
|
174
|
-
};
|
|
175
|
-
}
|
package/src/langchain.test.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* langchain.test.ts
|
|
3
|
-
*
|
|
4
|
-
* Vitest unit tests for @datacules/agent-identity-langchain.
|
|
5
|
-
*
|
|
6
|
-
* Covers createAgentIdentityModel, AgentIdentityCallbackHandler,
|
|
7
|
-
* and createAgentIdentityNode.
|
|
8
|
-
*
|
|
9
|
-
* @langchain/core/callbacks/base is mocked via vi.mock() factory.
|
|
10
|
-
* vi.mock() calls are hoisted to the top of the module by vitest so
|
|
11
|
-
* the mock is in place before the source file is imported. This means
|
|
12
|
-
* the tests work whether or not @langchain/core is installed in the
|
|
13
|
-
* root node_modules.
|
|
14
|
-
*/
|
|
15
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
16
|
-
|
|
17
|
-
// ─── Mock @langchain/core ─────────────────────────────────────────────────────
|
|
18
|
-
// Must be declared before the import of ./index because vi.mock() is hoisted.
|
|
19
|
-
vi.mock('@langchain/core/callbacks/base', () => ({
|
|
20
|
-
BaseCallbackHandler: class MockBaseCallbackHandler {
|
|
21
|
-
name: string = '';
|
|
22
|
-
},
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
createAgentIdentityModel,
|
|
27
|
-
AgentIdentityCallbackHandler,
|
|
28
|
-
createAgentIdentityNode,
|
|
29
|
-
} from './index';
|
|
30
|
-
import type { AgentRequestContext, Credential, RoutingRule } from '@datacules/agent-identity';
|
|
31
|
-
|
|
32
|
-
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
const credentials: Credential[] = [
|
|
35
|
-
{
|
|
36
|
-
id: 'cred-openai',
|
|
37
|
-
kind: 'fixed',
|
|
38
|
-
name: 'OpenAI prod',
|
|
39
|
-
scope: 'openai:all',
|
|
40
|
-
status: 'active',
|
|
41
|
-
provider: 'openai',
|
|
42
|
-
ref: 'openai-prod-slot',
|
|
43
|
-
},
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const rules: RoutingRule[] = [
|
|
47
|
-
{
|
|
48
|
-
id: 'rule-openai',
|
|
49
|
-
description: 'Route all OpenAI requests',
|
|
50
|
-
credentialRef: 'openai-prod-slot',
|
|
51
|
-
credentialKind: 'fixed',
|
|
52
|
-
priority: 10,
|
|
53
|
-
matchProvider: 'openai',
|
|
54
|
-
},
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
// Empty rules — no credential will resolve for any context
|
|
58
|
-
const rulesNoMatch: RoutingRule[] = [];
|
|
59
|
-
|
|
60
|
-
function makeCtx(overrides: Partial<AgentRequestContext> = {}): AgentRequestContext {
|
|
61
|
-
return {
|
|
62
|
-
userId: 'user-alice',
|
|
63
|
-
resourceId: 'res-001',
|
|
64
|
-
resourceKind: 'personal',
|
|
65
|
-
provider: 'openai',
|
|
66
|
-
model: 'gpt-4o',
|
|
67
|
-
action: 'read',
|
|
68
|
-
traceId: 'trace-abc',
|
|
69
|
-
requestedAt: new Date().toISOString(),
|
|
70
|
-
...overrides,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ─── createAgentIdentityModel ─────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
describe('createAgentIdentityModel', () => {
|
|
77
|
-
it('returns resolved credential metadata', () => {
|
|
78
|
-
const fetchSecret = vi.fn(async () => 'sk-secret');
|
|
79
|
-
const result = createAgentIdentityModel(makeCtx(), { credentials, rules, fetchSecret });
|
|
80
|
-
expect(result.resolved).toBeDefined();
|
|
81
|
-
expect(result.resolved.ref).toBe('openai-prod-slot');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('resolved has correct credentialId and resolvedFor', () => {
|
|
85
|
-
const fetchSecret = vi.fn(async () => 'sk-secret');
|
|
86
|
-
const { resolved } = createAgentIdentityModel(makeCtx(), { credentials, rules, fetchSecret });
|
|
87
|
-
expect(resolved.credentialId).toBe('cred-openai');
|
|
88
|
-
// kind=fixed → resolvedFor is 'service'
|
|
89
|
-
expect(resolved.resolvedFor).toBe('service');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('returns a getModel function', () => {
|
|
93
|
-
const fetchSecret = vi.fn(async () => 'sk-secret');
|
|
94
|
-
const { getModel } = createAgentIdentityModel(makeCtx(), { credentials, rules, fetchSecret });
|
|
95
|
-
expect(typeof getModel).toBe('function');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('throws when no routing rule matches the context', () => {
|
|
99
|
-
const fetchSecret = vi.fn(async () => 'sk-secret');
|
|
100
|
-
expect(() =>
|
|
101
|
-
createAgentIdentityModel(makeCtx(), { credentials, rules: rulesNoMatch, fetchSecret })
|
|
102
|
-
).toThrow('[agent-identity] No credential resolved');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('getModel() calls fetchSecret with the resolved ref then throws for unsupported provider', async () => {
|
|
106
|
-
const fetchSecret = vi.fn(async () => 'sk-secret');
|
|
107
|
-
// Use 'local' provider — it matches a rule but has no provider adapter in getModel()
|
|
108
|
-
const localCredential: Credential = { ...credentials[0], provider: 'local', ref: 'local-slot' };
|
|
109
|
-
const localRule: RoutingRule = { ...rules[0], credentialRef: 'local-slot', matchProvider: 'local' };
|
|
110
|
-
const { getModel } = createAgentIdentityModel(
|
|
111
|
-
makeCtx({ provider: 'local' }),
|
|
112
|
-
{ credentials: [localCredential], rules: [localRule], fetchSecret }
|
|
113
|
-
);
|
|
114
|
-
// getModel() calls fetchSecret first, then throws because 'local' is unsupported
|
|
115
|
-
await expect(getModel()).rejects.toThrow('not yet supported');
|
|
116
|
-
expect(fetchSecret).toHaveBeenCalledWith('local-slot');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// ─── AgentIdentityCallbackHandler ────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
describe('AgentIdentityCallbackHandler', () => {
|
|
123
|
-
const resolved = {
|
|
124
|
-
credentialId: 'cred-openai',
|
|
125
|
-
kind: 'fixed' as const,
|
|
126
|
-
ref: 'openai-prod-slot',
|
|
127
|
-
resolvedFor: 'service',
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
it('instantiates without errors', () => {
|
|
131
|
-
expect(() => new AgentIdentityCallbackHandler(resolved)).not.toThrow();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('handleLLMStart attaches agentIdentity metadata to extraParams', async () => {
|
|
135
|
-
const handler = new AgentIdentityCallbackHandler(resolved);
|
|
136
|
-
const extraParams: Record<string, unknown> = {};
|
|
137
|
-
// Pass extraParams as the 5th argument; _llm and _prompts are unused
|
|
138
|
-
await handler.handleLLMStart({} as never, [], 'run-001', undefined, extraParams);
|
|
139
|
-
expect(extraParams['agentIdentityCredentialId']).toBe('cred-openai');
|
|
140
|
-
expect(extraParams['agentIdentityResolvedFor']).toBe('service');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('handleLLMEnd resolves without throwing', async () => {
|
|
144
|
-
const handler = new AgentIdentityCallbackHandler(resolved);
|
|
145
|
-
await expect(handler.handleLLMEnd({} as never, 'run-001')).resolves.toBeUndefined();
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ─── createAgentIdentityNode ──────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
describe('createAgentIdentityNode', () => {
|
|
152
|
-
it('injects resolvedCredential into state', async () => {
|
|
153
|
-
const node = createAgentIdentityNode(credentials, rules);
|
|
154
|
-
const state = { agentContext: makeCtx() };
|
|
155
|
-
const result = await node(state);
|
|
156
|
-
expect(result.resolvedCredential).toBeDefined();
|
|
157
|
-
expect((result.resolvedCredential as { ref: string }).ref).toBe('openai-prod-slot');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('preserves all existing state properties alongside resolvedCredential', async () => {
|
|
161
|
-
const node = createAgentIdentityNode(credentials, rules);
|
|
162
|
-
const state = { agentContext: makeCtx(), sessionId: 'sess-xyz', count: 42 };
|
|
163
|
-
const result = await node(state);
|
|
164
|
-
expect(result.sessionId).toBe('sess-xyz');
|
|
165
|
-
expect(result.count).toBe(42);
|
|
166
|
-
expect(result.agentContext).toStrictEqual(state.agentContext);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('throws when state.agentContext is missing', async () => {
|
|
170
|
-
const node = createAgentIdentityNode(credentials, rules);
|
|
171
|
-
await expect(node({ otherField: true })).rejects.toThrow('state.agentContext is required');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('throws when no credential resolves for the context', async () => {
|
|
175
|
-
const node = createAgentIdentityNode(credentials, rulesNoMatch);
|
|
176
|
-
const state = { agentContext: makeCtx() };
|
|
177
|
-
await expect(node(state)).rejects.toThrow('No credential resolved');
|
|
178
|
-
});
|
|
179
|
-
});
|