fix(config): קריאת config מותאמת ל-scope של חברה (Paperclip 2026.722.0)

מיגרציה 0164_plugin_config_company_scope הפכה את config של פלאגינים
ל-company-scoped, ו-`ctx.config.get()` בלי companyId זורק
"company context is required" כשה-host לא יכול לגזור חברה מההקשר.
ב-setup() אין חברה בהגדרה, ולכן הקריאה שם הפילה את הפעלת הפלאגין כולו
בשדרוג מ-2026.609.0 (status=error, 0 כלים רשומים).

- setup() כבר לא קורא config; LegalApi מקבל resolver עצל במקום URL קבוע
  (baseUrl שימש במקום אחד בלבד, ולכן 17 ה-tool handlers לא נגעו)
- resolveApiBase() פותר לפי הסדר: companyId מפורש → גזירת ה-host
  מההקשר → סריקת החברות ב-CEO_AGENT_IDS → ברירת מחדל
- data handlers מעבירים params.companyId; jobs מתוזמנים חסרי חברה
  (PluginJobContext לא כולל companyId) ולכן נשענים על הסריקה
- שדרוג @paperclipai/plugin-sdk ל-2026.722.0

מאומת: הפלאגין עולה עם אותה חתימת רישום כמו לפני השדרוג —
eventSubscriptions:2, jobs:4, webhooks:1, tools:9, בלי warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 16:18:30 +00:00
parent d0669a0878
commit 6294d94bed
4 changed files with 99 additions and 32 deletions

