@formio/uag 2.0.0-rc.3 → 2.0.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/uag-eval.js +7 -3
- package/lib/UAGFormInterface.js +25 -9
- package/lib/UAGProjectInterface.js +11 -4
- package/lib/actionLogs.js +5 -3
- package/lib/actions/runTask.js +4 -3
- package/lib/agent/anonymous.js +5 -4
- package/lib/agent/chat.js +4 -2
- package/lib/agent/engines.js +3 -3
- package/lib/agent/errors.d.ts +16 -0
- package/lib/agent/errors.js +112 -0
- package/lib/agent/router.js +23 -17
- package/lib/config.d.ts +0 -2
- package/lib/database.d.ts +1 -1
- package/lib/formflow.d.ts +4 -4
- package/lib/formflow.js +5 -5
- package/lib/index.d.ts +19 -6
- package/lib/index.js +74 -10
- package/lib/runner.d.ts +2 -0
- package/lib/runner.js +11 -3
- package/lib/runs.d.ts +3 -1
- package/lib/tools/SchemaBuilder.d.ts +0 -5
- package/lib/tools/SchemaBuilder.js +0 -8
- package/lib/tools/agentDecide.js +2 -2
- package/lib/tools/agentProvide.d.ts +0 -5
- package/lib/tools/agentProvide.js +7 -40
- package/lib/tools/collectData.js +2 -1
- package/lib/tools/index.d.ts +2 -2
- package/lib/tools/index.js +1 -4
- package/lib/version.d.ts +2 -0
- package/lib/version.js +5 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ A person talks to Claude, ChatGPT or your own chat app, and the agent fills in a
|
|
|
55
55
|
|
|
56
56
|
An application saves a submission through the UAG; an **Agent: Run task** action runs the form's task after the save and saves the result, which can start the next task. [Try it](https://ai.form.io/examples/workflow) · [source](./demo/server/workflow.mjs) · build it with [Workflows](https://ai.form.io/developers/workflows) · [prompt for your coding agent](https://github.com/formio/ai/blob/main/examples/agents/admissions-workflow.md).
|
|
57
57
|
|
|
58
|
-

