@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
@@ -7,6 +7,10 @@ declare module 'fastify' {
7
7
  }
8
8
  export declare const PUBLIC_API_PATHS: string[];
9
9
  export declare function startPasswordSession(reply: FastifyReply, login: string, password: string): Promise<AuthUser | null>;
10
+ export declare function parseBasicCredential(header: string | undefined): {
11
+ login: string;
12
+ secret: string;
13
+ } | null;
10
14
  export declare function resolveRequestUser(req: FastifyRequest): Promise<AuthUser | null>;
11
15
  export declare function requireAdmin(req: FastifyRequest, reply: FastifyReply): boolean;
12
16
  export declare function registerAuthApi(app: FastifyInstance): Promise<void>;
package/dist/auth-api.js CHANGED
@@ -2,6 +2,8 @@ import { field } from '@coffer-org/sdk/fields';
2
2
  import { materializeTree } from '@coffer-org/sdk/materialize/pipeline';
3
3
  import { countUsers, createUser, findUserByLogin, findUserById, getPasswordHash, listUsers, updateUser, deleteUser, countAdmins, createSession, resolveSession, deleteSession, createApiToken, resolveApiToken, listApiTokens, revokeApiToken, } from "./auth-store.js";
4
4
  import { resolveAccessToken } from "./oauth-store.js";
5
+ import { listLinks, unlink, mintLinkCode } from "./identity-link.js";
6
+ import { listLinkableConnectors, connectorLinkUrl } from "./orchestrator/index.js";
5
7
  import { mcpResource } from "./public-url.js";
6
8
  import { verifyPassword } from "./auth-crypto.js";
7
9
  import { maskSecrets, preserveSecrets } from "./field-masking.js";
@@ -11,6 +13,7 @@ import { USERS_SHELF } from '@coffer-org/sdk/users-shelf';
11
13
  const PASSWORD_FIELDS = materializeTree({ password: field.password({}) });
12
14
  const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
13
15
  const COOKIE_NAME = 'sid';
16
+ const LINK_CODE_TTL_MS = 10 * 60 * 1000;
14
17
  export const PUBLIC_API_PATHS = ['/api/auth/status', '/api/auth/login', '/api/auth/setup'];
