@lenne.tech/nest-server 11.32.1 → 11.32.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/configurable-features.md +52 -1
- package/FRAMEWORK-API.md +4 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +3 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js +5 -3
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js +2 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js.map +1 -1
- package/dist/core/modules/ai/models/core-ai-mode.model.js.map +1 -1
- package/dist/core/modules/ai/models/core-ai-tool-policy.model.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js +68 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.d.ts +6 -1
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +66 -7
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai.service.d.ts +1 -0
- package/dist/core/modules/ai/services/core-ai.service.js +13 -10
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/migration-guides/11.25.x-to-11.26.0.md +3 -7
- package/migration-guides/11.32.1-to-11.32.2.md +173 -0
- package/migration-guides/11.32.2-to-11.32.3.md +129 -0
- package/package.json +1 -1
- package/src/core/common/interfaces/server-options.interface.ts +72 -0
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +32 -10
- package/src/core/modules/ai/README.md +82 -14
- package/src/core/modules/ai/core-ai-mcp.controller.ts +28 -12
- package/src/core/modules/ai/inputs/core-ai-connection.input.ts +2 -0
- package/src/core/modules/ai/interfaces/ai-hook.interface.ts +2 -1
- package/src/core/modules/ai/interfaces/ai-tool.interface.ts +30 -7
- package/src/core/modules/ai/models/core-ai-mode.model.ts +2 -1
- package/src/core/modules/ai/models/core-ai-tool-grant.model.ts +1 -1
- package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +2 -1
- package/src/core/modules/ai/services/core-ai-connection.service.ts +135 -0
- package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +140 -7
- package/src/core/modules/ai/services/core-ai.service.ts +36 -13
|
@@ -74,13 +74,9 @@ Tools self-register in the global `AiToolRegistry`. Implement `IAiTool` (or exte
|
|
|
74
74
|
|
|
75
75
|
#### 4. Enable the MCP server (optional)
|
|
76
76
|
|
|
77
|
-
If you set `ai: { mcp: true }` (or `ai: { mcp: { oauth: true, oauthSecret: '…' } }`),
|
|
77
|
+
If you set `ai: { mcp: true }` (or `ai: { mcp: { oauth: true, oauthSecret: '…' } }`), no install step is needed.
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
pnpm add @modelcontextprotocol/sdk
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
The SDK is a peer-style optional dependency that the controller lazy-imports on the first MCP request — projects without MCP pay no install cost. When `ai.mcp` is set but the SDK is missing, `/ai/mcp` returns **503 Service Unavailable** with the install hint instead of a 500 trace.
|
|
79
|
+
> **Correction (11.32.2):** this guide originally instructed `pnpm add @modelcontextprotocol/sdk` and called the SDK a "peer-style optional dependency". That was wrong. It is a regular `dependency` of `@lenne.tech/nest-server` and reaches both consumption modes — npm-mode transitively, CLI-vendored projects through the dependency merge. The controller still lazy-imports it on the first MCP request, so a project without MCP pays no startup cost. A 503 from `/ai/mcp` therefore means the module could not be RESOLVED; the underlying error is in the server log.
|
|
84
80
|
|
|
85
81
|
For OAuth 2.1 (generic MCP clients with auto-discovery + dynamic registration), mount the OAuth router in `main.ts`:
|
|
86
82
|
|
|
@@ -264,7 +260,7 @@ If you cast to `as any` to pass these to `CoreModule.forRoot(...)`, you can drop
|
|
|
264
260
|
| `aiPrompt` returns "No AI service is currently available" | No usable connection exists — create one (admin) or set `AI_BASE_URL` to seed a default |
|
|
265
261
|
| API keys encrypted with a dev default (warning logged) | Set `NSC__AI__ENCRYPTION_SECRET` (32+ chars) in production |
|
|
266
262
|
| Custom MCP controller endpoints return 404 | Ensure the MCP controller is enabled (`ai.mcp`) and OAuth is mounted in `main.ts` when using `ai.mcp.oauth` |
|
|
267
|
-
| `/ai/mcp` returns 503
|
|
263
|
+
| `/ai/mcp` returns 503 | Since 11.32.2 the body carries `#LTNS_0901` and points at the log. The SDK is a regular dependency and reaches both npm and vendor mode, so this means it could not be RESOLVED, not that it is missing — check the server log for the underlying error |
|
|
268
264
|
| Token budgets not enforced | Budgets require `ai.audit: true` (usage is read from `aiInteractions`) |
|
|
269
265
|
| `aiPrompt` returns 500 "BSONError: input must be a 24 character hex string" | A client sent `conversationId: "null"` (literal string). The orchestrator now treats `"null"`/`"undefined"` strings as "no conversation" — upgrade to the latest 11.26.x patch if you still see this |
|
|
270
266
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Migration Guide: 11.32.1 → 11.32.2
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None |
|
|
8
|
+
| **New Features** | `ai.deferToolSummaryChars` — cap tool descriptions in the deferred tool catalog |
|
|
9
|
+
| **Bugfixes** | Quadratic-backtracking regex in the tool-catalog builder (event-loop stall); three orchestrator messages were English-only; `context` was not framed as untrusted; misleading MCP 503 message |
|
|
10
|
+
| **Migration Effort** | ~5 minutes — read §2 if you assert on AI response text, §3 if you use `deferToolSchemas` |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm update @lenne.tech/nest-server@11.32.2
|
|
18
|
+
pnpm run build
|
|
19
|
+
pnpm test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
No configuration change is required. Everything below is either opt-in or a
|
|
23
|
+
correction you inherit automatically.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 1. Fixed: event-loop stall in the tool-catalog builder
|
|
28
|
+
|
|
29
|
+
`CoreAiPromptBuilderService` used a sentence-splitting regex whose leading greedy
|
|
30
|
+
character class made it **quadratic** in the description length. A tool description
|
|
31
|
+
of 100 KB without a sentence terminator blocked the event loop for **~6.7 seconds**
|
|
32
|
+
— on every prompt build, single-threaded, for the whole API.
|
|
33
|
+
|
|
34
|
+
This is relevant to you if tool descriptions can come from outside your codebase:
|
|
35
|
+
`CoreAiMcpClientService.buildWrapperTool` adopts whatever a remote MCP server
|
|
36
|
+
advertises, uncapped.
|
|
37
|
+
|
|
38
|
+
Fixed by matching the terminator via lookahead instead. Runtime is now bounded by
|
|
39
|
+
the configured cap rather than by input length (100 KB: 6.7 s → 0.02 ms).
|
|
40
|
+
|
|
41
|
+
**Action: none.** Behaviour is unchanged for realistic descriptions. The only inputs
|
|
42
|
+
that summarise differently are those that *begin* with punctuation or place a
|
|
43
|
+
terminator directly after whitespace — the result stays a prefix of the original and
|
|
44
|
+
still respects the cap.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 2. Three orchestrator messages are now translated
|
|
49
|
+
|
|
50
|
+
These were hard-coded English regardless of the requested language:
|
|
51
|
+
|
|
52
|
+
| Situation | Now returns |
|
|
53
|
+
|-----------|-------------|
|
|
54
|
+
| Confirmation required for an action | `translate('confirm_required')` |
|
|
55
|
+
| No final answer within `maxIterations` | `translate('no_final_answer')` |
|
|
56
|
+
| Action blocked by policy | `translate('blocked_by_policy')` — the English `\|\|` fallback was removed |
|
|
57
|
+
|
|
58
|
+
**Action required if** your frontend or tests match on the English strings — e.g.
|
|
59
|
+
`expect(response.text).toContain('could not produce a final answer')`. With
|
|
60
|
+
`language: 'de'` these now return German. Assert on `requiresConfirmation`,
|
|
61
|
+
`actions` or the response shape instead of on prose.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 3. New: `ai.deferToolSummaryChars` (opt-in, default `0`)
|
|
66
|
+
|
|
67
|
+
Caps each tool description in the **deferred** catalog (`deferToolSchemas: true`).
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
ai: {
|
|
71
|
+
deferToolSchemas: true,
|
|
72
|
+
deferToolSummaryChars: 300, // 0 (default) = untruncated
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
With a large registry the descriptions alone can re-inflate the prompt that
|
|
77
|
+
`deferToolSchemas` was meant to shrink. Whole sentences are kept up to the cap, a
|
|
78
|
+
`…` marks the cut (appended **on top of** the cap, so a shortened entry is
|
|
79
|
+
`cap + 1` characters), and `search_tools` still returns the full text.
|
|
80
|
+
|
|
81
|
+
**The default `0` is deliberate**: enabling `deferToolSchemas` alone never changes
|
|
82
|
+
what a description says. The saving is therefore opt-in — set both together.
|
|
83
|
+
|
|
84
|
+
Two scoping notes:
|
|
85
|
+
|
|
86
|
+
- In **auto mode**, a connection with `supportsNativeTools: true` receives full
|
|
87
|
+
descriptions and schemas through the native `tools` payload anyway, so truncation
|
|
88
|
+
and the `search_tools` banner are skipped there.
|
|
89
|
+
- **Plan mode always uses the emulated protocol**, so a native connection *is*
|
|
90
|
+
truncated there — and since plan mode produces a complete plan in one call without
|
|
91
|
+
executing, it cannot follow the `search_tools` hint. Keep the cap generous if you
|
|
92
|
+
rely on plan mode.
|
|
93
|
+
|
|
94
|
+
> The truncated tail is typically where preconditions and role limits are written.
|
|
95
|
+
> This is model **guidance only** — authorization is enforced by the registry role
|
|
96
|
+
> filter, the execution-time re-check and the `mutating`/`destructive` flags, none of
|
|
97
|
+
> which read the description.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 4. `input.context` is now framed as UNTRUSTED
|
|
102
|
+
|
|
103
|
+
`appendClientContext()` previously labelled the structured `context` block
|
|
104
|
+
`Context (structured):` while `metadata` was explicitly marked untrusted. Both
|
|
105
|
+
arrive on the same request from the same client, so the asymmetry invited smuggling
|
|
106
|
+
instructions through the half that read as trusted. Both blocks now carry identical
|
|
107
|
+
UNTRUSTED framing, and U+2028/U+2029 (which `JSON.stringify` does **not** escape) are
|
|
108
|
+
neutralised so a client cannot fake a line break out of its block.
|
|
109
|
+
|
|
110
|
+
**Action required if** you have a prompt fragment, `aiSlots` override or test that
|
|
111
|
+
matches the old `Context (structured):` label.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 5. Corrected documentation: `AiTool.authorize()` runs in PLAN MODE only
|
|
116
|
+
|
|
117
|
+
No code change — but the docs previously implied `authorize()` was a general
|
|
118
|
+
enforcement layer. It is not: auto mode (the default) and the MCP endpoint call
|
|
119
|
+
`execute()` directly.
|
|
120
|
+
|
|
121
|
+
**Check your tools.** A data-level check (ownership, tenant scope) that lives *only*
|
|
122
|
+
in `authorize()` does not run for most callers. Put it inside `execute()`, routed
|
|
123
|
+
through `CrudService` with `context.serviceOptions`, and treat `authorize()` as the
|
|
124
|
+
plan-mode pre-flight that can reject a whole plan before any step runs.
|
|
125
|
+
|
|
126
|
+
> Also note there is **no confirmation gate over MCP**: `mcpCallTool` does not consult
|
|
127
|
+
> the `mutating`/`destructive` flags. A destructive tool invoked through `/ai/mcp`
|
|
128
|
+
> executes immediately.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 6. MCP 503 message changed shape
|
|
133
|
+
|
|
134
|
+
`/ai/mcp` returned an install hint claiming `@modelcontextprotocol/sdk` was a missing
|
|
135
|
+
"peer-style optional dependency". That was wrong: it is a regular `dependency` and
|
|
136
|
+
reaches both consumption modes (npm-mode transitively, CLI-vendored projects via the
|
|
137
|
+
dependency merge). A failure there is a **resolution** problem, not an absent package.
|
|
138
|
+
|
|
139
|
+
The body now carries the stable `#LTNS_0901` ErrorCode and points at the server log.
|
|
140
|
+
The underlying error (which contains filesystem paths) stays out of the response.
|
|
141
|
+
|
|
142
|
+
**Action required if** you match on the old message text. Branch on the ErrorCode.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 7. Test-runner change (framework repo only)
|
|
147
|
+
|
|
148
|
+
`restoreMocks: true` was added to `vitest.config.ts` and `vitest-e2e.config.ts`.
|
|
149
|
+
These files are **not** part of the vendored file set, so vendor-mode consumers are
|
|
150
|
+
unaffected. Adopting it in your own project is recommended: without it, a
|
|
151
|
+
`vi.spyOn` whose manual `mockRestore()` is skipped by a throwing assertion stays
|
|
152
|
+
installed for the rest of the worker. Note vitest restores in `onBeforeTryTask`,
|
|
153
|
+
i.e. **before** each attempt — a spy installed in `beforeAll` will not survive.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Module Documentation
|
|
158
|
+
|
|
159
|
+
- [AI module README](../src/core/modules/ai/README.md) — see "Deferred tool schemas (large registries)"
|
|
160
|
+
- [AI INTEGRATION-CHECKLIST](../src/core/modules/ai/INTEGRATION-CHECKLIST.md) — advanced configuration
|
|
161
|
+
- [Configurable features](../.claude/rules/configurable-features.md) — the Numeric Sentinel pattern
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Troubleshooting
|
|
166
|
+
|
|
167
|
+
| Symptom | Cause | Fix |
|
|
168
|
+
|---------|-------|-----|
|
|
169
|
+
| Test asserts on English AI text and fails | §2 — messages are translated now | Assert on `requiresConfirmation` / `actions`, not prose |
|
|
170
|
+
| `deferToolSummaryChars` has no effect | `deferToolSchemas` is off — the framework logs a warning once | Enable both together |
|
|
171
|
+
| Tool descriptions truncated on a native connection | Plan mode always uses the emulated protocol (§3) | Raise the cap, or set `deferToolSummaryChars: 0` |
|
|
172
|
+
| `/ai/mcp` returns 503 | The SDK could not be **resolved** (it is installed) | Check the server log for the underlying resolution error |
|
|
173
|
+
| A prompt fragment no longer matches | §4 — the `context` label changed | Update the fragment / `aiSlots` override |
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Migration Guide: 11.32.2 → 11.32.3
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None |
|
|
8
|
+
| **New Features** | `ai.capabilityDriftCheck` — opt-in boot warning when an explicit connection capability contradicts the endpoint; `ai.defaultConnection.contextWindow` — seed a connection's context window from config |
|
|
9
|
+
| **Bugfixes** | The AI connection `contextWindow` admin input now rejects non-positive / non-integer values |
|
|
10
|
+
| **Migration Effort** | ~2 minutes — everything is opt-in; read §3 only if your admin tooling writes `contextWindow` |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm update @lenne.tech/nest-server@11.32.3
|
|
18
|
+
pnpm run build
|
|
19
|
+
pnpm test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
No configuration change is required. Everything below is opt-in or a
|
|
23
|
+
tightening you inherit automatically.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 1. New: `ai.capabilityDriftCheck` (opt-in, default `false`)
|
|
28
|
+
|
|
29
|
+
A connection's `supportsNativeTools` / `supportsJsonResponse` flags are
|
|
30
|
+
auto-detected while they are left **undefined**, but an **explicit** value is
|
|
31
|
+
authoritative and is never re-probed. A wrong explicit flag therefore degrades the
|
|
32
|
+
assistant silently — e.g. `supportsNativeTools: false` on a backend that *does*
|
|
33
|
+
support native function calling forces fragile emulated tool-calling.
|
|
34
|
+
|
|
35
|
+
Enable this opt-in boot self-check to surface such a mismatch:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
ai: {
|
|
39
|
+
capabilityDriftCheck: true, // default false
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
When enabled, at startup the module probes each enabled connection that declares an
|
|
44
|
+
explicit flag and logs a **warning** on mismatch. It never changes the stored value
|
|
45
|
+
(your explicit choice stays authoritative — clear the flag in the admin UI to
|
|
46
|
+
re-enable auto-detection).
|
|
47
|
+
|
|
48
|
+
**It is off by default because it makes outbound calls to the LLM endpoints on
|
|
49
|
+
every boot** (and is additionally skipped in the `ci`/`e2e` runners). Note this is a
|
|
50
|
+
diagnostic log only: if you deliberately override a flag against what the endpoint
|
|
51
|
+
reports (e.g. native tools are advertised but unreliable on your model), the warning
|
|
52
|
+
is expected and can be ignored.
|
|
53
|
+
|
|
54
|
+
**Action: none** unless you want the diagnostic — then set the flag.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 2. New: `ai.defaultConnection.contextWindow`
|
|
59
|
+
|
|
60
|
+
The one-time `ai.defaultConnection` seed now accepts `contextWindow`, so a
|
|
61
|
+
config-seeded connection can carry its context window from the start instead of
|
|
62
|
+
relying on auto-detection:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
ai: {
|
|
66
|
+
defaultConnection: {
|
|
67
|
+
name: 'default',
|
|
68
|
+
baseUrl: '...',
|
|
69
|
+
model: '...',
|
|
70
|
+
contextWindow: 32768, // optional — omit to auto-detect
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Omit it to keep the existing behaviour (auto-detect by probing the endpoint /
|
|
76
|
+
`knownContextWindow()`, falling back to the global `ai.contextWindow` default of
|
|
77
|
+
8192).
|
|
78
|
+
|
|
79
|
+
**Action: none** — additive and optional.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 3. Tightened: `contextWindow` admin input validation
|
|
84
|
+
|
|
85
|
+
The `contextWindow` field on the AI connection create/update input is now validated
|
|
86
|
+
as a **positive integer** (`@IsInt` + `@Min(1)`). Previously any number (including
|
|
87
|
+
`0`, a negative, or a float) was accepted verbatim, and an explicit `0` is not
|
|
88
|
+
"unset" — it would have fed the orchestrator's context budget as a real (broken)
|
|
89
|
+
value instead of falling back to the safe default.
|
|
90
|
+
|
|
91
|
+
**Action required if** your admin tooling or tests send a non-integer or
|
|
92
|
+
`< 1` `contextWindow` to `createAiConnection` / `updateAiConnection` — those calls
|
|
93
|
+
now return a validation error. Send a positive integer, or omit the field to
|
|
94
|
+
auto-detect.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 4. Documentation: the MCP confirmation-gate boundary
|
|
99
|
+
|
|
100
|
+
No code change — a clarification of the `IAiTool.destructive` / `.mutating` JSDoc
|
|
101
|
+
(reinforcing §5 of the 11.32.2 guide). Over `/ai/mcp` there is **no confirmation
|
|
102
|
+
gate**: `mcpCallTool` consults neither flag, so a destructive tool executes on the
|
|
103
|
+
first call. The barriers that *do* hold on every path are the registry **role
|
|
104
|
+
filter** (`forUser()`, applied before `execute()`) and the authorization inside
|
|
105
|
+
`execute()` itself — so a destructive tool restricted to a real role stays
|
|
106
|
+
unreachable by lesser-privileged MCP clients; MCP only skips the extra confirmation
|
|
107
|
+
step for clients that may already see the tool.
|
|
108
|
+
|
|
109
|
+
**Action:** expose `/ai/mcp` only to clients you trust to obtain user consent
|
|
110
|
+
themselves, and keep data-level authorization inside `execute()` (not only in the
|
|
111
|
+
plan-mode `authorize()`).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Module Documentation
|
|
116
|
+
|
|
117
|
+
- [AI module README](../src/core/modules/ai/README.md) — see "Capability auto-detection"
|
|
118
|
+
- [AI INTEGRATION-CHECKLIST](../src/core/modules/ai/INTEGRATION-CHECKLIST.md) — advanced configuration
|
|
119
|
+
- [Configurable features](../.claude/rules/configurable-features.md) — the AI Assistant row
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Troubleshooting
|
|
124
|
+
|
|
125
|
+
| Symptom | Cause | Fix |
|
|
126
|
+
|---------|-------|-----|
|
|
127
|
+
| Boot logs "capability drift" for a connection | §1 — an explicit flag disagrees with the endpoint | Correct the flag in the admin UI, or clear it to auto-detect; ignore if the override is deliberate |
|
|
128
|
+
| `createAiConnection` rejects `contextWindow` | §3 — it must be a positive integer now | Send an integer `>= 1`, or omit to auto-detect |
|
|
129
|
+
| A destructive tool ran over `/ai/mcp` without confirmation | §4 — MCP has no confirmation gate by design | Only expose MCP to trusted clients; enforce data-level checks in `execute()` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.32.
|
|
3
|
+
"version": "11.32.3",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -1110,6 +1110,16 @@ export interface IAiDefaultConnection {
|
|
|
1110
1110
|
/** Capability tags (free-form, e.g. 'analysis', 'vision'). */
|
|
1111
1111
|
capabilities?: string[];
|
|
1112
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* Total context window (input + output tokens) the model supports. Drives the
|
|
1115
|
+
* orchestrator's context budget (system prompt + history + tool results). Omit to
|
|
1116
|
+
* auto-detect by probing the endpoint / `knownContextWindow()`; set it explicitly
|
|
1117
|
+
* when the endpoint exposes no limit and the model id is unknown to the heuristic
|
|
1118
|
+
* (otherwise the orchestrator assumes the conservative `ai.contextWindow` default
|
|
1119
|
+
* of 8192 and trims the prompt + tool results on every turn).
|
|
1120
|
+
*/
|
|
1121
|
+
contextWindow?: number;
|
|
1122
|
+
|
|
1113
1123
|
/** Default maximum number of tokens for completions. */
|
|
1114
1124
|
defaultMaxTokens?: number;
|
|
1115
1125
|
|
|
@@ -1212,6 +1222,19 @@ export interface IAi {
|
|
|
1212
1222
|
user?: { maxPrompts?: number; maxTokens?: number };
|
|
1213
1223
|
};
|
|
1214
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* Opt-in boot self-check: after startup, probe each enabled connection that declares
|
|
1227
|
+
* an EXPLICIT `supportsNativeTools` / `supportsJsonResponse` and warn (log only) when
|
|
1228
|
+
* the declared value contradicts what the endpoint actually reports — a wrong explicit
|
|
1229
|
+
* flag otherwise silently degrades the assistant (e.g. forcing fragile emulated
|
|
1230
|
+
* tool-calling on a backend that supports native function calling). OFF by default
|
|
1231
|
+
* because it makes outbound calls to the LLM endpoints on every boot; the declared
|
|
1232
|
+
* value is never changed (clear it in the admin UI to re-enable auto-detection). Also
|
|
1233
|
+
* skipped in the ci/e2e runners.
|
|
1234
|
+
* @default false
|
|
1235
|
+
*/
|
|
1236
|
+
capabilityDriftCheck?: boolean;
|
|
1237
|
+
|
|
1215
1238
|
/**
|
|
1216
1239
|
* Confirmation policy for mutating tool actions (create/update/delete).
|
|
1217
1240
|
* `destructive` tools always require confirmation regardless of this policy.
|
|
@@ -1294,6 +1317,55 @@ export interface IAi {
|
|
|
1294
1317
|
*/
|
|
1295
1318
|
deferToolSchemas?: boolean;
|
|
1296
1319
|
|
|
1320
|
+
/**
|
|
1321
|
+
* Maximum characters per tool description in the DEFERRED catalog
|
|
1322
|
+
* (`deferToolSchemas: true`). Whole sentences are kept up to this cap (always
|
|
1323
|
+
* at least the first one); when the first sentence alone exceeds the cap the
|
|
1324
|
+
* text is cut on a word boundary. A shortened description always ends in `…`,
|
|
1325
|
+
* which is appended ON TOP of the cap — a shortened entry is therefore
|
|
1326
|
+
* `deferToolSummaryChars + 1` characters, not exactly the cap. The full text
|
|
1327
|
+
* stays available through the `search_tools` meta-tool.
|
|
1328
|
+
*
|
|
1329
|
+
* Without a cap a large tool registry re-inflates the very prompt that
|
|
1330
|
+
* `deferToolSchemas` was meant to shrink: with enough tools the descriptions
|
|
1331
|
+
* alone can outweigh the schemas they replaced and consume the majority of a
|
|
1332
|
+
* small context window. With a cap, mind that the omitted tail is where tool
|
|
1333
|
+
* authors typically put preconditions and role restrictions; the catalog
|
|
1334
|
+
* banner tells the model to fetch the full text before calling such a tool.
|
|
1335
|
+
* Note this is model GUIDANCE only — it never relaxes authorization. Which tools
|
|
1336
|
+
* a caller sees and may run is decided server-side by the registry's role filter
|
|
1337
|
+
* (`AiToolRegistry.forUser()`), re-checked at execution time, and the
|
|
1338
|
+
* confirmation gate reads the `mutating`/`destructive` FLAGS, never the
|
|
1339
|
+
* description text. (`AiTool.authorize()` runs in plan mode only — in auto mode
|
|
1340
|
+
* and over MCP, data-level checks belong inside `execute()`.)
|
|
1341
|
+
*
|
|
1342
|
+
* Applies to the EMULATED tool protocol, not to a provider as such. In auto mode
|
|
1343
|
+
* a connection with native tool calling receives every full description and schema
|
|
1344
|
+
* through `buildToolSchemas()` regardless, so no truncation is applied there and no
|
|
1345
|
+
* `search_tools` banner is emitted — capping would otherwise assert a truncation
|
|
1346
|
+
* that the tool payload right next to it contradicts.
|
|
1347
|
+
*
|
|
1348
|
+
* **Plan mode always uses the emulated protocol** (it sends no native schemas), so a
|
|
1349
|
+
* native connection IS truncated there. Because plan mode returns a complete plan in
|
|
1350
|
+
* one call and executes nothing, the model cannot act on a `search_tools` hint before
|
|
1351
|
+
* committing — the catalog therefore carries a different banner there, telling it the
|
|
1352
|
+
* descriptions are abbreviated and to plan conservatively. The dropped tail stays
|
|
1353
|
+
* unrecoverable for planning, so keep the cap generous if you rely on plan mode.
|
|
1354
|
+
*
|
|
1355
|
+
* `0` (the default) keeps the untruncated descriptions, so enabling
|
|
1356
|
+
* `deferToolSchemas` alone never changes what a tool description says. That
|
|
1357
|
+
* backward-compatible default means the prompt saving is opt-in: when you turn
|
|
1358
|
+
* on `deferToolSchemas` to reclaim context, set this too — roughly 200–400 is
|
|
1359
|
+
* a good starting point, low enough to shrink a large catalog while still
|
|
1360
|
+
* carrying a full first sentence per tool.
|
|
1361
|
+
*
|
|
1362
|
+
* Setting this WITHOUT `deferToolSchemas` does nothing (the cap only applies to
|
|
1363
|
+
* the deferred catalog); the framework logs a warning once at runtime rather
|
|
1364
|
+
* than ignoring it silently.
|
|
1365
|
+
* @default 0
|
|
1366
|
+
*/
|
|
1367
|
+
deferToolSummaryChars?: number;
|
|
1368
|
+
|
|
1297
1369
|
/** Maximum number of agent-loop iterations (tool round-trips). @default 5 */
|
|
1298
1370
|
maxIterations?: number;
|
|
1299
1371
|
|
|
@@ -137,6 +137,8 @@ ai: {
|
|
|
137
137
|
},
|
|
138
138
|
contextWindow: 8192, // fallback when a connection has no auto-detected window
|
|
139
139
|
maxToolResultChars: 12000, // cap tool-results fed back to the model
|
|
140
|
+
deferToolSchemas: false, // catalog = names + descriptions only; schemas via search_tools
|
|
141
|
+
deferToolSummaryChars: 0, // 0 = full descriptions; set ~200-400 WITH deferToolSchemas
|
|
140
142
|
promptLearning: { autoApply: false },// governed self-improvement (admins approve learned hints)
|
|
141
143
|
mcp: { oauth: true, oauthSecret: process.env.NSC__AI__ENCRYPTION_SECRET },
|
|
142
144
|
}
|
|
@@ -165,21 +167,41 @@ ai: {
|
|
|
165
167
|
- **Context window:** detected automatically per connection and persisted; no setup needed.
|
|
166
168
|
Override per connection (`contextWindow`) or globally (`ai.contextWindow`) if a backend
|
|
167
169
|
isn't recognized.
|
|
170
|
+
- **Many tools?** `deferToolSchemas: true` keeps the full JSON schemas out of the system
|
|
171
|
+
prompt (the model fetches one on demand via the built-in `search_tools` meta-tool). Set
|
|
172
|
+
`deferToolSummaryChars` (~200–400) ALONGSIDE it — otherwise the descriptions alone
|
|
173
|
+
re-inflate the prompt you just shrank. Both are scoped to the **emulated tool protocol**:
|
|
174
|
+
in auto mode a connection with `supportsNativeTools: true` gets every full description and
|
|
175
|
+
schema in the native `tools` payload regardless, so truncation and the banner are skipped
|
|
176
|
+
there. **Plan mode always uses the emulated protocol**, so a native connection IS truncated
|
|
177
|
+
there — and since plan mode plans in one call without executing, it cannot follow the
|
|
178
|
+
`search_tools` hint. Keep the cap generous if you use plan mode.
|
|
179
|
+
The truncated tail is typically where a tool's preconditions and role limits are written;
|
|
180
|
+
the model is told to fetch the full text first, but that is guidance only — authorization
|
|
181
|
+
stays with the registry's role filter, the execution-time re-check, and the
|
|
182
|
+
`mutating`/`destructive` flags behind the confirmation gate, none of which read the
|
|
183
|
+
description.
|
|
184
|
+
- **Where data-level checks belong:** `authorize()` runs in **plan mode only**. In auto mode
|
|
185
|
+
(the default) and over MCP, the orchestrator calls `execute()` directly. Put ownership and
|
|
186
|
+
tenant checks INSIDE `execute()` (via `CrudService` + `context.serviceOptions`) — a check
|
|
187
|
+
that lives only in `authorize()` will not run for most callers.
|
|
168
188
|
|
|
169
189
|
### MCP server (only when `ai.mcp.enabled` or `ai.mcp` is truthy)
|
|
170
190
|
|
|
171
|
-
**
|
|
191
|
+
**No install step needed.** `@modelcontextprotocol/sdk` is a regular dependency of
|
|
192
|
+
`@lenne.tech/nest-server` and reaches both consumption modes — npm-mode projects
|
|
193
|
+
resolve it transitively, CLI-vendored projects get it merged into their own
|
|
194
|
+
`package.json`. `CoreAiMcpController` still `import()`s it lazily, so a project
|
|
195
|
+
that never enables MCP pays no startup cost.
|
|
172
196
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```
|
|
197
|
+
A **503 Service Unavailable** from `/ai/mcp` therefore means the module could not be
|
|
198
|
+
_resolved_, not that it is missing — check the server log for the underlying error.
|
|
199
|
+
The response body carries the stable `#LTNS_0901` code, never the raw error.
|
|
177
200
|
|
|
178
|
-
|
|
179
|
-
`
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
install-hint message instead of a 500 trace.
|
|
201
|
+
> **No confirmation gate over MCP.** `mcpCallTool` does not consult the
|
|
202
|
+
> `mutating`/`destructive` flags, and `authorize()` does not run there either. A
|
|
203
|
+
> destructive tool invoked through `/ai/mcp` executes immediately. Only expose MCP to
|
|
204
|
+
> clients you trust, and put every permission check inside `execute()`.
|
|
183
205
|
|
|
184
206
|
### MCP OAuth 2.1 (only when `ai.mcp.oauth` is enabled)
|
|
185
207
|
|
|
@@ -56,6 +56,8 @@ ai: {
|
|
|
56
56
|
systemPrompt: 'You are a helpful assistant for …',
|
|
57
57
|
contextWindow: 8192, // fallback when a connection has no detected window
|
|
58
58
|
maxToolResultChars: 12000, // cap a tool-results payload fed back to the model
|
|
59
|
+
deferToolSchemas: false, // catalog lists names + descriptions only; schemas via search_tools
|
|
60
|
+
deferToolSummaryChars: 0, // 0 = full descriptions; set ~200-400 together with deferToolSchemas
|
|
59
61
|
promptLearning: { autoApply: false }, // governed self-improvement (admin approves hints)
|
|
60
62
|
// Optional one-time seed of a default connection (DB is the source of truth):
|
|
61
63
|
defaultConnection: {
|
|
@@ -109,6 +111,12 @@ never probed). Detection runs in two complementary ways:
|
|
|
109
111
|
once, persists, and uses the result. Until then the safe emulated baseline applies.
|
|
110
112
|
- **On demand:** admins can re-probe via `detectAiConnectionCapabilities` /
|
|
111
113
|
`POST /ai/connections/:id/detect-capabilities` (e.g. after changing `baseUrl`/`model`).
|
|
114
|
+
- **Boot drift check (opt-in):** set `ai.capabilityDriftCheck: true` to probe every enabled
|
|
115
|
+
connection that has an EXPLICIT flag once at startup and log a warning when the declared
|
|
116
|
+
value contradicts what the endpoint reports (a wrong explicit flag otherwise degrades the
|
|
117
|
+
assistant silently, e.g. forcing emulated tool-calling on a native-capable backend). It only
|
|
118
|
+
warns — the stored value is never changed. OFF by default because it makes outbound calls to
|
|
119
|
+
the LLM endpoints on every boot; also skipped in the ci/e2e runners.
|
|
112
120
|
|
|
113
121
|
The probe is provider-agnostic best effort: `response_format: json_object` is sent
|
|
114
122
|
(2xx → JSON supported); a trivial tool with `tool_choice: 'required'` is sent (2xx
|
|
@@ -345,17 +353,28 @@ export class TransferFundsTool extends AiTool {
|
|
|
345
353
|
readonly name = 'transfer_funds';
|
|
346
354
|
readonly mutating = true; // governed by the confirmation policy
|
|
347
355
|
readonly destructive = true; // always requires confirmation
|
|
348
|
-
|
|
356
|
+
|
|
357
|
+
// PLAN-MODE pre-flight, without mutating: lets the whole plan be rejected before
|
|
358
|
+
// any step runs. It does NOT run in auto mode or over MCP — so it must never be
|
|
359
|
+
// the only place a permission is checked.
|
|
349
360
|
async authorize(args, context) {
|
|
350
361
|
const account = await this.accountService.get(args.fromId, context.serviceOptions).catch(() => null);
|
|
351
362
|
return { allowed: !!account, reason: account ? undefined : 'No access to source account' };
|
|
352
363
|
}
|
|
364
|
+
|
|
353
365
|
async execute(args, context) {
|
|
354
|
-
|
|
366
|
+
// The REAL gate, on every path: route through the service with the caller's
|
|
367
|
+
// serviceOptions so `@Restricted`, `securityCheck()` and tenant scoping apply.
|
|
368
|
+
// Do not rely on authorize() having run.
|
|
369
|
+
const account = await this.accountService.getForUser(args.fromId, context.serviceOptions);
|
|
370
|
+
return this.accountService.transfer(account, args.toId, args.amount, context.serviceOptions);
|
|
355
371
|
}
|
|
356
372
|
}
|
|
357
373
|
```
|
|
358
374
|
|
|
375
|
+
> **Do not put a permission check only in `authorize()`.** It runs in plan mode
|
|
376
|
+
> only — auto mode (the default) and MCP go straight to `execute()`.
|
|
377
|
+
|
|
359
378
|
## Confirmation for changes
|
|
360
379
|
|
|
361
380
|
- `destructive` tools always require confirmation.
|
|
@@ -421,9 +440,10 @@ The model also receives **structured tool errors** (`{ error: { code, message, h
|
|
|
421
440
|
so it can recover within the run.
|
|
422
441
|
|
|
423
442
|
> **Security:** learned hints and template overrides only ever **add textual guidance** —
|
|
424
|
-
> they can never relax the permission model. Tool role-filtering,
|
|
443
|
+
> they can never relax the permission model. Tool role-filtering,
|
|
425
444
|
> `CrudService`/`@Restricted` and `secretFields` are enforced backend-side regardless of
|
|
426
|
-
> the prompt.
|
|
445
|
+
> the prompt. (`authorize()` is _not_ part of that unconditional set — it runs in plan
|
|
446
|
+
> mode only; see [Deferred tool schemas](#deferred-tool-schemas-large-registries) below.)
|
|
427
447
|
|
|
428
448
|
```typescript
|
|
429
449
|
ai: {
|
|
@@ -449,6 +469,55 @@ When a user's session history would overflow, the **oldest non-system turns are
|
|
|
449
469
|
`ai.maxToolResultChars` (default `12000`) — so long-running conversations never exceed the
|
|
450
470
|
model's limit.
|
|
451
471
|
|
|
472
|
+
## Deferred tool schemas (large registries)
|
|
473
|
+
|
|
474
|
+
The system prompt normally carries every tool's full JSON parameter schema. With a large
|
|
475
|
+
registry that catalog can dominate a small context window. Set `ai.deferToolSchemas: true`
|
|
476
|
+
and the catalog lists only tool **names + descriptions**; the model fetches a specific
|
|
477
|
+
schema on demand through the built-in `search_tools` meta-tool.
|
|
478
|
+
|
|
479
|
+
With many tools the **descriptions alone** can then re-inflate the prompt the deferral was
|
|
480
|
+
meant to shrink. `ai.deferToolSummaryChars` caps each description in that deferred catalog:
|
|
481
|
+
whole sentences up to the cap (always at least the first one), a word-boundary cut when the
|
|
482
|
+
first sentence already exceeds it, and a `…` marker appended **on top of** the cap.
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
ai: {
|
|
486
|
+
deferToolSchemas: true,
|
|
487
|
+
deferToolSummaryChars: 300, // 0 (default) = keep descriptions untruncated
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
The default of `0` is deliberate: enabling `deferToolSchemas` alone never changes what a
|
|
492
|
+
tool description _says_. The saving is therefore opt-in — when you defer schemas to reclaim
|
|
493
|
+
context, set `deferToolSummaryChars` alongside it (roughly 200–400 works well).
|
|
494
|
+
|
|
495
|
+
> **Scoped to the emulated tool protocol, not to the provider.** In auto mode a connection
|
|
496
|
+
> with `supportsNativeTools: true` receives every full description and JSON schema through
|
|
497
|
+
> the native `tools` payload anyway, so truncation is skipped there and no `search_tools`
|
|
498
|
+
> banner is emitted — telling the model a description was cut while handing it the full text
|
|
499
|
+
> in the same request would only buy a wasted round-trip against `maxIterations`.
|
|
500
|
+
>
|
|
501
|
+
> **Plan mode always uses the emulated protocol**, so a native connection IS truncated there.
|
|
502
|
+
> Because plan mode returns a complete plan in one call without executing anything, the model
|
|
503
|
+
> cannot act on a `search_tools` hint before committing — the catalog therefore carries a
|
|
504
|
+
> different banner there ("descriptions are abbreviated, plan conservatively") instead of
|
|
505
|
+
> promising a lookup that would arrive too late. The dropped tail stays unrecoverable for
|
|
506
|
+
> planning, so keep the cap generous if you rely on plan mode.
|
|
507
|
+
|
|
508
|
+
> **The truncated tail is where preconditions and role restrictions usually live.** The
|
|
509
|
+
> catalog banner instructs the model to fetch the full text via `search_tools` before
|
|
510
|
+
> calling a `…`-marked tool. This is model **guidance only** — it never affects
|
|
511
|
+
> authorization. Which tools a user sees and may run is decided server-side by the registry's
|
|
512
|
+
> role filter (`AiToolRegistry.forUser()`), re-checked when the call executes; and the
|
|
513
|
+
> confirmation gate reads a tool's `mutating`/`destructive` **flags**, never its description
|
|
514
|
+
> — so a truncated "requires confirmation" sentence cannot disable it.
|
|
515
|
+
>
|
|
516
|
+
> Note `AiTool.authorize()` is **not** part of that chain in every mode: it runs in **plan
|
|
517
|
+
> mode only**. In auto mode (the default) and over MCP, tools go straight to `execute()` — so
|
|
518
|
+
> data-level checks (ownership, tenant scope) must live **inside `execute()`**, routed through
|
|
519
|
+
> `CrudService` with `context.serviceOptions`, not in `authorize()` alone.
|
|
520
|
+
|
|
452
521
|
## Token budgets & usage
|
|
453
522
|
|
|
454
523
|
Token/prompt limits are **per user AND per tenant**, with **config defaults** so you
|
|
@@ -483,17 +552,16 @@ The `AiToolRegistry` also feeds a real **MCP server** at `POST/GET/DELETE /ai/mc
|
|
|
483
552
|
(Streamable HTTP), so external MCP clients use the same backend tools with the
|
|
484
553
|
same role gating. Enable with `ai: { mcp: true }`.
|
|
485
554
|
|
|
486
|
-
**
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
pnpm add @modelcontextprotocol/sdk
|
|
492
|
-
```
|
|
555
|
+
**No install step needed.** `@modelcontextprotocol/sdk` ships as a regular
|
|
556
|
+
dependency of `@lenne.tech/nest-server` and reaches both consumption modes:
|
|
557
|
+
npm-mode projects resolve it transitively, and CLI-vendored projects get it
|
|
558
|
+
merged into their own `package.json`. The controller still `import()`s it
|
|
559
|
+
lazily, so a project that never enables MCP does not pay the startup cost.
|
|
493
560
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
561
|
+
If `/ai/mcp` nevertheless returns **503 Service Unavailable**, the module could
|
|
562
|
+
not be _resolved_ — a bundler or test runner with its own module resolution can
|
|
563
|
+
fail on the subpath export while plain Node succeeds. The underlying error is in
|
|
564
|
+
the server log; the response carries only a stable `#LTNS_0901` code.
|
|
497
565
|
|
|
498
566
|
- Auth: the request must carry a valid Bearer token/session (resolved by the
|
|
499
567
|
framework's existing auth) — the MCP session is bound to that user, and
|