ont-run 0.0.1 → 0.0.3

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.
package/README.md CHANGED
@@ -21,12 +21,14 @@ export default defineOntology({
21
21
  getTicket: {
22
22
  description: 'Get ticket details',
23
23
  access: ['support', 'admin'],
24
+ entities: ['Ticket'],
24
25
  inputs: z.object({ ticketId: z.string().uuid() }),
25
26
  resolver: './resolvers/getTicket.ts',
26
27
  },
27
28
  assignTicket: {
28
29
  description: 'Assign ticket to an agent',
29
30
  access: ['admin'], // If AI tries to add 'public' here, review is triggered
31
+ entities: ['Ticket'],
30
32
  inputs: z.object({ ticketId: z.string().uuid(), assignee: z.string() }),
31
33
  resolver: './resolvers/assignTicket.ts',
32
34
  },
package/dist/bin/ont.js CHANGED
@@ -6165,9 +6165,26 @@ function getFieldFromMetadata(schema) {
6165
6165
  }
6166
6166
  return null;
6167
6167
  }
6168
- var FIELD_FROM_METADATA;
6168
+ function hasUserContextMetadata(schema) {
6169
+ return schema !== null && typeof schema === "object" && USER_CONTEXT_METADATA in schema && schema[USER_CONTEXT_METADATA] === true;
6170
+ }
6171
+ function getUserContextFields(schema) {
6172
+ const fields = [];
6173
+ if (schema instanceof exports_external.ZodObject) {
6174
+ const shape = schema.shape;
6175
+ for (const [key, value] of Object.entries(shape)) {
6176
+ if (hasUserContextMetadata(value)) {
6177
+ fields.push(key);
6178
+ }
6179
+ }
6180
+ }
6181
+ return fields;
6182
+ }
6183
+ var FIELD_FROM_METADATA, USER_CONTEXT_METADATA;
6169
6184
  var init_categorical = __esm(() => {
6185
+ init_zod();
6170
6186
  FIELD_FROM_METADATA = Symbol.for("ont:fieldFrom");
6187
+ USER_CONTEXT_METADATA = Symbol.for("ont:userContext");
6171
6188
  });
6172
6189
 
6173
6190
  // src/lockfile/hasher.ts
@@ -6226,13 +6243,16 @@ function extractOntology(config) {
6226
6243
  }
6227
6244
  }
6228
6245
  const fieldReferences = extractFieldReferences(fn.inputs);
6246
+ const userContextFields = getUserContextFields(fn.inputs);
6247
+ const usesUserContext = userContextFields.length > 0;
6229
6248
  functions[name] = {
6230
6249
  description: fn.description,
6231
6250
  access: [...fn.access].sort(),
6232
6251
  entities: [...fn.entities].sort(),
6233
6252
  inputsSchema,
6234
6253
  outputsSchema,
6235
- fieldReferences: fieldReferences.length > 0 ? fieldReferences : undefined
6254
+ fieldReferences: fieldReferences.length > 0 ? fieldReferences : undefined,
6255
+ usesUserContext: usesUserContext || undefined
6236
6256
  };
6237
6257
  }
