@lenne.tech/nest-server 12.2.0 → 12.3.1
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/.claude/rules/better-auth.md +31 -4
- package/.claude/rules/configurable-features.md +8 -7
- package/.claude/rules/package-management.md +18 -2
- package/.claude/rules/persistence.md +2 -2
- package/.claude/rules/testing.md +5 -5
- package/.claude/rules/versioning.md +13 -2
- package/CLAUDE.md +5 -0
- package/FRAMEWORK-API.md +4 -3
- package/README.md +2 -2
- package/dist/config.env.js +2 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +4 -0
- package/dist/core/common/helpers/graphql-ws-connection.helper.js +17 -3
- package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
- package/dist/core/common/interceptors/check-security.interceptor.js +19 -2
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +8 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +3 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/core-ai.constants.d.ts +3 -0
- package/dist/core/modules/ai/core-ai.constants.js +3 -0
- package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
- package/dist/core/modules/ai/core-ai.controller.d.ts +9 -0
- package/dist/core/modules/ai/core-ai.controller.js +48 -1
- package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
- package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
- package/dist/core/modules/ai/core-ai.module.js +9 -1
- package/dist/core/modules/ai/core-ai.module.js.map +1 -1
- package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
- package/dist/core/modules/ai/core-ai.resolver.js +32 -0
- package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
- package/dist/core/modules/ai/index.d.ts +8 -0
- package/dist/core/modules/ai/index.js +7 -0
- package/dist/core/modules/ai/index.js.map +1 -1
- package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
- package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
- package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-status.model.d.ts +3 -0
- package/dist/core/modules/ai/models/core-ai-status.model.js +25 -0
- package/dist/core/modules/ai/models/core-ai-status.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
- package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
- package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
- package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
- package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
- package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
- package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
- package/dist/core/modules/ai/services/core-ai.service.js +68 -29
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +10 -4
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +18 -0
- package/dist/core/modules/error-code/error-codes.js +16 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/sync/core-sync.service.d.ts +1 -0
- package/dist/core/modules/sync/core-sync.service.js +31 -2
- package/dist/core/modules/sync/core-sync.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.js +10 -0
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
- package/dist/core/modules/tenant/core-tenant.service.js +153 -1
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +2 -0
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +9 -2
- package/dist/test/test.helper.js.map +1 -1
- package/docs/REQUEST-LIFECYCLE.md +5 -4
- package/docs/error-codes.md +14 -0
- package/docs/offline-sync-protocol.md +17 -8
- package/migration-guides/12.0.x-to-12.1.x.md +0 -1
- package/migration-guides/12.2.x-to-12.3.x.md +440 -0
- package/migration-guides/12.3.0-to-12.3.1.md +136 -0
- package/package.json +26 -26
- package/src/config.env.ts +6 -2
- package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
- package/src/core/common/interceptors/check-security.interceptor.ts +26 -2
- package/src/core/common/interfaces/server-options.interface.ts +84 -8
- package/src/core/common/persistence/sync-model.ts +1 -1
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
- package/src/core/modules/ai/README.md +178 -25
- package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
- package/src/core/modules/ai/core-ai.constants.ts +10 -0
- package/src/core/modules/ai/core-ai.controller.ts +55 -1
- package/src/core/modules/ai/core-ai.module.ts +14 -0
- package/src/core/modules/ai/core-ai.resolver.ts +36 -0
- package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
- package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
- package/src/core/modules/ai/index.ts +19 -0
- package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
- package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
- package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
- package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
- package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
- package/src/core/modules/ai/models/core-ai-status.model.ts +16 -0
- package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
- package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
- package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
- package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
- package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
- package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
- package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
- package/src/core/modules/ai/services/core-ai.service.ts +134 -35
- package/src/core/modules/api-token/README.md +4 -2
- package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +12 -2
- package/src/core/modules/better-auth/README.md +32 -1
- package/src/core/modules/better-auth/better-auth.config.ts +21 -5
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
- package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
- package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
- package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
- package/src/core/modules/better-auth/index.ts +1 -0
- package/src/core/modules/error-code/error-codes.ts +18 -0
- package/src/core/modules/sync/README.md +11 -1
- package/src/core/modules/sync/core-sync.service.ts +48 -3
- package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +12 -1
- package/src/core/modules/tenant/README.md +49 -3
- package/src/core/modules/tenant/core-tenant.guard.ts +18 -2
- package/src/core/modules/tenant/core-tenant.service.ts +287 -2
- package/src/test/test.helper.ts +17 -3
|
@@ -396,6 +396,16 @@ factory.registerBuilder('my-provider', (conn) => new MyProvider(conn));
|
|
|
396
396
|
`conversationId` to `aiPrompt` to load prior turns and append the new ones.
|
|
397
397
|
Manage via `createAiConversation` / `findAiConversations` / `getAiConversation`
|
|
398
398
|
/ `deleteAiConversation`.
|
|
399
|
+
A prompt only continues the caller's OWN conversation, on REST, the stream and
|
|
400
|
+
GraphQL alike. An id the caller did not create — another user's, or one that does
|
|
401
|
+
not exist — counts as no conversation: the run reads no history, writes nothing,
|
|
402
|
+
uses no conversation-scoped tool grants, and its response carries no
|
|
403
|
+
`conversationId` (12.3.0; before, the turns were written into the other user's
|
|
404
|
+
conversation). Admins are no exception here: they may READ every conversation
|
|
405
|
+
(`getAiConversation`, `findAiConversations` with `all`), never continue one.
|
|
406
|
+
`CoreAiConversationService.appendMessage(id, message, ownerId)` checks the owner
|
|
407
|
+
itself and writes nothing for anyone else; a project that calls it directly passes
|
|
408
|
+
the id of the user the turn belongs to.
|
|
399
409
|
- **Audit** (`ai.audit: true`): every run is persisted to `aiInteractions`
|
|
400
410
|
(admin-readable via `findAiInteractions` / `getAiInteraction`).
|
|
401
411
|
- **Streaming**: `POST /ai/stream` returns Server-Sent Events (`action`, `token`,
|
|
@@ -418,26 +428,32 @@ export class TransferFundsTool extends AiTool {
|
|
|
418
428
|
readonly mutating = true; // governed by the confirmation policy
|
|
419
429
|
readonly destructive = true; // always requires confirmation
|
|
420
430
|
|
|
421
|
-
//
|
|
422
|
-
// any step runs
|
|
423
|
-
// the
|
|
431
|
+
// Runs before EVERY execution (12.3.0+): auto mode, plan mode (for the whole plan
|
|
432
|
+
// before any step runs, and again per step) and MCP — on the arguments that will
|
|
433
|
+
// run, after the PreToolUse hooks. Never mutates. A throw counts as a refusal.
|
|
424
434
|
async authorize(args, context) {
|
|
425
435
|
const account = await this.accountService.get(args.fromId, context.serviceOptions).catch(() => null);
|
|
426
|
-
return
|
|
436
|
+
return account
|
|
437
|
+
? { allowed: true, data: account } // reaches execute() as context.authorization.data
|
|
438
|
+
: { allowed: false, reason: 'No access to source account' };
|
|
427
439
|
}
|
|
428
440
|
|
|
429
441
|
async execute(args, context) {
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
const account =
|
|
442
|
+
// Still route the write through the service with the caller's serviceOptions, so
|
|
443
|
+
// `@Restricted`, `securityCheck()` and tenant scoping apply: authorize() decides
|
|
444
|
+
// whether to try, the service decides what the user may change.
|
|
445
|
+
const account =
|
|
446
|
+
context.authorization?.data ?? (await this.accountService.getForUser(args.fromId, context.serviceOptions));
|
|
434
447
|
return this.accountService.transfer(account, args.toId, args.amount, context.serviceOptions);
|
|
435
448
|
}
|
|
436
449
|
}
|
|
437
450
|
```
|
|
438
451
|
|
|
439
|
-
>
|
|
440
|
-
> only — auto mode (the default) and MCP
|
|
452
|
+
> **`authorize()` runs on every path since 12.3.0.** Until 12.2 it ran in plan mode
|
|
453
|
+
> only — auto mode (the default) and MCP went straight to `execute()`. A refusal skips
|
|
454
|
+
> the call: the model receives `TOOL_NOT_AVAILABLE` with the `reason` (keep it free of
|
|
455
|
+
> anything the user may not know), an MCP client an error result. The refusal is not
|
|
456
|
+
> recorded as a learning signal.
|
|
441
457
|
|
|
442
458
|
## Confirmation for changes
|
|
443
459
|
|
|
@@ -447,6 +463,8 @@ export class TransferFundsTool extends AiTool {
|
|
|
447
463
|
admin set `enforced: true`.
|
|
448
464
|
- When confirmation is needed the response has `requiresConfirmation: true` +
|
|
449
465
|
`pendingActions`; re-send the prompt with `confirm: true` to proceed.
|
|
466
|
+
- Over MCP the same flags apply through MCP elicitation (12.3.0+) — see
|
|
467
|
+
[Authorization and confirmation over MCP](#authorization-and-confirmation-over-mcp-1230).
|
|
450
468
|
|
|
451
469
|
## Client metadata & prompt enrichment
|
|
452
470
|
|
|
@@ -455,6 +473,16 @@ export class TransferFundsTool extends AiTool {
|
|
|
455
473
|
- The system prompt is enriched with the user's roles + available tools and optional
|
|
456
474
|
`ai.documentation` (override `CoreAiPromptBuilderService.getDocumentation()` for RAG).
|
|
457
475
|
|
|
476
|
+
**What bounds the size.** `input.context` and `input.metadata` are capped at 4000 characters
|
|
477
|
+
each for the prompt — that cap protects the context window, not the CPU. The work is bounded
|
|
478
|
+
earlier, by the body parser: a request over `bodyParser.json.limit` (100 kB unless configured)
|
|
479
|
+
is refused with 413 before it is parsed. A project that raises the limit for `dataUrl`
|
|
480
|
+
attachments should raise it only as far as its images need; serializing a client block for the
|
|
481
|
+
cap then costs about half of what parsing the same body already did (measured 2026-11-03:
|
|
482
|
+
2 MB → ~10 ms parse, ~5 ms serialize). The same holds for tool results: `ai.maxToolResultChars`
|
|
483
|
+
keeps them out of the context window, while a tool's own query decides how much data it loads —
|
|
484
|
+
page large result sets in the tool rather than relying on the cap.
|
|
485
|
+
|
|
458
486
|
## Self-optimizing prompts (editable templates + governed learning)
|
|
459
487
|
|
|
460
488
|
The system prompt is assembled from **keyed fragments** by `CoreAiPromptBuilderService`,
|
|
@@ -506,8 +534,8 @@ so it can recover within the run.
|
|
|
506
534
|
> **Security:** learned hints and template overrides only ever **add textual guidance** —
|
|
507
535
|
> they can never relax the permission model. Tool role-filtering,
|
|
508
536
|
> `CrudService`/`@Restricted` and `secretFields` are enforced backend-side regardless of
|
|
509
|
-
> the prompt.
|
|
510
|
-
>
|
|
537
|
+
> the prompt. Since 12.3.0 a tool's `authorize()` belongs to that set too: it runs before
|
|
538
|
+
> every execution, in every mode and over MCP.
|
|
511
539
|
|
|
512
540
|
```typescript
|
|
513
541
|
ai: {
|
|
@@ -533,6 +561,37 @@ When a user's session history would overflow, the **oldest non-system turns are
|
|
|
533
561
|
`ai.maxToolResultChars` (default `12000`) — so long-running conversations never exceed the
|
|
534
562
|
model's limit.
|
|
535
563
|
|
|
564
|
+
**Native tool definitions count against the budget** (12.3.0+): a connection with
|
|
565
|
+
`supportsNativeTools: true` carries every description and schema in the `tools` payload, beside
|
|
566
|
+
the messages, and that payload occupies the same window. **A crowded window is reported**: when
|
|
567
|
+
the system prompt and the tool definitions take more than half of the input budget, the
|
|
568
|
+
framework warns once per connection and process, naming the connection, the numbers and whether
|
|
569
|
+
the window is the connection's own or the `ai.contextWindow` fallback. Before, the only trace was
|
|
570
|
+
a debug line, and the visible symptom was an assistant that answered generically because the
|
|
571
|
+
user's message had been cut.
|
|
572
|
+
|
|
573
|
+
**Guarding the prompt size in a project test:** measure against `ai.contextWindow` (the
|
|
574
|
+
fallback) and say so in the test — it guards the connections without a window of their own,
|
|
575
|
+
the worst case, not the window a pinned or detected connection has. Raising its limit removes
|
|
576
|
+
that guard; shrink the catalog instead (`deferToolSchemas`, `deferToolSummaryChars`, fewer
|
|
577
|
+
tools per role).
|
|
578
|
+
|
|
579
|
+
## Tool catalog for native connections (12.3.0+)
|
|
580
|
+
|
|
581
|
+
A connection with native tool calling receives every tool's description and parameter schema
|
|
582
|
+
in the `tools` payload. Its system-prompt catalog therefore lists **names only** — repeating
|
|
583
|
+
the descriptions (and, without `deferToolSchemas`, the schemas) paid for every tool twice.
|
|
584
|
+
Emulated connections and plan mode keep the full catalog: there it is the model's only source.
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
ai: {
|
|
588
|
+
nativeToolCatalog: 'full', // restores descriptions (and schemas) in the native catalog; default 'names'
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Use `'full'` only for a model that demonstrably picks tools better with the descriptions in the
|
|
593
|
+
prompt; the cost is the duplicated text on every call.
|
|
594
|
+
|
|
536
595
|
## Deferred tool schemas (large registries)
|
|
537
596
|
|
|
538
597
|
The system prompt normally carries every tool's full JSON parameter schema. With a large
|
|
@@ -558,9 +617,10 @@ context, set `deferToolSummaryChars` alongside it (roughly 200–400 works well)
|
|
|
558
617
|
|
|
559
618
|
> **Scoped to the emulated tool protocol, not to the provider.** In auto mode a connection
|
|
560
619
|
> with `supportsNativeTools: true` receives every full description and JSON schema through
|
|
561
|
-
> the native `tools` payload anyway,
|
|
562
|
-
>
|
|
563
|
-
>
|
|
620
|
+
> the native `tools` payload anyway, and its catalog lists names only (see above), so
|
|
621
|
+
> `deferToolSummaryChars` has no effect there and no `search_tools` banner is emitted —
|
|
622
|
+
> telling the model a description was cut while handing it the full text in the same request
|
|
623
|
+
> would only buy a wasted round-trip against `maxIterations`.
|
|
564
624
|
>
|
|
565
625
|
> **Plan mode always uses the emulated protocol**, so a native connection IS truncated there.
|
|
566
626
|
> Because plan mode returns a complete plan in one call without executing anything, the model
|
|
@@ -577,10 +637,8 @@ context, set `deferToolSummaryChars` alongside it (roughly 200–400 works well)
|
|
|
577
637
|
> confirmation gate reads a tool's `mutating`/`destructive` **flags**, never its description
|
|
578
638
|
> — so a truncated "requires confirmation" sentence cannot disable it.
|
|
579
639
|
>
|
|
580
|
-
>
|
|
581
|
-
> mode
|
|
582
|
-
> data-level checks (ownership, tenant scope) must live **inside `execute()`**, routed through
|
|
583
|
-
> `CrudService` with `context.serviceOptions`, not in `authorize()` alone.
|
|
640
|
+
> `AiTool.authorize()` is part of that chain on every path since 12.3.0 (auto mode, plan
|
|
641
|
+
> mode, MCP), and it reads the call's arguments, never the description.
|
|
584
642
|
|
|
585
643
|
## Token budgets & usage
|
|
586
644
|
|
|
@@ -641,6 +699,21 @@ them:
|
|
|
641
699
|
|
|
642
700
|
Without multi-tenancy, tool roles are compared with `user.roles` exactly as before.
|
|
643
701
|
|
|
702
|
+
## Is the assistant available? (`GET /ai/status`, 12.3.1+)
|
|
703
|
+
|
|
704
|
+
A frontend asks before anybody signs in, to decide whether it shows the assistant at all:
|
|
705
|
+
|
|
706
|
+
| Server | Answer |
|
|
707
|
+
| ----------------------------- | -------------------------------- |
|
|
708
|
+
| `ai` configured, `ai.mcp` off | `200 { "mcp": false }` |
|
|
709
|
+
| `ai` configured, `ai.mcp` on | `200 { "mcp": true }` |
|
|
710
|
+
| no `ai` (or `enabled: false`) | `404` — the route does not exist |
|
|
711
|
+
|
|
712
|
+
The route is public (`@Roles(S_EVERYONE)`): no session, token or cookie is needed, and the body names no user data.
|
|
713
|
+
`404` is the signal "AI is off" on purpose — a server without the module, also one older than 12.3.1, answers it the
|
|
714
|
+
same way, so a client needs no second rule. Every other `/ai` route keeps its roles. A project controller extending
|
|
715
|
+
`CoreAiController` inherits the route; `mcp` comes from what `CoreAiModule.forRoot()` registered (`AI_MCP_ENABLED`).
|
|
716
|
+
|
|
644
717
|
## MCP server (`ai.mcp: true`)
|
|
645
718
|
|
|
646
719
|
The `AiToolRegistry` also feeds a real **MCP server** at `POST/GET/DELETE /ai/mcp`
|
|
@@ -671,6 +744,53 @@ the server log; the response carries only a stable `#LTNS_0901` code.
|
|
|
671
744
|
- Answering a foreign id `404` rather than `403` is deliberate — a 403 would confirm
|
|
672
745
|
that the id exists, and the id travels in a response header and through client logs.
|
|
673
746
|
|
|
747
|
+
### Authorization and confirmation over MCP (12.3.0+)
|
|
748
|
+
|
|
749
|
+
`tools/call` applies the same gates as the chat orchestrator, in this order:
|
|
750
|
+
|
|
751
|
+
1. **Role filter** — the tool must be in the user's set (`AiToolRegistry.forUser()`).
|
|
752
|
+
2. **Tool policies** — the admin's `aiToolPolicies` rules for this call: `deny` answers an
|
|
753
|
+
error result, `ask` asks in step 4 whatever else is configured.
|
|
754
|
+
3. **`authorize()`** — the tool's data-level check on the call's arguments. A refusal, a
|
|
755
|
+
thrown error or an `allowed` that is not `true` answers an error result; nothing runs.
|
|
756
|
+
4. **Confirmation** — a `destructive` tool always, a `mutating` tool when
|
|
757
|
+
`ai.confirmation.mutating` says so (`enforced`, else `default`; MCP has no per-request
|
|
758
|
+
`requireConfirmation`), as long as MCP asks this user (below). The server asks through
|
|
759
|
+
**MCP elicitation** (a yes/no form the client shows) and runs the tool only on an
|
|
760
|
+
explicit yes. A no, a cancelled dialog, an error or a timeout (5 minutes) means "not
|
|
761
|
+
run". A client without the `elicitation` capability gets an error result instead of an
|
|
762
|
+
unconfirmed run.
|
|
763
|
+
5. **`execute()`** — with `context.authorization` from step 3.
|
|
764
|
+
|
|
765
|
+
**Whether MCP asks** is a server default each user may switch for themselves, like a
|
|
766
|
+
"bypass permissions" mode:
|
|
767
|
+
|
|
768
|
+
| `ai.mcp.confirmation` | Asks a user who has not chosen | A user's own switch |
|
|
769
|
+
| ------------------------------------ | ------------------------------ | -------------------------------------------- |
|
|
770
|
+
| unset, `true`, `{}` | yes | may switch it off |
|
|
771
|
+
| `false` | no | may switch it on |
|
|
772
|
+
| `{ default: true, enforced: true }` | yes | ignored; a change answers 403 (`#LTNS_0609`) |
|
|
773
|
+
| `{ default: false, enforced: true }` | no | ignored; a change answers 403 |
|
|
774
|
+
|
|
775
|
+
The user's switch is `PUT /ai/settings` with `{ mcpConfirmation: false }` (GraphQL
|
|
776
|
+
`updateAiSettings`), read back with `GET /ai/settings` (`aiSettings`), which answers the
|
|
777
|
+
effective value and `mcpConfirmationEnforced` so a UI can show the switch disabled. It is
|
|
778
|
+
stored per user (`aiUserSettings`, one record per user) and touches nobody else's calls.
|
|
779
|
+
Only `false` switches the default off and only `enforced: true` enforces — a misspelt value
|
|
780
|
+
never relaxes the gate. `authorize()` and the admin's `deny` / `ask` rules apply whatever
|
|
781
|
+
the switch says.
|
|
782
|
+
|
|
783
|
+
`tools/list` gives a `destructive` tool `annotations: { destructiveHint: true, readOnlyHint: false }`
|
|
784
|
+
and a `mutating` one `{ destructiveHint: false, readOnlyHint: false }` — hints for the client's own
|
|
785
|
+
UI, not a security boundary. A tool with neither flag carries no annotations: it may still write,
|
|
786
|
+
so it is never advertised as read-only, and clients keep the spec's cautious defaults.
|
|
787
|
+
|
|
788
|
+
The confirmation keeps the model from acting without the user; it cannot stop a hostile
|
|
789
|
+
client, which holds the user's credential and could call the API directly. The boundary
|
|
790
|
+
against that stays the role filter, `authorize()` and the `CrudService` inside `execute()`.
|
|
791
|
+
The question is English (an MCP request carries no language); override
|
|
792
|
+
`CoreAiMcpService.confirmationPrompt()` to localize it.
|
|
793
|
+
|
|
674
794
|
### Multi-replica / sticky sessions
|
|
675
795
|
|
|
676
796
|
An MCP session owns a **live Streamable-HTTP transport** — an open response stream
|
|
@@ -696,6 +816,16 @@ and sends operators looking in the wrong place.
|
|
|
696
816
|
|
|
697
817
|
Without Redis nothing changes: unknown ids answer `404` exactly as before.
|
|
698
818
|
|
|
819
|
+
**No sticky sessions available?** Set `ai.mcp.stateless: true` (12.3.0+). Every POST then builds its own
|
|
820
|
+
server and transport for the authenticated user (the SDK's stateless mode, `sessionIdGenerator: undefined`),
|
|
821
|
+
no `mcp-session-id` is issued, a session id a client still sends is ignored, and GET / DELETE answer `405`
|
|
822
|
+
with `Allow: POST`. Any replica can answer any request, and the user's rights and tenant are read per request
|
|
823
|
+
by construction. What is lost is what a session carries: server-initiated messages, the SSE stream, and
|
|
824
|
+
server-initiated requests — **MCP elicitation among them**. A tool that needs confirmation (above) is
|
|
825
|
+
therefore refused in this mode unless the user switched the question off (`PUT /ai/settings`) or
|
|
826
|
+
`ai.mcp.confirmation` is `false`. A server that only offers read tools is not affected.
|
|
827
|
+
`testHelper.mcpSession()` follows the setting (no session id expected or sent; `sessionId` is `''`).
|
|
828
|
+
|
|
699
829
|
### OAuth 2.1 (`ai.mcp.oauth: true`)
|
|
700
830
|
|
|
701
831
|
For generic MCP clients that auto-discover + register:
|
|
@@ -767,7 +897,13 @@ user decides whether an MCP client may act for them:
|
|
|
767
897
|
- **Allow** issues the code only with the page's CSRF token, signed with the OAuth secret and bound to
|
|
768
898
|
the user, the client, the redirect URI and the PKCE challenge (10 minutes); a stale token shows the
|
|
769
899
|
form again, a forged or tampered one too, and nothing is issued. **Deny** sends the client back with
|
|
770
|
-
`access_denied
|
|
900
|
+
`access_denied` — with the same token since 12.3.0; without it the form is shown again.
|
|
901
|
+
- **A malformed request** (a missing `code_challenge`, a wrong `response_type`, …) is answered with `400`
|
|
902
|
+
and `{ error, error_description }`, never redirected to the client (12.3.0). The redirect URI is whatever
|
|
903
|
+
the client's registrant wrote, so the SDK's error redirect made `/authorize` an open redirect on this
|
|
904
|
+
domain (RFC 9700 §4.11.2). Registration itself stays open — claude.ai, ChatGPT, Cursor and Claude Code
|
|
905
|
+
register their own URIs. Only what happens once the request reached the provider (the user's Allow or
|
|
906
|
+
Deny, and refusals raised in `authorizeConsent()`) still redirects.
|
|
771
907
|
|
|
772
908
|
The page carries its own `Content-Security-Policy` (script and style by nonce, `frame-ancestors 'none'`,
|
|
773
909
|
no `form-action`, which browsers would apply to the redirect to the client), `X-Frame-Options: DENY`,
|
|
@@ -792,8 +928,25 @@ the page offers no sign-in. Its sign-in goes through the IAM sign-in controller,
|
|
|
792
928
|
hands Better-Auth the request so Better-Auth's own login-CSRF protection runs — otherwise a hostile page
|
|
793
929
|
could sign the visitor in to the attacker's account before they approve a client.
|
|
794
930
|
|
|
795
|
-
**Your own
|
|
796
|
-
|
|
931
|
+
**Your own wording and look (12.3.0+).** Override `renderConsentPage(view)` and call
|
|
932
|
+
`renderMcpOAuthConsentPage(view, { texts, styles })`: `texts` replaces single texts per locale (merged over
|
|
933
|
+
the built-in ones, still escaped), `styles` is CSS appended inside the page's nonce'd `<style>` element. The
|
|
934
|
+
markup, the scripts and the security headers stay the framework's, so the page keeps receiving their fixes.
|
|
935
|
+
Fill both from code or configuration only, never from user or tenant input: a `<` in `styles` is neutralized
|
|
936
|
+
so it cannot end the `<style>` element, and the page's CSP blocks what CSS could load from elsewhere, but CSS
|
|
937
|
+
still decides what the page shows.
|
|
938
|
+
|
|
939
|
+
```typescript
|
|
940
|
+
protected override renderConsentPage(view: IMcpOAuthConsentView): string {
|
|
941
|
+
return renderMcpOAuthConsentPage(view, {
|
|
942
|
+
styles: ':root{--accent:#0b5}',
|
|
943
|
+
texts: { de: { intro: '{client} möchte in Ihrem Namen auf {app} zugreifen.' } },
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
**Your own page.** Override `renderConsentPage(view)` in a subclass to restyle it beyond that (keep every
|
|
949
|
+
value escaped), or `authorizeConsent(client, params, res)` for a consent UI of your own — same shape: no session
|
|
797
950
|
→ sign in, GET → show which client, POST with a CSRF token → `saveAuthorizationCode()` and redirect;
|
|
798
951
|
refuse with `AccessDeniedError` from `this.loadOAuthErrors()`. Register the subclass via
|
|
799
952
|
`CoreModule.forRoot(env, { ai: { mcpOAuthService } })`, so the MCP controller and the endpoints share it.
|
|
@@ -802,9 +955,9 @@ production secret check runs again before the endpoints are mounted (without a s
|
|
|
802
955
|
environment outside `local` / `ci` / `e2e` refuses to boot and to sign tokens at all — deployed `develop` and `test`
|
|
803
956
|
stages included).
|
|
804
957
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
958
|
+
Until 12.3.0 the SDK's error redirect made `/authorize` an open redirect in the sense of RFC 9700
|
|
959
|
+
§4.11.2, since any client may register any https URI; the framework now answers those errors itself
|
|
960
|
+
(see "A malformed request" under the consent page above).
|
|
808
961
|
|
|
809
962
|
**Refusals are OAuth errors (11.43.0).** The SDK's handlers answer an error with its OAuth code only
|
|
810
963
|
when it is one of the SDK's own `OAuthError` classes; anything else becomes `500 server_error`. The
|
|
@@ -48,7 +48,8 @@ import { AiToolUser } from './tools/ai-tool.registry.js';
|
|
|
48
48
|
* sessions behind a load balancer. When Redis is configured, session ids are additionally
|
|
49
49
|
* registered in a shared registry (replica + owning user), which turns a mis-routed request
|
|
50
50
|
* into an explicit 409 naming the owning replica instead of a misleading "unknown session" 404.
|
|
51
|
-
* Without Redis the behavior is unchanged.
|
|
51
|
+
* Without Redis the behavior is unchanged. `ai.mcp.stateless` removes the requirement altogether:
|
|
52
|
+
* every request then builds its own server and transport, and any replica can answer it.
|
|
52
53
|
*
|
|
53
54
|
* The registry entry records the OWNING USER, and only that user is ever told about the 409.
|
|
54
55
|
* Otherwise the cross-replica path would undo the local one: an authenticated caller probing
|
|
@@ -130,6 +131,11 @@ export class CoreAiMcpController implements OnModuleDestroy {
|
|
|
130
131
|
return;
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
if (this.statelessEnabled()) {
|
|
135
|
+
await this.handleStatelessPost(req, res, user);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
133
139
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
134
140
|
let entry = sessionId ? this.transports.get(sessionId) : undefined;
|
|
135
141
|
|
|
@@ -342,6 +348,42 @@ export class CoreAiMcpController implements OnModuleDestroy {
|
|
|
342
348
|
return body?.method === 'initialize';
|
|
343
349
|
}
|
|
344
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Whether `/ai/mcp` runs without sessions (`ai.mcp.stateless`).
|
|
353
|
+
*/
|
|
354
|
+
protected statelessEnabled(): boolean {
|
|
355
|
+
const mcp = ConfigService.get<{ stateless?: boolean }>('ai.mcp');
|
|
356
|
+
return typeof mcp === 'object' && mcp?.stateless === true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* One request, one server: the stateless mode of the SDK transport (`sessionIdGenerator: undefined`).
|
|
361
|
+
*
|
|
362
|
+
* Nothing outlives the response, so nothing has to reach the same replica again — the reason this
|
|
363
|
+
* mode exists. Rights are the user's as of THIS request by construction. A `mcp-session-id` a client
|
|
364
|
+
* still sends (from before a switch to this mode) is ignored rather than answered 404: there is no
|
|
365
|
+
* session it could refer to, and a 404 would only send the client into a reconnect.
|
|
366
|
+
*/
|
|
367
|
+
protected async handleStatelessPost(req: Request, res: Response, user: AiToolUser): Promise<void> {
|
|
368
|
+
let StreamableHTTPServerTransport: any;
|
|
369
|
+
try {
|
|
370
|
+
({ StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js'));
|
|
371
|
+
} catch (err) {
|
|
372
|
+
// Same lazy import and 503 reasoning as the session path in handlePost().
|
|
373
|
+
return this.mcpUnavailable(res, err as Error);
|
|
374
|
+
}
|
|
375
|
+
const transport: any = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
376
|
+
const server = await this.mcpService.createServer(user);
|
|
377
|
+
// Released with the response — an aborted request included, so a client that hangs up cannot
|
|
378
|
+
// leave a server behind.
|
|
379
|
+
res.on('close', () => {
|
|
380
|
+
Promise.resolve(transport.close?.()).catch(() => undefined);
|
|
381
|
+
Promise.resolve(server.close?.()).catch(() => undefined);
|
|
382
|
+
});
|
|
383
|
+
await server.connect(transport);
|
|
384
|
+
await transport.handleRequest(req, res, req.body);
|
|
385
|
+
}
|
|
386
|
+
|
|
345
387
|
/**
|
|
346
388
|
* Whether the OAuth 2.1 layer is enabled via `ai.mcp.oauth`.
|
|
347
389
|
*/
|
|
@@ -359,6 +401,11 @@ export class CoreAiMcpController implements OnModuleDestroy {
|
|
|
359
401
|
this.unauthorized(res);
|
|
360
402
|
return;
|
|
361
403
|
}
|
|
404
|
+
// Without sessions there is no stream to open and nothing to end (MCP spec: 405 for GET then).
|
|
405
|
+
if (this.statelessEnabled()) {
|
|
406
|
+
res.status(405).set('Allow', 'POST').json({ error: 'Method not allowed: this MCP server is stateless' });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
362
409
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
363
410
|
const held = sessionId ? this.transports.get(sessionId) : undefined;
|
|
364
411
|
// Same ownership rule as the POST path: another user's session is not ours to serve.
|
|
@@ -91,6 +91,12 @@ export const AI_TOOL_POLICY_CLASS = 'AI_TOOL_POLICY_CLASS';
|
|
|
91
91
|
/** Mongoose injection token. */
|
|
92
92
|
export const AI_TOOL_POLICY_MODEL = 'AiToolPolicy';
|
|
93
93
|
|
|
94
|
+
// ---- user-setting ----
|
|
95
|
+
/** DI token for the model constructor (used by CrudService mapping). */
|
|
96
|
+
export const AI_USER_SETTING_CLASS = 'AI_USER_SETTING_CLASS';
|
|
97
|
+
/** Mongoose injection token. */
|
|
98
|
+
export const AI_USER_SETTING_MODEL = 'AiUserSetting';
|
|
99
|
+
|
|
94
100
|
// ---- MCP OAuth (stored natively on MongoDB; these models give them their tables on a SQL source) ----
|
|
95
101
|
/** Mongoose model name of the registered OAuth clients (`aiMcpOAuthClients`). */
|
|
96
102
|
export const AI_MCP_OAUTH_CLIENT_MODEL = 'AiMcpOAuthClient';
|
|
@@ -100,3 +106,7 @@ export const AI_MCP_OAUTH_CODE_MODEL = 'AiMcpOAuthCode';
|
|
|
100
106
|
export const AI_MCP_OAUTH_REFRESH_TOKEN_MODEL = 'AiMcpOAuthRefreshToken';
|
|
101
107
|
/** How long an authorization code may be exchanged — checked on every read, not left to a TTL index. */
|
|
102
108
|
export const AI_MCP_OAUTH_CODE_TTL_SECONDS = 600;
|
|
109
|
+
|
|
110
|
+
// ---- module options ----
|
|
111
|
+
/** Whether `CoreAiModule.forRoot()` registered the MCP endpoint (`ai.mcp`) — what `GET /ai/status` answers. */
|
|
112
|
+
export const AI_MCP_ENABLED = 'AI_MCP_ENABLED';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Body, Controller, Delete, Get, Param, Post, Put, Query, Res } from '@nestjs/common';
|
|
1
|
+
import { Body, Controller, Delete, Get, Inject, Optional, Param, Post, Put, Query, Res } from '@nestjs/common';
|
|
2
2
|
import type { Response } from 'express';
|
|
3
3
|
|
|
4
4
|
import { RESTServiceOptions } from '../../common/decorators/rest-service-options.decorator.js';
|
|
@@ -6,6 +6,7 @@ import { Roles } from '../../common/decorators/roles.decorator.js';
|
|
|
6
6
|
import { RoleEnum } from '../../common/enums/role.enum.js';
|
|
7
7
|
import type { ServiceOptions } from '../../common/interfaces/service-options.interface.js';
|
|
8
8
|
import { RequestContext } from '../../common/services/request-context.service.js';
|
|
9
|
+
import { AI_MCP_ENABLED } from './core-ai.constants.js';
|
|
9
10
|
import { CoreAiBudgetLimitCreateInput } from './inputs/core-ai-budget-limit-create.input.js';
|
|
10
11
|
import { CoreAiBudgetLimitInput } from './inputs/core-ai-budget-limit.input.js';
|
|
11
12
|
import { CoreAiConnectionCreateInput } from './inputs/core-ai-connection-create.input.js';
|
|
@@ -19,6 +20,7 @@ import { CoreAiPromptUpdateInput } from './inputs/core-ai-prompt-update.input.js
|
|
|
19
20
|
import { CoreAiSlotCreateInput } from './inputs/core-ai-slot-create.input.js';
|
|
20
21
|
import { CoreAiSlotUpdateInput } from './inputs/core-ai-slot-update.input.js';
|
|
21
22
|
import { CoreAiPromptInput } from './inputs/core-ai-prompt.input.js';
|
|
23
|
+
import { CoreAiSettingsInput } from './inputs/core-ai-settings.input.js';
|
|
22
24
|
import { CoreAiAvailableConnection } from './models/core-ai-available-connection.model.js';
|
|
23
25
|
import { CoreAiBudgetLimit } from './models/core-ai-budget-limit.model.js';
|
|
24
26
|
import { CoreAiConnectionPreference } from './models/core-ai-connection-preference.model.js';
|
|
@@ -29,6 +31,8 @@ import { CoreAiPromptHint } from './models/core-ai-prompt-hint.model.js';
|
|
|
29
31
|
import { CoreAiPrompt } from './models/core-ai-prompt.model.js';
|
|
30
32
|
import { CoreAiSlot } from './models/core-ai-slot.model.js';
|
|
31
33
|
import { CoreAiResponse } from './models/core-ai-response.model.js';
|
|
34
|
+
import { CoreAiSettings } from './models/core-ai-settings.model.js';
|
|
35
|
+
import { CoreAiStatus } from './models/core-ai-status.model.js';
|
|
32
36
|
import { CoreAiUsageInfo } from './models/core-ai-usage-info.model.js';
|
|
33
37
|
import { CoreAiBudgetService } from './services/core-ai-budget.service.js';
|
|
34
38
|
import { CoreAiConnectionPreferenceService } from './services/core-ai-connection-preference.service.js';
|
|
@@ -37,6 +41,7 @@ import { CoreAiConnectionService } from './services/core-ai-connection.service.j
|
|
|
37
41
|
import { CoreAiConversationService } from './services/core-ai-conversation.service.js';
|
|
38
42
|
import { CoreAiInteractionService } from './services/core-ai-interaction.service.js';
|
|
39
43
|
import { CoreAiPlaceholderRegistry } from './services/core-ai-placeholder.registry.js';
|
|
44
|
+
import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
|
|
40
45
|
import { CoreAiPromptHintService } from './services/core-ai-prompt-hint.service.js';
|
|
41
46
|
import { CoreAiPromptService } from './services/core-ai-prompt.service.js';
|
|
42
47
|
import { CoreAiSlotService } from './services/core-ai-slot.service.js';
|
|
@@ -45,6 +50,7 @@ import { CoreAiService } from './services/core-ai.service.js';
|
|
|
45
50
|
/**
|
|
46
51
|
* REST controller for AI prompts and connection management.
|
|
47
52
|
*
|
|
53
|
+
* - `GET /ai/status`: everyone, also without a session.
|
|
48
54
|
* - `POST /ai/prompt`: any authenticated user.
|
|
49
55
|
* - `/ai/connections*`: admin only.
|
|
50
56
|
*
|
|
@@ -67,6 +73,29 @@ export class CoreAiController {
|
|
|
67
73
|
protected readonly placeholderRegistry: CoreAiPlaceholderRegistry,
|
|
68
74
|
) {}
|
|
69
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Personal AI settings (12.3.0+). Injected as a property, not through the constructor, so a project subclass
|
|
78
|
+
* written before it keeps compiling and still gets the settings endpoints.
|
|
79
|
+
*/
|
|
80
|
+
@Inject(CoreAiUserSettingService)
|
|
81
|
+
protected readonly userSettingService: CoreAiUserSettingService;
|
|
82
|
+
|
|
83
|
+
/** Whether the MCP endpoint is registered (`ai.mcp`, 12.3.1+). A property for the same reason as the settings. */
|
|
84
|
+
@Optional()
|
|
85
|
+
@Inject(AI_MCP_ENABLED)
|
|
86
|
+
protected readonly mcpEnabled?: boolean;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* What the AI module offers on this server (12.3.1+). Public on purpose: a frontend asks before anybody signs in,
|
|
90
|
+
* to decide whether it shows the assistant at all. The route exists only while the module is registered, so a
|
|
91
|
+
* `404` is the answer "AI is off"; the body names no user data.
|
|
92
|
+
*/
|
|
93
|
+
@Get('status')
|
|
94
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
95
|
+
async status(): Promise<CoreAiStatus> {
|
|
96
|
+
return { mcp: this.mcpEnabled === true };
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
/**
|
|
71
100
|
* Send a prompt to the AI assistant.
|
|
72
101
|
*/
|
|
@@ -362,6 +391,31 @@ export class CoreAiController {
|
|
|
362
391
|
return this.budgetService.getUsageInfo(serviceOptions?.currentUser?.id, RequestContext.getTenantId());
|
|
363
392
|
}
|
|
364
393
|
|
|
394
|
+
// ===================================================================================================================
|
|
395
|
+
// Personal settings
|
|
396
|
+
// ===================================================================================================================
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The current user's AI settings as they take effect.
|
|
400
|
+
*/
|
|
401
|
+
@Get('settings')
|
|
402
|
+
@Roles(RoleEnum.S_USER)
|
|
403
|
+
async settings(@RESTServiceOptions() serviceOptions: ServiceOptions): Promise<CoreAiSettings> {
|
|
404
|
+
return this.userSettingService.getSettings(serviceOptions?.currentUser?.id);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Change the current user's own AI settings (403 for a setting the server enforces).
|
|
409
|
+
*/
|
|
410
|
+
@Put('settings')
|
|
411
|
+
@Roles(RoleEnum.S_USER)
|
|
412
|
+
async updateSettings(
|
|
413
|
+
@RESTServiceOptions() serviceOptions: ServiceOptions,
|
|
414
|
+
@Body() input: CoreAiSettingsInput,
|
|
415
|
+
): Promise<CoreAiSettings> {
|
|
416
|
+
return this.userSettingService.updateSettings(serviceOptions?.currentUser?.id, input);
|
|
417
|
+
}
|
|
418
|
+
|
|
365
419
|
/**
|
|
366
420
|
* Find AI budget limits (admin).
|
|
367
421
|
*/
|
|
@@ -8,9 +8,12 @@ import { CoreAiMcpOAuthService } from './services/core-ai-mcp-oauth.service.js';
|
|
|
8
8
|
import { CoreAiMcpService } from './services/core-ai-mcp.service.js';
|
|
9
9
|
import { CorePersistenceModule } from '../../common/persistence/core-persistence.module.js';
|
|
10
10
|
import {
|
|
11
|
+
AI_MCP_ENABLED,
|
|
11
12
|
AI_MCP_OAUTH_CLIENT_MODEL,
|
|
12
13
|
AI_MCP_OAUTH_CODE_MODEL,
|
|
13
14
|
AI_MCP_OAUTH_REFRESH_TOKEN_MODEL,
|
|
15
|
+
AI_USER_SETTING_CLASS,
|
|
16
|
+
AI_USER_SETTING_MODEL,
|
|
14
17
|
} from './core-ai.constants.js';
|
|
15
18
|
import { AiBudgetLimitSchema, CoreAiBudgetLimit } from './models/core-ai-budget-limit.model.js';
|
|
16
19
|
import {
|
|
@@ -31,6 +34,7 @@ import { AiSlotSchema, CoreAiSlot } from './models/core-ai-slot.model.js';
|
|
|
31
34
|
import { AiModeSchema, CoreAiMode } from './models/core-ai-mode.model.js';
|
|
32
35
|
import { AiToolGrantSchema, CoreAiToolGrant } from './models/core-ai-tool-grant.model.js';
|
|
33
36
|
import { AiToolPolicySchema, CoreAiToolPolicy } from './models/core-ai-tool-policy.model.js';
|
|
37
|
+
import { AiUserSettingSchema, CoreAiUserSetting } from './models/core-ai-user-setting.model.js';
|
|
34
38
|
import { LlmProviderFactory } from './providers/llm-provider.factory.js';
|
|
35
39
|
import { AiCryptoService } from './services/ai-crypto.service.js';
|
|
36
40
|
import {
|
|
@@ -79,6 +83,7 @@ import {
|
|
|
79
83
|
CoreAiToolPolicyService,
|
|
80
84
|
} from './services/core-ai-tool-policy.service.js';
|
|
81
85
|
import { AI_MODE_CLASS, AI_MODE_MODEL, CoreAiModeService } from './services/core-ai-mode.service.js';
|
|
86
|
+
import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
|
|
82
87
|
import { CoreAiMcpClientService } from './services/core-ai-mcp-client.service.js';
|
|
83
88
|
import { CoreAiService } from './services/core-ai.service.js';
|
|
84
89
|
import { AiHookRegistry } from './hooks/ai-hook.registry.js';
|
|
@@ -149,6 +154,9 @@ export interface CoreAiModuleOptions {
|
|
|
149
154
|
/** Custom scoped tool-policy store (extends CoreAiToolPolicyService). */
|
|
150
155
|
toolPolicyService?: Type<CoreAiToolPolicyService>;
|
|
151
156
|
|
|
157
|
+
/** Custom personal-settings store (extends CoreAiUserSettingService). */
|
|
158
|
+
userSettingService?: Type<CoreAiUserSettingService>;
|
|
159
|
+
|
|
152
160
|
/** Custom named-mode store (extends CoreAiModeService). */
|
|
153
161
|
modeService?: Type<CoreAiModeService>;
|
|
154
162
|
|
|
@@ -208,6 +216,7 @@ export class CoreAiModule {
|
|
|
208
216
|
{ model: CoreAiToolGrant, name: AI_TOOL_GRANT_MODEL, schema: AiToolGrantSchema },
|
|
209
217
|
{ model: CoreAiToolPolicy, name: AI_TOOL_POLICY_MODEL, schema: AiToolPolicySchema },
|
|
210
218
|
{ model: CoreAiMode, name: AI_MODE_MODEL, schema: AiModeSchema },
|
|
219
|
+
{ model: CoreAiUserSetting, name: AI_USER_SETTING_MODEL, schema: AiUserSettingSchema },
|
|
211
220
|
// Stored natively on MongoDB; the models give them their tables on a SQL source
|
|
212
221
|
{ name: AI_MCP_OAUTH_CLIENT_MODEL, schema: AiMcpOAuthClientSchema },
|
|
213
222
|
{ name: AI_MCP_OAUTH_CODE_MODEL, schema: AiMcpOAuthCodeSchema },
|
|
@@ -215,6 +224,7 @@ export class CoreAiModule {
|
|
|
215
224
|
]);
|
|
216
225
|
const ToolGrantServiceClass = options.toolGrantService || CoreAiToolGrantService;
|
|
217
226
|
const ToolPolicyServiceClass = options.toolPolicyService || CoreAiToolPolicyService;
|
|
227
|
+
const UserSettingServiceClass = options.userSettingService || CoreAiUserSettingService;
|
|
218
228
|
const ModeServiceClass = options.modeService || CoreAiModeService;
|
|
219
229
|
const McpClientServiceClass = options.mcpClientService || CoreAiMcpClientService;
|
|
220
230
|
const McpOAuthServiceClass = options.mcpOAuthService || CoreAiMcpOAuthService;
|
|
@@ -250,6 +260,7 @@ export class CoreAiModule {
|
|
|
250
260
|
CoreAiService,
|
|
251
261
|
CoreAiToolGrantService,
|
|
252
262
|
CoreAiToolPolicyService,
|
|
263
|
+
CoreAiUserSettingService,
|
|
253
264
|
CoreAiModeService,
|
|
254
265
|
CoreAiMcpClientService,
|
|
255
266
|
LlmProviderFactory,
|
|
@@ -264,6 +275,7 @@ export class CoreAiModule {
|
|
|
264
275
|
AskUserQuestionAiTool,
|
|
265
276
|
SearchToolsAiTool,
|
|
266
277
|
LlmProviderFactory,
|
|
278
|
+
{ provide: AI_MCP_ENABLED, useValue: options.mcpEnabled === true },
|
|
267
279
|
{ provide: AI_BUDGET_LIMIT_CLASS, useValue: CoreAiBudgetLimit },
|
|
268
280
|
{ provide: AI_CONNECTION_CLASS, useValue: CoreAiConnection },
|
|
269
281
|
{ provide: AI_CONNECTION_PREFERENCE_CLASS, useValue: CoreAiConnectionPreference },
|
|
@@ -275,6 +287,7 @@ export class CoreAiModule {
|
|
|
275
287
|
{ provide: AI_TOOL_GRANT_CLASS, useValue: CoreAiToolGrant },
|
|
276
288
|
{ provide: AI_TOOL_POLICY_CLASS, useValue: CoreAiToolPolicy },
|
|
277
289
|
{ provide: AI_MODE_CLASS, useValue: CoreAiMode },
|
|
290
|
+
{ provide: AI_USER_SETTING_CLASS, useValue: CoreAiUserSetting },
|
|
278
291
|
{ provide: CoreAiBudgetService, useClass: BudgetServiceClass },
|
|
279
292
|
{ provide: CoreAiConversationService, useClass: ConversationServiceClass },
|
|
280
293
|
{ provide: CoreAiInteractionService, useClass: InteractionServiceClass },
|
|
@@ -293,6 +306,7 @@ export class CoreAiModule {
|
|
|
293
306
|
{ provide: CoreAiService, useClass: ServiceClass },
|
|
294
307
|
{ provide: CoreAiToolGrantService, useClass: ToolGrantServiceClass },
|
|
295
308
|
{ provide: CoreAiToolPolicyService, useClass: ToolPolicyServiceClass },
|
|
309
|
+
{ provide: CoreAiUserSettingService, useClass: UserSettingServiceClass },
|
|
296
310
|
{ provide: CoreAiModeService, useClass: ModeServiceClass },
|
|
297
311
|
{ provide: CoreAiMcpClientService, useClass: McpClientServiceClass },
|
|
298
312
|
{ provide: CoreAiMcpOAuthService, useClass: McpOAuthServiceClass },
|