add authentication support

This commit is contained in:
2025-11-24 14:21:49 +00:00
parent 05ebf84bb9
commit 9941d61fb8
27 changed files with 1126 additions and 69 deletions

View File

@@ -33,7 +33,7 @@ body:
description: How can we reproduce this issue? (as minimally and as precisely as possible) description: How can we reproduce this issue? (as minimally and as precisely as possible)
placeholder: A clear and concise description of how to reproduce the issue. placeholder: A clear and concise description of how to reproduce the issue.
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:

View File

@@ -30,14 +30,14 @@ body:
label: Describe the solution you'd like label: Describe the solution you'd like
placeholder: A clear and concise description of what you want to happen. placeholder: A clear and concise description of what you want to happen.
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: alternatives id: alternatives
attributes: attributes:
label: Describe alternatives you've considered label: Describe alternatives you've considered
placeholder: A clear and concise description of any alternative solutions or features you've considered. placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: context id: context
attributes: attributes:

132
README.md
View File

@@ -1,10 +1,10 @@
# JLINC Langchain Integration # JLINC Langchain Integration
The JLINC Langchain Integration is the official way to implement the zero-knowledge third-party auditing provided by the [JLINC Server](https://gitea.jlinc.io/jlinc-labs/jlinc-server) inside any Langchain-based infrastructure. The JLINC Langchain Integration is the official way to implement the zero-knowledge third-party auditing and authorization provided by the [JLINC Server](https://gitea.jlinc.io/jlinc-labs/jlinc-server) inside any Langchain-based infrastructure.
By embedding JLINC's trusted protocol directly into Langchain's tracing system, organizations can prove compliance, accountability, and data integrity without ever exposing sensitive information. This seamless integration enables developers to track, verify, and audit model interactions with full transparency while preserving confidentiality through cryptographically verifiable zero-knowledge proofs. Whether for regulated industries, enterprise governance, or AI safety applications, the JLINC Langchain Tracer ensures that trust, privacy, and accountability are built in from the ground up. By embedding JLINC's trusted protocol directly into Langchain's tracing system, organizations can prove compliance, accountability, and data integrity without ever exposing sensitive information. This seamless integration enables developers to track, verify, and audit model interactions with full transparency while preserving confidentiality through cryptographically verifiable zero-knowledge proofs. Whether for regulated industries, enterprise governance, or AI safety applications, the JLINC Langchain Tracer ensures that trust, privacy, and accountability are built in from the ground up.
## Sample application ## Sample auditing application
The below code sample is a demonstration of the JLINC Langchain Integration in action. As data moves through the chain, it is cryptographically signed with a unique key for each element in the chain, and zero-knowledge audit records are delivered to the JLINC Archive Server. The below code sample is a demonstration of the JLINC Langchain Integration in action. As data moves through the chain, it is cryptographically signed with a unique key for each element in the chain, and zero-knowledge audit records are delivered to the JLINC Archive Server.
```javascript ```javascript
@@ -16,7 +16,7 @@ const { ChatPromptTemplate } = require("@langchain/core/prompts");
const { JLINCTracer } = require("@jlinc/langchain"); const { JLINCTracer } = require("@jlinc/langchain");
async function main() { async function main() {
const tracer = new JLINCTracer({ const config = {
dataStoreApiUrl: "http://localhost:9090", dataStoreApiUrl: "http://localhost:9090",
dataStoreApiKey: process.env.JLINC_DATA_STORE_API_KEY, dataStoreApiKey: process.env.JLINC_DATA_STORE_API_KEY,
archiveApiUrl: "http://localhost:9090", archiveApiUrl: "http://localhost:9090",
@@ -24,7 +24,9 @@ async function main() {
agreementId: "00000000-0000-0000-0000-000000000000", agreementId: "00000000-0000-0000-0000-000000000000",
systemPrefix: "TracerTest", systemPrefix: "TracerTest",
debug: true, debug: true,
}); }
const tracer = new JLINCTracer(config);
const llm = new ChatOpenAI({ const llm = new ChatOpenAI({
openAIApiKey: "n/a", openAIApiKey: "n/a",
@@ -66,6 +68,128 @@ async function main() {
main() main()
``` ```
## Sample authorization code
The JLINC integration also supports [AuthZEN](https://openid.net/wg/authzen/)-styled authorization pass through to any provider. For instance, the below modifications to the above code would add in authorization to determine if a "public" or "private" tool or LLM can be utilized:
```javascript
const { ChatOpenAI } = require("@langchain/openai");
const { awaitAllCallbacks } = require("@langchain/core/callbacks/promises");
const { Calculator } = require("@langchain/community/tools/calculator");
const { AgentExecutor, createToolCallingAgent } = require("langchain/agents");
const { ChatPromptTemplate } = require("@langchain/core/prompts");
const { JLINCTracer, JLINCAuthDecision, JLINCAuthBaseChatModel, JLINCAuthTool } = require("../src/index.js");
class CalculatorPrivate extends Calculator {
static lc_name() {
return "CalculatorPrivate";
}
}
class CalculatorPublic extends Calculator {
static lc_name() {
return "CalculatorPublic";
}
}
async function main() {
const config = {
dataStoreApiUrl: "http://localhost:9090",
dataStoreApiKey: process.env.JLINC_DATA_STORE_API_KEY,
archiveApiUrl: "http://localhost:9090",
archiveApiKey: process.env.JLINC_ARCHIVE_API_KEY,
agreementId: "00000000-0000-0000-0000-000000000000",
systemPrefix: "TestTracerJlinc",
debug: true,
}
const jlincAuthDecision = new JLINCAuthDecision(config);
const auth = {
subject: {
type: "user",
id: "tester",
},
action: {
name: "read",
},
resource: {
type: "data",
id: "1234",
properties: {
ownerID: "tester@test.com",
}
}
}
await jlincAuthDecision.evaluate(auth);
const tracer = new JLINCTracer(config);
const authorizedLlm = new ChatOpenAI({
openAIApiKey: "n/a",
configuration: {
baseURL: "http://localhost:1234/v1",
},
modelName: "meta-llama-3.1-8b-instruct",
});
const notAuthorizedLlm = new ChatOpenAI({
openAIApiKey: "n/a",
configuration: {
baseURL: "http://localhost:1234/v1",
},
modelName: "hermes-3-llama-3.1-8b",
});
const llm = new JLINCAuthBaseChatModel({
config,
jlincAuthDecision,
targetAuthorized: authorizedLlm,
targetNotAuthorized: notAuthorizedLlm, // Optional
});
const calculatorPublic = new CalculatorPublic();
calculatorPublic.name = 'calculator_public';
const calculatorPrivate = new CalculatorPrivate();
calculatorPrivate.name = 'calculator_private';
const jlincAuthTool = new JLINCAuthTool({
config,
jlincAuthDecision,
targetAuthorized: calculatorPublic,
targetNotAuthorized: calculatorPrivate, // Optional
});
const tools = [jlincAuthTool];
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant"],
["placeholder", "{chat_history}"],
["human", "{input}"],
["placeholder", "{agent_scratchpad}"],
]);
const agent = createToolCallingAgent({ llm, tools, prompt });
const agentExecutor = new AgentExecutor({
agent,
tools,
});
try {
const r = await agentExecutor.invoke({ input: "Add 1 + 1. If a function call is used, tell me the output of the function call." }, { callbacks: [tracer] });
// The next invocation requires a reauth for any future calls to the agent:
auth.action.name = "write";
jlincAuthDecision.evaluate(auth);
console.log(`\nResult`)
console.log(`---------------------------------------------`)
console.log(r)
} catch (err) {
console.error("Error calling LLM:", err);
} finally {
await awaitAllCallbacks();
}
}
main()
```
## Additional information ## Additional information

51
dist/auth/JLINCAuthBaseChatModel.d.ts vendored Normal file
View File

@@ -0,0 +1,51 @@
export type JLINCConfig = import("./common").JLINCConfig;
export type JLINCAuthBaseChatModelFields = {
config: JLINCConfig;
jlincAuthDecision: Tool;
targetAuthorized: BaseChatModel;
targetNotAuthorized: BaseChatModel | null;
};
/**
* @typedef {import('./common').JLINCConfig} JLINCConfig
*/
/**
* @typedef {Object} JLINCAuthBaseChatModelFields
* @property {JLINCConfig} config
* @property {Tool} jlincAuthDecision
* @property {BaseChatModel} targetAuthorized
* @property {BaseChatModel|null} targetNotAuthorized
*/
export class JLINCAuthBaseChatModel extends BaseChatModel<import("@langchain/core/language_models/chat_models").BaseChatModelCallOptions, import("@langchain/core/dist/messages/ai.js").AIMessageChunk> {
/**
* @param {JLINCAuthBaseChatModelFields} fields
*/
constructor({ config, jlincAuthDecision, targetAuthorized, targetNotAuthorized }: JLINCAuthBaseChatModelFields);
/** @type {JLINCConfig} */
config: JLINCConfig;
/** @type {Tool} */
jlincAuthDecision: Tool;
/** @type {BaseChatModel} */
targetAuthorized: BaseChatModel;
/** @type {BaseChatModel|null} */
targetNotAuthorized: BaseChatModel | null;
/** @type {JLINCTracer} */
tracer: JLINCTracer;
/** @type {string} */
authType: string;
/**
* @param {BindToolsInput[]} tools - The tools to bind to the underlying models.
* @param {Partial<CallOptions>} [kwargs] - Optional call options.
* @returns {Runnable<BaseLanguageModelInput, OutputMessageType, CallOptions>}
* A new JLINCAuthBaseChatModel instance whose underlying models
* have been bound with the provided tools.
*/
bindTools(tools: BindToolsInput[], kwargs?: Partial<CallOptions>): Runnable<BaseLanguageModelInput, OutputMessageType, CallOptions>;
/**
* @param {BaseLanguageModelInput} input
* @param {Partial<CallOptions>} [options] - Optional call options.
* @returns {Promise<OutputMessageType>} A promise that resolves with the output.
*/
invoke(input: BaseLanguageModelInput, runManager: any): Promise<OutputMessageType>;
}
import { BaseChatModel } from "@langchain/core/dist/language_models/chat_models.js";
import { JLINCTracer } from "../tracer/index.js";

96
dist/auth/JLINCAuthBaseChatModel.js vendored Normal file
View File

@@ -0,0 +1,96 @@
const { BaseChatModel } = require("@langchain/core/language_models/chat_models");
const { JLINCTracer } = require("../tracer/index.js");
const { authDecide, authInvoke, getDescription, getName } = require("./common.js");
/**
* @typedef {import('./common').JLINCConfig} JLINCConfig
*/
/**
* @typedef {Object} JLINCAuthBaseChatModelFields
* @property {JLINCConfig} config
* @property {Tool} jlincAuthDecision
* @property {BaseChatModel} targetAuthorized
* @property {BaseChatModel|null} targetNotAuthorized
*/
class JLINCAuthBaseChatModel extends BaseChatModel {
/**
* @param {JLINCAuthBaseChatModelFields} fields
*/
constructor({ config, jlincAuthDecision, targetAuthorized, targetNotAuthorized }) {
super({
name: "jlinc_auth_base_chat_model",
description: "", // overridden dynamically
});
Object.defineProperty(this, "name", {
enumerable: true,
configurable: true,
writable: true,
value: getName(targetAuthorized, targetNotAuthorized)
});
Object.defineProperty(this, "description", {
enumerable: true,
configurable: true,
writable: true,
value: getDescription(targetAuthorized, targetNotAuthorized, 'BaseChatModel')
});
/** @type {JLINCConfig} */
this.config = config;
/** @type {Tool} */
this.jlincAuthDecision = jlincAuthDecision;
/** @type {BaseChatModel} */
this.targetAuthorized = targetAuthorized;
/** @type {BaseChatModel|null} */
this.targetNotAuthorized = targetNotAuthorized;
/** @type {JLINCTracer} */
this.tracer = new JLINCTracer(config);
/** @type {string} */
this.authType = 'BaseChatModel';
}
/**
* @param {BindToolsInput[]} tools - The tools to bind to the underlying models.
* @param {Partial<CallOptions>} [kwargs] - Optional call options.
* @returns {Runnable<BaseLanguageModelInput, OutputMessageType, CallOptions>}
* A new JLINCAuthBaseChatModel instance whose underlying models
* have been bound with the provided tools.
*/
bindTools(tools, kwargs = {}) {
if (this.config.debug)
console.log(`[JLINCAuth] Binding tools to BaseChatModels`);
const authorizedBound = this.targetAuthorized.bindTools(tools, kwargs);
const notAuthorizedBound = this.targetNotAuthorized
? this.targetNotAuthorized.bindTools(tools, kwargs)
: null;
return new JLINCAuthBaseChatModel({
config: this.config,
jlincAuthDecision: this.jlincAuthDecision,
targetAuthorized: authorizedBound,
targetNotAuthorized: notAuthorizedBound,
});
}
/**
* @returns {string} - type
*/
_llmType() {
const type = this.targetAuthorized?._llmType?.() ?? "unknown";
if (this.config?.debug)
console.log(`[JLINCAuth] Returning LLM type: ${type}`);
return type;
}
/**
* @param {BaseLanguageModelInput} input
* @param {Partial<CallOptions>} [options] - Optional call options.
* @returns {Promise<OutputMessageType>} A promise that resolves with the output.
*/
async invoke(input, runManager) {
return await authInvoke(this, input);
}
}
module.exports = {
JLINCAuthBaseChatModel,
};

29
dist/auth/JLINCAuthDecision.d.ts vendored Normal file
View File

@@ -0,0 +1,29 @@
export type JLINCConfig = import("./common").JLINCConfig;
/**
* @typedef {import('./common').JLINCConfig} JLINCConfig
*/
export class JLINCAuthDecision extends Tool<any> {
/**
* @param {JLINCConfig} fields
*/
constructor(config: any);
/** @type {JLINCConfig} */
config: JLINCConfig;
/** @type {Boolean} */
authenticated: boolean;
/**
* @param {any} payload
* @returns {Promise<any>}
*/
postToApi(auth: any): Promise<any>;
/**
* @param {auth} auth
* @returns {Promise<boolean>} - authenticated
*/
evaluate(auth: any): Promise<boolean>;
/**
* @returns {boolean} - authenticated
*/
getAuth(): boolean;
}
import { Tool } from "@langchain/core/dist/tools";

79
dist/auth/JLINCAuthDecision.js vendored Normal file
View File

@@ -0,0 +1,79 @@
const { Tool } = require("@langchain/core/tools");
const axios = require("axios");
/**
* @typedef {import('./common').JLINCConfig} JLINCConfig
*/
class JLINCAuthDecision extends Tool {
/**
* @param {JLINCConfig} fields
*/
constructor(config) {
super({
name: "jlinc_auth",
description: "Authorization via AuthZEN format.",
});
Object.defineProperty(this, "name", {
enumerable: true,
configurable: true,
writable: true,
value: "jlinc-auth"
});
Object.defineProperty(this, "description", {
enumerable: true,
configurable: true,
writable: true,
value: "Authorization via AuthZEN format."
});
/** @type {JLINCConfig} */
this.config = config;
/** @type {Boolean} */
this.authenticated = false;
}
/**
* @param {any} payload
* @returns {Promise<any>}
*/
async postToApi(auth) {
const response = await axios.post(
`${this.config.dataStoreApiUrl}/api/v1/auth`,
auth,
{
headers: {
'Authorization': `Bearer ${this.config.dataStoreApiKey}`,
}
}
);
return response.data;
}
/**
* @param {auth} auth
* @returns {Promise<boolean>} - authenticated
*/
async evaluate(auth) {
this.authenticated = false;
try {
this.authenticated = (await this.postToApi(auth)).decision;
} catch (e) {
console.log(`[JLINCAuthTool] Error connecting to API, flagging as unauthorized`);
if (this.config.debug) console.error(e)
}
if (this.config.debug)
console.log(`[JLINCAuth] Got authorization of: ${this.authenticated}`);
return this.authenticated;
}
/**
* @returns {boolean} - authenticated
*/
getAuth() {
return this.authenticated;
}
}
module.exports = {
JLINCAuthDecision,
};

47
dist/auth/JLINCAuthTool.d.ts vendored Normal file
View File

@@ -0,0 +1,47 @@
export type JLINCConfig = import("./common").JLINCConfig;
export type JLINCAuthToolFields = {
config: JLINCConfig;
jlincAuthDecision: Tool;
targetAuthorized: Tool;
targetNotAuthorized: Tool | null;
};
/**
* @typedef {import('./common').JLINCConfig} JLINCConfig
*/
/**
* @typedef {Object} JLINCAuthToolFields
* @property {JLINCConfig} config
* @property {Tool} jlincAuthDecision
* @property {Tool} targetAuthorized
* @property {Tool|null} targetNotAuthorized
*/
export class JLINCAuthTool extends Tool<any> {
/**
* @param {JLINCAuthToolFields} fields
*/
constructor({ config, jlincAuthDecision, targetAuthorized, targetNotAuthorized }: JLINCAuthToolFields);
/** @type {JLINCConfig} */
config: JLINCConfig;
/** @type {Tool} */
jlincAuthDecision: Tool;
/** @type {Tool} */
targetAuthorized: Tool;
/** @type {Tool|null} */
targetNotAuthorized: Tool | null;
/** @type {JLINCTracer} */
tracer: JLINCTracer;
/** @type {string} */
authType: string;
/**
* @template TInput
* @template {undefined | ToolRunnableConfig} TConfig
* @template TOutput
*
* @param {TInput} input
* @param {TConfig} [config]
* @returns {Promise<ToolReturnType<TInput, TConfig, TOutput>>}
*/
invoke<TInput, TConfig extends undefined | ToolRunnableConfig, TOutput>(input: TInput, runManager: any): Promise<ToolReturnType<TInput, TConfig, TOutput>>;
}
import { Tool } from "@langchain/core/dist/tools/index.js";
import { JLINCTracer } from "../tracer/index.js";

68
dist/auth/JLINCAuthTool.js vendored Normal file
View File

@@ -0,0 +1,68 @@
const { Tool } = require("@langchain/core/tools");
const { JLINCTracer } = require("../tracer/index.js");
const { authDecide, authInvoke, getDescription, getName } = require("./common.js");
/**
* @typedef {import('./common').JLINCConfig} JLINCConfig
*/
/**
* @typedef {Object} JLINCAuthToolFields
* @property {JLINCConfig} config
* @property {Tool} jlincAuthDecision
* @property {Tool} targetAuthorized
* @property {Tool|null} targetNotAuthorized
*/
class JLINCAuthTool extends Tool {
/**
* @param {JLINCAuthToolFields} fields
*/
constructor({ config, jlincAuthDecision, targetAuthorized, targetNotAuthorized }) {
super({
name: "jlinc_auth_tool",
description: "", // overridden dynamically
});
Object.defineProperty(this, "name", {
enumerable: true,
configurable: true,
writable: true,
value: getName(targetAuthorized, targetNotAuthorized)
});
Object.defineProperty(this, "description", {
enumerable: true,
configurable: true,
writable: true,
value: getDescription(targetAuthorized, targetNotAuthorized, 'Tool')
});
/** @type {JLINCConfig} */
this.config = config;
/** @type {Tool} */
this.jlincAuthDecision = jlincAuthDecision;
/** @type {Tool} */
this.targetAuthorized = targetAuthorized;
/** @type {Tool|null} */
this.targetNotAuthorized = targetNotAuthorized;
/** @type {JLINCTracer} */
this.tracer = new JLINCTracer(config);
/** @type {string} */
this.authType = 'Tool';
}
/**
* @template TInput
* @template {undefined | ToolRunnableConfig} TConfig
* @template TOutput
*
* @param {TInput} input
* @param {TConfig} [config]
* @returns {Promise<ToolReturnType<TInput, TConfig, TOutput>>}
*/
async invoke(input, runManager) {
return await authInvoke(this, input);
}
}
module.exports = {
JLINCAuthTool,
};

37
dist/auth/common.d.ts vendored Normal file
View File

@@ -0,0 +1,37 @@
export type JLINCConfig = {
dataStoreApiUrl?: string | undefined;
dataStoreApiKey?: string | undefined;
archiveApiUrl?: string | undefined;
archiveApiKey?: string | undefined;
systemPrefix?: string | undefined;
agreementId?: string | null | undefined;
debug?: boolean | undefined;
};
/**
* @param {any} settings
* @param {any} input
* @returns {Promise<any>} - The result of invoking the selected tool.
*/
export function authInvoke(settings: any, input: any): Promise<any>;
/**
* @param {any} - Authorized
* @param {any|null} - NotAuthorized
* @returns {string} - The description
*/
export function getDescription(authorized: any, unauthorized: any, type: any): string;
/**
* @typedef {Object} JLINCConfig
* @property {string} [dataStoreApiUrl]
* @property {string} [dataStoreApiKey]
* @property {string} [archiveApiUrl]
* @property {string} [archiveApiKey]
* @property {string} [systemPrefix]
* @property {string|null} [agreementId]
* @property {boolean} [debug]
*/
/**
* @param {any} - Authorized
* @param {any|null} - NotAuthorized
* @returns {string} - The description
*/
export function getName(authorized: any, unauthorized: any): string;

85
dist/auth/common.js vendored Normal file
View File

@@ -0,0 +1,85 @@
const axios = require("axios");
/**
* @typedef {Object} JLINCConfig
* @property {string} [dataStoreApiUrl]
* @property {string} [dataStoreApiKey]
* @property {string} [archiveApiUrl]
* @property {string} [archiveApiKey]
* @property {string} [systemPrefix]
* @property {string|null} [agreementId]
* @property {boolean} [debug]
*/
/**
* @param {any} - Authorized
* @param {any|null} - NotAuthorized
* @returns {string} - The description
*/
function getName(authorized, unauthorized) {
return unauthorized
? `${authorized.name}-or-${unauthorized.name}`
: `authorized-${authorized.name}`
}
/**
* @param {any} - Authorized
* @param {any|null} - NotAuthorized
* @returns {string} - The description
*/
function getDescription(authorized, unauthorized, type) {
return unauthorized ? `
This is an authorization router ${type}. It decides whether
to route input to an "authorized" or "not authorized" ${type}.
Authorized ${type}: ${authorized.name}
Description: ${authorized.description}
Not Authorized ${type}: ${unauthorized.name}
Description: ${unauthorized.description}
The LLM can assume both ${type}s are available, but JLINC Auth will
only allow the Authorized ${type} to be called if you are authorized.
` : `
This is an authorization router ${type}. It decides whether
to route input to an "authorized" ${type}.
Authorized ${type}: ${authorized.name}
Description: ${authorized.description}
The LLM can assume the ${type} is available, but JLINC Auth will
only allow the Authorized ${type} to be called if you are authorized.
`
}
function getLogName(target) {
if (target.name) return target.name;
if (target.lc_kwargs?.bound?.model) return target.lc_kwargs.bound.model;
return 'no-name';
}
/**
* @param {any} settings
* @param {any} input
* @returns {Promise<any>} - The result of invoking the selected tool.
*/
async function authInvoke(settings, input) {
const authorized = await settings.jlincAuthDecision.getAuth();
let selectedTarget = settings.targetNotAuthorized
if (authorized) {
selectedTarget = settings.targetAuthorized
}
if (!selectedTarget)
throw new Error("No valid resource available");
if (settings.config.debug)
console.log(`[JLINCAuth] Invoking ${getLogName(selectedTarget)} (${settings.authType}|${authorized})`);
return await selectedTarget.invoke(input, {
callbacks: [settings.tracer],
});
}
module.exports = {
authInvoke,
getDescription,
getName,
};

4
dist/auth/index.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
import { JLINCAuthDecision } from "./JLINCAuthDecision.js";
import { JLINCAuthBaseChatModel } from "./JLINCAuthBaseChatModel.js";
import { JLINCAuthTool } from "./JLINCAuthTool.js";
export { JLINCAuthDecision, JLINCAuthBaseChatModel, JLINCAuthTool };

9
dist/auth/index.js vendored Normal file
View File

@@ -0,0 +1,9 @@
const { JLINCAuthDecision } = require("./JLINCAuthDecision.js");
const { JLINCAuthBaseChatModel } = require("./JLINCAuthBaseChatModel.js");
const { JLINCAuthTool } = require("./JLINCAuthTool.js");
module.exports = {
JLINCAuthDecision,
JLINCAuthBaseChatModel,
JLINCAuthTool,
};

5
dist/index.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import { JLINCAuthDecision } from "./auth/index.js";
import { JLINCAuthBaseChatModel } from "./auth/index.js";
import { JLINCAuthTool } from "./auth/index.js";
import { JLINCTracer } from "./tracer/index.js";
export { JLINCAuthDecision, JLINCAuthBaseChatModel, JLINCAuthTool, JLINCTracer };

13
dist/index.js vendored Normal file
View File

@@ -0,0 +1,13 @@
const {
JLINCAuthDecision,
JLINCAuthBaseChatModel,
JLINCAuthTool,
} = require("./auth/index.js");
const { JLINCTracer } = require("./tracer/index.js");
module.exports = {
JLINCAuthDecision,
JLINCAuthBaseChatModel,
JLINCAuthTool,
JLINCTracer,
};

View File

@@ -235,7 +235,7 @@ class JLINCTracer extends LangChainTracer {
console.error("[Tracer] Failed to log event:", error.message); console.error("[Tracer] Failed to log event:", error.message);
} }
} }
} }
module.exports = { JLINCTracer } module.exports = { JLINCTracer }

View File

@@ -2,11 +2,11 @@
"name": "@jlinc/langchain", "name": "@jlinc/langchain",
"version": "0.1.3", "version": "0.1.3",
"description": "A LangChain integration that logs events to the JLINC API.", "description": "A LangChain integration that logs events to the JLINC API.",
"main": "dist/tracer.js", "main": "dist/index.js",
"types": "dist/tracer.d.ts", "types": "dist/index.d.ts",
"scripts": { "scripts": {
"test": "node test/test_tracer.js", "test": "node test/test.js",
"build": "tsc && cp src/tracer.js dist/" "build": "rm -rf dist && cp -a src dist && tsc"
}, },
"homepage": "https://www.jlinc.com/", "homepage": "https://www.jlinc.com/",
"repository": { "repository": {

View File

@@ -0,0 +1,96 @@
const { BaseChatModel } = require("@langchain/core/language_models/chat_models");
const { JLINCTracer } = require("../tracer/index.js");
const { authDecide, authInvoke, getDescription, getName } = require("./common.js");
/**
* @typedef {import('./common').JLINCConfig} JLINCConfig
*/
/**
* @typedef {Object} JLINCAuthBaseChatModelFields
* @property {JLINCConfig} config
* @property {Tool} jlincAuthDecision
* @property {BaseChatModel} targetAuthorized
* @property {BaseChatModel|null} targetNotAuthorized
*/
class JLINCAuthBaseChatModel extends BaseChatModel {
/**
* @param {JLINCAuthBaseChatModelFields} fields
*/
constructor({ config, jlincAuthDecision, targetAuthorized, targetNotAuthorized }) {
super({
name: "jlinc_auth_base_chat_model",
description: "", // overridden dynamically
});
Object.defineProperty(this, "name", {
enumerable: true,
configurable: true,
writable: true,
value: getName(targetAuthorized, targetNotAuthorized)
});
Object.defineProperty(this, "description", {
enumerable: true,
configurable: true,
writable: true,
value: getDescription(targetAuthorized, targetNotAuthorized, 'BaseChatModel')
});
/** @type {JLINCConfig} */
this.config = config;
/** @type {Tool} */
this.jlincAuthDecision = jlincAuthDecision;
/** @type {BaseChatModel} */
this.targetAuthorized = targetAuthorized;
/** @type {BaseChatModel|null} */
this.targetNotAuthorized = targetNotAuthorized;
/** @type {JLINCTracer} */
this.tracer = new JLINCTracer(config);
/** @type {string} */
this.authType = 'BaseChatModel';
}
/**
* @param {BindToolsInput[]} tools - The tools to bind to the underlying models.
* @param {Partial<CallOptions>} [kwargs] - Optional call options.
* @returns {Runnable<BaseLanguageModelInput, OutputMessageType, CallOptions>}
* A new JLINCAuthBaseChatModel instance whose underlying models
* have been bound with the provided tools.
*/
bindTools(tools, kwargs = {}) {
if (this.config.debug)
console.log(`[JLINCAuth] Binding tools to BaseChatModels`);
const authorizedBound = this.targetAuthorized.bindTools(tools, kwargs);
const notAuthorizedBound = this.targetNotAuthorized
? this.targetNotAuthorized.bindTools(tools, kwargs)
: null;
return new JLINCAuthBaseChatModel({
config: this.config,
jlincAuthDecision: this.jlincAuthDecision,
targetAuthorized: authorizedBound,
targetNotAuthorized: notAuthorizedBound,
});
}
/**
* @returns {string} - type
*/
_llmType() {
const type = this.targetAuthorized?._llmType?.() ?? "unknown";
if (this.config?.debug)
console.log(`[JLINCAuth] Returning LLM type: ${type}`);
return type;
}
/**
* @param {BaseLanguageModelInput} input
* @param {Partial<CallOptions>} [options] - Optional call options.
* @returns {Promise<OutputMessageType>} A promise that resolves with the output.
*/
async invoke(input, runManager) {
return await authInvoke(this, input);
}
}
module.exports = {
JLINCAuthBaseChatModel,
};

View File

@@ -0,0 +1,79 @@
const { Tool } = require("@langchain/core/tools");
const axios = require("axios");
/**
* @typedef {import('./common').JLINCConfig} JLINCConfig
*/
class JLINCAuthDecision extends Tool {
/**
* @param {JLINCConfig} fields
*/
constructor(config) {
super({
name: "jlinc_auth",
description: "Authorization via AuthZEN format.",
});
Object.defineProperty(this, "name", {
enumerable: true,
configurable: true,
writable: true,
value: "jlinc-auth"
});
Object.defineProperty(this, "description", {
enumerable: true,
configurable: true,
writable: true,
value: "Authorization via AuthZEN format."
});
/** @type {JLINCConfig} */
this.config = config;
/** @type {Boolean} */
this.authenticated = false;
}
/**
* @param {any} payload
* @returns {Promise<any>}
*/
async postToApi(auth) {
const response = await axios.post(
`${this.config.dataStoreApiUrl}/api/v1/auth`,
auth,
{
headers: {
'Authorization': `Bearer ${this.config.dataStoreApiKey}`,
}
}
);
return response.data;
}
/**
* @param {auth} auth
* @returns {Promise<boolean>} - authenticated
*/
async evaluate(auth) {
this.authenticated = false;
try {
this.authenticated = (await this.postToApi(auth)).decision;
} catch (e) {
console.log(`[JLINCAuthTool] Error connecting to API, flagging as unauthorized`);
if (this.config.debug) console.error(e)
}
if (this.config.debug)
console.log(`[JLINCAuth] Got authorization of: ${this.authenticated}`);
return this.authenticated;
}
/**
* @returns {boolean} - authenticated
*/
getAuth() {
return this.authenticated;
}
}
module.exports = {
JLINCAuthDecision,
};

68
src/auth/JLINCAuthTool.js Normal file
View File

@@ -0,0 +1,68 @@
const { Tool } = require("@langchain/core/tools");
const { JLINCTracer } = require("../tracer/index.js");
const { authDecide, authInvoke, getDescription, getName } = require("./common.js");
/**
* @typedef {import('./common').JLINCConfig} JLINCConfig
*/
/**
* @typedef {Object} JLINCAuthToolFields
* @property {JLINCConfig} config
* @property {Tool} jlincAuthDecision
* @property {Tool} targetAuthorized
* @property {Tool|null} targetNotAuthorized
*/
class JLINCAuthTool extends Tool {
/**
* @param {JLINCAuthToolFields} fields
*/
constructor({ config, jlincAuthDecision, targetAuthorized, targetNotAuthorized }) {
super({
name: "jlinc_auth_tool",
description: "", // overridden dynamically
});
Object.defineProperty(this, "name", {
enumerable: true,
configurable: true,
writable: true,
value: getName(targetAuthorized, targetNotAuthorized)
});
Object.defineProperty(this, "description", {
enumerable: true,
configurable: true,
writable: true,
value: getDescription(targetAuthorized, targetNotAuthorized, 'Tool')
});
/** @type {JLINCConfig} */
this.config = config;
/** @type {Tool} */
this.jlincAuthDecision = jlincAuthDecision;
/** @type {Tool} */
this.targetAuthorized = targetAuthorized;
/** @type {Tool|null} */
this.targetNotAuthorized = targetNotAuthorized;
/** @type {JLINCTracer} */
this.tracer = new JLINCTracer(config);
/** @type {string} */
this.authType = 'Tool';
}
/**
* @template TInput
* @template {undefined | ToolRunnableConfig} TConfig
* @template TOutput
*
* @param {TInput} input
* @param {TConfig} [config]
* @returns {Promise<ToolReturnType<TInput, TConfig, TOutput>>}
*/
async invoke(input, runManager) {
return await authInvoke(this, input);
}
}
module.exports = {
JLINCAuthTool,
};

85
src/auth/common.js Normal file
View File

@@ -0,0 +1,85 @@
const axios = require("axios");
/**
* @typedef {Object} JLINCConfig
* @property {string} [dataStoreApiUrl]
* @property {string} [dataStoreApiKey]
* @property {string} [archiveApiUrl]
* @property {string} [archiveApiKey]
* @property {string} [systemPrefix]
* @property {string|null} [agreementId]
* @property {boolean} [debug]
*/
/**
* @param {any} - Authorized
* @param {any|null} - NotAuthorized
* @returns {string} - The description
*/
function getName(authorized, unauthorized) {
return unauthorized
? `${authorized.name}-or-${unauthorized.name}`
: `authorized-${authorized.name}`
}
/**
* @param {any} - Authorized
* @param {any|null} - NotAuthorized
* @returns {string} - The description
*/
function getDescription(authorized, unauthorized, type) {
return unauthorized ? `
This is an authorization router ${type}. It decides whether
to route input to an "authorized" or "not authorized" ${type}.
Authorized ${type}: ${authorized.name}
Description: ${authorized.description}
Not Authorized ${type}: ${unauthorized.name}
Description: ${unauthorized.description}
The LLM can assume both ${type}s are available, but JLINC Auth will
only allow the Authorized ${type} to be called if you are authorized.
` : `
This is an authorization router ${type}. It decides whether
to route input to an "authorized" ${type}.
Authorized ${type}: ${authorized.name}
Description: ${authorized.description}
The LLM can assume the ${type} is available, but JLINC Auth will
only allow the Authorized ${type} to be called if you are authorized.
`
}
function getLogName(target) {
if (target.name) return target.name;
if (target.lc_kwargs?.bound?.model) return target.lc_kwargs.bound.model;
return 'no-name';
}
/**
* @param {any} settings
* @param {any} input
* @returns {Promise<any>} - The result of invoking the selected tool.
*/
async function authInvoke(settings, input) {
const authorized = await settings.jlincAuthDecision.getAuth();
let selectedTarget = settings.targetNotAuthorized
if (authorized) {
selectedTarget = settings.targetAuthorized
}
if (!selectedTarget)
throw new Error("No valid resource available");
if (settings.config.debug)
console.log(`[JLINCAuth] Invoking ${getLogName(selectedTarget)} (${settings.authType}|${authorized})`);
return await selectedTarget.invoke(input, {
callbacks: [settings.tracer],
});
}
module.exports = {
authInvoke,
getDescription,
getName,
};

9
src/auth/index.js Normal file
View File

@@ -0,0 +1,9 @@
const { JLINCAuthDecision } = require("./JLINCAuthDecision.js");
const { JLINCAuthBaseChatModel } = require("./JLINCAuthBaseChatModel.js");
const { JLINCAuthTool } = require("./JLINCAuthTool.js");
module.exports = {
JLINCAuthDecision,
JLINCAuthBaseChatModel,
JLINCAuthTool,
};

13
src/index.js Normal file
View File

@@ -0,0 +1,13 @@
const {
JLINCAuthDecision,
JLINCAuthBaseChatModel,
JLINCAuthTool,
} = require("./auth/index.js");
const { JLINCTracer } = require("./tracer/index.js");
module.exports = {
JLINCAuthDecision,
JLINCAuthBaseChatModel,
JLINCAuthTool,
JLINCTracer,
};

View File

@@ -235,7 +235,7 @@ class JLINCTracer extends LangChainTracer {
console.error("[Tracer] Failed to log event:", error.message); console.error("[Tracer] Failed to log event:", error.message);
} }
} }
} }
module.exports = { JLINCTracer } module.exports = { JLINCTracer }

116
test/test.js Normal file
View File

@@ -0,0 +1,116 @@
const { ChatOpenAI } = require("@langchain/openai");
const { awaitAllCallbacks } = require("@langchain/core/callbacks/promises");
const { Calculator } = require("@langchain/community/tools/calculator");
const { AgentExecutor, createToolCallingAgent } = require("langchain/agents");
const { ChatPromptTemplate } = require("@langchain/core/prompts");
const { JLINCTracer, JLINCAuthDecision, JLINCAuthBaseChatModel, JLINCAuthTool } = require("../src/index.js");
class CalculatorPrivate extends Calculator {
static lc_name() {
return "CalculatorPrivate";
}
}
class CalculatorPublic extends Calculator {
static lc_name() {
return "CalculatorPublic";
}
}
async function main() {
const config = {
dataStoreApiUrl: "http://localhost:9090",
dataStoreApiKey: process.env.JLINC_DATA_STORE_API_KEY,
archiveApiUrl: "http://localhost:9090",
archiveApiKey: process.env.JLINC_ARCHIVE_API_KEY,
agreementId: "00000000-0000-0000-0000-000000000000",
systemPrefix: "TestTracerJlinc",
debug: true,
}
const jlincAuthDecision = new JLINCAuthDecision(config);
const auth = {
subject: {
type: "user",
id: "tester",
},
action: {
name: "read",
},
resource: {
type: "data",
id: "1234",
properties: {
ownerID: "tester@test.com",
}
}
}
await jlincAuthDecision.evaluate(auth);
const tracer = new JLINCTracer(config);
const authorizedLlm = new ChatOpenAI({
openAIApiKey: "n/a",
configuration: {
baseURL: "http://localhost:1234/v1",
},
modelName: "meta-llama-3.1-8b-instruct",
});
const notAuthorizedLlm = new ChatOpenAI({
openAIApiKey: "n/a",
configuration: {
baseURL: "http://localhost:1234/v1",
},
modelName: "hermes-3-llama-3.1-8b",
});
const llm = new JLINCAuthBaseChatModel({
config,
jlincAuthDecision,
targetAuthorized: authorizedLlm,
targetNotAuthorized: notAuthorizedLlm, // Optional
});
const calculatorPublic = new CalculatorPublic();
calculatorPublic.name = 'calculator_public';
const calculatorPrivate = new CalculatorPrivate();
calculatorPrivate.name = 'calculator_private';
const jlincAuthTool = new JLINCAuthTool({
config,
jlincAuthDecision,
targetAuthorized: calculatorPublic,
targetNotAuthorized: calculatorPrivate, // Optional
});
const tools = [jlincAuthTool];
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant"],
["placeholder", "{chat_history}"],
["human", "{input}"],
["placeholder", "{agent_scratchpad}"],
]);
const agent = createToolCallingAgent({ llm, tools, prompt });
const agentExecutor = new AgentExecutor({
agent,
tools,
});
try {
const r = await agentExecutor.invoke({ input: "Add 1 + 1. If a function call is used, tell me the output of the function call." }, { callbacks: [tracer] });
// The next invocation requires a reauth for any future calls to the agent:
auth.action.name = "write";
jlincAuthDecision.evaluate(auth);
console.log(`\nResult`)
console.log(`---------------------------------------------`)
console.log(r)
} catch (err) {
console.error("Error calling LLM:", err);
} finally {
await awaitAllCallbacks();
}
}
main()

