@lenne.tech/nest-server 12.2.0 → 12.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.
- package/.claude/rules/better-auth.md +31 -4
- package/.claude/rules/configurable-features.md +7 -6
- package/.claude/rules/package-management.md +13 -1
- 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/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 +2 -0
- package/dist/core/modules/ai/core-ai.constants.js +2 -0
- package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
- package/dist/core/modules/ai/core-ai.controller.d.ts +6 -0
- package/dist/core/modules/ai/core-ai.controller.js +31 -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 +8 -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 +7 -0
- package/dist/core/modules/ai/index.js +6 -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-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 +3 -1
- 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 +3 -2
- 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/package.json +20 -20
- package/src/config.env.ts +6 -2
- package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
- package/src/core/common/interfaces/server-options.interface.ts +80 -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 +163 -25
- package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
- package/src/core/modules/ai/core-ai.constants.ts +6 -0
- package/src/core/modules/ai/core-ai.controller.ts +36 -1
- package/src/core/modules/ai/core-ai.module.ts +12 -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 +18 -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-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 +7 -0
- package/src/core/modules/better-auth/README.md +32 -1
- package/src/core/modules/better-auth/better-auth.config.ts +12 -1
- 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
|
|
|
@@ -671,6 +729,53 @@ the server log; the response carries only a stable `#LTNS_0901` code.
|
|
|
671
729
|
- Answering a foreign id `404` rather than `403` is deliberate — a 403 would confirm
|
|
672
730
|
that the id exists, and the id travels in a response header and through client logs.
|
|
673
731
|
|
|
732
|
+
### Authorization and confirmation over MCP (12.3.0+)
|
|
733
|
+
|
|
734
|
+
`tools/call` applies the same gates as the chat orchestrator, in this order:
|
|
735
|
+
|
|
736
|
+
1. **Role filter** — the tool must be in the user's set (`AiToolRegistry.forUser()`).
|
|
737
|
+
2. **Tool policies** — the admin's `aiToolPolicies` rules for this call: `deny` answers an
|
|
738
|
+
error result, `ask` asks in step 4 whatever else is configured.
|
|
739
|
+
3. **`authorize()`** — the tool's data-level check on the call's arguments. A refusal, a
|
|
740
|
+
thrown error or an `allowed` that is not `true` answers an error result; nothing runs.
|
|
741
|
+
4. **Confirmation** — a `destructive` tool always, a `mutating` tool when
|
|
742
|
+
`ai.confirmation.mutating` says so (`enforced`, else `default`; MCP has no per-request
|
|
743
|
+
`requireConfirmation`), as long as MCP asks this user (below). The server asks through
|
|
744
|
+
**MCP elicitation** (a yes/no form the client shows) and runs the tool only on an
|
|
745
|
+
explicit yes. A no, a cancelled dialog, an error or a timeout (5 minutes) means "not
|
|
746
|
+
run". A client without the `elicitation` capability gets an error result instead of an
|
|
747
|
+
unconfirmed run.
|
|
748
|
+
5. **`execute()`** — with `context.authorization` from step 3.
|
|
749
|
+
|
|
750
|
+
**Whether MCP asks** is a server default each user may switch for themselves, like a
|
|
751
|
+
"bypass permissions" mode:
|
|
752
|
+
|
|
753
|
+
| `ai.mcp.confirmation` | Asks a user who has not chosen | A user's own switch |
|
|
754
|
+
| ------------------------------------ | ------------------------------ | -------------------------------------------- |
|
|
755
|
+
| unset, `true`, `{}` | yes | may switch it off |
|
|
756
|
+
| `false` | no | may switch it on |
|
|
757
|
+
| `{ default: true, enforced: true }` | yes | ignored; a change answers 403 (`#LTNS_0609`) |
|
|
758
|
+
| `{ default: false, enforced: true }` | no | ignored; a change answers 403 |
|
|
759
|
+
|
|
760
|
+
The user's switch is `PUT /ai/settings` with `{ mcpConfirmation: false }` (GraphQL
|
|
761
|
+
`updateAiSettings`), read back with `GET /ai/settings` (`aiSettings`), which answers the
|
|
762
|
+
effective value and `mcpConfirmationEnforced` so a UI can show the switch disabled. It is
|
|
763
|
+
stored per user (`aiUserSettings`, one record per user) and touches nobody else's calls.
|
|
764
|
+
Only `false` switches the default off and only `enforced: true` enforces — a misspelt value
|
|
765
|
+
never relaxes the gate. `authorize()` and the admin's `deny` / `ask` rules apply whatever
|
|
766
|
+
the switch says.
|
|
767
|
+
|
|
768
|
+
`tools/list` gives a `destructive` tool `annotations: { destructiveHint: true, readOnlyHint: false }`
|
|
769
|
+
and a `mutating` one `{ destructiveHint: false, readOnlyHint: false }` — hints for the client's own
|
|
770
|
+
UI, not a security boundary. A tool with neither flag carries no annotations: it may still write,
|
|
771
|
+
so it is never advertised as read-only, and clients keep the spec's cautious defaults.
|
|
772
|
+
|
|
773
|
+
The confirmation keeps the model from acting without the user; it cannot stop a hostile
|
|
774
|
+
client, which holds the user's credential and could call the API directly. The boundary
|
|
775
|
+
against that stays the role filter, `authorize()` and the `CrudService` inside `execute()`.
|
|
776
|
+
The question is English (an MCP request carries no language); override
|
|
777
|
+
`CoreAiMcpService.confirmationPrompt()` to localize it.
|
|
778
|
+
|
|
674
779
|
### Multi-replica / sticky sessions
|
|
675
780
|
|
|
676
781
|
An MCP session owns a **live Streamable-HTTP transport** — an open response stream
|
|
@@ -696,6 +801,16 @@ and sends operators looking in the wrong place.
|
|
|
696
801
|
|
|
697
802
|
Without Redis nothing changes: unknown ids answer `404` exactly as before.
|
|
698
803
|
|
|
804
|
+
**No sticky sessions available?** Set `ai.mcp.stateless: true` (12.3.0+). Every POST then builds its own
|
|
805
|
+
server and transport for the authenticated user (the SDK's stateless mode, `sessionIdGenerator: undefined`),
|
|
806
|
+
no `mcp-session-id` is issued, a session id a client still sends is ignored, and GET / DELETE answer `405`
|
|
807
|
+
with `Allow: POST`. Any replica can answer any request, and the user's rights and tenant are read per request
|
|
808
|
+
by construction. What is lost is what a session carries: server-initiated messages, the SSE stream, and
|
|
809
|
+
server-initiated requests — **MCP elicitation among them**. A tool that needs confirmation (above) is
|
|
810
|
+
therefore refused in this mode unless the user switched the question off (`PUT /ai/settings`) or
|
|
811
|
+
`ai.mcp.confirmation` is `false`. A server that only offers read tools is not affected.
|
|
812
|
+
`testHelper.mcpSession()` follows the setting (no session id expected or sent; `sessionId` is `''`).
|
|
813
|
+
|
|
699
814
|
### OAuth 2.1 (`ai.mcp.oauth: true`)
|
|
700
815
|
|
|
701
816
|
For generic MCP clients that auto-discover + register:
|
|
@@ -767,7 +882,13 @@ user decides whether an MCP client may act for them:
|
|
|
767
882
|
- **Allow** issues the code only with the page's CSRF token, signed with the OAuth secret and bound to
|
|
768
883
|
the user, the client, the redirect URI and the PKCE challenge (10 minutes); a stale token shows the
|
|
769
884
|
form again, a forged or tampered one too, and nothing is issued. **Deny** sends the client back with
|
|
770
|
-
`access_denied
|
|
885
|
+
`access_denied` — with the same token since 12.3.0; without it the form is shown again.
|
|
886
|
+
- **A malformed request** (a missing `code_challenge`, a wrong `response_type`, …) is answered with `400`
|
|
887
|
+
and `{ error, error_description }`, never redirected to the client (12.3.0). The redirect URI is whatever
|
|
888
|
+
the client's registrant wrote, so the SDK's error redirect made `/authorize` an open redirect on this
|
|
889
|
+
domain (RFC 9700 §4.11.2). Registration itself stays open — claude.ai, ChatGPT, Cursor and Claude Code
|
|
890
|
+
register their own URIs. Only what happens once the request reached the provider (the user's Allow or
|
|
891
|
+
Deny, and refusals raised in `authorizeConsent()`) still redirects.
|
|
771
892
|
|
|
772
893
|
The page carries its own `Content-Security-Policy` (script and style by nonce, `frame-ancestors 'none'`,
|
|
773
894
|
no `form-action`, which browsers would apply to the redirect to the client), `X-Frame-Options: DENY`,
|
|
@@ -792,8 +913,25 @@ the page offers no sign-in. Its sign-in goes through the IAM sign-in controller,
|
|
|
792
913
|
hands Better-Auth the request so Better-Auth's own login-CSRF protection runs — otherwise a hostile page
|
|
793
914
|
could sign the visitor in to the attacker's account before they approve a client.
|
|
794
915
|
|
|
795
|
-
**Your own
|
|
796
|
-
|
|
916
|
+
**Your own wording and look (12.3.0+).** Override `renderConsentPage(view)` and call
|
|
917
|
+
`renderMcpOAuthConsentPage(view, { texts, styles })`: `texts` replaces single texts per locale (merged over
|
|
918
|
+
the built-in ones, still escaped), `styles` is CSS appended inside the page's nonce'd `<style>` element. The
|
|
919
|
+
markup, the scripts and the security headers stay the framework's, so the page keeps receiving their fixes.
|
|
920
|
+
Fill both from code or configuration only, never from user or tenant input: a `<` in `styles` is neutralized
|
|
921
|
+
so it cannot end the `<style>` element, and the page's CSP blocks what CSS could load from elsewhere, but CSS
|
|
922
|
+
still decides what the page shows.
|
|
923
|
+
|
|
924
|
+
```typescript
|
|
925
|
+
protected override renderConsentPage(view: IMcpOAuthConsentView): string {
|
|
926
|
+
return renderMcpOAuthConsentPage(view, {
|
|
927
|
+
styles: ':root{--accent:#0b5}',
|
|
928
|
+
texts: { de: { intro: '{client} möchte in Ihrem Namen auf {app} zugreifen.' } },
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
**Your own page.** Override `renderConsentPage(view)` in a subclass to restyle it beyond that (keep every
|
|
934
|
+
value escaped), or `authorizeConsent(client, params, res)` for a consent UI of your own — same shape: no session
|
|
797
935
|
→ sign in, GET → show which client, POST with a CSRF token → `saveAuthorizationCode()` and redirect;
|
|
798
936
|
refuse with `AccessDeniedError` from `this.loadOAuthErrors()`. Register the subclass via
|
|
799
937
|
`CoreModule.forRoot(env, { ai: { mcpOAuthService } })`, so the MCP controller and the endpoints share it.
|
|
@@ -802,9 +940,9 @@ production secret check runs again before the endpoints are mounted (without a s
|
|
|
802
940
|
environment outside `local` / `ci` / `e2e` refuses to boot and to sign tokens at all — deployed `develop` and `test`
|
|
803
941
|
stages included).
|
|
804
942
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
943
|
+
Until 12.3.0 the SDK's error redirect made `/authorize` an open redirect in the sense of RFC 9700
|
|
944
|
+
§4.11.2, since any client may register any https URI; the framework now answers those errors itself
|
|
945
|
+
(see "A malformed request" under the consent page above).
|
|
808
946
|
|
|
809
947
|
**Refusals are OAuth errors (11.43.0).** The SDK's handlers answer an error with its OAuth code only
|
|
810
948
|
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';
|
|
@@ -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, 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';
|
|
@@ -19,6 +19,7 @@ import { CoreAiPromptUpdateInput } from './inputs/core-ai-prompt-update.input.js
|
|
|
19
19
|
import { CoreAiSlotCreateInput } from './inputs/core-ai-slot-create.input.js';
|
|
20
20
|
import { CoreAiSlotUpdateInput } from './inputs/core-ai-slot-update.input.js';
|
|
21
21
|
import { CoreAiPromptInput } from './inputs/core-ai-prompt.input.js';
|
|
22
|
+
import { CoreAiSettingsInput } from './inputs/core-ai-settings.input.js';
|
|
22
23
|
import { CoreAiAvailableConnection } from './models/core-ai-available-connection.model.js';
|
|
23
24
|
import { CoreAiBudgetLimit } from './models/core-ai-budget-limit.model.js';
|
|
24
25
|
import { CoreAiConnectionPreference } from './models/core-ai-connection-preference.model.js';
|
|
@@ -29,6 +30,7 @@ import { CoreAiPromptHint } from './models/core-ai-prompt-hint.model.js';
|
|
|
29
30
|
import { CoreAiPrompt } from './models/core-ai-prompt.model.js';
|
|
30
31
|
import { CoreAiSlot } from './models/core-ai-slot.model.js';
|
|
31
32
|
import { CoreAiResponse } from './models/core-ai-response.model.js';
|
|
33
|
+
import { CoreAiSettings } from './models/core-ai-settings.model.js';
|
|
32
34
|
import { CoreAiUsageInfo } from './models/core-ai-usage-info.model.js';
|
|
33
35
|
import { CoreAiBudgetService } from './services/core-ai-budget.service.js';
|
|
34
36
|
import { CoreAiConnectionPreferenceService } from './services/core-ai-connection-preference.service.js';
|
|
@@ -37,6 +39,7 @@ import { CoreAiConnectionService } from './services/core-ai-connection.service.j
|
|
|
37
39
|
import { CoreAiConversationService } from './services/core-ai-conversation.service.js';
|
|
38
40
|
import { CoreAiInteractionService } from './services/core-ai-interaction.service.js';
|
|
39
41
|
import { CoreAiPlaceholderRegistry } from './services/core-ai-placeholder.registry.js';
|
|
42
|
+
import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
|
|
40
43
|
import { CoreAiPromptHintService } from './services/core-ai-prompt-hint.service.js';
|
|
41
44
|
import { CoreAiPromptService } from './services/core-ai-prompt.service.js';
|
|
42
45
|
import { CoreAiSlotService } from './services/core-ai-slot.service.js';
|
|
@@ -67,6 +70,13 @@ export class CoreAiController {
|
|
|
67
70
|
protected readonly placeholderRegistry: CoreAiPlaceholderRegistry,
|
|
68
71
|
) {}
|
|
69
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Personal AI settings (12.3.0+). Injected as a property, not through the constructor, so a project subclass
|
|
75
|
+
* written before it keeps compiling and still gets the settings endpoints.
|
|
76
|
+
*/
|
|
77
|
+
@Inject(CoreAiUserSettingService)
|
|
78
|
+
protected readonly userSettingService: CoreAiUserSettingService;
|
|
79
|
+
|
|
70
80
|
/**
|
|
71
81
|
* Send a prompt to the AI assistant.
|
|
72
82
|
*/
|
|
@@ -362,6 +372,31 @@ export class CoreAiController {
|
|
|
362
372
|
return this.budgetService.getUsageInfo(serviceOptions?.currentUser?.id, RequestContext.getTenantId());
|
|
363
373
|
}
|
|
364
374
|
|
|
375
|
+
// ===================================================================================================================
|
|
376
|
+
// Personal settings
|
|
377
|
+
// ===================================================================================================================
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The current user's AI settings as they take effect.
|
|
381
|
+
*/
|
|
382
|
+
@Get('settings')
|
|
383
|
+
@Roles(RoleEnum.S_USER)
|
|
384
|
+
async settings(@RESTServiceOptions() serviceOptions: ServiceOptions): Promise<CoreAiSettings> {
|
|
385
|
+
return this.userSettingService.getSettings(serviceOptions?.currentUser?.id);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Change the current user's own AI settings (403 for a setting the server enforces).
|
|
390
|
+
*/
|
|
391
|
+
@Put('settings')
|
|
392
|
+
@Roles(RoleEnum.S_USER)
|
|
393
|
+
async updateSettings(
|
|
394
|
+
@RESTServiceOptions() serviceOptions: ServiceOptions,
|
|
395
|
+
@Body() input: CoreAiSettingsInput,
|
|
396
|
+
): Promise<CoreAiSettings> {
|
|
397
|
+
return this.userSettingService.updateSettings(serviceOptions?.currentUser?.id, input);
|
|
398
|
+
}
|
|
399
|
+
|
|
365
400
|
/**
|
|
366
401
|
* Find AI budget limits (admin).
|
|
367
402
|
*/
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
AI_MCP_OAUTH_CLIENT_MODEL,
|
|
12
12
|
AI_MCP_OAUTH_CODE_MODEL,
|
|
13
13
|
AI_MCP_OAUTH_REFRESH_TOKEN_MODEL,
|
|
14
|
+
AI_USER_SETTING_CLASS,
|
|
15
|
+
AI_USER_SETTING_MODEL,
|
|
14
16
|
} from './core-ai.constants.js';
|
|
15
17
|
import { AiBudgetLimitSchema, CoreAiBudgetLimit } from './models/core-ai-budget-limit.model.js';
|
|
16
18
|
import {
|
|
@@ -31,6 +33,7 @@ import { AiSlotSchema, CoreAiSlot } from './models/core-ai-slot.model.js';
|
|
|
31
33
|
import { AiModeSchema, CoreAiMode } from './models/core-ai-mode.model.js';
|
|
32
34
|
import { AiToolGrantSchema, CoreAiToolGrant } from './models/core-ai-tool-grant.model.js';
|
|
33
35
|
import { AiToolPolicySchema, CoreAiToolPolicy } from './models/core-ai-tool-policy.model.js';
|
|
36
|
+
import { AiUserSettingSchema, CoreAiUserSetting } from './models/core-ai-user-setting.model.js';
|
|
34
37
|
import { LlmProviderFactory } from './providers/llm-provider.factory.js';
|
|
35
38
|
import { AiCryptoService } from './services/ai-crypto.service.js';
|
|
36
39
|
import {
|
|
@@ -79,6 +82,7 @@ import {
|
|
|
79
82
|
CoreAiToolPolicyService,
|
|
80
83
|
} from './services/core-ai-tool-policy.service.js';
|
|
81
84
|
import { AI_MODE_CLASS, AI_MODE_MODEL, CoreAiModeService } from './services/core-ai-mode.service.js';
|
|
85
|
+
import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
|
|
82
86
|
import { CoreAiMcpClientService } from './services/core-ai-mcp-client.service.js';
|
|
83
87
|
import { CoreAiService } from './services/core-ai.service.js';
|
|
84
88
|
import { AiHookRegistry } from './hooks/ai-hook.registry.js';
|
|
@@ -149,6 +153,9 @@ export interface CoreAiModuleOptions {
|
|
|
149
153
|
/** Custom scoped tool-policy store (extends CoreAiToolPolicyService). */
|
|
150
154
|
toolPolicyService?: Type<CoreAiToolPolicyService>;
|
|
151
155
|
|
|
156
|
+
/** Custom personal-settings store (extends CoreAiUserSettingService). */
|
|
157
|
+
userSettingService?: Type<CoreAiUserSettingService>;
|
|
158
|
+
|
|
152
159
|
/** Custom named-mode store (extends CoreAiModeService). */
|
|
153
160
|
modeService?: Type<CoreAiModeService>;
|
|
154
161
|
|
|
@@ -208,6 +215,7 @@ export class CoreAiModule {
|
|
|
208
215
|
{ model: CoreAiToolGrant, name: AI_TOOL_GRANT_MODEL, schema: AiToolGrantSchema },
|
|
209
216
|
{ model: CoreAiToolPolicy, name: AI_TOOL_POLICY_MODEL, schema: AiToolPolicySchema },
|
|
210
217
|
{ model: CoreAiMode, name: AI_MODE_MODEL, schema: AiModeSchema },
|
|
218
|
+
{ model: CoreAiUserSetting, name: AI_USER_SETTING_MODEL, schema: AiUserSettingSchema },
|
|
211
219
|
// Stored natively on MongoDB; the models give them their tables on a SQL source
|
|
212
220
|
{ name: AI_MCP_OAUTH_CLIENT_MODEL, schema: AiMcpOAuthClientSchema },
|
|
213
221
|
{ name: AI_MCP_OAUTH_CODE_MODEL, schema: AiMcpOAuthCodeSchema },
|
|
@@ -215,6 +223,7 @@ export class CoreAiModule {
|
|
|
215
223
|
]);
|
|
216
224
|
const ToolGrantServiceClass = options.toolGrantService || CoreAiToolGrantService;
|
|
217
225
|
const ToolPolicyServiceClass = options.toolPolicyService || CoreAiToolPolicyService;
|
|
226
|
+
const UserSettingServiceClass = options.userSettingService || CoreAiUserSettingService;
|
|
218
227
|
const ModeServiceClass = options.modeService || CoreAiModeService;
|
|
219
228
|
const McpClientServiceClass = options.mcpClientService || CoreAiMcpClientService;
|
|
220
229
|
const McpOAuthServiceClass = options.mcpOAuthService || CoreAiMcpOAuthService;
|
|
@@ -250,6 +259,7 @@ export class CoreAiModule {
|
|
|
250
259
|
CoreAiService,
|
|
251
260
|
CoreAiToolGrantService,
|
|
252
261
|
CoreAiToolPolicyService,
|
|
262
|
+
CoreAiUserSettingService,
|
|
253
263
|
CoreAiModeService,
|
|
254
264
|
CoreAiMcpClientService,
|
|
255
265
|
LlmProviderFactory,
|
|
@@ -275,6 +285,7 @@ export class CoreAiModule {
|
|
|
275
285
|
{ provide: AI_TOOL_GRANT_CLASS, useValue: CoreAiToolGrant },
|
|
276
286
|
{ provide: AI_TOOL_POLICY_CLASS, useValue: CoreAiToolPolicy },
|
|
277
287
|
{ provide: AI_MODE_CLASS, useValue: CoreAiMode },
|
|
288
|
+
{ provide: AI_USER_SETTING_CLASS, useValue: CoreAiUserSetting },
|
|
278
289
|
{ provide: CoreAiBudgetService, useClass: BudgetServiceClass },
|
|
279
290
|
{ provide: CoreAiConversationService, useClass: ConversationServiceClass },
|
|
280
291
|
{ provide: CoreAiInteractionService, useClass: InteractionServiceClass },
|
|
@@ -293,6 +304,7 @@ export class CoreAiModule {
|
|
|
293
304
|
{ provide: CoreAiService, useClass: ServiceClass },
|
|
294
305
|
{ provide: CoreAiToolGrantService, useClass: ToolGrantServiceClass },
|
|
295
306
|
{ provide: CoreAiToolPolicyService, useClass: ToolPolicyServiceClass },
|
|
307
|
+
{ provide: CoreAiUserSettingService, useClass: UserSettingServiceClass },
|
|
296
308
|
{ provide: CoreAiModeService, useClass: ModeServiceClass },
|
|
297
309
|
{ provide: CoreAiMcpClientService, useClass: McpClientServiceClass },
|
|
298
310
|
{ provide: CoreAiMcpOAuthService, useClass: McpOAuthServiceClass },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Inject } from '@nestjs/common';
|
|
1
2
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
|
2
3
|
|
|
3
4
|
import { FilterArgs } from '../../common/args/filter.args.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,7 @@ 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';
|
|
32
35
|
import { CoreAiUsageInfo } from './models/core-ai-usage-info.model.js';
|
|
33
36
|
import { CoreAiBudgetService } from './services/core-ai-budget.service.js';
|
|
34
37
|
import { CoreAiConnectionPreferenceService } from './services/core-ai-connection-preference.service.js';
|
|
@@ -40,6 +43,7 @@ import { CoreAiPromptHintService } from './services/core-ai-prompt-hint.service.
|
|
|
40
43
|
import { CoreAiPromptService } from './services/core-ai-prompt.service.js';
|
|
41
44
|
import { CoreAiSlotService } from './services/core-ai-slot.service.js';
|
|
42
45
|
import { CoreAiService } from './services/core-ai.service.js';
|
|
46
|
+
import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
49
|
* GraphQL resolver for AI prompts and connection management.
|
|
@@ -67,6 +71,13 @@ export class CoreAiResolver {
|
|
|
67
71
|
protected readonly promptService: CoreAiPromptService,
|
|
68
72
|
) {}
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Personal AI settings (12.3.0+). Injected as a property, not through the constructor, so a project subclass
|
|
76
|
+
* written before it keeps compiling and still gets the settings endpoints.
|
|
77
|
+
*/
|
|
78
|
+
@Inject(CoreAiUserSettingService)
|
|
79
|
+
protected readonly userSettingService: CoreAiUserSettingService;
|
|
80
|
+
|
|
70
81
|
// ===================================================================================================================
|
|
71
82
|
// Prompt
|
|
72
83
|
// ===================================================================================================================
|
|
@@ -348,6 +359,31 @@ export class CoreAiResolver {
|
|
|
348
359
|
return this.budgetService.getUsageInfo(serviceOptions?.currentUser?.id, RequestContext.getTenantId());
|
|
349
360
|
}
|
|
350
361
|
|
|
362
|
+
// ===================================================================================================================
|
|
363
|
+
// Personal settings
|
|
364
|
+
// ===================================================================================================================
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The current user's AI settings as they take effect.
|
|
368
|
+
*/
|
|
369
|
+
@Query(() => CoreAiSettings, { description: "The current user's effective AI settings" })
|
|
370
|
+
@Roles(RoleEnum.S_USER)
|
|
371
|
+
async aiSettings(@GraphQLServiceOptions() serviceOptions: ServiceOptions): Promise<CoreAiSettings> {
|
|
372
|
+
return this.userSettingService.getSettings(serviceOptions?.currentUser?.id);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Change the current user's own AI settings (403 for a setting the server enforces).
|
|
377
|
+
*/
|
|
378
|
+
@Mutation(() => CoreAiSettings, { description: "Change the current user's own AI settings" })
|
|
379
|
+
@Roles(RoleEnum.S_USER)
|
|
380
|
+
async updateAiSettings(
|
|
381
|
+
@GraphQLServiceOptions() serviceOptions: ServiceOptions,
|
|
382
|
+
@Args('input') input: CoreAiSettingsInput,
|
|
383
|
+
): Promise<CoreAiSettings> {
|
|
384
|
+
return this.userSettingService.updateSettings(serviceOptions?.currentUser?.id, input);
|
|
385
|
+
}
|
|
386
|
+
|
|
351
387
|
/**
|
|
352
388
|
* Create an AI budget limit (admin).
|
|
353
389
|
*/
|