@coffer-org/server 7.1.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/auth-api.d.ts +4 -0
  2. package/dist/auth-api.js +53 -0
  3. package/dist/auth-store.d.ts +2 -0
  4. package/dist/auth-store.js +1 -0
  5. package/dist/background-scheduler.d.ts +1 -0
  6. package/dist/background-scheduler.js +2 -1
  7. package/dist/entity-schema.d.ts +2 -0
  8. package/dist/entity-schema.js +26 -0
  9. package/dist/identity-link.d.ts +15 -0
  10. package/dist/identity-link.js +76 -0
  11. package/dist/index.js +8 -1
  12. package/dist/mcp-contract/schema.d.ts +34 -1
  13. package/dist/mcp-contract/schema.js +32 -9
  14. package/dist/mcp-contract/tools.js +3 -1
  15. package/dist/mcp-http.js +7 -3
  16. package/dist/mcp-tools.d.ts +7 -5
  17. package/dist/mcp-tools.js +95 -91
  18. package/dist/media/image.d.ts +23 -0
  19. package/dist/media/image.js +103 -0
  20. package/dist/media/index.d.ts +1 -0
  21. package/dist/media/index.js +1 -0
  22. package/dist/migrations.js +1 -1
  23. package/dist/orchestrator/agent-capabilities.d.ts +2 -2
  24. package/dist/orchestrator/agent-capabilities.js +3 -3
  25. package/dist/orchestrator/allow.d.ts +1 -16
  26. package/dist/orchestrator/allow.js +3 -53
  27. package/dist/orchestrator/config.js +0 -1
  28. package/dist/orchestrator/context-facts.d.ts +27 -0
  29. package/dist/orchestrator/context-facts.js +89 -0
  30. package/dist/orchestrator/conversation-access.d.ts +9 -0
  31. package/dist/orchestrator/conversation-access.js +12 -0
  32. package/dist/orchestrator/environment.d.ts +1 -0
  33. package/dist/orchestrator/environment.js +10 -0
  34. package/dist/orchestrator/file-inspection.d.ts +2 -2
  35. package/dist/orchestrator/file-inspection.js +41 -19
  36. package/dist/orchestrator/index.d.ts +16 -9
  37. package/dist/orchestrator/index.js +14 -7
  38. package/dist/orchestrator/live-message.d.ts +7 -4
  39. package/dist/orchestrator/live-message.js +48 -31
  40. package/dist/orchestrator/pipeline.d.ts +25 -4
  41. package/dist/orchestrator/pipeline.js +214 -94
  42. package/dist/orchestrator/registry.d.ts +4 -2
  43. package/dist/orchestrator/registry.js +10 -1
  44. package/dist/orchestrator/system-areas.d.ts +12 -0
  45. package/dist/orchestrator/system-areas.js +63 -0
  46. package/dist/orchestrator/system-capabilities.js +1 -1
  47. package/dist/orchestrator/turn-context.d.ts +18 -0
  48. package/dist/orchestrator/turn-context.js +39 -0
  49. package/dist/orchestrator/types.d.ts +106 -44
  50. package/dist/plugin-hooks.d.ts +26 -0
  51. package/dist/plugin-http-mounts.d.ts +18 -0
  52. package/dist/plugin-http-mounts.js +94 -0
  53. package/dist/plugin-runtime.js +2 -2
  54. package/dist/records-api.js +15 -3
  55. package/dist/system-settings.js +0 -1
  56. package/dist/thread-state.d.ts +14 -0
  57. package/dist/thread-state.js +71 -11
  58. package/dist/thread-store.d.ts +5 -3
  59. package/dist/thread-store.js +12 -9
  60. package/dist/turn-gate.d.ts +8 -0
  61. package/dist/turn-gate.js +39 -0
  62. package/package.json +7 -2
package/dist/mcp-tools.js CHANGED
@@ -10,6 +10,7 @@ import { pluginHooks, pluginCtx } from "./plugin-hooks.js";
10
10
  import { getActiveRegistry } from "./registry-context.js";
11
11
  import { countTargetsFor, recordCounts } from "./counts.js";
12
12
  import { describeCondition } from '@coffer-org/sdk/condition';
13
+ import { normalizeAgentMeta } from '@coffer-org/sdk/library';
13
14
  import { getEm } from "./db.js";
14
15
  import { getPluginSettings } from "./plugin-runtime.js";
15
16
  import { configuredPublicUrl } from "./public-url.js";
@@ -46,7 +47,7 @@ export async function resolveRagDeps() {
46
47
  return null;
47
48
  return { embeddingApiKey };
48
49
  }