|
|
59
59
|
|
|
60
60
|
### Agentic Formflow
|
|
61
61
|
|
|
@@ -90,7 +90,7 @@ services:
|
|
|
90
90
|
retries: 40
|
|
91
91
|
|
|
92
92
|
formio-uag:
|
|
93
|
-
image: formio/uag
|
|
93
|
+
image: formio/uag:rc
|
|
94
94
|
depends_on:
|
|
95
95
|
formio: { condition: service_healthy }
|
|
96
96
|
ports: ["3200:3200"]
|
|
@@ -267,7 +267,7 @@ app.use(await server.router());
|
|
|
267
267
|
app.listen(process.env.PORT || 3200);
|
|
268
268
|
```
|
|
269
269
|
|
|
270
|
-
Run Node 22 or newer with `--no-node-snapshot` (isolated-vm, which runs form logic, needs it), and install the engines you use next to the UAG (`npm install @formio/uag-claude`). See [Node.js API](https://ai.form.io/developers/node-api).
|
|
270
|
+
Run Node 22 or newer with `--no-node-snapshot` (isolated-vm, which runs form logic, needs it), and install the engines you use next to the UAG (`npm install @formio/uag-claude@rc`). See [Node.js API](https://ai.form.io/developers/node-api).
|
|
271
271
|
- **[`@formio/uag-js`](./packages/uag-js)** connects a formio.js application to the UAG. Register it once with `Formio.use(UAG('https://uag.example.com'))`: submissions of forms tagged `uag` are then saved through the UAG, and Agent Input fills those forms from what the user wrote, through `{form}/agent/extract`, when they leave the input, pause, move to the next page, or press a button. `Formio.createChat(element, formUrl)` renders any of them as a chat instead, templated, translated and themed like a formio.js form. See [JavaScript (formio.js)](https://ai.form.io/developers/renderer) and [Chat widget](https://ai.form.io/developers/chat).
|
|
272
272
|
|
|
273
273
|
## Deployment
|
|
@@ -323,7 +323,7 @@ The full table, with the Agent API's errors: [Troubleshooting](https://ai.form.i
|
|
|
323
323
|
| [`@formio/uag-sdk`](./packages/engine-sdk) | 2.x | ≥ 2.0.0 | Engine contract version 1, remote engine protocol version 1. |
|
|
324
324
|
| [`@formio/uag-engines`](./packages/engine-server), image `formio/uag-engines` | 2.x | ≥ 2.0.0 | The engine server. |
|
|
325
325
|
| [`@formio/uag-js`](./packages/uag-js) | 2.x | ≥ 2.0.0 | The formio.js module: saves `uag` forms through the UAG, adds Agent Input, and renders forms as a chat. Runs in the browser. |
|
|
326
|
-
| `@formio/uag-claude` | 1.3.x |
|
|
326
|
+
| `@formio/uag-claude` | 1.3.x | 1.12.0 – 1.13.x | The retired Claude integration (against 2.0, only its direct commands). Security fixes only; see [MIGRATING-2.0.md](./MIGRATING-2.0.md). |
|
|
327
327
|
|
|
328
328
|
`@formio/uag-claude` 2.x is the Claude *engine*, not the 1.x integration; pin `@formio/uag-claude@^1` if you still install the integration. Upgrading from 1.x: [MIGRATING-2.0.md](./MIGRATING-2.0.md).
|
|
329
329
|
|
package/bin/uag-eval.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* [--engine jev] [--json report.json]
|
|
7
7
|
*
|
|
8
8
|
* --form A Form.io form definition (JSON export).
|
|
9
|
-
* --task The task to evaluate (default: the form's first).
|
|
9
|
+
* --task The task to evaluate (default: the form's first).
|
|
10
10
|
* --examples A JSON array of { "id", "data": { ... }, "expected": { "<path>": <value or null> } }.
|
|
11
11
|
* null means "the input does not say", which expects the field to stay empty.
|
|
12
12
|
* --engine The decision engine to evaluate: an instance named in UAG_ENGINES, or an
|
|
@@ -25,7 +25,7 @@ require('dotenv/config');
|
|
|
25
25
|
const { readFileSync, writeFileSync } = require('fs');
|
|
26
26
|
const { parseInstances } = require('@formio/uag-sdk');
|
|
27
27
|
const { ProjectInterface, UAGProjectInterface, UAGFormInterface, loadUAGEngines } = require('../lib');
|
|
28
|
-
const { FormflowService, formatReport
|
|
28
|
+
const { FormflowService, formatReport } = require('../lib/formflow');
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* The instance to evaluate: the one UAG_ENGINES names `name`, so a deployed
|
|
@@ -51,7 +51,11 @@ async function main() {
|
|
|
51
51
|
console.error('Usage: uag-eval --form <form.json> --examples <examples.json> [--task <name>] [--engine <name>] [--json <out.json>] [--project <url>]');
|
|
52
52
|
process.exit(2);
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
if (opts.persona !== undefined) {
|
|
55
|
+
console.error('uag-eval: --persona was renamed --task in UAG 2.0.');
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
const task = opts.task || undefined;
|
|
55
59
|
const formDef = JSON.parse(readFileSync(opts.form, 'utf8'));
|
|
56
60
|
const examples = JSON.parse(readFileSync(opts.examples, 'utf8'));
|
|
57
61
|
const entry = engineEntry(opts.engine, process.env);
|
package/lib/UAGFormInterface.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.UAGFormInterface = void 0;
|
|
4
4
|
const appserver_1 = require("@formio/appserver");
|
|
5
|
+
const formflow_1 = require("./formflow");
|
|
5
6
|
const core_1 = require("@formio/core");
|
|
6
7
|
const lodash_1 = require("lodash");
|
|
7
8
|
const node_html_markdown_1 = require("node-html-markdown");
|
|
@@ -11,6 +12,22 @@ const CHAT_UNSUPPORTED = new Set([
|
|
|
11
12
|
'datagrid', 'editgrid', 'datatable', 'dynamicWizard', 'tagpad', 'form', 'datamap', 'address',
|
|
12
13
|
'file', 'signature', 'sketchpad', 'datasource', 'password', 'captcha'
|
|
13
14
|
]);
|
|
15
|
+
/**
|
|
16
|
+
* The components inside a layout or container flagged with `uag`, which makes
|
|
17
|
+
* each input inside it one of the task's fields. Panels, fieldsets, wells, tabs
|
|
18
|
+
* and containers hold theirs in `components`; columns in each column's
|
|
19
|
+
* `components`; tables in each cell's. Null for a component that holds none.
|
|
20
|
+
*/
|
|
21
|
+
function layoutChildren(component) {
|
|
22
|
+
const comp = component;
|
|
23
|
+
if (Array.isArray(comp.components))
|
|
24
|
+
return comp.components;
|
|
25
|
+
if (Array.isArray(comp.columns))
|
|
26
|
+
return comp.columns.flatMap((column) => column?.components || []);
|
|
27
|
+
if (Array.isArray(comp.rows))
|
|
28
|
+
return comp.rows.flat().flatMap((cell) => cell?.components || []);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
14
31
|
class UAGFormInterface extends appserver_1.FormInterface {
|
|
15
32
|
/**
|
|
16
33
|
* The form's routes, with its agent operations (`agent/extract`, `agent/decide`,
|
|
@@ -67,8 +84,9 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
67
84
|
}
|
|
68
85
|
}
|
|
69
86
|
else {
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
const children = layoutChildren(component);
|
|
88
|
+
if (children) {
|
|
89
|
+
core_1.Utils.eachComponent(children, (subComp, subPath) => {
|
|
72
90
|
subPath = paths?.dataPath ? `${paths.dataPath}.${subPath}` : subPath;
|
|
73
91
|
const compInfo = this.getComponentInfo(subComp, subPath);
|
|
74
92
|
compInfo.rule = this.getComponentValueRule(subComp);
|
|
@@ -332,12 +350,10 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
332
350
|
* Override the process methods for the UAG.
|
|
333
351
|
*/
|
|
334
352
|
async process(submission, auth, headers = {}, findQuery = {}, additional = []) {
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
processors.splice(fetchIndex, 1);
|
|
340
|
-
}
|
|
353
|
+
// Leave out 'fetch', since the UAG handles it with the fetch_external_data tool, and
|
|
354
|
+
// 'dereference', which expands a "Save as reference" select or a Data Table from the
|
|
355
|
+
// database the UAG does not have: it would throw for every form with one.
|
|
356
|
+
const processors = [...core_1.Processors, ...additional].filter((p) => p.name !== 'fetch' && p.name !== 'dereference');
|
|
341
357
|
// Return the processed submission data.
|
|
342
358
|
const context = await super.process(submission, auth, headers, findQuery, processors);
|
|
343
359
|
// Filter the "required" validation since the UAG retrieves required fields separately.
|
|
@@ -519,7 +535,7 @@ class UAGFormInterface extends appserver_1.FormInterface {
|
|
|
519
535
|
return;
|
|
520
536
|
if (component.calculateValue && !component.allowCalculateOverride)
|
|
521
537
|
return;
|
|
522
|
-
if (owned.has(path) || component.properties?.uagAuthoritative
|
|
538
|
+
if (owned.has(path) || (0, formflow_1.flagOn)(component.properties?.uagAuthoritative))
|
|
523
539
|
return;
|
|
524
540
|
result.fields.push({
|
|
525
541
|
...this.getComponentInfo(component, path),
|
|
@@ -12,6 +12,7 @@ const express_1 = require("express");
|
|
|
12
12
|
const router_1 = require("./router");
|
|
13
13
|
const template_1 = require("./template");
|
|
14
14
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
15
|
+
const version_1 = require("./version");
|
|
15
16
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
16
17
|
const router_2 = require("./agent/router");
|
|
17
18
|
const tools_1 = require("./agent/tools");
|
|
@@ -28,7 +29,7 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
|
|
|
28
29
|
this.uagTemplate = null;
|
|
29
30
|
/** Engines enabled at startup; empty until UAGServer.router() resolves them. */
|
|
30
31
|
this.engines = new formflow_1.EngineRegistry();
|
|
31
|
-
this.mcpServer = new mcp_js_1.McpServer({ name: 'formio-uag', version:
|
|
32
|
+
this.mcpServer = new mcp_js_1.McpServer({ name: 'formio-uag', version: version_1.UAG_VERSION });
|
|
32
33
|
}
|
|
33
34
|
async initialize() {
|
|
34
35
|
await super.initialize();
|
|
@@ -40,7 +41,7 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
|
|
|
40
41
|
// A scoped token narrows the tools to its own list, and only ever to tools
|
|
41
42
|
// annotated read-only: no model may write through /mcp with one.
|
|
42
43
|
async buildMcpServer(scope) {
|
|
43
|
-
const server = new mcp_js_1.McpServer({ name: 'formio-uag', version:
|
|
44
|
+
const server = new mcp_js_1.McpServer({ name: 'formio-uag', version: version_1.UAG_VERSION });
|
|
44
45
|
// The standard UAG tools, plus the custom tools from config.
|
|
45
46
|
let tools = await (0, tools_1.allTools)(this);
|
|
46
47
|
if (scope) {
|
|
@@ -164,10 +165,16 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
|
|
|
164
165
|
// Point MCP clients at OAuth sign-in only when it is on (MCP_OAUTH, with the database).
|
|
165
166
|
const pkce = this.provider('auth.pkce');
|
|
166
167
|
const metadata = pkce?.oauthReady ? `, resource_metadata="${pkce.publicUrl(req)}/.well-known/oauth-protected-resource"` : '';
|
|
168
|
+
// RFC 6750 3.1: a request that presented a token is told it is invalid; one
|
|
169
|
+
// that presented none gets no error code, only the challenge.
|
|
170
|
+
const presented = !!(0, formflow_1.bearerOf)(req.get('authorization'));
|
|
171
|
+
const description = String(err?.message || err || 'Unauthorized').replace(/["\\]/g, '');
|
|
172
|
+
const challenge = presented ? `, error="invalid_token", error_description="${description}"` : '';
|
|
167
173
|
return res.status(401).set({
|
|
168
|
-
'WWW-Authenticate': `Bearer realm="uag-server"${metadata}`
|
|
174
|
+
'WWW-Authenticate': `Bearer realm="uag-server"${challenge}${metadata}`
|
|
169
175
|
}).json({
|
|
170
|
-
|
|
176
|
+
error: presented ? 'invalid_token' : 'unauthorized',
|
|
177
|
+
error_description: presented ? description : 'Send an access token: Authorization: Bearer <token>.'
|
|
171
178
|
});
|
|
172
179
|
}
|
|
173
180
|
}
|
package/lib/actionLogs.js
CHANGED
|
@@ -35,12 +35,14 @@ function actionItem(record) {
|
|
|
35
35
|
if (record.finished) {
|
|
36
36
|
const written = record.written || [];
|
|
37
37
|
const cleared = record.cleared || [];
|
|
38
|
-
const
|
|
38
|
+
const refused = record.refused || [];
|
|
39
|
+
const left = refused.length ? ` Left out answers the form refused: ${refused.join(', ')}.` : '';
|
|
40
|
+
const info = (record.status === 'failed'
|
|
39
41
|
? `Failed: ${record.error || 'unknown error'}`
|
|
40
42
|
: record.saved
|
|
41
43
|
? `Saved ${written.length} field(s)${written.length ? `: ${written.join(', ')}` : ''}${cleared.length ? `; cleared ${cleared.join(', ')}` : ''}.`
|
|
42
|
-
: 'Nothing changed, so nothing was saved.';
|
|
43
|
-
messages.push({ datetime: record.finished.toISOString(), info, data: { engines: record.engines || [], written, cleared } });
|
|
44
|
+
: 'Nothing changed, so nothing was saved.') + left;
|
|
45
|
+
messages.push({ datetime: record.finished.toISOString(), info, data: { engines: record.engines || [], written, cleared, ...(refused.length && { refused }) } });
|
|
44
46
|
}
|
|
45
47
|
return {
|
|
46
48
|
_id: String(record._id),
|
package/lib/actions/runTask.js
CHANGED
|
@@ -9,6 +9,7 @@ const formflow_1 = require("../formflow");
|
|
|
9
9
|
const runner_1 = require("../runner");
|
|
10
10
|
const protocol_1 = require("@formio/uag-sdk/protocol");
|
|
11
11
|
const requestId_1 = require("../agent/requestId");
|
|
12
|
+
const errors_1 = require("../agent/errors");
|
|
12
13
|
const identity_1 = require("../identity");
|
|
13
14
|
const debug = require('debug')('formio:uag:actions:runTask');
|
|
14
15
|
exports.RUN_TASK = 'agentRunTask';
|
|
@@ -89,12 +90,12 @@ function onBehalfHook() {
|
|
|
89
90
|
if (roles.length > 1) {
|
|
90
91
|
console.error(`[formio-uag] The Run task actions of "${form.name}" that run on create name different agent roles ` +
|
|
91
92
|
`(${actions.map((action) => action.title || action.machineName).join(', ')}). Choose one agent role for them all.`);
|
|
92
|
-
return res.status(500).json({ name: 'ConfigurationError', message: `The form "${form.name}" is not configured correctly: its Run task actions name different agent roles
|
|
93
|
+
return res.status(500).json({ name: 'ConfigurationError', message: `The form "${form.name}" is not configured correctly: its Run task actions name different agent roles.`, code: 'configuration_error' });
|
|
93
94
|
}
|
|
94
95
|
const [role] = roles;
|
|
95
96
|
const submitters = actions.flatMap((action) => action.settings.submitterRoles || []).map(String);
|
|
96
97
|
if (!appserver_1.AuthRequest.hasRole([role], access[appserver_1.AuthorizedStateType.ALL], project) || !has(submitters)) {
|
|
97
|
-
return res.status(403).json({ name: 'ForbiddenError', message: 'You may not submit this form.' });
|
|
98
|
+
return res.status(403).json({ name: 'ForbiddenError', message: 'You may not submit this form.', code: 'forbidden' });
|
|
98
99
|
}
|
|
99
100
|
try {
|
|
100
101
|
req.forwardAuth = await (0, identity_1.agentAuth)(project, actingFor, role, { task: actions[0].settings.task });
|
|
@@ -127,7 +128,7 @@ function refuse(res, err) {
|
|
|
127
128
|
res.status(err.status).json({
|
|
128
129
|
name: err.name,
|
|
129
130
|
message: err.message,
|
|
130
|
-
|
|
131
|
+
code: err.code || (0, errors_1.errorCode)(err.status),
|
|
131
132
|
...(err.edition && { edition: err.edition })
|
|
132
133
|
});
|
|
133
134
|
}
|
package/lib/agent/anonymous.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.AnonymousLimiter = void 0;
|
|
4
4
|
exports.anonymousLimits = anonymousLimits;
|
|
5
5
|
exports.anonymousGate = anonymousGate;
|
|
6
|
+
const formflow_1 = require("../formflow");
|
|
6
7
|
const debug = require('debug')('formio:uag:agent');
|
|
7
8
|
const WINDOW_MS = 60000;
|
|
8
9
|
/** A number from the configuration, else the environment, else the default. */
|
|
@@ -74,14 +75,14 @@ function anonymousGate(project) {
|
|
|
74
75
|
const request = req;
|
|
75
76
|
const form = request.form;
|
|
76
77
|
const limits = anonymousLimits(project);
|
|
77
|
-
if (limits.perAddress === 0 || limits.total === 0 ||
|
|
78
|
-
return res.status(401).json({
|
|
78
|
+
if (limits.perAddress === 0 || limits.total === 0 || !(0, formflow_1.flagOn)(form?.form?.properties?.uagAnonymous)) {
|
|
79
|
+
return res.status(401).json({ error: 'Sign in to use this form\'s agent operations.', code: 'sign_in_required' });
|
|
79
80
|
}
|
|
80
81
|
const taken = limiterFor(project).take(String(req.ip || req.socket?.remoteAddress || ''), limits);
|
|
81
82
|
if (!taken.allowed) {
|
|
82
83
|
debug(`Refused an anonymous ${req.method} ${req.path}: over the anonymous rate limit.`);
|
|
83
84
|
res.set('Retry-After', String(taken.retryAfter));
|
|
84
|
-
return res.status(429).json({
|
|
85
|
+
return res.status(429).json({ error: 'Too many requests. Try again shortly.', code: 'rate_limited' });
|
|
85
86
|
}
|
|
86
87
|
let permissions = {};
|
|
87
88
|
try {
|
|
@@ -91,7 +92,7 @@ function anonymousGate(project) {
|
|
|
91
92
|
permissions = {};
|
|
92
93
|
}
|
|
93
94
|
if (!permissions.create) {
|
|
94
|
-
return res.status(403).json({
|
|
95
|
+
return res.status(403).json({ error: 'Anonymous visitors may not submit this form.', code: 'forbidden' });
|
|
95
96
|
}
|
|
96
97
|
next();
|
|
97
98
|
};
|
package/lib/agent/chat.js
CHANGED
|
@@ -162,7 +162,9 @@ function fieldSpec(form, field) {
|
|
|
162
162
|
label: field.label,
|
|
163
163
|
type: field.type,
|
|
164
164
|
description: [field.required ? 'Required.' : 'Optional.', field.description, field.prompt && `Placeholder: ${field.prompt}`].filter(Boolean).join(' '),
|
|
165
|
-
|
|
165
|
+
// The host's rule, then the component's validation (lengths, pattern, range, mask), so a
|
|
166
|
+
// first answer is likely to pass the form's checks.
|
|
167
|
+
rule: (0, formflow_1.ruleWithConstraints)(field.rule, component),
|
|
166
168
|
};
|
|
167
169
|
if (field.format)
|
|
168
170
|
spec.format = field.format;
|
|
@@ -387,7 +389,7 @@ async function chatTurn(project, form, body, context) {
|
|
|
387
389
|
const request = readChatRequest(body, settings);
|
|
388
390
|
const engines = chatEngines(project, settings);
|
|
389
391
|
if (!engines.length) {
|
|
390
|
-
throw new formflow_1.FormflowError(503, 'No enabled engine can serve chat. Enable an agent engine with UAG_ENGINES.');
|
|
392
|
+
throw new formflow_1.FormflowError(503, 'No enabled engine can serve chat. Enable an agent engine with UAG_ENGINES.', 'no_engine');
|
|
391
393
|
}
|
|
392
394
|
const service = (0, formflow_1.formflow)(project);
|
|
393
395
|
const before = await form.chatFields({ data: (0, lodash_1.cloneDeep)(request.data) }, context.auth);
|
package/lib/agent/engines.js
CHANGED
|
@@ -8,7 +8,7 @@ exports.loadUAGEngines = loadUAGEngines;
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const uag_sdk_1 = require("@formio/uag-sdk");
|
|
10
10
|
const loader_1 = require("@formio/uag-sdk/loader");
|
|
11
|
-
const
|
|
11
|
+
const appserver_1 = require("@formio/appserver");
|
|
12
12
|
const remote_1 = require("./remote");
|
|
13
13
|
const stubs_1 = require("./stubs");
|
|
14
14
|
/** The UAG's own installation, where engine packages are looked up first. */
|
|
@@ -31,7 +31,7 @@ async function loadUAGEngines(config = {}, env = process.env) {
|
|
|
31
31
|
paths: (0, loader_1.enginePaths)(exports.UAG_ROOT, env),
|
|
32
32
|
env,
|
|
33
33
|
config,
|
|
34
|
-
apiVersion:
|
|
34
|
+
apiVersion: appserver_1.Formflow.ENGINE_API_VERSION,
|
|
35
35
|
});
|
|
36
|
-
return
|
|
36
|
+
return appserver_1.Formflow.engineRegistry(loaded);
|
|
37
37
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Request, RequestHandler, Response } from "express";
|
|
2
|
+
export declare function isAgentApiRequest(req: Request): boolean;
|
|
3
|
+
/** The code for a status, when the error names none. */
|
|
4
|
+
export declare function errorCode(status: number): string;
|
|
5
|
+
export declare const INTERNAL_ERROR = "The request failed. See the UAG logs for this request id.";
|
|
6
|
+
/** The request's id, assigned once: the caller's, when it is a short token, or a new one. */
|
|
7
|
+
export declare function requestIdOf(req: Request, res: Response): string;
|
|
8
|
+
/** The error body for a status and whatever a handler sent: an object, JSON text, plain text or nothing. */
|
|
9
|
+
export declare function errorBody(requestId: string, status: number, sent: unknown): Record<string, any>;
|
|
10
|
+
/**
|
|
11
|
+
* Mounted ahead of every route: for an Agent API request, assigns the request
|
|
12
|
+
* id and rewrites any error response into the one shape, so refusals from
|
|
13
|
+
* middleware the UAG does not own (the project's authentication answers plain
|
|
14
|
+
* text, the portal's team access an empty 401) look like every other.
|
|
15
|
+
*/
|
|
16
|
+
export declare function agentErrorShape(): RequestHandler;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INTERNAL_ERROR = void 0;
|
|
4
|
+
exports.isAgentApiRequest = isAgentApiRequest;
|
|
5
|
+
exports.errorCode = errorCode;
|
|
6
|
+
exports.requestIdOf = requestIdOf;
|
|
7
|
+
exports.errorBody = errorBody;
|
|
8
|
+
exports.agentErrorShape = agentErrorShape;
|
|
9
|
+
const crypto_1 = require("crypto");
|
|
10
|
+
const protocol_1 = require("@formio/uag-sdk/protocol");
|
|
11
|
+
/**
|
|
12
|
+
* The Agent API's one error shape. Every error a form's agent operation or
|
|
13
|
+
* `/agent/capabilities` answers is JSON
|
|
14
|
+
*
|
|
15
|
+
* { "requestId": "...", "error": "a message for people", "code": "a_stable_code", ... }
|
|
16
|
+
*
|
|
17
|
+
* with the same id in `X-Request-Id`, whatever refused it: the UAG, the
|
|
18
|
+
* project's authentication, the portal's team access, the body parser.
|
|
19
|
+
* Clients branch on `code`; `error` is for people. A 500 never carries details:
|
|
20
|
+
* those are in the UAG's log under the request id.
|
|
21
|
+
*/
|
|
22
|
+
/** A form's agent operations, and the capabilities. */
|
|
23
|
+
const OPERATION = /\/agent\/(extract|decide|generate|chat)\/?$/i;
|
|
24
|
+
const CAPABILITIES = /^\/agent\/capabilities\/?$/i;
|
|
25
|
+
function isAgentApiRequest(req) {
|
|
26
|
+
return (req.method === 'POST' && OPERATION.test(req.path)) || (req.method === 'GET' && CAPABILITIES.test(req.path));
|
|
27
|
+
}
|
|
28
|
+
/** The code for a status, when the error names none. */
|
|
29
|
+
function errorCode(status) {
|
|
30
|
+
switch (status) {
|
|
31
|
+
case 400: return 'invalid_request';
|
|
32
|
+
case 401: return 'unauthorized';
|
|
33
|
+
case 403: return 'forbidden';
|
|
34
|
+
case 404: return 'not_found';
|
|
35
|
+
case 409: return 'conflict';
|
|
36
|
+
case 413: return 'payload_too_large';
|
|
37
|
+
case 429: return 'rate_limited';
|
|
38
|
+
case 502: return 'engines_failed';
|
|
39
|
+
case 503: return 'unavailable';
|
|
40
|
+
case 504: return 'timeout';
|
|
41
|
+
default: return status >= 500 ? 'internal_error' : 'invalid_request';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.INTERNAL_ERROR = 'The request failed. See the UAG logs for this request id.';
|
|
45
|
+
/** The request's id, assigned once: the caller's, when it is a short token, or a new one. */
|
|
46
|
+
function requestIdOf(req, res) {
|
|
47
|
+
const request = req;
|
|
48
|
+
if (!request.requestId) {
|
|
49
|
+
request.requestId = (0, protocol_1.safeRequestId)(req.get('x-request-id')) || (0, crypto_1.randomUUID)();
|
|
50
|
+
}
|
|
51
|
+
if (!res.headersSent)
|
|
52
|
+
res.set('X-Request-Id', request.requestId);
|
|
53
|
+
return request.requestId;
|
|
54
|
+
}
|
|
55
|
+
/** The error body for a status and whatever a handler sent: an object, JSON text, plain text or nothing. */
|
|
56
|
+
function errorBody(requestId, status, sent) {
|
|
57
|
+
let value = sent;
|
|
58
|
+
if (Buffer.isBuffer(value))
|
|
59
|
+
value = value.toString('utf8');
|
|
60
|
+
if (typeof value === 'string') {
|
|
61
|
+
try {
|
|
62
|
+
value = JSON.parse(value);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
value = { error: value };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const given = value && typeof value === 'object' ? value : {};
|
|
69
|
+
const text = [given.error, given.detail, given.message].find((part) => typeof part === 'string' && part.trim());
|
|
70
|
+
// A 500's own message may carry internals: only the UAG's fixed sentence goes out.
|
|
71
|
+
const error = status >= 500 && status !== 502 && status !== 503 && status !== 504 ? exports.INTERNAL_ERROR : (text || defaultMessage(status));
|
|
72
|
+
return {
|
|
73
|
+
requestId,
|
|
74
|
+
error,
|
|
75
|
+
code: typeof given.code === 'string' && given.code ? given.code : errorCode(status),
|
|
76
|
+
...(typeof given.edition === 'string' && { edition: given.edition })
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function defaultMessage(status) {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case 401: return 'Unauthorized.';
|
|
82
|
+
case 403: return 'Forbidden.';
|
|
83
|
+
case 404: return 'Not found.';
|
|
84
|
+
case 413: return 'The request body is too large.';
|
|
85
|
+
case 429: return 'Too many requests. Try again shortly.';
|
|
86
|
+
default: return 'The request could not be completed.';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mounted ahead of every route: for an Agent API request, assigns the request
|
|
91
|
+
* id and rewrites any error response into the one shape, so refusals from
|
|
92
|
+
* middleware the UAG does not own (the project's authentication answers plain
|
|
93
|
+
* text, the portal's team access an empty 401) look like every other.
|
|
94
|
+
*/
|
|
95
|
+
function agentErrorShape() {
|
|
96
|
+
return (req, res, next) => {
|
|
97
|
+
if (!isAgentApiRequest(req))
|
|
98
|
+
return next();
|
|
99
|
+
const requestId = requestIdOf(req, res);
|
|
100
|
+
const send = res.send.bind(res);
|
|
101
|
+
let shaped = false;
|
|
102
|
+
res.send = ((body) => {
|
|
103
|
+
// An event stream writes its own error events; only whole responses are shaped.
|
|
104
|
+
if (shaped || res.statusCode < 400)
|
|
105
|
+
return send(body);
|
|
106
|
+
shaped = true;
|
|
107
|
+
res.type('application/json');
|
|
108
|
+
return send(JSON.stringify(errorBody(requestId, res.statusCode, body)));
|
|
109
|
+
});
|
|
110
|
+
next();
|
|
111
|
+
};
|
|
112
|
+
}
|
package/lib/agent/router.js
CHANGED
|
@@ -38,10 +38,9 @@ exports.AgentRouter = AgentRouter;
|
|
|
38
38
|
exports.agentNotFound = agentNotFound;
|
|
39
39
|
exports.agentCredentials = agentCredentials;
|
|
40
40
|
exports.FormAgentRouter = FormAgentRouter;
|
|
41
|
-
const crypto_1 = require("crypto");
|
|
42
41
|
const lodash_1 = require("lodash");
|
|
43
42
|
const express_1 = __importStar(require("express"));
|
|
44
|
-
const
|
|
43
|
+
const errors_1 = require("./errors");
|
|
45
44
|
const formflow_1 = require("../formflow");
|
|
46
45
|
const requestId_1 = require("./requestId");
|
|
47
46
|
const anonymous_1 = require("./anonymous");
|
|
@@ -67,10 +66,17 @@ function openStream(res) {
|
|
|
67
66
|
function contextOf(req) {
|
|
68
67
|
return { auth: req.auth, requestId: req.requestId, identity: (0, formflow_1.callerIdentity)(req.auth, req) };
|
|
69
68
|
}
|
|
70
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* The task the request names, if any. A request that still names a 1.x
|
|
71
|
+
* `persona` is refused: ignoring it would run every task, an engine call each.
|
|
72
|
+
*/
|
|
71
73
|
function taskOf(req) {
|
|
74
|
+
const named = (value) => typeof value === 'string' && value.trim() !== '';
|
|
72
75
|
const task = req.body?.task;
|
|
73
|
-
|
|
76
|
+
if (named(req.body?.persona) && !named(task)) {
|
|
77
|
+
throw new formflow_1.FormflowError(400, 'UAG 2.0 renamed "persona" to "task". Send "task" instead.');
|
|
78
|
+
}
|
|
79
|
+
return named(task) ? task : undefined;
|
|
74
80
|
}
|
|
75
81
|
/**
|
|
76
82
|
* Answer extract, streaming per-engine results when the client asks for a
|
|
@@ -113,15 +119,15 @@ function agentEndpoints(project) {
|
|
|
113
119
|
return parse(req, res, next);
|
|
114
120
|
const length = Number(req.get('content-length')) || Buffer.byteLength(JSON.stringify(req.body ?? null));
|
|
115
121
|
if (length > exports.AGENT_BODY_LIMIT) {
|
|
116
|
-
return res.status(413).json({ error: `The request body is larger than the agent operations' limit of ${exports.AGENT_BODY_LIMIT / (1024 * 1024)} MB
|
|
122
|
+
return res.status(413).json({ error: `The request body is larger than the agent operations' limit of ${exports.AGENT_BODY_LIMIT / (1024 * 1024)} MB.`, code: 'payload_too_large' });
|
|
117
123
|
}
|
|
118
124
|
next();
|
|
119
125
|
};
|
|
120
|
-
//
|
|
121
|
-
// is not a short token is replaced, so it is never echoed,
|
|
126
|
+
// The request id, usually assigned already by agentErrorShape, ahead of every
|
|
127
|
+
// refusal. One that is not a short token is replaced, so it is never echoed,
|
|
128
|
+
// logged or sent on.
|
|
122
129
|
const requestId = (req, res, next) => {
|
|
123
|
-
|
|
124
|
-
res.set('X-Request-Id', req.requestId);
|
|
130
|
+
(0, errors_1.requestIdOf)(req, res);
|
|
125
131
|
next();
|
|
126
132
|
};
|
|
127
133
|
const handle = (operation, fn) => async (req, res) => {
|
|
@@ -142,8 +148,8 @@ function agentEndpoints(project) {
|
|
|
142
148
|
}
|
|
143
149
|
const body = {
|
|
144
150
|
requestId: agentReq.requestId,
|
|
145
|
-
error: status === 500 ?
|
|
146
|
-
|
|
151
|
+
error: status === 500 ? errors_1.INTERNAL_ERROR : err.message,
|
|
152
|
+
code: (err instanceof formflow_1.FormflowError && err.code) || (0, errors_1.errorCode)(status),
|
|
147
153
|
...(err?.edition && { edition: err.edition })
|
|
148
154
|
};
|
|
149
155
|
if (res.headersSent) {
|
|
@@ -207,11 +213,11 @@ function agentNotFound(project) {
|
|
|
207
213
|
: !!formPath && forms.some((form) => String(form.path || '').toLowerCase() === formPath.toLowerCase());
|
|
208
214
|
if (known)
|
|
209
215
|
return next();
|
|
210
|
-
res.status(404).json({ error: `The form "${byId ? byId[1] : formPath}" does not exist or is not tagged "uag"
|
|
216
|
+
res.status(404).json({ error: `The form "${byId ? byId[1] : formPath}" does not exist or is not tagged "uag".`, code: 'form_not_found' });
|
|
211
217
|
};
|
|
212
218
|
}
|
|
213
|
-
function refuse(res,
|
|
214
|
-
return res.status(401).json({
|
|
219
|
+
function refuse(res, error) {
|
|
220
|
+
return res.status(401).json({ error, code: 'unauthorized' });
|
|
215
221
|
}
|
|
216
222
|
/**
|
|
217
223
|
* Who may call a form's agent operations, on top of the project's own
|
|
@@ -283,10 +289,10 @@ function FormAgentRouter(project, credentials) {
|
|
|
283
289
|
route('chat', 'chat', async (req, res, service) => {
|
|
284
290
|
const form = await project.formflowForm(formOf(req));
|
|
285
291
|
if (!form) {
|
|
286
|
-
throw new formflow_1.FormflowError(404, `The form "${formOf(req)}" does not exist or is not tagged "uag"
|
|
292
|
+
throw new formflow_1.FormflowError(404, `The form "${formOf(req)}" does not exist or is not tagged "uag".`, 'form_not_found');
|
|
287
293
|
}
|
|
288
|
-
if (
|
|
289
|
-
throw new formflow_1.FormflowError(403, `Chat is turned off for the form "${form.formflowName}"
|
|
294
|
+
if ((0, formflow_1.flagOff)(form.form?.properties?.uagChat)) {
|
|
295
|
+
throw new formflow_1.FormflowError(403, `Chat is turned off for the form "${form.formflowName}".`, 'chat_disabled');
|
|
290
296
|
}
|
|
291
297
|
service.requireAccess(req.auth, form, ['create']);
|
|
292
298
|
res.json(await (0, chat_1.chatTurn)(project, form, req.body, { auth: req.auth, requestId: req.requestId }));
|
package/lib/config.d.ts
CHANGED
|
@@ -47,8 +47,6 @@ export type UAGToolOverride = {
|
|
|
47
47
|
submission_update?: ToolInfo;
|
|
48
48
|
find_submissions?: ToolInfo;
|
|
49
49
|
agent_provide?: ToolInfo;
|
|
50
|
-
/** @deprecated The earlier name of `agent_provide`; also applied to it when `agent_provide` is not set. */
|
|
51
|
-
agent_provide_data?: ToolInfo;
|
|
52
50
|
agent_decide?: ToolInfo;
|
|
53
51
|
fetch_external_data?: ToolInfo;
|
|
54
52
|
};
|
package/lib/database.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Collection, Db, MongoClient, MongoClientOptions } from 'mongodb';
|
|
2
|
-
import type { AuthCodeStore } from '@formio/appserver
|
|
2
|
+
import type { AuthCodeStore } from '@formio/appserver';
|
|
3
3
|
/**
|
|
4
4
|
* The UAG's own database (MONGO, with the database named by UAG_DATABASE). It
|
|
5
5
|
* holds only the UAG's state: authorization codes for remote MCP clients, the
|
package/lib/formflow.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Formflow } from '@formio/appserver
|
|
1
|
+
import { Formflow } from '@formio/appserver';
|
|
2
2
|
/**
|
|
3
|
-
* Agentic Formflow from @formio/appserver-core,
|
|
4
|
-
*
|
|
3
|
+
* Agentic Formflow from @formio/appserver-core, through @formio/appserver (see
|
|
4
|
+
* assertSingleCore), under the names the UAG uses. Internal to the UAG:
|
|
5
5
|
* applications reach Formflow as `Formflow` from this package's exports.
|
|
6
6
|
*/
|
|
7
|
-
export declare const EngineConfigError: typeof Formflow.EngineConfigError, EngineRegistry: typeof Formflow.EngineRegistry, FormflowError: typeof Formflow.FormflowError, FormflowService: typeof Formflow.FormflowService, RateLimitError: typeof Formflow.RateLimitError, ScopedTokenError: typeof Formflow.ScopedTokenError, bearerOf: typeof Formflow.bearerOf, engineRegistry: typeof Formflow.engineRegistry, formflow: typeof Formflow.formflow, formflowAuthenticate: typeof Formflow.formflowAuthenticate, identityOf: typeof Formflow.identityOf, issueScopedToken: typeof Formflow.issueScopedToken, isScopedToken: typeof Formflow.isScopedToken, readScopedToken: typeof Formflow.readScopedToken,
|
|
7
|
+
export declare const EngineConfigError: typeof Formflow.EngineConfigError, EngineRegistry: typeof Formflow.EngineRegistry, FormflowError: typeof Formflow.FormflowError, FormflowService: typeof Formflow.FormflowService, RateLimitError: typeof Formflow.RateLimitError, ScopedTokenError: typeof Formflow.ScopedTokenError, bearerOf: typeof Formflow.bearerOf, engineRegistry: typeof Formflow.engineRegistry, formflow: typeof Formflow.formflow, formflowAuthenticate: typeof Formflow.formflowAuthenticate, identityOf: typeof Formflow.identityOf, issueScopedToken: typeof Formflow.issueScopedToken, isScopedToken: typeof Formflow.isScopedToken, readScopedToken: typeof Formflow.readScopedToken, resultEvent: typeof Formflow.resultEvent, withoutOwnedValues: typeof Formflow.withoutOwnedValues, stubAgentEngine: typeof Formflow.stubAgentEngine, stubDecisionEngine: typeof Formflow.stubDecisionEngine, toolsForScope: typeof Formflow.toolsForScope, EngineError: typeof Formflow.EngineError, formatReport: typeof Formflow.formatReport, flagOn: typeof Formflow.flagOn, flagOff: typeof Formflow.flagOff, ruleWithConstraints: typeof Formflow.ruleWithConstraints;
|
|
8
8
|
export type AgentEngine = Formflow.AgentEngine;
|
|
9
9
|
export type CallerIdentity = Formflow.CallerIdentity;
|
|
10
10
|
export type DecisionEngine = Formflow.DecisionEngine;
|
package/lib/formflow.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.ruleWithConstraints = exports.flagOff = exports.flagOn = exports.formatReport = exports.EngineError = exports.toolsForScope = exports.stubDecisionEngine = exports.stubAgentEngine = exports.withoutOwnedValues = exports.resultEvent = exports.readScopedToken = exports.isScopedToken = exports.issueScopedToken = exports.identityOf = exports.formflowAuthenticate = exports.formflow = exports.engineRegistry = exports.bearerOf = exports.ScopedTokenError = exports.RateLimitError = exports.FormflowService = exports.FormflowError = exports.EngineRegistry = exports.EngineConfigError = void 0;
|
|
4
4
|
exports.callerIdentity = callerIdentity;
|
|
5
|
-
const
|
|
5
|
+
const appserver_1 = require("@formio/appserver");
|
|
6
6
|
/**
|
|
7
|
-
* Agentic Formflow from @formio/appserver-core,
|
|
8
|
-
*
|
|
7
|
+
* Agentic Formflow from @formio/appserver-core, through @formio/appserver (see
|
|
8
|
+
* assertSingleCore), under the names the UAG uses. Internal to the UAG:
|
|
9
9
|
* applications reach Formflow as `Formflow` from this package's exports.
|
|
10
10
|
*/
|
|
11
|
-
({ EngineConfigError: exports.EngineConfigError, EngineRegistry: exports.EngineRegistry, FormflowError: exports.FormflowError, FormflowService: exports.FormflowService, RateLimitError: exports.RateLimitError, ScopedTokenError: exports.ScopedTokenError, bearerOf: exports.bearerOf, engineRegistry: exports.engineRegistry, formflow: exports.formflow, formflowAuthenticate: exports.formflowAuthenticate, identityOf: exports.identityOf, issueScopedToken: exports.issueScopedToken, isScopedToken: exports.isScopedToken, readScopedToken: exports.readScopedToken,
|
|
11
|
+
({ EngineConfigError: exports.EngineConfigError, EngineRegistry: exports.EngineRegistry, FormflowError: exports.FormflowError, FormflowService: exports.FormflowService, RateLimitError: exports.RateLimitError, ScopedTokenError: exports.ScopedTokenError, bearerOf: exports.bearerOf, engineRegistry: exports.engineRegistry, formflow: exports.formflow, formflowAuthenticate: exports.formflowAuthenticate, identityOf: exports.identityOf, issueScopedToken: exports.issueScopedToken, isScopedToken: exports.isScopedToken, readScopedToken: exports.readScopedToken, resultEvent: exports.resultEvent, withoutOwnedValues: exports.withoutOwnedValues, stubAgentEngine: exports.stubAgentEngine, stubDecisionEngine: exports.stubDecisionEngine, toolsForScope: exports.toolsForScope, EngineError: exports.EngineError, formatReport: exports.formatReport, flagOn: exports.flagOn, flagOff: exports.flagOff, ruleWithConstraints: exports.ruleWithConstraints } = appserver_1.Formflow);
|
|
12
12
|
/**
|
|
13
13
|
* Who an operation's research acts as: core's `identityOf`, and for a caller
|
|
14
14
|
* signed in with a Form.io token (`x-jwt-token`), that token, so a scoped token
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server, ServerModule } from '@formio/appserver';
|
|
2
|
-
import { Router } from 'express';
|
|
2
|
+
import { Request, Response, Router } from 'express';
|
|
3
3
|
import { UAGProjectInterface } from './UAGProjectInterface';
|
|
4
4
|
import { UAGFormInterface } from './UAGFormInterface';
|
|
5
5
|
import { UAGConfig } from './config';
|
|
@@ -8,12 +8,19 @@ export type UAGModule = ServerModule & {
|
|
|
8
8
|
config?: UAGConfig;
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* Agentic Formflow keeps its license state inside @formio/appserver-core
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Agentic Formflow keeps its license state inside @formio/appserver-core. The UAG
|
|
12
|
+
* loads it only through @formio/appserver, so the two always share one copy,
|
|
13
|
+
* which must be 2.25.2 or later. An older @formio/appserver, or an old lockfile,
|
|
14
|
+
* can still resolve an older one, which would fail in confusing ways; say what
|
|
15
|
+
* to do instead.
|
|
15
16
|
*/
|
|
16
|
-
export declare function assertSingleCore(
|
|
17
|
+
export declare function assertSingleCore(version?: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* isolated-vm, which runs form logic, needs Node's startup snapshot off. Without
|
|
20
|
+
* it nothing fails at once: the process can crash later, in native code. So an
|
|
21
|
+
* embedding server that forgot the flag is stopped here, saying what to do.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertNodeSnapshotDisabled(execArgv?: string[], nodeOptions?: string): void;
|
|
17
24
|
export declare class UAGServer extends Server {
|
|
18
25
|
config: UAGConfig;
|
|
19
26
|
/** The UAG's own database, once it may be used. */
|
|
@@ -36,6 +43,12 @@ export declare class UAGServer extends Server {
|
|
|
36
43
|
* otherwise leave it untouched, close it, and say why.
|
|
37
44
|
*/
|
|
38
45
|
protected enableDatabase(project: UAGProjectInterface): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* An Agent API request that failed before its handlers (a body that is not JSON,
|
|
48
|
+
* one over the size limit) is answered in the Agent API's error shape; every other
|
|
49
|
+
* route keeps Form.io's plain-text answer.
|
|
50
|
+
*/
|
|
51
|
+
protected sendError(req: Request, res: Response, status: number, message: string, err?: any): void;
|
|
39
52
|
/** Browsers reading Agent API responses need the request id too. */
|
|
40
53
|
get exposedHeaders(): string[];
|
|
41
54
|
/** The loaded UAG project, once the project router has been built. */
|
package/lib/index.js
CHANGED
|
@@ -13,11 +13,16 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
|
|
|
13
13
|
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
16
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
20
|
exports.UAGFormInterface = exports.UAGProjectInterface = exports.UAGServer = void 0;
|
|
18
21
|
exports.assertSingleCore = assertSingleCore;
|
|
22
|
+
exports.assertNodeSnapshotDisabled = assertNodeSnapshotDisabled;
|
|
19
23
|
const appserver_1 = require("@formio/appserver");
|
|
20
24
|
const formflow_1 = require("./formflow");
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
21
26
|
const UAGProjectInterface_1 = require("./UAGProjectInterface");
|
|
22
27
|
Object.defineProperty(exports, "UAGProjectInterface", { enumerable: true, get: function () { return UAGProjectInterface_1.UAGProjectInterface; } });
|
|
23
28
|
const UAGFormInterface_1 = require("./UAGFormInterface");
|
|
@@ -25,26 +30,63 @@ Object.defineProperty(exports, "UAGFormInterface", { enumerable: true, get: func
|
|
|
25
30
|
const lodash_1 = require("lodash");
|
|
26
31
|
const engines_1 = require("./agent/engines");
|
|
27
32
|
const router_1 = require("./agent/router");
|
|
33
|
+
const errors_1 = require("./agent/errors");
|
|
28
34
|
const database_1 = require("./database");
|
|
29
35
|
const runs_1 = require("./runs");
|
|
30
36
|
const actionLogs_1 = require("./actionLogs");
|
|
31
37
|
const runner_1 = require("./runner");
|
|
32
38
|
const runTask_1 = require("./actions/runTask");
|
|
39
|
+
const version_1 = require("./version");
|
|
33
40
|
const debug = require('debug')('formio:uag');
|
|
34
41
|
/**
|
|
35
|
-
* Agentic Formflow keeps its license state inside @formio/appserver-core
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
42
|
+
* Agentic Formflow keeps its license state inside @formio/appserver-core. The UAG
|
|
43
|
+
* loads it only through @formio/appserver, so the two always share one copy,
|
|
44
|
+
* which must be 2.25.2 or later. An older @formio/appserver, or an old lockfile,
|
|
45
|
+
* can still resolve an older one, which would fail in confusing ways; say what
|
|
46
|
+
* to do instead.
|
|
39
47
|
*/
|
|
40
|
-
function assertSingleCore(
|
|
48
|
+
function assertSingleCore(version = appserverCoreVersion()) {
|
|
41
49
|
const [major, minor, patch] = version.split('.').map((part) => parseInt(part, 10) || 0);
|
|
42
|
-
if (major < 2 || (major === 2 && (minor <
|
|
43
|
-
throw new Error(`@formio/uag needs @formio/appserver-core 2.
|
|
50
|
+
if (major < 2 || (major === 2 && (minor < 25 || (minor === 25 && patch < 2)))) {
|
|
51
|
+
throw new Error(`@formio/uag needs @formio/appserver-core 2.25.2 or later, through @formio/appserver; ${version} is installed. Upgrade @formio/appserver.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The version of the @formio/appserver-core that @formio/appserver loads. */
|
|
55
|
+
function appserverCoreVersion() {
|
|
56
|
+
const appserver = path_1.default.dirname(require.resolve('@formio/appserver/package.json'));
|
|
57
|
+
return String(require(require.resolve('@formio/appserver-core/package.json', { paths: [appserver] })).version || '0.0.0');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* isolated-vm, which runs form logic, needs Node's startup snapshot off. Without
|
|
61
|
+
* it nothing fails at once: the process can crash later, in native code. So an
|
|
62
|
+
* embedding server that forgot the flag is stopped here, saying what to do.
|
|
63
|
+
*/
|
|
64
|
+
function assertNodeSnapshotDisabled(execArgv = process.execArgv, nodeOptions = process.env.NODE_OPTIONS || '') {
|
|
65
|
+
if (execArgv.includes('--no-node-snapshot') || /(^|\s)--no-node-snapshot(\s|$)/.test(nodeOptions))
|
|
66
|
+
return;
|
|
67
|
+
throw new Error('@formio/uag needs Node started with --no-node-snapshot: isolated-vm, which runs form logic, can crash the process without it. ' +
|
|
68
|
+
'Start node with --no-node-snapshot, or set NODE_OPTIONS=--no-node-snapshot. The uag command and the formio/uag image do this for you.');
|
|
69
|
+
}
|
|
70
|
+
/** A host name a browser cannot reach: a container or service name, with no dot. */
|
|
71
|
+
function internalHost(url) {
|
|
72
|
+
try {
|
|
73
|
+
const { hostname } = new URL(url);
|
|
74
|
+
return !!hostname && hostname !== 'localhost' && !hostname.includes('.') && !hostname.includes(':');
|
|
44
75
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Warnings for settings that start fine and fail later, in ways far from their cause. */
|
|
81
|
+
function warnAboutSettings(config, secretGiven) {
|
|
82
|
+
if (!secretGiven) {
|
|
83
|
+
console.warn('[formio-uag] JWT_SECRET is not set, so the UAG signs its tokens with a random secret that lasts only as long as this process: ' +
|
|
84
|
+
'every token stops working when it restarts, and other instances refuse it. Set JWT_SECRET, the same on every instance.');
|
|
85
|
+
}
|
|
86
|
+
const auth = config.auth || {};
|
|
87
|
+
if (auth.mcpOAuth && !auth.loginForm && internalHost(String(config.project || ''))) {
|
|
88
|
+
console.warn(`[formio-uag] MCP_OAUTH is on, but LOGIN_FORM is not set: the sign-in page loads ${String(config.project).replace(/\/+$/, '')}/user/login, ` +
|
|
89
|
+
'an address a person\'s browser cannot reach. Set LOGIN_FORM to the login form\'s public URL.');
|
|
48
90
|
}
|
|
49
91
|
}
|
|
50
92
|
/** The UAG's own database from the environment, when `MONGO` is set. */
|
|
@@ -62,8 +104,11 @@ function databaseFromEnv(env = process.env) {
|
|
|
62
104
|
}
|
|
63
105
|
class UAGServer extends appserver_1.Server {
|
|
64
106
|
constructor(config) {
|
|
107
|
+
assertNodeSnapshotDisabled();
|
|
65
108
|
assertSingleCore();
|
|
66
109
|
const configuredDb = !!config?.db;
|
|
110
|
+
// Read before the server fills in a random one.
|
|
111
|
+
const secretGiven = !!(config?.auth?.secret || (0, lodash_1.get)(process.env, 'JWT_SECRET', '').toString());
|
|
67
112
|
const uagLicense = (0, lodash_1.get)(process.env, 'UAG_LICENSE', '').toString();
|
|
68
113
|
if (uagLicense) {
|
|
69
114
|
console.warn('[formio-uag] UAG_LICENSE is deprecated. Put a Form.io license that includes the UAG in LICENSE_KEY instead; UAG_LICENSE keeps working until UAG 3.0.');
|
|
@@ -85,6 +130,10 @@ class UAGServer extends appserver_1.Server {
|
|
|
85
130
|
retryDelay: parseInt((0, lodash_1.get)(process.env, 'PROJECT_RETRY_DELAY', 5000).toString(), 10),
|
|
86
131
|
submissionProxy: true,
|
|
87
132
|
processAsClient: true,
|
|
133
|
+
// GET /status names the UAG's version beside the Form.io packages it runs on.
|
|
134
|
+
status: { '@formio/uag': version_1.UAG_VERSION },
|
|
135
|
+
// Bodies are parsed before authentication, so keep them to what Form.io accepts.
|
|
136
|
+
maxBodySize: (0, lodash_1.get)(process.env, 'MAX_BODY_SIZE', '16mb').toString(),
|
|
88
137
|
auth: { pkce: true, mcpOAuth: (0, lodash_1.get)(process.env, 'MCP_OAUTH', '').toString() === 'true' },
|
|
89
138
|
database: databaseFromEnv()
|
|
90
139
|
}));
|
|
@@ -94,6 +143,7 @@ class UAGServer extends appserver_1.Server {
|
|
|
94
143
|
delete this.module.db;
|
|
95
144
|
delete this.config.db;
|
|
96
145
|
}
|
|
146
|
+
warnAboutSettings(this.config, secretGiven);
|
|
97
147
|
this.use({
|
|
98
148
|
ProjectInterface: UAGProjectInterface_1.UAGProjectInterface,
|
|
99
149
|
FormInterface: UAGFormInterface_1.UAGFormInterface,
|
|
@@ -163,6 +213,18 @@ class UAGServer extends appserver_1.Server {
|
|
|
163
213
|
console.warn('[formio-uag] MCP_OAUTH is set, but remote MCP sign-in is off: it needs the UAG\'s database (MONGO), and a license that includes the UAG to use it.');
|
|
164
214
|
}
|
|
165
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* An Agent API request that failed before its handlers (a body that is not JSON,
|
|
218
|
+
* one over the size limit) is answered in the Agent API's error shape; every other
|
|
219
|
+
* route keeps Form.io's plain-text answer.
|
|
220
|
+
*/
|
|
221
|
+
sendError(req, res, status, message, err) {
|
|
222
|
+
if (!(0, errors_1.isAgentApiRequest)(req))
|
|
223
|
+
return super.sendError(req, res, status, message, err);
|
|
224
|
+
const error = err?.type === 'entity.parse.failed' ? 'The request body is not valid JSON.' : message;
|
|
225
|
+
// Written past any shaping wrapper, which may not have run for this request.
|
|
226
|
+
res.status(status).type('application/json').end(JSON.stringify((0, errors_1.errorBody)((0, errors_1.requestIdOf)(req, res), status, { error })));
|
|
227
|
+
}
|
|
166
228
|
/** Browsers reading Agent API responses need the request id too. */
|
|
167
229
|
get exposedHeaders() {
|
|
168
230
|
return [...super.exposedHeaders, 'X-Request-Id'];
|
|
@@ -181,6 +243,8 @@ class UAGServer extends appserver_1.Server {
|
|
|
181
243
|
await super.extendRouter(router);
|
|
182
244
|
const uagProject = this.uagProject;
|
|
183
245
|
if (uagProject) {
|
|
246
|
+
// First: the Agent API's request id and one error shape, for every refusal after it.
|
|
247
|
+
router.use((0, errors_1.agentErrorShape)());
|
|
184
248
|
router.use(uagProject.agentRouter());
|
|
185
249
|
// An agent operation of a form the project does not have is answered here, not passed to Form.io.
|
|
186
250
|
router.use((0, router_1.agentNotFound)(uagProject));
|
package/lib/runner.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export type TaskRunOutcome = {
|
|
|
53
53
|
}[];
|
|
54
54
|
/** Whether the results were saved (nothing is saved when nothing changed). */
|
|
55
55
|
saved: boolean;
|
|
56
|
+
/** Answers the form's validation refused, left out: path to the form's message. */
|
|
57
|
+
refused?: Record<string, string>;
|
|
56
58
|
error?: string;
|
|
57
59
|
};
|
|
58
60
|
/**
|
package/lib/runner.js
CHANGED
|
@@ -268,8 +268,9 @@ class TaskRunner {
|
|
|
268
268
|
const service = (0, formflow_1.formflow)(this.project);
|
|
269
269
|
const load = async () => {
|
|
270
270
|
const stored = await form.loadSubmission(job.submission, await act());
|
|
271
|
+
// Form.io answers a submission the caller may not read as missing, so say both.
|
|
271
272
|
if (!stored)
|
|
272
|
-
throw new formflow_1.FormflowError(404, `Submission "${job.submission}" was not found.`);
|
|
273
|
+
throw new formflow_1.FormflowError(404, `Submission "${job.submission}" was not found, or the user the run acts as may not read it (give submitters Read Own access, or the action an agent role).`);
|
|
273
274
|
return stored;
|
|
274
275
|
};
|
|
275
276
|
let stored = await load();
|
|
@@ -278,8 +279,14 @@ class TaskRunner {
|
|
|
278
279
|
const engines = run.results.map((result) => ({ kind: result.kind, engine: result.engine ?? null, model: result.model ?? null }));
|
|
279
280
|
const failed = run.failed.map((group) => group.error || 'no engine could answer').join('; ');
|
|
280
281
|
const changed = Object.keys(run.written).length > 0 || run.cleared.length > 0;
|
|
282
|
+
const refused = run.refused || {};
|
|
283
|
+
if (Object.keys(refused).length) {
|
|
284
|
+
console.warn(`[formio-uag] Run task "${job.task}" left out answers the form refused for submission ${job.submission} of "${job.form}": ` +
|
|
285
|
+
Object.entries(refused).map(([path, message]) => `${path} (${message})`).join('; '));
|
|
286
|
+
}
|
|
287
|
+
const left = Object.keys(refused).length ? { refused } : {};
|
|
281
288
|
if (!changed) {
|
|
282
|
-
return { status: failed ? 'failed' : 'done', written: {}, cleared: [], engines, saved: false, ...(failed && { error: failed }) };
|
|
289
|
+
return { status: failed ? 'failed' : 'done', written: {}, cleared: [], engines, saved: false, ...left, ...(failed && { error: failed }) };
|
|
283
290
|
}
|
|
284
291
|
try {
|
|
285
292
|
// Only the task's fields changed; `modified` refuses the save if a person saved in the meantime.
|
|
@@ -291,7 +298,7 @@ class TaskRunner {
|
|
|
291
298
|
...(job.caller ? { [exports.RUN_TASK_CALLER]: job.caller, [exports.RUN_TASK_IDENTITY]: job.identity } : { [exports.RUN_TASK_AS_USER]: true }),
|
|
292
299
|
[exports.RUN_TASK_CHAIN]: (job.chain || 0) + 1
|
|
293
300
|
});
|
|
294
|
-
return { status: failed ? 'failed' : 'done', written: run.written, cleared: run.cleared, engines, saved: true, ...(failed && { error: failed }) };
|
|
301
|
+
return { status: failed ? 'failed' : 'done', written: run.written, cleared: run.cleared, engines, saved: true, ...left, ...(failed && { error: failed }) };
|
|
295
302
|
}
|
|
296
303
|
catch (err) {
|
|
297
304
|
if (err?.name !== 'ConflictError' || attempt >= 2) {
|
|
@@ -313,6 +320,7 @@ function resultOf(outcome) {
|
|
|
313
320
|
engines: outcome.engines,
|
|
314
321
|
written: Object.keys(outcome.written),
|
|
315
322
|
cleared: outcome.cleared,
|
|
323
|
+
...(outcome.refused && { refused: Object.keys(outcome.refused) }),
|
|
316
324
|
saved: outcome.saved,
|
|
317
325
|
...(outcome.error && { error: outcome.error })
|
|
318
326
|
};
|
package/lib/runs.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export type TaskRunRecord = {
|
|
|
40
40
|
}[];
|
|
41
41
|
written?: string[];
|
|
42
42
|
cleared?: string[];
|
|
43
|
+
/** Paths whose answers the form's validation refused, left out of the save. */
|
|
44
|
+
refused?: string[];
|
|
43
45
|
saved?: boolean;
|
|
44
46
|
error?: string;
|
|
45
47
|
created: Date;
|
|
@@ -77,7 +79,7 @@ export declare class TaskRunStore {
|
|
|
77
79
|
/** Keep holding a run. False when another replica took it over. */
|
|
78
80
|
renew(id: ObjectId): Promise<boolean>;
|
|
79
81
|
/** Record how a run ended. Only the replica holding a durable run can. */
|
|
80
|
-
finish(id: ObjectId, result: Pick<TaskRunRecord, 'engines' | 'written' | 'cleared' | 'saved' | 'error'> & {
|
|
82
|
+
finish(id: ObjectId, result: Pick<TaskRunRecord, 'engines' | 'written' | 'cleared' | 'refused' | 'saved' | 'error'> & {
|
|
81
83
|
status: 'done' | 'failed';
|
|
82
84
|
}): Promise<void>;
|
|
83
85
|
get(id: string): Promise<TaskRunRecord | null>;
|
|
@@ -113,11 +113,6 @@ export declare class SchemaBuilder {
|
|
|
113
113
|
* components' `uag` property).
|
|
114
114
|
*/
|
|
115
115
|
task(): this;
|
|
116
|
-
/**
|
|
117
|
-
* The legacy name for `task`. Kept in the schema so clients that still send
|
|
118
|
-
* it are not silently given the default task.
|
|
119
|
-
*/
|
|
120
|
-
persona(): this;
|
|
121
116
|
/**
|
|
122
117
|
* The data_path of a specific field within the form.
|
|
123
118
|
*/
|
|
@@ -175,14 +175,6 @@ class SchemaBuilder {
|
|
|
175
175
|
this.schema.task = zod_1.default.string().optional().describe('The name of a task defined on the form: the value of its components\' `uag` property, such as "triage". Not a description of the work. If not provided, the form\'s first task is used.');
|
|
176
176
|
return this;
|
|
177
177
|
}
|
|
178
|
-
/**
|
|
179
|
-
* The legacy name for `task`. Kept in the schema so clients that still send
|
|
180
|
-
* it are not silently given the default task.
|
|
181
|
-
*/
|
|
182
|
-
persona() {
|
|
183
|
-
this.schema.persona = zod_1.default.string().optional().describe('Deprecated: the former name for `task`. Send `task` instead.');
|
|
184
|
-
return this;
|
|
185
|
-
}
|
|
186
178
|
/**
|
|
187
179
|
* The data_path of a specific field within the form.
|
|
188
180
|
*/
|
package/lib/tools/agentDecide.js
CHANGED
|
@@ -22,7 +22,7 @@ const agentDecide = async (project) => {
|
|
|
22
22
|
description: 'Return the decision questions for a task\'s closed-set fields (radio, select, checkbox, select boxes, survey and small resource selects) as structured JSON: the state to decide on, every question, and how each answer maps back to a field value. Answer the questions with a decision model, then write the values with `submission_update`. Provide either `submission_id` or `form_data`.',
|
|
23
23
|
annotations: (0, annotations_1.reads)('Get decision questions'),
|
|
24
24
|
inputSchema: {
|
|
25
|
-
...(new SchemaBuilder_1.SchemaBuilder(project)).form_name().submission_id().task().
|
|
25
|
+
...(new SchemaBuilder_1.SchemaBuilder(project)).form_name().submission_id().task().schema,
|
|
26
26
|
form_data: zod_1.default.record(zod_1.default.string(), zod_1.default.any()).optional().describe('The current form data as a submission data object (nested by data path). Used when `submission_id` is not given.')
|
|
27
27
|
},
|
|
28
28
|
execute: async (args, extra) => {
|
|
@@ -40,7 +40,7 @@ const agentDecide = async (project) => {
|
|
|
40
40
|
data = submission.data || {};
|
|
41
41
|
}
|
|
42
42
|
try {
|
|
43
|
-
const task =
|
|
43
|
+
const task = args.task || undefined;
|
|
44
44
|
const out = await (0, formflow_1.formflow)(project).questions(form, { task, data, submissionId: submission_id }, extra.authInfo);
|
|
45
45
|
return project.mcpJSONResponse({ form: form_name, ...out });
|
|
46
46
|
}
|
|
@@ -6,8 +6,3 @@ import { ToolInfo } from "./utils";
|
|
|
6
6
|
* agent writes its values with `submission_update`.
|
|
7
7
|
*/
|
|
8
8
|
export declare const agentProvide: (project: UAGProjectInterface) => Promise<ToolInfo>;
|
|
9
|
-
/**
|
|
10
|
-
* `agent_provide_data`: the earlier name of `agent_provide`, kept so MCP clients
|
|
11
|
-
* and integrations that call it keep working. Removed in a future major release.
|
|
12
|
-
*/
|
|
13
|
-
export declare const agentProvideData: (project: UAGProjectInterface) => Promise<ToolInfo>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.agentProvide = void 0;
|
|
4
4
|
const template_1 = require("../template");
|
|
5
5
|
const lodash_1 = require("lodash");
|
|
6
6
|
const SchemaBuilder_1 = require("./SchemaBuilder");
|
|
@@ -29,66 +29,33 @@ async function refusal(project) {
|
|
|
29
29
|
* agent writes its values with `submission_update`.
|
|
30
30
|
*/
|
|
31
31
|
const agentProvide = async (project) => {
|
|
32
|
-
// Overrides written for the tool's earlier name still apply, except a rename,
|
|
33
|
-
// which belongs to the deprecated tool and would clash with it.
|
|
34
|
-
const overrides = project.config?.toolOverrides;
|
|
35
|
-
const own = overrides?.agent_provide || (overrides?.agent_provide_data ? (0, lodash_1.omit)(overrides.agent_provide_data, 'name') : {});
|
|
36
32
|
// Merged into a new object: the overrides are shared config, and the tools are
|
|
37
33
|
// built again for every request.
|
|
38
|
-
return (0, lodash_1.defaultsDeep)({},
|
|
34
|
+
return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.agent_provide, provideTool(project));
|
|
39
35
|
};
|
|
40
36
|
exports.agentProvide = agentProvide;
|
|
41
|
-
|
|
42
|
-
* `agent_provide_data`: the earlier name of `agent_provide`, kept so MCP clients
|
|
43
|
-
* and integrations that call it keep working. Removed in a future major release.
|
|
44
|
-
*/
|
|
45
|
-
const agentProvideData = async (project) => {
|
|
46
|
-
const tool = provideTool(project, 'agent_provide_data');
|
|
47
|
-
const execute = tool.execute;
|
|
48
|
-
return (0, lodash_1.defaultsDeep)({}, project.config?.toolOverrides?.agent_provide_data, {
|
|
49
|
-
...tool,
|
|
50
|
-
title: 'Agent Provide Data (deprecated)',
|
|
51
|
-
description: `Deprecated: use \`agent_provide\`, which takes the same arguments. ${DESCRIPTION}`,
|
|
52
|
-
execute: (args, extra) => {
|
|
53
|
-
(0, formflow_1.warnLegacy)('agent_provide_data', 'the agent_provide_data MCP tool; use agent_provide.');
|
|
54
|
-
return execute(args, extra);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
};
|
|
58
|
-
exports.agentProvideData = agentProvideData;
|
|
59
|
-
function provideTool(project, name) {
|
|
37
|
+
function provideTool(project) {
|
|
60
38
|
return {
|
|
61
|
-
name,
|
|
39
|
+
name: 'agent_provide',
|
|
62
40
|
title: 'Agent Provide',
|
|
63
41
|
description: DESCRIPTION,
|
|
64
42
|
annotations: (0, annotations_1.reads)('Process submission data'),
|
|
65
43
|
inputSchema: (new SchemaBuilder_1.SchemaBuilder(project))
|
|
66
44
|
.form_name()
|
|
67
45
|
.submission_id()
|
|
68
|
-
.task()
|
|
69
|
-
.persona().schema,
|
|
46
|
+
.task().schema,
|
|
70
47
|
execute: async (args, extra) => {
|
|
71
48
|
const { form_name, submission_id } = args;
|
|
72
49
|
const refused = await refusal(project);
|
|
73
50
|
if (refused)
|
|
74
51
|
return project.mcpJSONResponse(refused, true);
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
requested = (0, formflow_1.resolveTaskName)(args, name);
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
if (err instanceof formflow_1.FormflowError)
|
|
81
|
-
return project.mcpJSONResponse({ error: err.message }, true);
|
|
82
|
-
throw err;
|
|
83
|
-
}
|
|
52
|
+
const requested = args.task || undefined;
|
|
84
53
|
const form = await project.getForm(form_name);
|
|
85
54
|
if (!form) {
|
|
86
55
|
return project.mcpResponse(template_1.ResponseTemplate.formNotFound, { formName: form_name }, true);
|
|
87
56
|
}
|
|
88
57
|
const task = requested || Object.keys(form.uagFields)[0] || 'default';
|
|
89
|
-
|
|
90
|
-
// customized a template that still renders `persona`.
|
|
91
|
-
const names = { task, persona: task };
|
|
58
|
+
const names = { task };
|
|
92
59
|
const uag = form.uagFields[task];
|
|
93
60
|
if (!uag) {
|
|
94
61
|
return project.mcpResponse(template_1.ResponseTemplate.uagComponentNotFound, {
|
package/lib/tools/collectData.js
CHANGED
|
@@ -34,7 +34,8 @@ const collectData = async (project) => {
|
|
|
34
34
|
const fields = await form.getFields(submission, extra.authInfo, parent?.data_path);
|
|
35
35
|
if (fields.errors.length > 0) {
|
|
36
36
|
if (as_json) {
|
|
37
|
-
|
|
37
|
+
// structuredContent must be an object: an MCP client refuses a bare array.
|
|
38
|
+
return project.mcpJSONResponse({ errors: fields.errors }, true);
|
|
38
39
|
}
|
|
39
40
|
return project.mcpResponse(template_1.ResponseTemplate.fieldValidationErrors, { invalidFields: fields.errors });
|
|
40
41
|
}
|
package/lib/tools/index.d.ts
CHANGED
|
@@ -8,10 +8,10 @@ import { findSubmission } from "./findSubmission";
|
|
|
8
8
|
import { submissionUpdate } from "./submissionUpdate";
|
|
9
9
|
import { ToolInfo } from "./utils";
|
|
10
10
|
import { UAGProjectInterface } from "../UAGProjectInterface";
|
|
11
|
-
import { agentProvide
|
|
11
|
+
import { agentProvide } from "./agentProvide";
|
|
12
12
|
import { agentDecide } from "./agentDecide";
|
|
13
13
|
import { fetchExternalData } from "./fetchExternalData";
|
|
14
14
|
export * from './utils';
|
|
15
15
|
export * from './annotations';
|
|
16
|
-
export { getForms, getFormFields, getFieldInfo, collectData, confirmSubmission, submitCompletedForm, findSubmission, submissionUpdate, fetchExternalData, agentProvide,
|
|
16
|
+
export { getForms, getFormFields, getFieldInfo, collectData, confirmSubmission, submitCompletedForm, findSubmission, submissionUpdate, fetchExternalData, agentProvide, agentDecide };
|
|
17
17
|
export declare const getTools: (project: UAGProjectInterface) => Promise<ToolInfo[]>;
|
package/lib/tools/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.getTools = exports.agentDecide = exports.
|
|
17
|
+
exports.getTools = exports.agentDecide = exports.agentProvide = exports.fetchExternalData = exports.submissionUpdate = exports.findSubmission = exports.submitCompletedForm = exports.confirmSubmission = exports.collectData = exports.getFieldInfo = exports.getFormFields = exports.getForms = void 0;
|
|
18
18
|
const getForms_1 = require("./getForms");
|
|
19
19
|
Object.defineProperty(exports, "getForms", { enumerable: true, get: function () { return getForms_1.getForms; } });
|
|
20
20
|
const getFormFields_1 = require("./getFormFields");
|
|
@@ -33,7 +33,6 @@ const submissionUpdate_1 = require("./submissionUpdate");
|
|
|
33
33
|
Object.defineProperty(exports, "submissionUpdate", { enumerable: true, get: function () { return submissionUpdate_1.submissionUpdate; } });
|
|
34
34
|
const agentProvide_1 = require("./agentProvide");
|
|
35
35
|
Object.defineProperty(exports, "agentProvide", { enumerable: true, get: function () { return agentProvide_1.agentProvide; } });
|
|
36
|
-
Object.defineProperty(exports, "agentProvideData", { enumerable: true, get: function () { return agentProvide_1.agentProvideData; } });
|
|
37
36
|
const agentDecide_1 = require("./agentDecide");
|
|
38
37
|
Object.defineProperty(exports, "agentDecide", { enumerable: true, get: function () { return agentDecide_1.agentDecide; } });
|
|
39
38
|
const fetchExternalData_1 = require("./fetchExternalData");
|
|
@@ -52,8 +51,6 @@ const getTools = async (project) => {
|
|
|
52
51
|
(0, submissionUpdate_1.submissionUpdate)(project),
|
|
53
52
|
(0, agentProvide_1.agentProvide)(project),
|
|
54
53
|
(0, agentDecide_1.agentDecide)(project),
|
|
55
|
-
// Deprecated: the earlier name of agent_provide.
|
|
56
|
-
(0, agentProvide_1.agentProvideData)(project),
|
|
57
54
|
(0, fetchExternalData_1.fetchExternalData)(project)
|
|
58
55
|
]);
|
|
59
56
|
};
|
package/lib/version.d.ts
ADDED
package/lib/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formio/uag",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.5",
|
|
4
4
|
"mcpName": "io.form/formio-uag",
|
|
5
5
|
"description": "The Form.io Universal Agent Gateway (UAG).",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"build:docker": "docker build -t formio/uag$(node -p \"require('./package.json').version.includes('-rc.') ? ':rc' : ''\") --platform=linux/amd64 .",
|
|
27
27
|
"test": "tsc --noEmit -p tsconfig.test.json && mocha --no-node-snapshot",
|
|
28
28
|
"test:e2e": "mocha --config .mocharc.e2e.json",
|
|
29
|
+
"test:getting-started": "mocha --no-config --node-option no-node-snapshot --require dotenv/config --timeout 1800000 test/e2e/layer3.getting-started.e2e.mjs",
|
|
29
30
|
"release": "node scripts/release.mjs",
|
|
30
31
|
"release:version": "node scripts/set-version.mjs",
|
|
31
32
|
"dev": "tsx --no-node-snapshot index.js",
|
|
@@ -42,10 +43,9 @@
|
|
|
42
43
|
"author": "Form.io Developers",
|
|
43
44
|
"license": "MIT",
|
|
44
45
|
"dependencies": {
|
|
45
|
-
"@formio/appserver": "^2.
|
|
46
|
-
"@formio/appserver-core": "^2.24.1",
|
|
46
|
+
"@formio/appserver": "^2.25.2",
|
|
47
47
|
"@formio/core": "2.9.0",
|
|
48
|
-
"@formio/uag-sdk": "^2.0.0-rc.
|
|
48
|
+
"@formio/uag-sdk": "^2.0.0-rc.5",
|
|
49
49
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
50
50
|
"cors": "^2.8.6",
|
|
51
51
|
"debug": "^4.4.3",
|
|
@@ -68,20 +68,20 @@
|
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@anthropic-ai/sdk": "^1.3.0",
|
|
71
|
-
"@formio/js": "5.
|
|
71
|
+
"@formio/js": "5.7.0",
|
|
72
72
|
"@types/chai": "^5.2.3",
|
|
73
73
|
"@types/cors": "^2.8.19",
|
|
74
74
|
"@types/debug": "^4.1.13",
|
|
75
75
|
"@types/dotenv": "^8.2.3",
|
|
76
76
|
"@types/express": "^5.0.6",
|
|
77
|
-
"@types/jsdom": "^30",
|
|
77
|
+
"@types/jsdom": "^30.0.0",
|
|
78
78
|
"@types/jsonwebtoken": "^9.0.10",
|
|
79
79
|
"@types/lodash": "^4.17.27",
|
|
80
80
|
"@types/mocha": "^10.0.10",
|
|
81
|
-
"@types/node": "^26.6.
|
|
81
|
+
"@types/node": "^26.6.6",
|
|
82
82
|
"@types/supertest": "^7.2.1",
|
|
83
83
|
"chai": "^6.3.0",
|
|
84
|
-
"jsdom": "30.1.
|
|
84
|
+
"jsdom": "30.1.3",
|
|
85
85
|
"mocha": "^12.0.3",
|
|
86
86
|
"mongodb-memory-server": "^11.3.0",
|
|
87
87
|
"playwright": "1.64.1",
|