6238
6258
  return {
@@ -6330,7 +6350,8 @@ function diffOntology(oldOntology, newOntology) {
6330
6350
  const outputsChanged = JSON.stringify(oldFn.outputsSchema) !== JSON.stringify(newFn.outputsSchema);
6331
6351
  const entitiesChanged = JSON.stringify(oldFn.entities) !== JSON.stringify(newFn.entities);
6332
6352
  const fieldReferencesChanged = JSON.stringify(oldFn.fieldReferences) !== JSON.stringify(newFn.fieldReferences);
6333
- if (accessChanged || descriptionChanged || inputsChanged || outputsChanged || entitiesChanged || fieldReferencesChanged) {
6353
+ const userContextChanged = !!oldFn.usesUserContext !== !!newFn.usesUserContext;
6354
+ if (accessChanged || descriptionChanged || inputsChanged || outputsChanged || entitiesChanged || fieldReferencesChanged || userContextChanged) {
6334
6355
  functions.push({
6335
6356
  name,
6336
6357
  type: "modified",
@@ -6343,7 +6364,9 @@ function diffOntology(oldOntology, newOntology) {
6343
6364
  entitiesChanged: entitiesChanged || undefined,
6344
6365
  oldEntities: entitiesChanged ? oldFn.entities : undefined,
6345
6366
  newEntities: entitiesChanged ? newFn.entities : undefined,
6346
- fieldReferencesChanged: fieldReferencesChanged || undefined
6367
+ fieldReferencesChanged: fieldReferencesChanged || undefined,
6368
+ userContextChanged: userContextChanged || undefined,
6369
+ usesUserContext: userContextChanged ? newFn.usesUserContext : undefined
6347
6370
  });
6348
6371
  }
6349
6372
  }
@@ -6421,6 +6444,9 @@ function formatDiffForConsole(diff) {
6421
6444
  if (fn.fieldReferencesChanged) {
6422
6445
  lines.push(` Field references: changed`);
6423
6446
  }
6447
+ if (fn.userContextChanged) {
6448
+ lines.push(` User context: ${fn.usesUserContext ? "added" : "removed"}`);
6449
+ }
6424
6450
  }
6425
6451
  }
6426
6452
  }
@@ -8405,7 +8431,7 @@ var initCommand = defineCommand({
8405
8431
  if (!existsSync(resolversDir)) {
8406
8432
  mkdirSync(resolversDir, { recursive: true });
8407
8433
  }
8408
- const configTemplate = `import { defineOntology } from 'ont-run';
8434
+ const configTemplate = `import { defineOntology, userContext } from 'ont-run';
8409
8435
  import { z } from 'zod';
8410
8436
 
8411
8437
  export default defineOntology({
@@ -8417,13 +8443,22 @@ export default defineOntology({
8417
8443
  },
8418
8444
 
8419
8445
  // Pluggable auth - customize this for your use case
8446
+ // Return { groups, user } for row-level access control
8420
8447
  auth: async (req) => {
8421
8448
  const token = req.headers.get('Authorization');
8422
- // Return access groups based on auth
8449
+ // Return access groups and optional user data
8423
8450
  // This is where you'd verify JWTs, API keys, etc.
8424
- if (!token) return ['public'];
8425
- if (token === 'admin-secret') return ['admin', 'support', 'public'];
8426
- return ['support', 'public'];
8451
+ if (!token) return { groups: ['public'] };
8452
+ if (token === 'admin-secret') {
8453
+ return {
8454
+ groups: ['admin', 'support', 'public'],
8455
+ user: { id: 'admin-1', email: 'admin@example.com' },
8456
+ };
8457
+ }
8458
+ return {
8459
+ groups: ['support', 'public'],
8460
+ user: { id: 'user-1', email: 'user@example.com' },
8461
+ };
8427
8462
  },
8428
8463
 
8429
8464
  accessGroups: {
@@ -8432,21 +8467,32 @@ export default defineOntology({
8432
8467
  admin: { description: 'Administrators' },
8433
8468
  },
8434
8469
 
8470
+ entities: {
8471
+ User: { description: 'A user account' },
8472
+ },
8473
+
8435
8474
  functions: {
8436
8475
  // Example: Public function
8437
8476
  healthCheck: {
8438
8477
  description: 'Check API health status',
8439
8478
  access: ['public', 'support', 'admin'],
8479
+ entities: [],
8440
8480
  inputs: z.object({}),
8441
8481
  resolver: './resolvers/healthCheck.ts',
8442
8482
  },
8443
8483
 
8444
- // Example: Restricted function
8484
+ // Example: Restricted function with row-level access
8445
8485
  getUser: {
8446
8486
  description: 'Get user details by ID',
8447
8487
  access: ['support', 'admin'],
8488
+ entities: ['User'],
8448
8489
  inputs: z.object({
8449
8490
  userId: z.string().uuid(),
8491
+ // currentUser is injected from auth - not visible to API callers
8492
+ currentUser: userContext(z.object({
8493
+ id: z.string(),
8494
+ email: z.string(),
8495
+ })),
8450
8496
  }),
8451
8497
  resolver: './resolvers/getUser.ts',
8452
8498
  },
@@ -8455,6 +8501,7 @@ export default defineOntology({
8455
8501
  deleteUser: {
8456
8502
  description: 'Delete a user account',
8457
8503
  access: ['admin'],
8504
+ entities: ['User'],
8458
8505
  inputs: z.object({
8459
8506
  userId: z.string().uuid(),
8460
8507
  reason: z.string().optional(),
@@ -8482,10 +8529,21 @@ export default async function healthCheck(ctx: ResolverContext, args: {}) {
8482
8529
 
8483
8530
  interface GetUserArgs {
8484
8531
  userId: string;
8532
+ currentUser: {
8533
+ id: string;
8534
+ email: string;
8535
+ };
8485
8536
  }
8486
8537
 
8487
8538
  export default async function getUser(ctx: ResolverContext, args: GetUserArgs) {
8488
8539
  ctx.logger.info(\`Getting user: \${args.userId}\`);
8540
+ ctx.logger.info(\`Requested by: \${args.currentUser.email}\`);
8541
+
8542
+ // Example: Check if user can access this resource
8543
+ // Support can only view their own account
8544
+ if (!ctx.accessGroups.includes('admin') && args.userId !== args.currentUser.id) {
8545
+ throw new Error('You can only view your own account');
8546
+ }
8489
8547
 
8490
8548
  // This is where you'd query your database
8491
8549
  // Example response:
@@ -8598,10 +8656,21 @@ async function loadConfig(configPath) {
8598
8656
  }
8599
8657
  return { config, configDir, configPath: resolvedPath };
8600
8658
  } catch (error) {
8601
- if (error.code === "ERR_MODULE_NOT_FOUND") {
8602
- throw new Error(`Failed to load config: ${resolvedPath}`);
8659
+ const err = error;
8660
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
8661
+ const message = err.message || "";
8662
+ if (message.includes("ont-run") || message.includes("zod")) {
8663
+ throw new Error(`Failed to load config: ${resolvedPath}
8664
+
8665
+ Missing dependencies. Run 'bun install' first.`);
8666
+ }
8667
+ throw new Error(`Failed to load config: ${resolvedPath}
8668
+
8669
+ Module not found: ${message}`);
8603
8670
  }
8604
- throw error;
8671
+ throw new Error(`Failed to load config: ${resolvedPath}
8672
+
8673
+ ${err.message || error}`);
8605
8674
  }
8606
8675
  }
8607
8676
 
@@ -10786,6 +10855,8 @@ function transformToGraphData(config) {
10786
10855
  for (const entity of fn.entities) {
10787
10856
  entityCounts[entity] = (entityCounts[entity] || 0) + 1;
10788
10857
  }
10858
+ const userContextFields = getUserContextFields(fn.inputs);
10859
+ const usesUserContext = userContextFields.length > 0;
10789
10860
  nodes.push({
10790
10861
  id: `function:${name}`,
10791
10862
  type: "function",
@@ -10794,7 +10865,8 @@ function transformToGraphData(config) {
10794
10865
  metadata: {
10795
10866
  inputs: safeZodToJsonSchema(fn.inputs),
10796
10867
  outputs: fn.outputs ? safeZodToJsonSchema(fn.outputs) : undefined,
10797
- resolver: fn.resolver
10868
+ resolver: fn.resolver,
10869
+ usesUserContext: usesUserContext || undefined
10798
10870
  }
10799
10871
  });
10800
10872
  for (const group of fn.access) {
@@ -11386,6 +11458,27 @@ function generateBrowserUI(graphData) {
11386
11458
  color: var(--change-modified);
11387
11459
  }
11388
11460
 
11461
+ /* User Context Badge */
11462
+ .user-context-badge {
11463
+ display: inline-flex;
11464
+ align-items: center;
11465
+ gap: 4px;
11466
+ padding: 2px 8px;
11467
+ border-radius: 9999px;
11468
+ font-size: 10px;
11469
+ font-weight: 500;
11470
+ background: rgba(21, 168, 168, 0.12);
11471
+ color: var(--vanna-teal);
11472
+ text-transform: uppercase;
11473
+ letter-spacing: 0.03em;
11474
+ margin-left: 8px;
11475
+ }
11476
+
11477
+ .user-context-badge svg {
11478
+ width: 12px;
11479
+ height: 12px;
11480
+ }
11481
+
11389
11482
  /* Review Footer */
11390
11483
  .review-footer {
11391
11484
  position: fixed;
@@ -12854,9 +12947,14 @@ function generateBrowserUI(graphData) {
12854
12947
  ? \`<span class="detail-change-badge \${changeStatus}">\${changeStatus === 'added' ? 'New' : changeStatus === 'removed' ? 'Removed' : 'Modified'}</span>\`
12855
12948
  : '';
12856
12949
 
12950
+ // Build user context badge if applicable
12951
+ const userContextBadge = data.metadata?.usesUserContext
12952
+ ? \`<span class="user-context-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>User Context</span>\`
12953
+ : '';
12954
+
12857
12955
  let html = \`
12858
12956
  <div class="detail-header">
12859
- <div class="detail-type \${data.type}">\${formatType(data.type)}\${changeBadge}</div>
12957
+ <div class="detail-type \${data.type}">\${formatType(data.type)}\${changeBadge}\${userContextBadge}</div>
12860
12958
  <div class="detail-name">\${data.label}</div>
12861
12959
  <div class="detail-description">\${data.description || 'No description'}</div>
12862
12960
  </div>
package/dist/index.js CHANGED
@@ -4007,10 +4007,31 @@ function getFieldFromMetadata(schema) {
4007
4007
  }
4008
4008
  return null;
4009
4009
  }
4010
- var FIELD_FROM_METADATA;
4010
+ function userContext(schema) {
4011
+ const marked = schema;
4012
+ marked[USER_CONTEXT_METADATA] = true;
4013
+ return marked;
4014
+ }
4015
+ function hasUserContextMetadata(schema) {
4016
+ return schema !== null && typeof schema === "object" && USER_CONTEXT_METADATA in schema && schema[USER_CONTEXT_METADATA] === true;
4017
+ }
4018
+ function getUserContextFields(schema) {
4019
+ const fields = [];
4020
+ if (schema instanceof exports_external.ZodObject) {
4021
+ const shape = schema.shape;
4022
+ for (const [key, value] of Object.entries(shape)) {
4023
+ if (hasUserContextMetadata(value)) {
4024
+ fields.push(key);
4025
+ }
4026
+ }
4027
+ }
4028
+ return fields;
4029
+ }
4030
+ var FIELD_FROM_METADATA, USER_CONTEXT_METADATA;
4011
4031
  var init_categorical = __esm(() => {
4012
4032
  init_zod();
4013
4033
  FIELD_FROM_METADATA = Symbol.for("ont:fieldFrom");
4034
+ USER_CONTEXT_METADATA = Symbol.for("ont:userContext");
4014
4035
  });
4015
4036
 
4016
4037
  // node_modules/consola/dist/chunks/prompt.mjs
@@ -6229,13 +6250,16 @@ function extractOntology(config) {
6229
6250
  }
6230
6251
  }
6231
6252
  const fieldReferences = extractFieldReferences(fn.inputs);
6253
+ const userContextFields = getUserContextFields(fn.inputs);
6254
+ const usesUserContext = userContextFields.length > 0;
6232
6255
  functions[name] = {
6233
6256
  description: fn.description,
6234
6257
  access: [...fn.access].sort(),
6235
6258
  entities: [...fn.entities].sort(),
6236
6259
  inputsSchema,
6237
6260
  outputsSchema,
6238
- fieldReferences: fieldReferences.length > 0 ? fieldReferences : undefined
6261
+ fieldReferences: fieldReferences.length > 0 ? fieldReferences : undefined,
6262
+ usesUserContext: usesUserContext || undefined
6239
6263
  };
6240
6264
  }
6241
6265
  return {
@@ -6333,7 +6357,8 @@ function diffOntology(oldOntology, newOntology) {
6333
6357
  const outputsChanged = JSON.stringify(oldFn.outputsSchema) !== JSON.stringify(newFn.outputsSchema);
6334
6358
  const entitiesChanged = JSON.stringify(oldFn.entities) !== JSON.stringify(newFn.entities);
6335
6359
  const fieldReferencesChanged = JSON.stringify(oldFn.fieldReferences) !== JSON.stringify(newFn.fieldReferences);
6336
- if (accessChanged || descriptionChanged || inputsChanged || outputsChanged || entitiesChanged || fieldReferencesChanged) {
6360
+ const userContextChanged = !!oldFn.usesUserContext !== !!newFn.usesUserContext;
6361
+ if (accessChanged || descriptionChanged || inputsChanged || outputsChanged || entitiesChanged || fieldReferencesChanged || userContextChanged) {
6337
6362
  functions.push({
6338
6363
  name,
6339
6364
  type: "modified",
@@ -6346,7 +6371,9 @@ function diffOntology(oldOntology, newOntology) {
6346
6371
  entitiesChanged: entitiesChanged || undefined,
6347
6372
  oldEntities: entitiesChanged ? oldFn.entities : undefined,
6348
6373
  newEntities: entitiesChanged ? newFn.entities : undefined,
6349
- fieldReferencesChanged: fieldReferencesChanged || undefined
6374
+ fieldReferencesChanged: fieldReferencesChanged || undefined,
6375
+ userContextChanged: userContextChanged || undefined,
6376
+ usesUserContext: userContextChanged ? newFn.usesUserContext : undefined
6350
6377
  });
6351
6378
  }
6352
6379
  }
@@ -6424,6 +6451,9 @@ function formatDiffForConsole(diff) {
6424
6451
  if (fn.fieldReferencesChanged) {
6425
6452
  lines.push(` Field references: changed`);
6426
6453
  }
6454
+ if (fn.userContextChanged) {
6455
+ lines.push(` User context: ${fn.usesUserContext ? "added" : "removed"}`);
6456
+ }
6427
6457
  }
6428
6458
  }
6429
6459
  }
@@ -13531,6 +13561,41 @@ function validateFieldFromReferences(config) {
13531
13561
  }
13532
13562
  }
13533
13563
  }
13564
+ async function validateUserContextRequirements(config) {
13565
+ const functionsWithUserContext = [];
13566
+ for (const [fnName, fn] of Object.entries(config.functions)) {
13567
+ const userContextFields = getUserContextFields(fn.inputs);
13568
+ if (userContextFields.length > 0) {
13569
+ functionsWithUserContext.push(fnName);
13570
+ }
13571
+ }
13572
+ if (functionsWithUserContext.length === 0) {
13573
+ return;
13574
+ }
13575
+ const mockRequest = new Request("http://localhost/test", {
13576
+ headers: { Authorization: "test-token" }
13577
+ });
13578
+ try {
13579
+ const authResult = await config.auth(mockRequest);
13580
+ const hasUserField = authResult !== null && typeof authResult === "object" && !Array.isArray(authResult) && "user" in authResult;
13581
+ if (!hasUserField) {
13582
+ throw new Error(`The following functions use userContext() but auth() does not return a user object:
13583
+ ` + ` ${functionsWithUserContext.join(", ")}
13584
+
13585
+ ` + `To fix this, update your auth function to return an AuthResult:
13586
+ ` + ` auth: async (req) => {
13587
+ ` + ` return {
13588
+ ` + ` groups: ['user'],
13589
+ ` + ` user: { id: '...', email: '...' } // Add user data here
13590
+ ` + ` };
13591
+ ` + ` }`);
13592
+ }
13593
+ } catch (error) {
13594
+ if (error instanceof Error && error.message.includes("userContext")) {
13595
+ throw error;
13596
+ }
13597
+ }
13598
+ }
13534
13599
 
13535
13600
  // src/config/define.ts
13536
13601
  function defineOntology(config) {
@@ -14558,10 +14623,21 @@ async function loadConfig(configPath) {
14558
14623
  }
14559
14624
  return { config, configDir, configPath: resolvedPath };
14560
14625
  } catch (error) {
14561
- if (error.code === "ERR_MODULE_NOT_FOUND") {
14562
- throw new Error(`Failed to load config: ${resolvedPath}`);
14626
+ const err = error;
14627
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
14628
+ const message = err.message || "";
14629
+ if (message.includes("ont-run") || message.includes("zod")) {
14630
+ throw new Error(`Failed to load config: ${resolvedPath}
14631
+
14632
+ Missing dependencies. Run 'bun install' first.`);
14633
+ }
14634
+ throw new Error(`Failed to load config: ${resolvedPath}
14635
+
14636
+ Module not found: ${message}`);
14563
14637
  }
14564
- throw error;
14638
+ throw new Error(`Failed to load config: ${resolvedPath}
14639
+
14640
+ ${err.message || error}`);
14565
14641
  }
14566
14642
  }
14567
14643
 
@@ -16277,11 +16353,19 @@ function createLogger(debug = false) {
16277
16353
  }
16278
16354
 
16279
16355
  // src/server/api/middleware.ts
16356
+ function normalizeAuthResult(result) {
16357
+ if (Array.isArray(result)) {
16358
+ return { groups: result };
16359
+ }
16360
+ return result;
16361
+ }
16280
16362
  function createAuthMiddleware(config) {
16281
16363
  return createMiddleware(async (c3, next) => {
16282
16364
  try {
16283
- const accessGroups = await config.auth(c3.req.raw);
16284
- c3.set("accessGroups", accessGroups);
16365
+ const rawResult = await config.auth(c3.req.raw);
16366
+ const authResult = normalizeAuthResult(rawResult);
16367
+ c3.set("authResult", authResult);
16368
+ c3.set("accessGroups", authResult.groups);
16285
16369
  await next();
16286
16370
  } catch (error) {
16287
16371
  return c3.json({
@@ -16336,15 +16420,24 @@ function errorHandler2() {
16336
16420
  }
16337
16421
 
16338
16422
  // src/server/api/router.ts
16423
+ init_categorical();
16339
16424
  function createApiRoutes(config, configDir) {
16340
16425
  const router = new Hono2;
16341
16426
  for (const [name, fn] of Object.entries(config.functions)) {
16342
16427
  const path = `/${name}`;
16428
+ const userContextFields = getUserContextFields(fn.inputs);
16343
16429
  router.post(path, createAccessControlMiddleware(fn.access), async (c3) => {
16344
16430
  const resolverContext = c3.get("resolverContext");
16431
+ const authResult = c3.get("authResult");
16345
16432
  let args;
16346
16433
  try {
16347
- const body = await c3.req.json();
16434
+ let body = await c3.req.json();
16435
+ if (userContextFields.length > 0 && authResult.user) {
16436
+ body = { ...body };
16437
+ for (const field of userContextFields) {
16438
+ body[field] = authResult.user;
16439
+ }
16440
+ }
16348
16441
  const parsed = fn.inputs.safeParse(body);
16349
16442
  if (!parsed.success) {
16350
16443
  return c3.json({
@@ -16354,7 +16447,13 @@ function createApiRoutes(config, configDir) {
16354
16447
  }
16355
16448
  args = parsed.data;
16356
16449
  } catch {
16357
- const parsed = fn.inputs.safeParse({});
16450
+ let emptyBody = {};
16451
+ if (userContextFields.length > 0 && authResult.user) {
16452
+ for (const field of userContextFields) {
16453
+ emptyBody[field] = authResult.user;
16454
+ }
16455
+ }
16456
+ const parsed = fn.inputs.safeParse(emptyBody);
16358
16457
  if (!parsed.success) {
16359
16458
  return c3.json({
16360
16459
  error: "Validation failed",
@@ -22822,6 +22921,31 @@ data:
22822
22921
  init_zod();
22823
22922
  init_esm();
22824
22923
  init_categorical();
22924
+ function stripUserContextFromJsonSchema(jsonSchema, zodSchema) {
22925
+ if (jsonSchema.type !== "object" || !jsonSchema.properties) {
22926
+ return jsonSchema;
22927
+ }
22928
+ const userContextFields = getUserContextFields(zodSchema);
22929
+ if (userContextFields.length === 0) {
22930
+ return jsonSchema;
22931
+ }
22932
+ const properties = { ...jsonSchema.properties };
22933
+ const required2 = jsonSchema.required ? [...jsonSchema.required] : undefined;
22934
+ for (const field of userContextFields) {
22935
+ delete properties[field];
22936
+ if (required2) {
22937
+ const idx = required2.indexOf(field);
22938
+ if (idx !== -1) {
22939
+ required2.splice(idx, 1);
22940
+ }
22941
+ }
22942
+ }
22943
+ return {
22944
+ ...jsonSchema,
22945
+ properties,
22946
+ required: required2 && required2.length > 0 ? required2 : undefined
22947
+ };
22948
+ }
22825
22949
  function extractFieldReferencesForMcp(schema, path = "") {
22826
22950
  const results = [];
22827
22951
  const metadata = getFieldFromMetadata(schema);
@@ -22861,6 +22985,7 @@ function generateMcpTools(config2) {
22861
22985
  $refStrategy: "none"
22862
22986
  });
22863
22987
  delete inputSchema.$schema;
22988
+ inputSchema = stripUserContextFromJsonSchema(inputSchema, fn.inputs);
22864
22989
  } catch {
22865
22990
  inputSchema = { type: "object", properties: {} };
22866
22991
  }
@@ -22892,16 +23017,28 @@ function filterToolsByAccess(tools, accessGroups) {
22892
23017
  return tools.filter((tool) => tool.access.some((group) => accessGroups.includes(group)));
22893
23018
  }
22894
23019
  function createToolExecutor(config2, configDir, env2, envConfig, logger) {
22895
- return async (toolName, args, accessGroups) => {
23020
+ const userContextFieldsCache = new Map;
23021
+ for (const [name, fn] of Object.entries(config2.functions)) {
23022
+ userContextFieldsCache.set(name, getUserContextFields(fn.inputs));
23023
+ }
23024
+ return async (toolName, args, authResult) => {
22896
23025
  const fn = config2.functions[toolName];
22897
23026
  if (!fn) {
22898
23027
  throw new Error(`Unknown tool: ${toolName}`);
22899
23028
  }
22900
- const hasAccess = fn.access.some((group) => accessGroups.includes(group));
23029
+ const hasAccess = fn.access.some((group) => authResult.groups.includes(group));
22901
23030
  if (!hasAccess) {
22902
23031
  throw new Error(`Access denied to tool "${toolName}". Requires: ${fn.access.join(", ")}`);
22903
23032
  }
22904
- const parsed = fn.inputs.safeParse(args);
23033
+ const userContextFields = userContextFieldsCache.get(toolName) || [];
23034
+ let argsWithContext = args;
23035
+ if (userContextFields.length > 0 && authResult.user) {
23036
+ argsWithContext = { ...args };
23037
+ for (const field of userContextFields) {
23038
+ argsWithContext[field] = authResult.user;
23039
+ }
23040
+ }
23041
+ const parsed = fn.inputs.safeParse(argsWithContext);
22905
23042
  if (!parsed.success) {
22906
23043
  throw new Error(`Invalid input for tool "${toolName}": ${parsed.error.message}`);
22907
23044
  }
@@ -22909,7 +23046,7 @@ function createToolExecutor(config2, configDir, env2, envConfig, logger) {
22909
23046
  env: env2,
22910
23047
  envConfig,
22911
23048
  logger,
22912
- accessGroups
23049
+ accessGroups: authResult.groups
22913
23050
  };
22914
23051
  const resolver = await loadResolver(fn.resolver, configDir);
22915
23052
  return resolver(resolverContext, parsed.data);
@@ -22949,10 +23086,20 @@ async function serve2(app, port) {
22949
23086
  }
22950
23087
 
22951
23088
  // src/server/mcp/index.ts
22952
- function getAccessGroups(authInfo) {
22953
- if (!authInfo?.extra?.accessGroups)
22954
- return [];
22955
- return authInfo.extra.accessGroups;
23089
+ function normalizeAuthResult2(result) {
23090
+ if (Array.isArray(result)) {
23091
+ return { groups: result };
23092
+ }
23093
+ return result;
23094
+ }
23095
+ function getAuthResult(authInfo) {
23096
+ if (!authInfo?.extra?.authResult) {
23097
+ if (authInfo?.extra?.accessGroups) {
23098
+ return { groups: authInfo.extra.accessGroups };
23099
+ }
23100
+ return { groups: [] };
23101
+ }
23102
+ return authInfo.extra.authResult;
22956
23103
  }
22957
23104
  function createMcpServer(options) {
22958
23105
  const { config: config2, configDir, env: env2 } = options;
@@ -22972,8 +23119,8 @@ function createMcpServer(options) {
22972
23119
  }
22973
23120
  });
22974
23121
  server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
22975
- const accessGroups = getAccessGroups(extra.authInfo);
22976
- const accessibleTools = filterToolsByAccess(allTools, accessGroups);
23122
+ const authResult = getAuthResult(extra.authInfo);
23123
+ const accessibleTools = filterToolsByAccess(allTools, authResult.groups);
22977
23124
  return {
22978
23125
  tools: accessibleTools.map((tool) => ({
22979
23126
  name: tool.name,
@@ -22984,9 +23131,9 @@ function createMcpServer(options) {
22984
23131
  });
22985
23132
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
22986
23133
  const { name, arguments: args } = request.params;
22987
- const accessGroups = getAccessGroups(extra.authInfo);
23134
+ const authResult = getAuthResult(extra.authInfo);
22988
23135
  try {
22989
- const result = await executeToolWithAccess(name, args || {}, accessGroups);
23136
+ const result = await executeToolWithAccess(name, args || {}, authResult);
22990
23137
  return {
22991
23138
  content: [
22992
23139
  {
@@ -23027,13 +23174,15 @@ async function startMcpServer(options) {
23027
23174
  });
23028
23175
  app.all("/mcp", async (c3) => {
23029
23176
  try {
23030
- const accessGroups = await config2.auth(c3.req.raw);
23177
+ const rawResult = await config2.auth(c3.req.raw);
23178
+ const authResult = normalizeAuthResult2(rawResult);
23031
23179
  const authInfo = {
23032
23180
  token: c3.req.header("Authorization") || "",
23033
23181
  clientId: "ontology-client",
23034
23182
  scopes: [],
23035
23183
  extra: {
23036
- accessGroups
23184
+ authResult,
23185
+ accessGroups: authResult.groups
23037
23186
  }
23038
23187
  };
23039
23188
  return transport.handleRequest(c3.req.raw, { authInfo });
@@ -23071,6 +23220,12 @@ async function startOnt(options = {}) {
23071
23220
  const isDev = mode === "development";
23072
23221
  consola.info("Loading ontology config...");
23073
23222
  const { config: config2, configDir } = await loadConfig();
23223
+ try {
23224
+ await validateUserContextRequirements(config2);
23225
+ } catch (error2) {
23226
+ consola.error("User context validation failed:");
23227
+ throw error2;
23228
+ }
23074
23229
  consola.info("Checking lockfile...");
23075
23230
  const { ontology, hash } = computeOntologyHash(config2);
23076
23231
  if (!lockfileExists(configDir)) {
@@ -23146,6 +23301,7 @@ Run \`bun run review\` to approve the changes.`;
23146
23301
  init_zod();
23147
23302
  export {
23148
23303
  exports_external as z,
23304
+ userContext,
23149
23305
  startOnt,
23150
23306
  fieldFrom,
23151
23307
  defineOntology
@@ -13,6 +13,7 @@ export interface GraphNode {
13
13
  outputs?: Record<string, unknown>;
14
14
  resolver?: string;
15
15
  functionCount?: number;
16
+ usesUserContext?: boolean;
16
17
  };
17
18
  }
18
19
  export interface GraphEdge {