49
- export async function collectMcpTools(opts = {}) {
50
+ export async function collectMcpTools(opts) {
50
51
  const out = [];
51
52
  const client = new LocalClient();
52
53
  const locales = await loadComposedLocales();
@@ -201,54 +202,52 @@ export async function collectMcpTools(opts = {}) {
201
202
  },
202
203
  });
203
204
  }
204
- if (opts.includeAdmin) {
205
- const actor = opts.actor ?? 'mcp';
206
- out.push({
207
- server: 'coffer',
208
- bareName: 'list_settings',
209
- httpName: 'list_settings',
210
- description: "List every settings group (the instance's own `system` group and each plugin's): each field's key/kind/required and the current values (secrets masked). Call before update_settings.",
211
- inputSchema: {},
212
- scope: 'settings',
213
- role: 'admin',
214
- handler: async () => {
215
- try {
216
- return ok(await listSettings());
217
- }
218
- catch (e) {
219
- return fail(`Error: ${e.message}`);
220
- }
221
- },
222
- });
223
- out.push({
224
- server: 'coffer',
225
- bareName: 'update_settings',
226
- httpName: 'update_settings',
227
- description: 'Update a plugin or system settings group. group — the settings group id (e.g. "system", "claude-agent"); fields — only the settings to change (see list_settings). Send real secret values; the masked placeholder (********) is treated as unchanged.',
228
- inputSchema: { group: z.string(), fields: z.record(z.string(), z.unknown()) },
229
- scope: 'settings',
230
- role: 'admin',
231
- handler: async (args) => {
232
- try {
233
- const row = await writePluginSettings(getEm().fork(), args.group, args.fields, actor);
234
- return ok(row);
235
- }
236
- catch (e) {
237
- if (e instanceof ValidationError) {
238
- const lines = e.issues.map((i) => {
239
- const o = i;
240
- return `- ${o.field ?? '?'}: ${o.code ?? 'invalid'}`;
241
- });
242
- return fail(`Validation error:\n${lines.join('\n')}`);
243
- }
244
- if (e instanceof NotFoundError)
245
- return fail(`No settings group '${String(args.group)}'.`);
246
- return fail(`Error: ${e.message}`);
205
+ const actor = opts.actor ?? 'mcp';
206
+ out.push({
207
+ server: 'coffer',
208
+ bareName: 'list_settings',
209
+ httpName: 'list_settings',
210
+ description: "List every settings group (the instance's own `system` group and each plugin's): each field's key/kind/required and the current values (secrets masked). Call before update_settings.",
211
+ inputSchema: {},
212
+ scope: 'settings',
213
+ role: 'admin',
214
+ handler: async () => {
215
+ try {
216
+ return ok(await listSettings());
217
+ }
218
+ catch (e) {
219
+ return fail(`Error: ${e.message}`);
220
+ }
221
+ },
222
+ });
223
+ out.push({
224
+ server: 'coffer',
225
+ bareName: 'update_settings',
226
+ httpName: 'update_settings',
227
+ description: 'Update a plugin or system settings group. group — the settings group id (e.g. "system", "claude-agent"); fields — only the settings to change (see list_settings). Send real secret values; the masked placeholder (********) is treated as unchanged.',
228
+ inputSchema: { group: z.string(), fields: z.record(z.string(), z.unknown()) },
229
+ scope: 'settings',
230
+ role: 'admin',
231
+ handler: async (args) => {
232
+ try {
233
+ const row = await writePluginSettings(getEm().fork(), args.group, args.fields, actor);
234
+ return ok(row);
235
+ }
236
+ catch (e) {
237
+ if (e instanceof ValidationError) {
238
+ const lines = e.issues.map((i) => {
239
+ const o = i;
240
+ return `- ${o.field ?? '?'}: ${o.code ?? 'invalid'}`;
241
+ });
242
+ return fail(`Validation error:\n${lines.join('\n')}`);
247
243
  }
248
- },
249
- });
250
- }
251
- return out;
244
+ if (e instanceof NotFoundError)
245
+ return fail(`No settings group '${String(args.group)}'.`);
246
+ return fail(`Error: ${e.message}`);
247
+ }
248
+ },
249
+ });
250
+ return opts.role === 'admin' ? out : out.filter((t) => t.role === 'member');
252
251
  }