16
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "@marcusgroup/plugin-legal-ai",
"version": "0.1.0",
"dependencies": {
"@paperclipai/plugin-sdk": "^2026.525.0",
"@paperclipai/plugin-sdk": "^2026.722.0",
"react": "^19.0.0"
},
"devDependencies": {
@@ -625,12 +625,12 @@
}
},
"node_modules/@paperclipai/plugin-sdk": {
"version": "2026.525.0",
"resolved": "https://registry.npmjs.org/@paperclipai/plugin-sdk/-/plugin-sdk-2026.525.0.tgz",
"integrity": "sha512-7ivXbFSwH7cS+/2WNbPwQve5vVtLnqgxX5lmqTKn+i3fOY03d5KHaYF2bbI5zeTkAAwwEJ0Nz3rfBWNPguRRUw==",
"version": "2026.722.0",
"resolved": "https://registry.npmjs.org/@paperclipai/plugin-sdk/-/plugin-sdk-2026.722.0.tgz",
"integrity": "sha512-2rMJCBo8ZAouuMsapdaY4/l+oHmrXKHW/k/HfPOLCxFng7+g04c5JhoFq+/ptGfNcW3kXzs34y7brviDCml06Q==",
"license": "MIT",
"dependencies": {
"@paperclipai/shared": "2026.525.0",
"@paperclipai/shared": "2026.722.0",
"zod": "^3.24.2"
},
"bin": {
@@ -646,9 +646,9 @@
}
},
"node_modules/@paperclipai/shared": {
"version": "2026.525.0",
"resolved": "https://registry.npmjs.org/@paperclipai/shared/-/shared-2026.525.0.tgz",
"integrity": "sha512-fbVrEx96oxkxXbRBFDLAgW5Z0lRC7Ii+BeCNhKqKRCg9m2IwtP97CGoOeAROdRrvRpP8Neb3vgaaDbDjXT74lQ==",
"version": "2026.722.0",
"resolved": "https://registry.npmjs.org/@paperclipai/shared/-/shared-2026.722.0.tgz",
"integrity": "sha512-JciI6EbtNwHSeoyN5ZkshwqpyfIluyO46JHp8I7pPCvpJ7Uv7MSZEBs3lKXdXLadFEN6qzfwNmf39xjIFC8isA==",
"license": "MIT",
"dependencies": {
"zod": "^3.24.2"

View File

@@ -17,7 +17,7 @@
"biome:fix": "biome check --write src/"
},
"dependencies": {
"@paperclipai/plugin-sdk": "^2026.525.0",
"@paperclipai/plugin-sdk": "^2026.722.0",
"react": "^19.0.0"
},
"devDependencies": {

View File

@@ -2,11 +2,23 @@
* HTTP client for Ezer Mishpati legal-ai REST API.
*/
/**
* Where the client gets its base URL. A plain string still works; a resolver is
* used when the URL comes from company-scoped plugin config, which cannot be
* read at worker startup (see worker.ts — `ctx.config.get` needs a company).
*/
export type BaseUrlSource = string | (() => string | Promise<string>);
export class LegalApi {
constructor(private baseUrl: string) {}
constructor(private baseUrl: BaseUrlSource) {}
private async resolveBaseUrl(): Promise<string> {
return typeof this.baseUrl === "string" ? this.baseUrl : this.baseUrl();
}
private async request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
const base = await this.resolveBaseUrl();
const res = await fetch(`${base}${path}`, {
...init,
headers: {
"Content-Type": "application/json",

View File

@@ -16,16 +16,79 @@ const CEO_AGENT_IDS: Record<string, string> = {
"cdbfa8bc-3d61-41a4-a2e7-677ec7d34562", // CMPA (היטלי השבחה)
};
const DEFAULT_LEGAL_API_BASE = "http://localhost:8085";
/**
* Resolve `legalApiBaseUrl` from company-scoped plugin config.
*
* ⚠️ Plugin config became company-scoped in @paperclipai/server 2026.722.0
* (migration `0164_plugin_config_company_scope`). A bare `ctx.config.get()` now
* throws `company context is required` unless the host can derive the company
* from the current invocation — which it CANNOT during `setup()`, because
* worker initialisation belongs to no company. Reading config at startup is what
* made the whole plugin fail to activate on the 722.0 upgrade.
*
* So: never call this from setup(); call it from a handler, passing the
* companyId that handler was given. Tool handlers get `runCtx.companyId`; data
* handlers get `params.companyId`. Scheduled jobs have no company at all, so
* they fall back to probing the companies we know about.
*/
/** Pull the companyId the host passes to `ctx.data` handlers, if present. */
function companyIdOf(params: unknown): string | undefined {
const id = (params as { companyId?: unknown } | null)?.companyId;
return typeof id === "string" && id.trim() ? id : undefined;
}
async function resolveApiBase(
ctx: PluginContext,
companyId?: string,
): Promise<string> {
const read = async (id?: string): Promise<string | null> => {
try {
const cfg = (await ctx.config.get(id)) as {
legalApiBaseUrl?: unknown;
} | null;
const url = cfg?.legalApiBaseUrl;
return typeof url === "string" && url.trim() ? url.trim() : null;
} catch {
return null;
}
};
if (companyId) {
const scoped = await read(companyId);
if (scoped) return scoped;
}
// No companyId passed in — but the host derives one itself when the call
// happens inside a host-scoped invocation (e.g. a tool handler). Ask before
// falling back to guesswork, so tools honour their own company's config.
const derived = await read(undefined);
if (derived) return derived;
// Genuinely no company (scheduled jobs): try each company we know of. All of
// them point at the same legal-ai instance in practice, so the first hit wins.
for (const knownCompanyId of Object.keys(CEO_AGENT_IDS)) {
const url = await read(knownCompanyId);
if (url) return url;
}
ctx.logger.warn("legalApiBaseUrl unresolved — using default", {
companyId: companyId ?? null,
fallback: DEFAULT_LEGAL_API_BASE,
});
return DEFAULT_LEGAL_API_BASE;
}
const plugin = definePlugin({
async setup(ctx) {
pluginCtx = ctx; // save for onWebhook
const config = (await ctx.config.get()) as {
legalApiBaseUrl?: string;
} | null;
const baseUrl = config?.legalApiBaseUrl || "http://localhost:8085";
const api = new LegalApi(baseUrl);
ctx.logger.info("Legal AI plugin starting", { url: baseUrl });
// Lazy: the URL is fetched on the first request, from inside a handler
// that has company context — not here. See resolveApiBase() above.
const api = new LegalApi(() => resolveApiBase(ctx));
ctx.logger.info("Legal AI plugin starting");
// ── Tools ──────────────────────────────────────────────────────
@@ -859,9 +922,8 @@ const plugin = definePlugin({
ctx.jobs.register("stale-case-reminder", async (_job) => {
ctx.logger.info("stale-case-reminder: starting");
const config = await ctx.config.get();
const apiBase =
(config.legalApiBaseUrl as string) ?? "http://localhost:8085";
// Scheduled job — no company context; resolveApiBase probes known companies.
const apiBase = await resolveApiBase(ctx);
let resp: Awaited<ReturnType<typeof ctx.http.fetch>>;
try {
@@ -933,9 +995,8 @@ const plugin = definePlugin({
ctx.jobs.register("weekly-feedback-analysis", async (_job) => {
ctx.logger.info("weekly-feedback-analysis: starting");
const config = await ctx.config.get();
const apiBase =
(config.legalApiBaseUrl as string) ?? "http://localhost:8085";
// Scheduled job — no company context; resolveApiBase probes known companies.
const apiBase = await resolveApiBase(ctx);
const resp = await ctx.http.fetch(
`${apiBase}/api/chair-feedback/weekly-summary`,
@@ -1129,9 +1190,7 @@ const plugin = definePlugin({
if (!issueId) return null;
const caseNumber = await resolveCaseNumber(issueId);
if (!caseNumber) return null;
const config2 = await ctx.config.get();
const apiBase =
(config2.legalApiBaseUrl as string) ?? "http://localhost:8085";
const apiBase = await resolveApiBase(ctx, companyIdOf(params));
try {
const resp = await ctx.http.fetch(
`${apiBase}/api/cases/${encodeURIComponent(caseNumber)}/legal-arguments`,
@@ -1168,9 +1227,7 @@ const plugin = definePlugin({
if (!issueId) return [];
const caseNumber = await resolveCaseNumber(issueId);
if (!caseNumber) return [];
const config2 = await ctx.config.get();
const apiBase =
(config2.legalApiBaseUrl as string) ?? "http://localhost:8085";
const apiBase = await resolveApiBase(ctx, companyIdOf(params));
try {
const resp = await ctx.http.fetch(
`${apiBase}/api/cases/${encodeURIComponent(caseNumber)}/precedents`,
@@ -1206,9 +1263,7 @@ const plugin = definePlugin({
if (!issueId) return [];
const caseNumber = await resolveCaseNumber(issueId);
if (!caseNumber) return [];
const config2 = await ctx.config.get();
const apiBase =
(config2.legalApiBaseUrl as string) ?? "http://localhost:8085";
const apiBase = await resolveApiBase(ctx, companyIdOf(params));
try {
const url = new URL(`${apiBase}/api/missing-precedents`);
url.searchParams.set("case_number", caseNumber);