View File

@@ -1,56 +0,0 @@
const { ChatOpenAI } = require("@langchain/openai");
const { awaitAllCallbacks } = require("@langchain/core/callbacks/promises");
const { Calculator } = require("@langchain/community/tools/calculator");
const { AgentExecutor, createToolCallingAgent } = require("langchain/agents");
const { ChatPromptTemplate } = require("@langchain/core/prompts");
const { JLINCTracer } = require("../src/tracer.js");
async function main() {
const tracer = new JLINCTracer({
dataStoreApiUrl: "http://localhost:9090",
dataStoreApiKey: process.env.JLINC_DATA_STORE_API_KEY,
archiveApiUrl: "http://localhost:9090",
archiveApiKey: process.env.JLINC_ARCHIVE_API_KEY,
agreementId: "00000000-0000-0000-0000-000000000000",
systemPrefix: "TracerTest",
debug: true,
});
const llm = new ChatOpenAI({
openAIApiKey: "n/a",
configuration: {
baseURL: "http://localhost:1234/v1",
},
modelName: "meta-llama-3.1-8b-instruct",
});
const calculator = new Calculator();
const tools = [calculator];
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant"],
["placeholder", "{chat_history}"],
["human", "{input}"],
["placeholder", "{agent_scratchpad}"],
]);
const agent = createToolCallingAgent({ llm, tools, prompt });
const agentExecutor = new AgentExecutor({
agent,
tools,
});
try {
const r = await agentExecutor.invoke({ input: "Add 1 + 1" }, {callbacks: [tracer]});
console.log(`\nResult`)
console.log(`---------------------------------------------`)
console.log(r)
} catch (err) {
console.error("Error calling LLM:", err);
} finally {
await awaitAllCallbacks();
}
}
main()