253
252
  export async function collectPluginInstructions(hooks = pluginHooks, emFactory = () => getEm().fork()) {
254
253
  const out = [];
@@ -296,60 +295,65 @@ export function collectLibraryPurposes(reg, locales) {
296
295
  if (!registry)
297
296
  return [];
298
297
  return registry.libraries
299
- .filter((v) => v.meta.agent)
298
+ .filter((v) => normalizeAgentMeta(v.meta.agent)?.description)
300
299
  .map((v) => ({
301
300
  id: v.meta.id,
302
301
  name: (v.meta.label ? locales?.resolve(v.meta.label) : undefined) ?? v.meta.id,
303
- agent: v.meta.agent,
304
- extends: registry.extends_.flatMap((e) => e.agent
305
- ? e.attachTo
306
- .filter((a) => a.library === v.meta.id)
307
- .map((a) => ({ id: e.id, shelf: a.shelf, agent: e.agent, showWhen: e.showWhen }))
308
- : []),
302
+ agent: normalizeAgentMeta(v.meta.agent).description,
303
+ extends: registry.extends_.flatMap((e) => {
304
+ const meta = normalizeAgentMeta(e.agent);
305
+ return meta?.description
306
+ ? e.attachTo
307
+ .filter((a) => a.library === v.meta.id)
308
+ .map((a) => ({ id: e.id, shelf: a.shelf, agent: meta.description, showWhen: e.showWhen }))
309
+ : [];
310
+ }),
309
311
  }));
310
312
  }
311
313
  export async function siteSection(siteUrl) {
312
314
  const site = await frontendInstructions(siteUrl);
313
315
  return site ? `## web\n${site}` : null;
314
316
  }
315
- export async function buildDomainSections(locales) {
317
+ const DATA_MODEL = '## Data model\n' +
318
+ 'Library (top-level area) → shelf (a kind of record, e.g. things/item) → record (addressed library/shelf/id) → fields. ' +
319
+ 'Some field values are JSON (e.g. quantity {"value":2000,"unit":"ml"}); some are relations (hold another record\'s id); ' +
320
+ "some are collections (nested rows — an array). Extends add extra field-sets to a shelf's records, shown only when a " +
321
+ 'condition holds (the "when …" notes below); in a fetched record they sit under `_extends`. ' +
322
+ 'Read: list_libraries → describe_shelf → list_records/get_record. Write: create_record/update_record (call describe_shelf first); ' +
323
+ 'delete_record moves a record to reversible trash — do not blank fields. Before editing, read the complete record and patch only requested fields. ' +
324
+ 'For duplicates, read both records completely, compare fields/collections/extends/attachments, recommend the less complete record for trash, and offer a field-by-field merge first. ' +
325
+ 'Use list_trash and restore_record for recovery; purge_record is irreversible and requires explicit confirmation. ' +
326
+ 'Each library below names what it holds and when to use it — pick the right one before searching. ' +
327
+ 'A single-record shelf holds exactly one document — read it directly, do not search the shelf.';
328
+ function renderLibrary(v) {
329
+ let s = v.name === v.id ? `### ${v.id} — ${v.agent}` : `### ${v.id} ("${v.name}") — ${v.agent}`;
330
+ if (v.extends.length) {
331
+ s +=
332
+ '\nExtra field-sets some records carry (which one depends on the record):\n' +
333
+ v.extends
334
+ .map((e) => {
335
+ const when = describeCondition(e.showWhen, (f) => f).join(' and ');
336
+ return `- ${e.id} (on ${e.shelf}${when ? `, when ${when}` : ''}): ${e.agent.replace(/\s*\n\s*/g, ' ')}`;
337
+ })
338
+ .join('\n');
339
+ }
340
+ return s;
341
+ }
342
+ export async function buildDomainAreas(locales) {
316
343
  const i18n = locales ?? (await loadComposedLocales());
317
- const libraries = collectLibraryPurposes(undefined, i18n);
318
- let overview = null;
319
- if (libraries.length) {
320
- const blocks = libraries.map((v) => {
321
- let s = v.name === v.id ? `### ${v.id} — ${v.agent}` : `### ${v.id} ("${v.name}") — ${v.agent}`;
322
- if (v.extends.length) {
323
- s +=
324
- '\nExtra field-sets some records carry (which one depends on the record):\n' +
325
- v.extends
326
- .map((e) => {
327
- const when = describeCondition(e.showWhen, (f) => f).join(' and ');
328
- return `- ${e.id} (on ${e.shelf}${when ? `, when ${when}` : ''}): ${e.agent.replace(/\s*\n\s*/g, ' ')}`;
329
- })
330
- .join('\n');
331
- }
332
- return s;
333
- });
334
- overview =
335
- '## Libraries (what each holds / when to use it — pick the right one before searching)\n\n' + blocks.join('\n\n');
344
+ const areas = { root: [DATA_MODEL] };
345
+ for (const v of collectLibraryPurposes(undefined, i18n)) {
346
+ areas[`library:${v.id}`] = [renderLibrary(v)];
347
+ }
348
+ for (const s of collectSingleShelves()) {
349
+ areas[`shelf:${s.library}/${s.shelf}`] = [
350
+ `### ${s.library}/${s.shelf} — single record: read it directly, do not search the shelf. ${s.agent.replace(/\s*\n\s*/g, ' ')}`,
351
+ ];
352
+ }
353
+ for (const { id, instructions } of await collectPluginInstructions()) {
354
+ areas[`plugin:${id}`] = [`## ${id}\n${instructions}`];
336
355
  }
337
- const singles = collectSingleShelves();
338
- const singleSection = singles.length
339
- ? '## Single-record shelves (one document each — read the record, do not search the shelf)\n\n' +
340
- singles.map((s) => `- ${s.library}/${s.shelf}: ${s.agent.replace(/\s*\n\s*/g, ' ')}`).join('\n')
341
- : null;
342
- const dataModel = '## Data model\n' +
343
- 'Library (top-level area) → shelf (a kind of record, e.g. things/item) → record (addressed library/shelf/id) → fields. ' +
344
- 'Some field values are JSON (e.g. quantity {"value":2000,"unit":"ml"}); some are relations (hold another record\'s id); ' +
345
- "some are collections (nested rows — an array). Extends add extra field-sets to a shelf's records, shown only when a " +
346
- 'condition holds (the "when …" notes below); in a fetched record they sit under `_extends`. ' +
347
- 'Read: list_libraries → describe_shelf → list_records/get_record. Write: create_record/update_record (call describe_shelf first); ' +
348
- 'delete_record moves a record to reversible trash — do not blank fields. Before editing, read the complete record and patch only requested fields. ' +
349
- 'For duplicates, read both records completely, compare fields/collections/extends/attachments, recommend the less complete record for trash, and offer a field-by-field merge first. ' +
350
- 'Use list_trash and restore_record for recovery; purge_record is irreversible and requires explicit confirmation.';
351
- const rules = (await collectPluginInstructions()).map(({ id, instructions }) => `## ${id}\n${instructions}`);
352
- return [dataModel, ...(overview ? [overview] : []), ...(singleSection ? [singleSection] : []), ...rules];
356
+ return areas;
353
357
  }
354
358
  export const CORE_STARTER_HINT = 'what this Coffer instance itself holds — which libraries are present and what kinds of ' +
355
359
  'questions the stored data can answer';
@@ -0,0 +1,23 @@
1
+ export interface ImageTarget {
2
+ maxEdge: number;
3
+ maxPixels?: number;
4
+ maxBytes: number;
5
+ encode: readonly string[];
6
+ }
7
+ export interface ImageFacts {
8
+ mime: string;
9
+ width: number;
10
+ height: number;
11
+ bytes: number;
12
+ }
13
+ export interface NormalizedImage {
14
+ bytes: Buffer;
15
+ mime: string;
16
+ width: number;
17
+ height: number;
18
+ source: ImageFacts;
19
+ changed: boolean;
20
+ }
21
+ export declare class ImageNormalizeError extends Error {
22
+ }
23
+ export declare function normalizeImage(bytes: Buffer, target: ImageTarget): Promise<NormalizedImage>;
@@ -0,0 +1,103 @@
1
+ import sharp from 'sharp';
2
+ export class ImageNormalizeError extends Error {
3
+ }
4
+ const MIME_BY_FORMAT = {
5
+ jpeg: 'image/jpeg',
6
+ jpg: 'image/jpeg',
7
+ png: 'image/png',
8
+ webp: 'image/webp',
9
+ gif: 'image/gif',
10
+ avif: 'image/avif',
11
+ tiff: 'image/tiff',
12
+ heif: 'image/heif',
13
+ };
14
+ const QUALITY_LADDER = [85, 72, 58, 44];
15
+ const MAX_ATTEMPTS = 6;
16
+ const QUALITY_CAN_CLOSE = 2;
17
+ function fit(width, height, maxEdge, maxPixels) {
18
+ let scale = Math.min(1, maxEdge / Math.max(width, height));
19
+ if (maxPixels)
20
+ scale = Math.min(scale, Math.sqrt(maxPixels / (width * height)));
21
+ return scaleTo(width, height, scale);
22
+ }
23
+ function scaleTo(width, height, scale) {
24
+ return {
25
+ width: Math.max(1, Math.floor(width * scale)),
26
+ height: Math.max(1, Math.floor(height * scale)),
27
+ };
28
+ }
29
+ function pickEncoding(target, hasAlpha) {
30
+ const accepts = (mime) => target.encode.includes(mime);
31
+ const order = hasAlpha ? ['image/webp', 'image/png', 'image/jpeg'] : ['image/jpeg', 'image/webp', 'image/png'];
32
+ for (const mime of order)
33
+ if (accepts(mime))
34
+ return mime;
35
+ throw new ImageNormalizeError(`the agent takes back none of the encodings this image could be produced in: ${target.encode.join(', ') || '(none declared)'}`);
36
+ }
37
+ function encode(pipeline, mime, quality) {
38
+ switch (mime) {
39
+ case 'image/jpeg':
40
+ return pipeline.jpeg({ quality, mozjpeg: true }).toBuffer();
41
+ case 'image/webp':
42
+ return pipeline.webp({ quality }).toBuffer();
43
+ case 'image/png':
44
+ return pipeline.png({ compressionLevel: 9, palette: true, quality }).toBuffer();
45
+ default:
46
+ throw new ImageNormalizeError(`Unsupported target encoding: ${mime}`);
47
+ }
48
+ }
49
+ export async function normalizeImage(bytes, target) {
50
+ let meta;
51
+ try {
52
+ meta = await sharp(bytes).metadata();
53
+ }
54
+ catch (err) {
55
+ throw new ImageNormalizeError(`Not a decodable image: ${err instanceof Error ? err.message : String(err)}`);
56
+ }
57
+ const format = meta.format ? MIME_BY_FORMAT[meta.format] : undefined;
58
+ const swap = (meta.orientation ?? 0) >= 5;
59
+ const width = swap ? meta.height : meta.width;
60
+ const height = swap ? meta.width : meta.height;
61
+ if (!format || !width || !height)
62
+ throw new ImageNormalizeError('Image has no readable format or dimensions.');
63
+ const source = { mime: format, width, height, bytes: bytes.length };
64
+ const target1 = fit(width, height, target.maxEdge, target.maxPixels);
65
+ const fitsNow = target.encode.includes(format) &&
66
+ bytes.length <= target.maxBytes &&
67
+ target1.width === width &&
68
+ target1.height === height;
69
+ if (fitsNow)
70
+ return { bytes, mime: format, width, height, source, changed: false };
71
+ const mime = pickEncoding(target, meta.hasAlpha === true);
72
+ const flatten = meta.hasAlpha === true && mime === 'image/jpeg';
73
+ let dims = target1;
74
+ let qualityStep = 0;
75
+ let last;
76
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
77
+ let pipeline = sharp(bytes).rotate();
78
+ if (flatten)
79
+ pipeline = pipeline.flatten({ background: '#ffffff' });
80
+ pipeline = pipeline.resize({ ...dims, fit: 'inside', withoutEnlargement: true });
81
+ const quality = QUALITY_LADDER[Math.min(qualityStep, QUALITY_LADDER.length - 1)];
82
+ last = await encode(pipeline, mime, quality);
83
+ if (last.length <= target.maxBytes) {
84
+ const out = await sharp(last).metadata();
85
+ return {
86
+ bytes: last,
87
+ mime,
88
+ width: out.width ?? dims.width,
89
+ height: out.height ?? dims.height,
90
+ source,
91
+ changed: true,
92
+ };
93
+ }
94
+ if (last.length <= target.maxBytes * QUALITY_CAN_CLOSE && qualityStep < QUALITY_LADDER.length - 1) {
95
+ qualityStep += 1;
96
+ }
97
+ else {
98
+ const shrink = Math.sqrt(target.maxBytes / last.length) * 0.9;
99
+ dims = scaleTo(dims.width, dims.height, Math.min(0.9, shrink));
100
+ }
101
+ }
102
+ throw new ImageNormalizeError(`Could not bring the image within ${target.maxBytes} bytes: ${last?.length ?? source.bytes} bytes after ${MAX_ATTEMPTS} attempts.`);
103
+ }
@@ -0,0 +1 @@
1
+ export { normalizeImage, ImageNormalizeError, type ImageTarget, type ImageFacts, type NormalizedImage, } from './image.ts';
@@ -0,0 +1 @@
1
+ export { normalizeImage, ImageNormalizeError, } from "./image.js";
@@ -394,7 +394,7 @@ export function makeTable(em, table, record) {
394
394
  export async function renameSystemShelfKey(em) {
395
395
  await makeTable(em, '_embeddings').renameColumn('type', 'shelf_key');
396
396
  }
397
- const LEGACY_ORCHESTRATOR_FIELDS = ['agent_id', 'access_password', 'trigger_prefix', 'reply_window'];
397
+ const LEGACY_ORCHESTRATOR_FIELDS = ['agent_id', 'trigger_prefix', 'reply_window'];
398
398
  export const SYSTEM_MIGRATIONS = [
399
399
  {
400
400
  name: 'embeddings-shelf-key',
@@ -1,2 +1,2 @@
1
- import type { AgentToolProvider, AttachmentRef } from './types.ts';
2
- export declare function makeAttachmentCapabilities(attachments: AttachmentRef[]): Promise<AgentToolProvider>;
1
+ import type { AgentMediaLimits, AgentToolProvider, AttachmentRef } from './types.ts';
2
+ export declare function makeAttachmentCapabilities(attachments: AttachmentRef[], limits: AgentMediaLimits): Promise<AgentToolProvider>;
@@ -57,8 +57,8 @@ function fileItems(value) {
57
57
  function findTool(defs, name) {
58
58
  return defs.find((def) => def.server === 'coffer' && def.bareName === name);
59
59
  }
60
- export async function makeAttachmentCapabilities(attachments) {
61
- const defs = await collectMcpTools();
60
+ export async function makeAttachmentCapabilities(attachments, limits) {
61
+ const defs = await collectMcpTools({ role: 'member' });
62
62
  const getRecord = findTool(defs, 'get_record');
63
63
  const updateRecord = findTool(defs, 'update_record');
64
64
  const tools = [];
@@ -115,7 +115,7 @@ export async function makeAttachmentCapabilities(attachments) {
115
115
  return inspectUpload(item['name'], {
116
116
  ...(typeof item['mime'] === 'string' ? { mime: item['mime'] } : {}),
117
117
  ...(typeof item['size'] === 'number' ? { size: item['size'] } : {}),
118
- });
118
+ }, limits);
119
119
  },
120
120
  });
121
121
  }
@@ -1,16 +1 @@
1
- import type { GatePolicy } from './types.ts';
2
- export declare const stateDir: () => string;
3
- export declare function carryLegacyState(): void;
4
- export declare function allowFileFor(connectorId: string): string;
5
- export interface AllowState {
6
- ids: Record<string, number>;
7
- }
8
- export declare function emptyAllowed(): AllowState;
9
- export declare function loadAllowed(file: string): AllowState;
10
- export declare function saveAllowed(file: string, state: AllowState): void;
11
- export declare function isAllowed(state: AllowState, id: string | number | null | undefined): boolean;
12
- export declare function addAllowed(state: AllowState, id: string | number, now: number): AllowState;
13
- export declare function isSenderAllowed(connectorId: string, senderId: string, deps?: {
14
- policy?: GatePolicy;
15
- }): Promise<boolean>;
16
- export declare function makeThrottle(maxAttempts?: number, windowMs?: number): (id: string | number, now?: number) => boolean;
1
+ export declare function makeThrottle(maxNotices?: number, windowMs?: number): (id: string | number, now?: number) => boolean;
@@ -1,63 +1,13 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { loadGatePolicy } from "./config.js";
4
- import { carryInto, dataDir } from "../data-dir.js";
5
- export const stateDir = () => process.env['ORCHESTRATOR_STATE_DIR'] ?? path.join(dataDir(), 'state');
6
- function legacyStateDirs() {
7
- return [
8
- path.join(process.cwd(), 'packages', 'plugin-orchestrator', 'runtime', 'state'),
9
- path.join(process.cwd(), 'node_modules', '@coffer-org', 'plugin-orchestrator', 'runtime', 'state'),
10
- ];
11
- }
12
- export function carryLegacyState() {
13
- carryInto(stateDir(), ...legacyStateDirs());
14
- }
15
- export function allowFileFor(connectorId) {
16
- return path.join(stateDir(), `${connectorId}.allowed.json`);
17
- }
18
- export function emptyAllowed() {
19
- return { ids: {} };
20
- }
21
- export function loadAllowed(file) {
22
- try {
23
- const obj = JSON.parse(fs.readFileSync(file, 'utf-8'));
24
- if (obj && typeof obj === 'object' && 'ids' in obj && typeof obj.ids === 'object') {
25
- return obj;
26
- }
27
- return emptyAllowed();
28
- }
29
- catch {
30
- return emptyAllowed();
31
- }
32
- }
33
- export function saveAllowed(file, state) {
34
- fs.mkdirSync(path.dirname(file), { recursive: true });
35
- const tmp = `${file}.${process.pid}.tmp`;
36
- fs.writeFileSync(tmp, JSON.stringify(state), 'utf-8');
37
- fs.renameSync(tmp, file);
38
- }
39
- export function isAllowed(state, id) {
40
- return id != null && Object.prototype.hasOwnProperty.call(state.ids, String(id));
41
- }
42
- export function addAllowed(state, id, now) {
43
- return { ids: { ...state.ids, [String(id)]: now } };
44
- }
45
- export async function isSenderAllowed(connectorId, senderId, deps) {
46
- const policy = deps?.policy ?? (await loadGatePolicy());
47
- if (!policy.accessPassword)
48
- return true;
49
- return isAllowed(loadAllowed(allowFileFor(connectorId)), senderId);
50
- }
51
- export function makeThrottle(maxAttempts = 5, windowMs = 60_000) {
1
+ export function makeThrottle(maxNotices = 5, windowMs = 60_000) {
52
2
  const hits = new Map();
53
- return function allowAttempt(id, now = Date.now()) {
3
+ return function allowNotice(id, now = Date.now()) {
54
4
  const key = String(id);
55
5
  const rec = hits.get(key);
56
6
  if (!rec || now - rec.start >= windowMs) {
57
7
  hits.set(key, { start: now, count: 1 });
58
8
  return true;
59
9
  }
60
- if (rec.count >= maxAttempts)
10
+ if (rec.count >= maxNotices)
61
11
  return false;
62
12
  rec.count += 1;
63
13
  return true;
@@ -3,7 +3,6 @@ export function buildPolicy(dbSettings = {}) {
3
3
  const db = dbSettings;
4
4
  return {
5
5
  ...(typeof db.agent_id === 'string' && db.agent_id ? { agentId: db.agent_id } : {}),
6
- accessPassword: db.access_password ?? '',
7
6
  triggerPrefix: db.trigger_prefix ?? '',
8
7
  replyWindow: Number(db.reply_window ?? 1800) || 1800,
9
8
  };
@@ -0,0 +1,27 @@
1
+ import type { AuthRole } from '../plugin-hooks.ts';
2
+ export interface ContextFact {
3
+ name: string;
4
+ value: string;
5
+ attrs?: Record<string, string>;
6
+ }
7
+ export declare const CONNECTOR_FACT_NAME: RegExp;
8
+ export declare const CONTEXT_GAP_MS: number;
9
+ export declare const MAX_SPEAKER_NAME = 80;
10
+ export declare const MAX_FACT_VALUE = 200;
11
+ export declare const MAX_CONNECTOR_FACTS = 12;
12
+ export declare function oneLine(value: string, maxLen?: number): string;
13
+ export interface ContextSpeaker {
14
+ id: string;
15
+ name: string;
16
+ role: AuthRole;
17
+ }
18
+ export interface ContextInput {
19
+ connectorFacts: readonly ContextFact[];
20
+ speaker: ContextSpeaker;
21
+ now: Date;
22
+ timeZone: string;
23
+ previous: readonly ContextFact[] | null;
24
+ previousAt: Date | null;
25
+ }
26
+ export declare function humanizeGap(ms: number): string;
27
+ export declare function buildContextFacts(input: ContextInput): ContextFact[];
@@ -0,0 +1,89 @@
1
+ import { currentMoment } from "./environment.js";
2
+ export const CONNECTOR_FACT_NAME = /^[a-z][a-z0-9-]*$/;
3
+ export const CONTEXT_GAP_MS = 60 * 60_000;
4
+ export const MAX_SPEAKER_NAME = 80;
5
+ export const MAX_FACT_VALUE = 200;
6
+ export const MAX_CONNECTOR_FACTS = 12;
7
+ const FORGERY_CHARS = /[\p{Cc}\p{Zl}\p{Zp}\p{Bidi_Control}]+/gu;
8
+ export function oneLine(value, maxLen) {
9
+ const flat = value.replace(FORGERY_CHARS, ' ').replace(/\s+/g, ' ').trim();
10
+ return maxLen !== undefined && flat.length > maxLen ? `${flat.slice(0, maxLen - 1).trimEnd()}…` : flat;
11
+ }
12
+ const ORCHESTRATOR_FACT_NAMES = new Set(['at', 'gap', 'speaker', 'cleared']);
13
+ function sameFact(a, b) {
14
+ if (a.value !== b.value)
15
+ return false;
16
+ const ka = Object.keys(a.attrs ?? {}).sort();
17
+ const kb = Object.keys(b.attrs ?? {}).sort();
18
+ if (ka.length !== kb.length || ka.some((k, i) => k !== kb[i]))
19
+ return false;
20
+ return ka.every((k) => a.attrs[k] === b.attrs[k]);
21
+ }
22
+ function dayAndZone(atValue) {
23
+ const [date, , ...zone] = atValue.split(' ');
24
+ return `${date} ${zone.join(' ')}`;
25
+ }
26
+ export function humanizeGap(ms) {
27
+ const minutes = Math.round(ms / 60_000);
28
+ if (minutes < 90)
29
+ return `about ${minutes} minutes`;
30
+ const hours = Math.round(ms / 3_600_000);
31
+ if (hours < 48)
32
+ return `about ${hours} hours`;
33
+ return `about ${Math.round(ms / 86_400_000)} days`;
34
+ }
35
+ export function buildContextFacts(input) {
36
+ const prev = input.previous;
37
+ const stated = (name) => prev?.find((f) => f.name === name);
38
+ const moment = currentMoment(input.now, input.timeZone);
39
+ const at = { name: 'at', value: `${moment} ${input.timeZone}` };
40
+ const speakerName = oneLine(input.speaker.name, MAX_SPEAKER_NAME) || 'unknown';
41
+ const speaker = {
42
+ name: 'speaker',
43
+ value: speakerName,
44
+ attrs: { id: oneLine(input.speaker.id, MAX_SPEAKER_NAME), role: input.speaker.role },
45
+ };
46
+ const facts = [];
47
+ const gapMs = input.previousAt ? input.now.getTime() - input.previousAt.getTime() : null;
48
+ const prevAt = stated('at');
49
+ const dayChanged = prevAt !== undefined && dayAndZone(prevAt.value) !== dayAndZone(at.value);
50
+ const stale = gapMs !== null && gapMs >= CONTEXT_GAP_MS;
51
+ if (!prevAt || dayChanged || stale) {
52
+ facts.push(at);
53
+ if (stale && gapMs !== null)
54
+ facts.push({ name: 'gap', value: humanizeGap(gapMs) });
55
+ }
56
+ const prevSpeaker = stated('speaker');
57
+ if (!prevSpeaker || !sameFact(prevSpeaker, speaker))
58
+ facts.push(speaker);
59
+ const validConnectorFacts = input.connectorFacts
60
+ .filter((f) => typeof f?.name === 'string' &&
61
+ CONNECTOR_FACT_NAME.test(f.name) &&
62
+ !ORCHESTRATOR_FACT_NAMES.has(f.name) &&
63
+ typeof f.value === 'string' &&
64
+ Object.entries(f.attrs ?? {}).every(([k, v]) => CONNECTOR_FACT_NAME.test(k) && typeof v === 'string'))
65
+ .slice(0, MAX_CONNECTOR_FACTS);
66
+ const currentConnectorNames = new Set(validConnectorFacts.map((f) => f.name));
67
+ for (const raw of validConnectorFacts) {
68
+ const fact = {
69
+ name: raw.name,
70
+ value: oneLine(raw.value, MAX_FACT_VALUE),
71
+ ...(raw.attrs
72
+ ? { attrs: Object.fromEntries(Object.entries(raw.attrs).map(([k, v]) => [k, oneLine(v, MAX_FACT_VALUE)])) }
73
+ : {}),
74
+ };
75
+ const before = stated(fact.name);
76
+ if (!before || !sameFact(before, fact))
77
+ facts.push(fact);
78
+ }
79
+ if (prev) {
80
+ for (const priorFact of prev) {
81
+ if (ORCHESTRATOR_FACT_NAMES.has(priorFact.name))
82
+ continue;
83
+ if (currentConnectorNames.has(priorFact.name))
84
+ continue;
85
+ facts.push({ name: 'cleared', value: oneLine(priorFact.name) });
86
+ }
87
+ }
88
+ return facts;
89
+ }
@@ -0,0 +1,9 @@
1
+ export interface ConversationAccess {
2
+ owner: string | null;
3
+ visibility: 'private' | null;
4
+ }
5
+ export declare function mayRead(access: ConversationAccess, viewerId: string): boolean;
6
+ export declare function mayWrite(access: ConversationAccess, viewerId: string): boolean;
7
+ export declare function mayBePrivate(capabilities: {
8
+ privateChats: boolean;
9
+ }): boolean;
@@ -0,0 +1,12 @@
1
+ function isOwner(access, viewerId) {
2
+ return access.owner !== null && access.owner === viewerId;
3
+ }
4
+ export function mayRead(access, viewerId) {
5
+ return access.visibility !== 'private' || isOwner(access, viewerId);
6
+ }
7
+ export function mayWrite(access, viewerId) {
8
+ return access.visibility !== 'private' || isOwner(access, viewerId);
9
+ }
10
+ export function mayBePrivate(capabilities) {
11
+ return capabilities.privateChats;
12
+ }
@@ -1,2 +1,3 @@
1
1
  export declare function systemTimeZone(): string;
2
2
  export declare function todayDateString(now?: Date, timeZone?: string): string;
3
+ export declare function currentMoment(now?: Date, timeZone?: string): string;