15
18
  function readCookie(req, name) {
16
19
  const header = req.headers.cookie;
@@ -37,12 +40,32 @@ export async function startPasswordSession(reply, login, password) {
37
40
  setCookie(reply, raw, SESSION_TTL_MS / 1000);
38
41
  return row;
39
42
  }
43
+ export function parseBasicCredential(header) {
44
+ if (!header?.startsWith('Basic '))
45
+ return null;
46
+ let decoded;
47
+ try {
48
+ decoded = Buffer.from(header.slice('Basic '.length).trim(), 'base64').toString('utf8');
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ const at = decoded.indexOf(':');
54
+ if (at <= 0)
55
+ return null;
56
+ return { login: decoded.slice(0, at), secret: decoded.slice(at + 1) };
57
+ }
40
58
  export async function resolveRequestUser(req) {
41
59
  const auth = req.headers.authorization;
42
60
  if (auth?.startsWith('Bearer ')) {
43
61
  const raw = auth.slice('Bearer '.length);
44
62
  return (await resolveApiToken(raw)) ?? (await resolveAccessToken(raw, await mcpResource(req)));
45
63
  }
64
+ const basic = parseBasicCredential(auth);
65
+ if (basic) {
66
+ const user = await resolveApiToken(basic.secret);
67
+ return user && user.login === basic.login ? user : null;
68
+ }
46
69
  const sid = readCookie(req, COOKIE_NAME);
47
70
  if (!sid)
48
71
  return null;
@@ -208,4 +231,34 @@ export async function registerAuthApi(app) {
208
231
  return reply.code(404).send({ error: 'not_found' });
209
232
  return { ok: true };
210
233
  });
234
+ app.get('/api/auth/links', async (req, reply) => {
235
+ if (!req.user)
236
+ return reply.code(401).send({ error: 'unauthorized' });
237
+ return { links: await listLinks(req.user.id), linkable: listLinkableConnectors() };
238
+ });
239
+ app.post('/api/auth/links', async (req, reply) => {
240
+ if (!req.user)
241
+ return reply.code(401).send({ error: 'unauthorized' });
242
+ const minted = await mintLinkCode(req.user.id, LINK_CODE_TTL_MS);
243
+ const links = {};
244
+ for (const connectorId of listLinkableConnectors()) {
245
+ const url = connectorLinkUrl(connectorId, minted.raw);
246
+ if (url)
247
+ links[connectorId] = url;
248
+ }
249
+ return { ...minted, links };
250
+ });
251
+ app.delete('/api/auth/links/:connector/:externalId', async (req, reply) => {
252
+ if (!req.user)
253
+ return reply.code(401).send({ error: 'unauthorized' });
254
+ const { connector, externalId } = req.params;
255
+ const own = await listLinks(req.user.id);
256
+ const owns = own.some((l) => l.connector === connector && l.externalId === externalId);
257
+ if (!owns)
258
+ return reply.code(404).send({ error: 'not_found' });
259
+ const ok = await unlink(connector, externalId);
260
+ if (!ok)
261
+ return reply.code(404).send({ error: 'not_found' });
262
+ return { ok: true };
263
+ });
211
264
  }
@@ -1,3 +1,5 @@
1
+ import { generateToken, hashToken } from './auth-crypto.ts';
2
+ export { generateToken, hashToken };
1
3
  export interface AuthUser {
2
4
  id: number;
3
5
  login: string;
@@ -1,5 +1,6 @@
1
1
  import { getEm } from "./db.js";
2
2
  import { hashPassword, generateToken, hashToken } from "./auth-crypto.js";
3
+ export { generateToken, hashToken };
3
4
  function toAuthUser(row) {
4
5
  return {
5
6
  id: row.id,
@@ -17,6 +17,7 @@ export interface BackgroundScheduler {
17
17
  start(): void;
18
18
  stop(): void;
19
19
  }
20
+ export declare const DEFAULT_TASK_TIMEOUT_MS = 600000;
20
21
  export declare function makeScheduler(opts?: SchedulerOpts): BackgroundScheduler;
21
22
  export declare function startScheduler(tasks: BackgroundTask[], opts?: SchedulerOpts): void;
22
23
  export declare function stopScheduler(): void;
@@ -6,10 +6,11 @@ const defaultSetTimer = (cb, ms) => {
6
6
  t.unref();
7
7
  return t;
8
8
  };
9
+ export const DEFAULT_TASK_TIMEOUT_MS = 600_000;
9
10
  export function makeScheduler(opts = {}) {
10
11
  const startupDelayMs = opts.startupDelayMs ?? (Number(process.env['BG_STARTUP_DELAY_MS']) || 600_000);
11
12
  const gapMs = opts.gapMs ?? (Number(process.env['BG_GAP_MS']) || 30_000);
12
- const defaultTimeoutMs = opts.taskTimeoutMs ?? (Number(process.env['BG_TASK_TIMEOUT_MS']) || 600_000);
13
+ const defaultTimeoutMs = opts.taskTimeoutMs ?? (Number(process.env['BG_TASK_TIMEOUT_MS']) || DEFAULT_TASK_TIMEOUT_MS);
13
14
  const setTimer = opts.setTimer ?? defaultSetTimer;
14
15
  const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
15
16
  const tasks = [];
@@ -25,6 +25,8 @@ export declare const MsgLogSchema: EntitySchema<any, never, import("@mikro-orm/c
25
25
  export declare const UserSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
26
26
  export declare const SessionSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
27
27
  export declare const ApiTokenSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
28
+ export declare const IdentityLinkSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
29
+ export declare const LinkCodeSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
28
30
  export declare const OAuthClientSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
29
31
  export declare const OAuthCodeSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
30
32
  export declare const OAuthTokenSchema: EntitySchema<any, never, import("@mikro-orm/core").EntityCtor<any>>;
@@ -209,6 +209,9 @@ export const ThreadStateSchema = new EntitySchema({
209
209
  chat_id: { type: 'text', primary: true },
210
210
  agent_id: { type: 'text', nullable: true },
211
211
  preset_id: { type: 'text', nullable: true },
212
+ title: { type: 'text', nullable: true },
213
+ owner: { type: 'text', nullable: true },
214
+ visibility: { type: 'text', nullable: true },
212
215
  updated_at: { type: 'text' },
213
216
  },
214
217
  });
@@ -274,6 +277,27 @@ export const ApiTokenSchema = new EntitySchema({
274
277
  revoked: { type: 'integer' },
275
278
  },
276
279
  });
280
+ export const IdentityLinkSchema = new EntitySchema({
281
+ name: '_IdentityLink',
282
+ tableName: '_identity_links',
283
+ properties: {
284
+ id: { type: 'integer', primary: true, autoincrement: true },
285
+ connector: { type: 'text' },
286
+ external_id: { type: 'text' },
287
+ user_id: { type: 'integer' },
288
+ linked_at: { type: 'text' },
289
+ },
290
+ });
291
+ export const LinkCodeSchema = new EntitySchema({
292
+ name: '_LinkCode',
293
+ tableName: '_link_codes',
294
+ properties: {
295
+ code_hash: { type: 'text', primary: true },
296
+ user_id: { type: 'integer' },
297
+ expires_at: { type: 'text' },
298
+ created_at: { type: 'text' },
299
+ },
300
+ });
277
301
  export const OAuthClientSchema = new EntitySchema({
278
302
  name: '_OAuthClient',
279
303
  tableName: '_oauth_clients',
@@ -331,5 +355,7 @@ export const systemEntities = [
331
355
  OAuthTokenSchema,
332
356
  ThreadMessageSchema,
333
357
  ThreadStateSchema,
358
+ IdentityLinkSchema,
359
+ LinkCodeSchema,
334
360
  buildSettingsEntitySchema(SYSTEM_SETTINGS_ID, SYSTEM_SETTINGS),
335
361
  ];
@@ -0,0 +1,15 @@
1
+ export interface IdentityLink {
2
+ connector: string;
3
+ externalId: string;
4
+ userId: number;
5
+ linkedAt: string;
6
+ }
7
+ export declare function findLinkedUser(connector: string, externalId: string): Promise<number | null>;
8
+ export declare function listLinks(userId: number): Promise<IdentityLink[]>;
9
+ export declare function unlink(connector: string, externalId: string): Promise<boolean>;
10
+ export declare function mintLinkCode(userId: number, ttlMs: number): Promise<{
11
+ raw: string;
12
+ expiresAt: string;
13
+ }>;
14
+ export declare function redeemLinkCode(raw: string, connector: string, externalId: string): Promise<number | null>;
15
+ export declare function pruneLinkCodes(): Promise<void>;
@@ -0,0 +1,76 @@
1
+ import { getEm } from "./db.js";
2
+ import { generateToken, hashToken, findUserById } from "./auth-store.js";
3
+ export async function findLinkedUser(connector, externalId) {
4
+ const em = getEm().fork();
5
+ const row = (await em.findOne('_IdentityLink', {
6
+ connector,
7
+ external_id: externalId,
8
+ }));
9
+ if (!row)
10
+ return null;
11
+ const user = await findUserById(row.user_id);
12
+ return user && !user.disabled ? row.user_id : null;
13
+ }
14
+ export async function listLinks(userId) {
15
+ const em = getEm().fork();
16
+ const rows = (await em.find('_IdentityLink', { user_id: userId }));
17
+ return rows.map((r) => ({
18
+ connector: r.connector,
19
+ externalId: r.external_id,
20
+ userId: r.user_id,
21
+ linkedAt: r.linked_at,
22
+ }));
23
+ }
24
+ export async function unlink(connector, externalId) {
25
+ const em = getEm().fork();
26
+ const deleted = await em.nativeDelete('_IdentityLink', { connector, external_id: externalId });
27
+ return deleted > 0;
28
+ }
29
+ export async function mintLinkCode(userId, ttlMs) {
30
+ const em = getEm().fork();
31
+ const raw = generateToken();
32
+ const expiresAt = new Date(Date.now() + ttlMs).toISOString();
33
+ em.create('_LinkCode', {
34
+ code_hash: hashToken(raw),
35
+ user_id: userId,
36
+ expires_at: expiresAt,
37
+ created_at: new Date().toISOString(),
38
+ });
39
+ await em.flush();
40
+ return { raw, expiresAt };
41
+ }
42
+ async function setLink(em, connector, externalId, userId) {
43
+ const existing = (await em.findOne('_IdentityLink', {
44
+ connector,
45
+ external_id: externalId,
46
+ }));
47
+ const linkedAt = new Date().toISOString();
48
+ if (existing) {
49
+ em.assign(existing, { user_id: userId, linked_at: linkedAt });
50
+ }
51
+ else {
52
+ em.create('_IdentityLink', { connector, external_id: externalId, user_id: userId, linked_at: linkedAt });
53
+ }
54
+ await em.flush();
55
+ }
56
+ export async function redeemLinkCode(raw, connector, externalId) {
57
+ const em = getEm().fork();
58
+ const hash = hashToken(raw);
59
+ const row = (await em.findOne('_LinkCode', { code_hash: hash }));
60
+ if (!row)
61
+ return null;
62
+ const deleted = await em.nativeDelete('_LinkCode', { code_hash: hash });
63
+ if (deleted === 0)
64
+ return null;
65
+ if (row.expires_at < new Date().toISOString())
66
+ return null;
67
+ const owner = await findUserById(row.user_id);
68
+ if (!owner || owner.disabled)
69
+ return null;
70
+ await setLink(em, connector, externalId, row.user_id);
71
+ return row.user_id;
72
+ }
73
+ export async function pruneLinkCodes() {
74
+ const em = getEm().fork();
75
+ await em.nativeDelete('_LinkCode', { expires_at: { $lt: new Date().toISOString() } });
76
+ }
package/dist/index.js CHANGED
@@ -15,10 +15,12 @@ import { NotFoundError } from "./mutate.js";
15
15
  import { httpErrorFor, registerErrorHandler } from "./http-errors.js";
16
16
  import { resolveEmbed } from "./embed.js";
17
17
  import { resolveWithinHome, filterSort, listDir } from "./fs-list.js";
18
- import { initPlugins, teardownPlugins, readDisabled, purgePluginData, getPluginSettings, getPlugins, } from "./plugin-runtime.js";
18
+ import { initPlugins, teardownPlugins, readDisabled, getDisabled, purgePluginData, getPluginSettings, getPlugins, } from "./plugin-runtime.js";
19
19
  import { registerPluginsApi } from "./plugins-api.js";
20
20
  import { orchestratorDiagnostics } from "./orchestrator/index.js";
21
+ import { pluginHooks } from "./plugin-hooks.js";
21
22
  import { registerPluginUserApi, registerPluginAdminApi } from "./plugin-user-api.js";
23
+ import { collectHttpMounts, registerPluginHttpMounts, registerTextBodyParsers } from "./plugin-http-mounts.js";
22
24
  import { registerAuthApi, resolveRequestUser, requireAdmin, PUBLIC_API_PATHS } from "./auth-api.js";
23
25
  import { registerMcpHttp } from "./mcp-http.js";
24
26
  import { registerOAuthApi } from "./oauth-api.js";
@@ -71,6 +73,7 @@ app.addHook('onResponse', async (req, reply) => {
71
73
  });
72
74
  await app.register(cors, { origin: true });
73
75
  await app.register(multipart, { limits: { fileSize: 25 * 1024 * 1024 } });
76
+ registerTextBodyParsers(app);
74
77
  await app.register(fastifyStatic, { root: UPLOADS, prefix: '/uploads/' });
75
78
  app.addHook('onSend', async (req, reply, payload) => {
76
79
  if (req.url.startsWith('/uploads/')) {
@@ -327,6 +330,10 @@ app.put('/api/settings/:id', (req, reply) => {
327
330
  });
328
331
  registerPluginAdminApi(app, requireAdmin);
329
332
  registerPluginUserApi(app);
333
+ const httpMounts = collectHttpMounts(pluginHooks);
334
+ registerPluginHttpMounts(app, httpMounts, { disabledSet: getDisabled, resolveUser: resolveRequestUser });
335
+ if (httpMounts.length)
336
+ app.log.info(`plugin http mounts: ${httpMounts.map((m) => `${m.pluginId}${m.mount.prefix}`).join(', ')}`);
330
337
  app.get('/api/fs/list', async (req, reply) => {
331
338
  const home = homedir();
332
339
  const q = req.query;
@@ -1,4 +1,30 @@
1
1
  import type { LocaleResolver } from '../locale-registry.ts';
2
+ import { type AgentMeta } from '@coffer-org/sdk/library';
3
+ import type { Condition } from '@coffer-org/sdk/condition';
4
+ interface ShelfClient {
5
+ shelf: string;
6
+ library: string;
7
+ label: string;
8
+ agent?: string;
9
+ standalone?: boolean;
10
+ fields: unknown[];
11
+ }
12
+ interface ExtendClient {
13
+ id: string;
14
+ attachTo: {
15
+ library: string;
16
+ shelf?: string;
17
+ }[];
18
+ showWhen?: Condition;
19
+ agent?: AgentMeta | string;
20
+ }
21
+ interface LibraryClient {
22
+ id: string;
23
+ label: string;
24
+ agent?: AgentMeta | string;
25
+ extends?: ExtendClient[];
26
+ shelves: ShelfClient[];
27
+ }
2
28
  export interface ShelfIndexEntry {
3
29
  library: string;
4
30
  libraryLabel: string;
@@ -25,13 +51,20 @@ export declare const FILE_WRITE_HINT: string;
25
51
  export declare const SOURCE_WRITE_HINT: string;
26
52
  export declare const DERIVED_WRITE_HINT: string;
27
53
  export declare const serverOwnedPartsHint: (roles: string[]) => string;
54
+ export interface OperatingRule {
55
+ source: string;
56
+ when?: string;
57
+ text: string;
58
+ }
28
59
  export interface ShelfDescription {
29
60
  library: string;
30
61
  shelf: string;
31
62
  label: string;
32
63
  agent?: string;
64
+ operatingRules?: OperatingRule[];
33
65
  fields: FieldInfo[];
34
66
  }
67
+ export declare function collectOperatingRules(lib: LibraryClient, shelf: string): OperatingRule[];
35
68
  interface SchemaClient {
36
69
  getSchema(): Promise<unknown>;
37
70
  }
@@ -46,6 +79,6 @@ export declare class SchemaCache {
46
79
  private load;
47
80
  index(): Promise<ShelfIndexEntry[]>;
48
81
  describeShelf(library: string, shelf: string): Promise<ShelfDescription>;
49
- private find;
82
+ private locate;
50
83
  }
51
84
  export {};
@@ -1,3 +1,6 @@
1
+ import { normalizeAgentMeta } from '@coffer-org/sdk/library';
2
+ import { extendMatches } from '@coffer-org/sdk/extend';
3
+ import { describeCondition } from '@coffer-org/sdk/condition';
1
4
  export const FILE_WRITE_HINT = 'Uploaded file only: {"name":"<filename returned by POST /api/upload>"} ' +
2
5
  '(or an array of those when the field is multiple). ' +
3
6
  'A remote URL is rejected — the server stores files, not links. ' +
@@ -13,6 +16,22 @@ export const DERIVED_WRITE_HINT = 'Read-only — the server recomputes this fiel
13
16
  export const serverOwnedPartsHint = (roles) => `Partly read-only — the server owns ${roles.map((r) => `\`${r}\``).join(', ')} in this value and ` +
14
17
  `computes ${roles.length > 1 ? 'them' : 'it'} on every write. Send the other roles only; anything ` +
15
18
  `sent for these is discarded, and a read returns the server's value.`;
19
+ export function collectOperatingRules(lib, shelf) {
20
+ const out = [];
21
+ const libAgent = normalizeAgentMeta(lib.agent);
22
+ if (libAgent?.instructions)
23
+ out.push({ source: lib.id, text: libAgent.instructions });
24
+ for (const e of lib.extends ?? []) {
25
+ if (!extendMatches(e, lib.id, shelf))
26
+ continue;
27
+ const meta = normalizeAgentMeta(e.agent);
28
+ if (!meta?.instructions)
29
+ continue;
30
+ const when = describeCondition(e.showWhen, (f) => f).join(' and ');
31
+ out.push({ source: e.id, ...(when ? { when } : {}), text: meta.instructions });
32
+ }
33
+ return out;
34
+ }
16
35
  function serverOwnedRoles(t) {
17
36
  const parts = t.parts ?? [];
18
37
  return parts.filter((p) => p.mode === 'computedStored').map((p) => p.role);
@@ -89,20 +108,24 @@ export class SchemaCache {
89
108
  })));
90
109
  }
91
110
  async describeShelf(library, shelf) {
92
- let m = this.find(await this.load(), library, shelf);
93
- if (!m)
94
- m = this.find(await this.load(true), library, shelf);
95
- if (!m)
111
+ let at = this.locate(await this.load(), library, shelf);
112
+ if (!at)
113
+ at = this.locate(await this.load(true), library, shelf);
114
+ if (!at)
96
115
  throw new Error(`unknown_shelf ${library}/${shelf}`);
116
+ const rules = collectOperatingRules(at.lib, shelf);
97
117
  return {
98
118
  library,
99
119
  shelf,
100
- label: this.display(m.label, m.shelf),
101
- ...(m.agent ? { agent: m.agent } : {}),
102
- fields: flattenFields(m.fields),
120
+ label: this.display(at.m.label, at.m.shelf),
121
+ ...(at.m.agent ? { agent: at.m.agent } : {}),
122
+ ...(rules.length ? { operatingRules: rules } : {}),
123
+ fields: flattenFields(at.m.fields),
103
124
  };
104
125
  }
105
- find(libraries, library, shelf) {
106
- return libraries.find((v) => v.id === library)?.shelves.find((m) => m.shelf === shelf);
126
+ locate(libraries, library, shelf) {
127
+ const lib = libraries.find((v) => v.id === library);
128
+ const m = lib?.shelves.find((s) => s.shelf === shelf);
129
+ return lib && m ? { lib, m } : undefined;
107
130
  }
108
131
  }
@@ -53,7 +53,9 @@ export function buildTools(client, cache, locales) {
53
53
  },
54
54
  {
55
55
  name: 'describe_shelf',
56
- description: 'Detailed shelf schema: the shelf note (agent), then fields with kind/prim/required, relation/options, and ' +
56
+ description: 'Detailed shelf schema: the shelf note (agent), the operating rules that govern writing here ' +
57
+ '(operatingRules — from the library and from any extra field-set attached to this shelf, each ' +
58
+ 'with the condition it applies under), then fields with kind/prim/required, relation/options, and ' +
57
59
  'a per-field note (agent) saying what that field means. A collection is one entry with multiple:true and its ' +
58
60
  'row shape in fields[]. A field with derived:true is computed by the server and rejects writes — omit it when ' +
59
61
  'writing. Call BEFORE create_record/update_record.',
package/dist/mcp-http.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
- import { collectMcpTools, resolveRagDeps, buildDomainSections, buildMcpInstructions, siteSection, } from "./mcp-tools.js";
3
+ import { collectMcpTools, resolveRagDeps, buildDomainAreas, buildMcpInstructions, siteSection } from "./mcp-tools.js";
4
+ import { assembleSystem, ALL_PRESENT } from "./orchestrator/system-areas.js";
4
5
  import { configuredPublicUrl } from "./public-url.js";
5
6
  import { getLogger } from "./log.js";
6
7
  const log = getLogger('mcp-http');
7
8
  export async function buildMcpServer(role, actor) {
8
9
  const rag = await resolveRagDeps();
9
- const tools = (await collectMcpTools({ rag, includeAdmin: role === 'admin', actor })).filter((t) => role === 'admin' || t.role === 'member');
10
+ const tools = await collectMcpTools({ rag, role, actor });
10
11
  const site = await siteSection(await configuredPublicUrl());
11
- const sections = [...(await buildDomainSections()), ...(site ? [site] : [])];
12
+ const areas = await buildDomainAreas();
13
+ if (site)
14
+ areas['channel'] = [site];
15
+ const sections = assembleSystem(areas, ALL_PRESENT);
12
16
  const server = new McpServer({ name: 'coffer', version: '1.0.0' }, { instructions: buildMcpInstructions(sections) });
13
17
  const registerTool = server.registerTool.bind(server);
14
18
  for (const t of tools) {
@@ -4,7 +4,9 @@ import { type ToolResult } from './mcp-contract/tools.ts';
4
4
  import { type LocaleResolver } from './locale-registry.ts';
5
5
  import { type AuthRole, type PluginHooks } from './plugin-hooks.ts';
6
6
  import { type Condition } from '@coffer-org/sdk/condition';
7
+ import { type AgentMeta } from '@coffer-org/sdk/library';
7
8
  import { type RagHit } from './rag-search.ts';
9
+ import type { SystemAreas } from './orchestrator/system-areas.ts';
8
10
  export interface McpToolDef {
9
11
  server: string;
10
12
  bareName: string;
@@ -20,9 +22,9 @@ export interface RagDeps {
20
22
  }
21
23
  export declare function formatHits(hits: RagHit[]): string;
22
24
  export declare function resolveRagDeps(): Promise<RagDeps | null>;
23
- export declare function collectMcpTools(opts?: {
25
+ export declare function collectMcpTools(opts: {
26
+ role: AuthRole;
24
27
  rag?: RagDeps | null;
25
- includeAdmin?: boolean;
26
28
  actor?: string;
27
29
  }): Promise<McpToolDef[]>;
28
30
  export declare function collectPluginInstructions(hooks?: Record<string, PluginHooks>, emFactory?: () => EntityManager): Promise<{
@@ -58,12 +60,12 @@ export declare function collectLibraryPurposes(reg?: {
58
60
  meta: {
59
61
  id: string;
60
62
  label?: string;
61
- agent?: string;
63
+ agent?: AgentMeta | string;
62
64
  };
63
65
  }[];
64
66
  extends_: {
65
67
  id: string;
66
- agent?: string;
68
+ agent?: AgentMeta | string;
67
69
  showWhen?: Condition;
68
70
  attachTo: {
69
71
  library: string;
@@ -72,7 +74,7 @@ export declare function collectLibraryPurposes(reg?: {
72
74
  }[];
73
75
  }, locales?: LocaleResolver): LibraryPurpose[];
74
76
  export declare function siteSection(siteUrl: string): Promise<string | null>;
75
- export declare function buildDomainSections(locales?: LocaleResolver): Promise<string[]>;
77
+ export declare function buildDomainAreas(locales?: LocaleResolver): Promise<SystemAreas>;
76
78
  export interface StarterHint {
77
79
  id: string;
78
80
  text: string;