From 9941d61fb8120ce975857f10dc52db0e6b61f7af Mon Sep 17 00:00:00 2001 From: JlincFM Date: Mon, 24 Nov 2025 14:21:49 +0000 Subject: [PATCH] add authentication support --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 4 +- README.md | 132 ++++++++++++++++++++- dist/auth/JLINCAuthBaseChatModel.d.ts | 51 ++++++++ dist/auth/JLINCAuthBaseChatModel.js | 96 +++++++++++++++ dist/auth/JLINCAuthDecision.d.ts | 29 +++++ dist/auth/JLINCAuthDecision.js | 79 ++++++++++++ dist/auth/JLINCAuthTool.d.ts | 47 ++++++++ dist/auth/JLINCAuthTool.js | 68 +++++++++++ dist/auth/common.d.ts | 37 ++++++ dist/auth/common.js | 85 +++++++++++++ dist/auth/index.d.ts | 4 + dist/auth/index.js | 9 ++ dist/index.d.ts | 5 + dist/index.js | 13 ++ dist/{tracer.d.ts => tracer/index.d.ts} | 0 dist/{tracer.js => tracer/index.js} | 2 +- package.json | 8 +- src/auth/JLINCAuthBaseChatModel.js | 96 +++++++++++++++ src/auth/JLINCAuthDecision.js | 79 ++++++++++++ src/auth/JLINCAuthTool.js | 68 +++++++++++ src/auth/common.js | 85 +++++++++++++ src/auth/index.js | 9 ++ src/index.js | 13 ++ src/{tracer.js => tracer/index.js} | 2 +- test/test.js | 116 ++++++++++++++++++ test/test_tracer.js | 56 --------- 27 files changed, 1126 insertions(+), 69 deletions(-) create mode 100644 dist/auth/JLINCAuthBaseChatModel.d.ts create mode 100644 dist/auth/JLINCAuthBaseChatModel.js create mode 100644 dist/auth/JLINCAuthDecision.d.ts create mode 100644 dist/auth/JLINCAuthDecision.js create mode 100644 dist/auth/JLINCAuthTool.d.ts create mode 100644 dist/auth/JLINCAuthTool.js create mode 100644 dist/auth/common.d.ts create mode 100644 dist/auth/common.js create mode 100644 dist/auth/index.d.ts create mode 100644 dist/auth/index.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.js rename dist/{tracer.d.ts => tracer/index.d.ts} (100%) rename dist/{tracer.js => tracer/index.js} (99%) create mode 100644 src/auth/JLINCAuthBaseChatModel.js create mode 100644 src/auth/JLINCAuthDecision.js create mode 100644 src/auth/JLINCAuthTool.js create mode 100644 src/auth/common.js create mode 100644 src/auth/index.js create mode 100644 src/index.js rename src/{tracer.js => tracer/index.js} (99%) create mode 100644 test/test.js delete mode 100644 test/test_tracer.js diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6681b9f..5a3d5b9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -33,7 +33,7 @@ body: 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. validations: - required: true + required: true - type: textarea id: logs attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index ef468ff..f20130d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -30,14 +30,14 @@ body: label: Describe the solution you'd like placeholder: A clear and concise description of what you want to happen. validations: - required: true + required: true - type: textarea id: alternatives attributes: label: Describe alternatives you've considered placeholder: A clear and concise description of any alternative solutions or features you've considered. validations: - required: true + required: true - type: textarea id: context attributes: diff --git a/README.md b/README.md index f1c6922..5e7f141 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # 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. -## 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. ```javascript @@ -16,7 +16,7 @@ const { ChatPromptTemplate } = require("@langchain/core/prompts"); const { JLINCTracer } = require("@jlinc/langchain"); async function main() { - const tracer = new JLINCTracer({ + const config = { dataStoreApiUrl: "http://localhost:9090", dataStoreApiKey: process.env.JLINC_DATA_STORE_API_KEY, archiveApiUrl: "http://localhost:9090", @@ -24,7 +24,9 @@ async function main() { agreementId: "00000000-0000-0000-0000-000000000000", systemPrefix: "TracerTest", debug: true, - }); + } + + const tracer = new JLINCTracer(config); const llm = new ChatOpenAI({ openAIApiKey: "n/a", @@ -66,6 +68,128 @@ async function 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 diff --git a/dist/auth/JLINCAuthBaseChatModel.d.ts b/dist/auth/JLINCAuthBaseChatModel.d.ts new file mode 100644 index 0000000..ce693d0 --- /dev/null +++ b/dist/auth/JLINCAuthBaseChatModel.d.ts @@ -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 { + /** + * @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} [kwargs] - Optional call options. + * @returns {Runnable} + * A new JLINCAuthBaseChatModel instance whose underlying models + * have been bound with the provided tools. + */ + bindTools(tools: BindToolsInput[], kwargs?: Partial): Runnable; + /** + * @param {BaseLanguageModelInput} input + * @param {Partial} [options] - Optional call options. + * @returns {Promise} A promise that resolves with the output. + */ + invoke(input: BaseLanguageModelInput, runManager: any): Promise; +} +import { BaseChatModel } from "@langchain/core/dist/language_models/chat_models.js"; +import { JLINCTracer } from "../tracer/index.js"; diff --git a/dist/auth/JLINCAuthBaseChatModel.js b/dist/auth/JLINCAuthBaseChatModel.js new file mode 100644 index 0000000..3f48dcd --- /dev/null +++ b/dist/auth/JLINCAuthBaseChatModel.js @@ -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} [kwargs] - Optional call options. + * @returns {Runnable} + * 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} [options] - Optional call options. + * @returns {Promise} A promise that resolves with the output. + */ + async invoke(input, runManager) { + return await authInvoke(this, input); + } +} + +module.exports = { + JLINCAuthBaseChatModel, +}; diff --git a/dist/auth/JLINCAuthDecision.d.ts b/dist/auth/JLINCAuthDecision.d.ts new file mode 100644 index 0000000..86be1d0 --- /dev/null +++ b/dist/auth/JLINCAuthDecision.d.ts @@ -0,0 +1,29 @@ +export type JLINCConfig = import("./common").JLINCConfig; +/** + * @typedef {import('./common').JLINCConfig} JLINCConfig + */ +export class JLINCAuthDecision extends Tool { + /** + * @param {JLINCConfig} fields + */ + constructor(config: any); + /** @type {JLINCConfig} */ + config: JLINCConfig; + /** @type {Boolean} */ + authenticated: boolean; + /** + * @param {any} payload + * @returns {Promise} + */ + postToApi(auth: any): Promise; + /** + * @param {auth} auth + * @returns {Promise} - authenticated + */ + evaluate(auth: any): Promise; + /** + * @returns {boolean} - authenticated + */ + getAuth(): boolean; +} +import { Tool } from "@langchain/core/dist/tools"; diff --git a/dist/auth/JLINCAuthDecision.js b/dist/auth/JLINCAuthDecision.js new file mode 100644 index 0000000..5d97c16 --- /dev/null +++ b/dist/auth/JLINCAuthDecision.js @@ -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} + */ + 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} - 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, +}; diff --git a/dist/auth/JLINCAuthTool.d.ts b/dist/auth/JLINCAuthTool.d.ts new file mode 100644 index 0000000..591fdbc --- /dev/null +++ b/dist/auth/JLINCAuthTool.d.ts @@ -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 { + /** + * @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>} + */ + invoke(input: TInput, runManager: any): Promise>; +} +import { Tool } from "@langchain/core/dist/tools/index.js"; +import { JLINCTracer } from "../tracer/index.js"; diff --git a/dist/auth/JLINCAuthTool.js b/dist/auth/JLINCAuthTool.js new file mode 100644 index 0000000..967f0d2 --- /dev/null +++ b/dist/auth/JLINCAuthTool.js @@ -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>} + */ + async invoke(input, runManager) { + return await authInvoke(this, input); + } +} + +module.exports = { + JLINCAuthTool, +}; diff --git a/dist/auth/common.d.ts b/dist/auth/common.d.ts new file mode 100644 index 0000000..89d666e --- /dev/null +++ b/dist/auth/common.d.ts @@ -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} - The result of invoking the selected tool. + */ +export function authInvoke(settings: any, input: any): Promise; +/** + * @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; diff --git a/dist/auth/common.js b/dist/auth/common.js new file mode 100644 index 0000000..4c0cc0d --- /dev/null +++ b/dist/auth/common.js @@ -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} - 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, +}; diff --git a/dist/auth/index.d.ts b/dist/auth/index.d.ts new file mode 100644 index 0000000..19a92b7 --- /dev/null +++ b/dist/auth/index.d.ts @@ -0,0 +1,4 @@ +import { JLINCAuthDecision } from "./JLINCAuthDecision.js"; +import { JLINCAuthBaseChatModel } from "./JLINCAuthBaseChatModel.js"; +import { JLINCAuthTool } from "./JLINCAuthTool.js"; +export { JLINCAuthDecision, JLINCAuthBaseChatModel, JLINCAuthTool }; diff --git a/dist/auth/index.js b/dist/auth/index.js new file mode 100644 index 0000000..24d6380 --- /dev/null +++ b/dist/auth/index.js @@ -0,0 +1,9 @@ +const { JLINCAuthDecision } = require("./JLINCAuthDecision.js"); +const { JLINCAuthBaseChatModel } = require("./JLINCAuthBaseChatModel.js"); +const { JLINCAuthTool } = require("./JLINCAuthTool.js"); + +module.exports = { + JLINCAuthDecision, + JLINCAuthBaseChatModel, + JLINCAuthTool, +}; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..145f05d --- /dev/null +++ b/dist/index.d.ts @@ -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 }; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..b68f077 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,13 @@ +const { + JLINCAuthDecision, + JLINCAuthBaseChatModel, + JLINCAuthTool, +} = require("./auth/index.js"); +const { JLINCTracer } = require("./tracer/index.js"); + +module.exports = { + JLINCAuthDecision, + JLINCAuthBaseChatModel, + JLINCAuthTool, + JLINCTracer, +}; \ No newline at end of file diff --git a/dist/tracer.d.ts b/dist/tracer/index.d.ts similarity index 100% rename from dist/tracer.d.ts rename to dist/tracer/index.d.ts diff --git a/dist/tracer.js b/dist/tracer/index.js similarity index 99% rename from dist/tracer.js rename to dist/tracer/index.js index ef78b85..d3ce40c 100644 --- a/dist/tracer.js +++ b/dist/tracer/index.js @@ -235,7 +235,7 @@ class JLINCTracer extends LangChainTracer { console.error("[Tracer] Failed to log event:", error.message); } } - + } module.exports = { JLINCTracer } diff --git a/package.json b/package.json index adfe2bc..c2ba1f8 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,11 @@ "name": "@jlinc/langchain", "version": "0.1.3", "description": "A LangChain integration that logs events to the JLINC API.", - "main": "dist/tracer.js", - "types": "dist/tracer.d.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { - "test": "node test/test_tracer.js", - "build": "tsc && cp src/tracer.js dist/" + "test": "node test/test.js", + "build": "rm -rf dist && cp -a src dist && tsc" }, "homepage": "https://www.jlinc.com/", "repository": { diff --git a/src/auth/JLINCAuthBaseChatModel.js b/src/auth/JLINCAuthBaseChatModel.js new file mode 100644 index 0000000..3f48dcd --- /dev/null +++ b/src/auth/JLINCAuthBaseChatModel.js @@ -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} [kwargs] - Optional call options. + * @returns {Runnable} + * 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} [options] - Optional call options. + * @returns {Promise} A promise that resolves with the output. + */ + async invoke(input, runManager) { + return await authInvoke(this, input); + } +} + +module.exports = { + JLINCAuthBaseChatModel, +}; diff --git a/src/auth/JLINCAuthDecision.js b/src/auth/JLINCAuthDecision.js new file mode 100644 index 0000000..5d97c16 --- /dev/null +++ b/src/auth/JLINCAuthDecision.js @@ -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} + */ + 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} - 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, +}; diff --git a/src/auth/JLINCAuthTool.js b/src/auth/JLINCAuthTool.js new file mode 100644 index 0000000..967f0d2 --- /dev/null +++ b/src/auth/JLINCAuthTool.js @@ -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>} + */ + async invoke(input, runManager) { + return await authInvoke(this, input); + } +} + +module.exports = { + JLINCAuthTool, +}; diff --git a/src/auth/common.js b/src/auth/common.js new file mode 100644 index 0000000..4c0cc0d --- /dev/null +++ b/src/auth/common.js @@ -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} - 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, +}; diff --git a/src/auth/index.js b/src/auth/index.js new file mode 100644 index 0000000..24d6380 --- /dev/null +++ b/src/auth/index.js @@ -0,0 +1,9 @@ +const { JLINCAuthDecision } = require("./JLINCAuthDecision.js"); +const { JLINCAuthBaseChatModel } = require("./JLINCAuthBaseChatModel.js"); +const { JLINCAuthTool } = require("./JLINCAuthTool.js"); + +module.exports = { + JLINCAuthDecision, + JLINCAuthBaseChatModel, + JLINCAuthTool, +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b68f077 --- /dev/null +++ b/src/index.js @@ -0,0 +1,13 @@ +const { + JLINCAuthDecision, + JLINCAuthBaseChatModel, + JLINCAuthTool, +} = require("./auth/index.js"); +const { JLINCTracer } = require("./tracer/index.js"); + +module.exports = { + JLINCAuthDecision, + JLINCAuthBaseChatModel, + JLINCAuthTool, + JLINCTracer, +}; \ No newline at end of file diff --git a/src/tracer.js b/src/tracer/index.js similarity index 99% rename from src/tracer.js rename to src/tracer/index.js index ef78b85..d3ce40c 100644 --- a/src/tracer.js +++ b/src/tracer/index.js @@ -235,7 +235,7 @@ class JLINCTracer extends LangChainTracer { console.error("[Tracer] Failed to log event:", error.message); } } - + } module.exports = { JLINCTracer } diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..92b9226 --- /dev/null +++ b/test/test.js @@ -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() \ No newline at end of file diff --git a/test/test_tracer.js b/test/test_tracer.js deleted file mode 100644 index aadad1a..0000000 --- a/test/test_tracer.js +++ /dev/null @@ -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() \ No newline at end of file