@formio/uag 1.13.2 → 2.0.0-rc.2
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 +235 -535
- package/bin/uag-eval.js +95 -0
- package/bin/uag.js +73 -0
- package/lib/UAGFormInterface.d.ts +60 -2
- package/lib/UAGFormInterface.js +172 -46
- package/lib/UAGProjectInterface.d.ts +37 -5
- package/lib/UAGProjectInterface.js +166 -33
- package/lib/actionLogs.d.ts +33 -0
- package/lib/actionLogs.js +147 -0
- package/lib/actions/runTask.d.ts +43 -0
- package/lib/actions/runTask.js +323 -0
- package/lib/agent/anonymous.d.ts +34 -0
- package/lib/agent/anonymous.js +98 -0
- package/lib/agent/chat.d.ts +113 -0
- package/lib/agent/chat.js +539 -0
- package/lib/agent/engines.d.ts +13 -0
- package/lib/agent/engines.js +37 -0
- package/lib/agent/index.d.ts +4 -0
- package/lib/agent/index.js +18 -0
- package/lib/agent/remote.d.ts +67 -0
- package/lib/agent/remote.js +229 -0
- package/lib/agent/requestId.d.ts +4 -0
- package/lib/agent/requestId.js +19 -0
- package/lib/agent/router.d.ts +31 -0
- package/lib/agent/router.js +295 -0
- package/lib/agent/stubs.d.ts +26 -0
- package/lib/agent/stubs.js +26 -0
- package/lib/agent/tools.d.ts +10 -0
- package/lib/agent/tools.js +33 -0
- package/lib/config.d.ts +67 -0
- package/lib/config.js +19 -0
- package/lib/database.d.ts +75 -0
- package/lib/database.js +229 -0
- package/lib/formflow.d.ts +40 -0
- package/lib/formflow.js +28 -0
- package/lib/identity.d.ts +53 -0
- package/lib/identity.js +107 -0
- package/lib/index.d.ts +41 -0
- package/lib/index.js +179 -7
- package/lib/router.js +15 -25
- package/lib/runner.d.ts +147 -0
- package/lib/runner.js +345 -0
- package/lib/runs.d.ts +89 -0
- package/lib/runs.js +74 -0
- package/lib/template.js +7 -9
- package/lib/templates/uagComponentNotFound.md +5 -5
- package/lib/tools/SchemaBuilder.d.ts +7 -2
- package/lib/tools/SchemaBuilder.js +11 -3
- package/lib/tools/agentDecide.d.ts +8 -0
- package/lib/tools/agentDecide.js +57 -0
- package/lib/tools/agentProvide.d.ts +13 -0
- package/lib/tools/agentProvide.js +130 -0
- package/lib/tools/collectData.js +1 -1
- package/lib/tools/confirmSubmission.js +1 -1
- package/lib/tools/fetchExternalData.js +8 -17
- package/lib/tools/findSubmission.js +62 -56
- package/lib/tools/getFieldInfo.js +1 -1
- package/lib/tools/getFormFields.js +1 -1
- package/lib/tools/getForms.js +1 -1
- package/lib/tools/index.d.ts +3 -1
- package/lib/tools/index.js +21 -14
- package/lib/tools/submissionUpdate.js +1 -1
- package/lib/tools/submitForm.js +1 -1
- package/package.json +42 -18
- package/lib/tools/agentProvideData.d.ts +0 -3
- package/lib/tools/agentProvideData.js +0 -62
package/lib/identity.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AgentIdentityError = exports.SIGNED_TOKEN_TTL = void 0;
|
|
7
|
+
exports.actingForOf = actingForOf;
|
|
8
|
+
exports.signFormioToken = signFormioToken;
|
|
9
|
+
exports.agentAuth = agentAuth;
|
|
10
|
+
exports.userAuth = userAuth;
|
|
11
|
+
exports.refusedSignedToken = refusedSignedToken;
|
|
12
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
13
|
+
const appserver_1 = require("@formio/appserver");
|
|
14
|
+
const formflow_1 = require("./formflow");
|
|
15
|
+
/**
|
|
16
|
+
* Who the UAG acts as at Form.io when it runs a task for someone: the caller,
|
|
17
|
+
* with the token they sent, or a Run task action's agent role, acting on the
|
|
18
|
+
* caller's behalf with a Form.io token the UAG signs.
|
|
19
|
+
*
|
|
20
|
+
* The UAG signs Form.io "external" tokens with JWT_SECRET, which must be Form.io
|
|
21
|
+
* Enterprise's: Form.io then takes the user, and their roles, from the token. A
|
|
22
|
+
* token the UAG signs is short-lived, never marks an administrator, and never
|
|
23
|
+
* carries an admin role.
|
|
24
|
+
*/
|
|
25
|
+
/** Seconds a token the UAG signs is valid for. */
|
|
26
|
+
exports.SIGNED_TOKEN_TTL = 300;
|
|
27
|
+
/** A token the UAG may not sign, or Form.io would not accept. */
|
|
28
|
+
class AgentIdentityError extends formflow_1.FormflowError {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(500, message, 'agent_identity');
|
|
31
|
+
this.name = 'AgentIdentityError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.AgentIdentityError = AgentIdentityError;
|
|
35
|
+
/** The user a request comes from, as the UAG records it. */
|
|
36
|
+
function actingForOf(auth, project) {
|
|
37
|
+
const user = auth?.user;
|
|
38
|
+
if (!user?._id) {
|
|
39
|
+
return { roles: project.anonymousRoles };
|
|
40
|
+
}
|
|
41
|
+
const email = user.data?.email;
|
|
42
|
+
return {
|
|
43
|
+
_id: String(user._id),
|
|
44
|
+
roles: (user.roles || []).map(String),
|
|
45
|
+
...(typeof email === 'string' && { email })
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The project's admin role ids, from its access settings. */
|
|
49
|
+
function adminRoles(project) {
|
|
50
|
+
return new Set(Object.values(project.access?.roles || {})
|
|
51
|
+
.filter((role) => role?.admin)
|
|
52
|
+
.map((role) => String(role._id)));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sign a Form.io token for `user` with `roles`. Refuses an admin role, and never
|
|
56
|
+
* marks the holder an administrator.
|
|
57
|
+
*/
|
|
58
|
+
function signFormioToken(project, user, roles, claim) {
|
|
59
|
+
const secret = project.config?.auth?.secret;
|
|
60
|
+
if (!secret)
|
|
61
|
+
throw new AgentIdentityError('Acting on behalf of a user needs JWT_SECRET, the same as Form.io Enterprise\'s.');
|
|
62
|
+
const admin = adminRoles(project);
|
|
63
|
+
if (roles.some((role) => admin.has(String(role)))) {
|
|
64
|
+
throw new AgentIdentityError('An agent cannot act with an admin role. Choose a role without admin access.');
|
|
65
|
+
}
|
|
66
|
+
const projectId = String(project.project?._id || '');
|
|
67
|
+
if (!projectId)
|
|
68
|
+
throw new AgentIdentityError('Acting on behalf of a user needs the project to be loaded.');
|
|
69
|
+
const payload = {
|
|
70
|
+
external: true,
|
|
71
|
+
// Form.io names an external user without an id from its data.
|
|
72
|
+
user: user._id
|
|
73
|
+
? { _id: user._id, data: { ...(user.email && { email: user.email }) }, roles: roles.map(String) }
|
|
74
|
+
: { _id: 'external', data: { id: 'uag-anonymous' }, roles: roles.map(String) },
|
|
75
|
+
project: { _id: projectId },
|
|
76
|
+
uag: claim
|
|
77
|
+
};
|
|
78
|
+
return jsonwebtoken_1.default.sign(payload, secret, { expiresIn: exports.SIGNED_TOKEN_TTL });
|
|
79
|
+
}
|
|
80
|
+
/** An identity that acts as `role` for `user`, with a newly signed token. */
|
|
81
|
+
async function agentAuth(project, user, role, claim) {
|
|
82
|
+
return authFor(project, user, [role], { agent: true, ...claim });
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* An identity that acts as `user`, with their recorded roles, for research and
|
|
86
|
+
* for the runs that act as them. A user's admin roles are left out: the UAG
|
|
87
|
+
* never signs a token with one, so the user acts with their other roles.
|
|
88
|
+
*/
|
|
89
|
+
async function userAuth(project, user, claim) {
|
|
90
|
+
const admin = adminRoles(project);
|
|
91
|
+
return authFor(project, user, user.roles.filter((role) => !admin.has(String(role))), { agent: false, ...claim });
|
|
92
|
+
}
|
|
93
|
+
async function authFor(project, user, roles, claim) {
|
|
94
|
+
const token = signFormioToken(project, user, roles, claim);
|
|
95
|
+
const auth = new appserver_1.AuthRequest(project);
|
|
96
|
+
await auth.fromFormioUser({ _id: user._id || 'external', roles, data: user.email ? { email: user.email } : {} }, token, project);
|
|
97
|
+
return auth;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Explain a Form.io refusal of a token the UAG signed: almost always a
|
|
101
|
+
* JWT_SECRET that is not Form.io Enterprise's.
|
|
102
|
+
*/
|
|
103
|
+
function refusedSignedToken(status) {
|
|
104
|
+
if (status !== 401 && status !== 440)
|
|
105
|
+
return null;
|
|
106
|
+
return new AgentIdentityError('Form.io refused the token the UAG signed to act on the user\'s behalf: JWT_SECRET must be the same as Form.io Enterprise\'s.');
|
|
107
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,16 +1,57 @@
|
|
|
1
1
|
import { Server, ServerModule } from '@formio/appserver';
|
|
2
|
+
import { Router } from 'express';
|
|
2
3
|
import { UAGProjectInterface } from './UAGProjectInterface';
|
|
3
4
|
import { UAGFormInterface } from './UAGFormInterface';
|
|
4
5
|
import { UAGConfig } from './config';
|
|
6
|
+
import { UAGDatabase } from './database';
|
|
5
7
|
export type UAGModule = ServerModule & {
|
|
6
8
|
config?: UAGConfig;
|
|
7
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Agentic Formflow keeps its license state inside @formio/appserver-core, so the
|
|
12
|
+
* UAG and @formio/appserver must load the same copy of it, at 2.24.1 or later.
|
|
13
|
+
* A second copy (an older lockfile, or a package manager that nests it) would
|
|
14
|
+
* otherwise fail in confusing ways; say what to do instead.
|
|
15
|
+
*/
|
|
16
|
+
export declare function assertSingleCore(core?: any, appserver?: any, version?: string): void;
|
|
8
17
|
export declare class UAGServer extends Server {
|
|
18
|
+
config: UAGConfig;
|
|
19
|
+
/** The UAG's own database, once it may be used. */
|
|
20
|
+
database?: UAGDatabase;
|
|
21
|
+
/** The database between connecting and the license check. */
|
|
22
|
+
private pendingDatabase?;
|
|
9
23
|
constructor(config?: UAGConfig);
|
|
24
|
+
/**
|
|
25
|
+
* Connect the UAG's own database before the project loads, and check it
|
|
26
|
+
* without writing: a database the UAG may not use stops startup. It is used
|
|
27
|
+
* (its collections created, its stores handed out) only once the license is
|
|
28
|
+
* known; see enableDatabase. Until then only the license check may write to
|
|
29
|
+
* it, to record a license server outage.
|
|
30
|
+
*/
|
|
31
|
+
protected connectDatabase(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Use the database once the project and its license are loaded: with a
|
|
34
|
+
* license that includes the UAG (or the project and the database both on
|
|
35
|
+
* development hosts), create its collections and hand out its stores;
|
|
36
|
+
* otherwise leave it untouched, close it, and say why.
|
|
37
|
+
*/
|
|
38
|
+
protected enableDatabase(project: UAGProjectInterface): Promise<void>;
|
|
39
|
+
/** Browsers reading Agent API responses need the request id too. */
|
|
40
|
+
get exposedHeaders(): string[];
|
|
41
|
+
/** The loaded UAG project, once the project router has been built. */
|
|
42
|
+
get uagProject(): UAGProjectInterface | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Mount `/agent/capabilities` ahead of the project routes. The project routes,
|
|
45
|
+
* and the admin passthrough after them, would otherwise answer for it and
|
|
46
|
+
* forward admin requests to the Form.io server. A form's agent operations are
|
|
47
|
+
* on the form's own routes.
|
|
48
|
+
*/
|
|
49
|
+
protected extendRouter(router: Router): Promise<void>;
|
|
10
50
|
router(): Promise<any>;
|
|
11
51
|
}
|
|
12
52
|
export * from '@formio/appserver';
|
|
13
53
|
export * from './config';
|
|
54
|
+
export * from './agent';
|
|
14
55
|
export * from './tools';
|
|
15
56
|
export * from './router';
|
|
16
57
|
export * from './template';
|
package/lib/index.js
CHANGED
|
@@ -15,33 +15,183 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.UAGFormInterface = exports.UAGProjectInterface = exports.UAGServer = void 0;
|
|
18
|
+
exports.assertSingleCore = assertSingleCore;
|
|
18
19
|
const appserver_1 = require("@formio/appserver");
|
|
20
|
+
const formflow_1 = require("./formflow");
|
|
19
21
|
const UAGProjectInterface_1 = require("./UAGProjectInterface");
|
|
20
22
|
Object.defineProperty(exports, "UAGProjectInterface", { enumerable: true, get: function () { return UAGProjectInterface_1.UAGProjectInterface; } });
|
|
21
23
|
const UAGFormInterface_1 = require("./UAGFormInterface");
|
|
22
24
|
Object.defineProperty(exports, "UAGFormInterface", { enumerable: true, get: function () { return UAGFormInterface_1.UAGFormInterface; } });
|
|
23
25
|
const lodash_1 = require("lodash");
|
|
26
|
+
const engines_1 = require("./agent/engines");
|
|
27
|
+
const router_1 = require("./agent/router");
|
|
28
|
+
const database_1 = require("./database");
|
|
29
|
+
const runs_1 = require("./runs");
|
|
30
|
+
const actionLogs_1 = require("./actionLogs");
|
|
31
|
+
const runner_1 = require("./runner");
|
|
32
|
+
const runTask_1 = require("./actions/runTask");
|
|
33
|
+
const debug = require('debug')('formio:uag');
|
|
34
|
+
/**
|
|
35
|
+
* Agentic Formflow keeps its license state inside @formio/appserver-core, so the
|
|
36
|
+
* UAG and @formio/appserver must load the same copy of it, at 2.24.1 or later.
|
|
37
|
+
* A second copy (an older lockfile, or a package manager that nests it) would
|
|
38
|
+
* otherwise fail in confusing ways; say what to do instead.
|
|
39
|
+
*/
|
|
40
|
+
function assertSingleCore(core = require('@formio/appserver-core'), appserver = require('@formio/appserver'), version = String(require('@formio/appserver-core/package.json').version || '0.0.0')) {
|
|
41
|
+
const [major, minor, patch] = version.split('.').map((part) => parseInt(part, 10) || 0);
|
|
42
|
+
if (major < 2 || (major === 2 && (minor < 24 || (minor === 24 && patch < 1)))) {
|
|
43
|
+
throw new Error(`@formio/uag needs @formio/appserver-core 2.24.1 or later; ${version} is installed.`);
|
|
44
|
+
}
|
|
45
|
+
if (appserver.Formflow !== core.Formflow || appserver.ProjectInterface !== core.ProjectInterface) {
|
|
46
|
+
throw new Error('Two copies of @formio/appserver-core are installed: one for @formio/uag and an older one for @formio/appserver. ' +
|
|
47
|
+
'Upgrade @formio/appserver to 2.24.1 or later and deduplicate (for example `yarn dedupe @formio/appserver-core`).');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** The UAG's own database from the environment, when `MONGO` is set. */
|
|
51
|
+
function databaseFromEnv(env = process.env) {
|
|
52
|
+
const url = (0, lodash_1.get)(env, 'MONGO', '').toString();
|
|
53
|
+
if (!url)
|
|
54
|
+
return undefined;
|
|
55
|
+
const retention = (0, lodash_1.get)(env, 'TASK_RUN_RETENTION_DAYS', '').toString();
|
|
56
|
+
return {
|
|
57
|
+
url,
|
|
58
|
+
name: (0, lodash_1.get)(env, 'UAG_DATABASE', '').toString() || 'uag',
|
|
59
|
+
config: (0, lodash_1.get)(env, 'MONGO_CONFIG', '').toString() || undefined,
|
|
60
|
+
...(retention && { retentionDays: parseInt(retention, 10) })
|
|
61
|
+
};
|
|
62
|
+
}
|
|
24
63
|
class UAGServer extends appserver_1.Server {
|
|
25
64
|
constructor(config) {
|
|
65
|
+
assertSingleCore();
|
|
66
|
+
const configuredDb = !!config?.db;
|
|
67
|
+
const uagLicense = (0, lodash_1.get)(process.env, 'UAG_LICENSE', '').toString();
|
|
68
|
+
if (uagLicense) {
|
|
69
|
+
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.');
|
|
70
|
+
}
|
|
26
71
|
super((0, lodash_1.defaultsDeep)(config || {}, {
|
|
27
72
|
baseUrl: (0, lodash_1.get)(process.env, 'BASE_URL', '').toString(),
|
|
28
|
-
|
|
73
|
+
// Deprecated: core also reads LICENSE_KEY, the Form.io license, and a
|
|
74
|
+
// feature is on when either key grants it.
|
|
75
|
+
license: uagLicense,
|
|
76
|
+
licenseName: 'UAG_LICENSE',
|
|
77
|
+
// The license server counts each UAG environment as a "uagServer", and the
|
|
78
|
+
// UAG runs on the "uag" feature: a key the license server accepts as a
|
|
79
|
+
// uagServer, or one whose terms carry the "uag" option.
|
|
80
|
+
licenseType: 'uagServer',
|
|
81
|
+
licenseFeature: 'uag',
|
|
29
82
|
// A project that is still starting up (a Form.io server in the same compose
|
|
30
83
|
// file, for example) is retried with backoff before the load is given up on.
|
|
31
84
|
retries: parseInt((0, lodash_1.get)(process.env, 'PROJECT_RETRIES', 5).toString(), 10),
|
|
32
85
|
retryDelay: parseInt((0, lodash_1.get)(process.env, 'PROJECT_RETRY_DELAY', 5000).toString(), 10),
|
|
33
86
|
submissionProxy: true,
|
|
34
87
|
processAsClient: true,
|
|
35
|
-
auth: { pkce: true }
|
|
88
|
+
auth: { pkce: true, mcpOAuth: (0, lodash_1.get)(process.env, 'MCP_OAUTH', '').toString() === 'true' },
|
|
89
|
+
database: databaseFromEnv()
|
|
36
90
|
}));
|
|
91
|
+
// MONGO is the UAG's own database, not a database for the appserver to keep
|
|
92
|
+
// submissions in: Form.io stores those.
|
|
93
|
+
if (!configuredDb && this.module.db) {
|
|
94
|
+
delete this.module.db;
|
|
95
|
+
delete this.config.db;
|
|
96
|
+
}
|
|
37
97
|
this.use({
|
|
38
98
|
ProjectInterface: UAGProjectInterface_1.UAGProjectInterface,
|
|
39
|
-
FormInterface: UAGFormInterface_1.UAGFormInterface
|
|
99
|
+
FormInterface: UAGFormInterface_1.UAGFormInterface,
|
|
100
|
+
actions: { agentRunTask: runTask_1.RunTaskAction },
|
|
101
|
+
hooks: (0, runTask_1.runTaskHooks)()
|
|
40
102
|
});
|
|
41
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Connect the UAG's own database before the project loads, and check it
|
|
106
|
+
* without writing: a database the UAG may not use stops startup. It is used
|
|
107
|
+
* (its collections created, its stores handed out) only once the license is
|
|
108
|
+
* known; see enableDatabase. Until then only the license check may write to
|
|
109
|
+
* it, to record a license server outage.
|
|
110
|
+
*/
|
|
111
|
+
async connectDatabase() {
|
|
112
|
+
const options = this.config.database;
|
|
113
|
+
if (!options?.url) {
|
|
114
|
+
console.log('[formio-uag] No database (MONGO): runs after the save are kept in memory, remote MCP sign-in (MCP_OAUTH) is unavailable, ' +
|
|
115
|
+
'and the licensed features turn off at once, with no grace period, whenever the license server cannot be reached.');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (this.database || this.pendingDatabase)
|
|
119
|
+
return;
|
|
120
|
+
const database = await database_1.UAGDatabase.connect(options);
|
|
121
|
+
try {
|
|
122
|
+
await database.inspect();
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
await database.close().catch(() => { });
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
this.pendingDatabase = database;
|
|
129
|
+
this.config.licenseGrace = database.licenseGrace();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Use the database once the project and its license are loaded: with a
|
|
133
|
+
* license that includes the UAG (or the project and the database both on
|
|
134
|
+
* development hosts), create its collections and hand out its stores;
|
|
135
|
+
* otherwise leave it untouched, close it, and say why.
|
|
136
|
+
*/
|
|
137
|
+
async enableDatabase(project) {
|
|
138
|
+
const database = this.pendingDatabase;
|
|
139
|
+
this.pendingDatabase = undefined;
|
|
140
|
+
if (database) {
|
|
141
|
+
let refusal = '';
|
|
142
|
+
try {
|
|
143
|
+
project.requireDatabaseLicense(this.config.database.url);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
refusal = err?.message || String(err);
|
|
147
|
+
}
|
|
148
|
+
if (refusal) {
|
|
149
|
+
console.warn(`[formio-uag] The database (MONGO) is not used: ${refusal} Runs after the save are kept in memory, and remote MCP sign-in (MCP_OAUTH) is off.`);
|
|
150
|
+
this.config.licenseGrace = undefined;
|
|
151
|
+
await database.close().catch(() => { });
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
await database.bootstrap();
|
|
155
|
+
this.database = database;
|
|
156
|
+
(0, lodash_1.set)(this.config, 'auth.authCodeStore', database.authCodeStore());
|
|
157
|
+
(0, runner_1.taskRunner)(project).useStore(new runs_1.TaskRunStore(database.taskRuns, String(project.project?._id || '')));
|
|
158
|
+
console.log(`[formio-uag] Using the database "${database.name}": runs are recorded, and runs that act with an agent role survive a restart.` +
|
|
159
|
+
(this.config.auth?.mcpOAuth ? ' Remote MCP sign-in (MCP_OAUTH) is on.' : ''));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (this.config.auth?.mcpOAuth && !this.database) {
|
|
163
|
+
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
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Browsers reading Agent API responses need the request id too. */
|
|
167
|
+
get exposedHeaders() {
|
|
168
|
+
return [...super.exposedHeaders, 'X-Request-Id'];
|
|
169
|
+
}
|
|
170
|
+
/** The loaded UAG project, once the project router has been built. */
|
|
171
|
+
get uagProject() {
|
|
172
|
+
return Object.values(this.projectRouter?.projects || {})[0];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Mount `/agent/capabilities` ahead of the project routes. The project routes,
|
|
176
|
+
* and the admin passthrough after them, would otherwise answer for it and
|
|
177
|
+
* forward admin requests to the Form.io server. A form's agent operations are
|
|
178
|
+
* on the form's own routes.
|
|
179
|
+
*/
|
|
180
|
+
async extendRouter(router) {
|
|
181
|
+
await super.extendRouter(router);
|
|
182
|
+
const uagProject = this.uagProject;
|
|
183
|
+
if (uagProject) {
|
|
184
|
+
router.use(uagProject.agentRouter());
|
|
185
|
+
// An agent operation of a form the project does not have is answered here, not passed to Form.io.
|
|
186
|
+
router.use((0, router_1.agentNotFound)(uagProject));
|
|
187
|
+
// Action Logs in a connected portal stage, with the UAG's run records.
|
|
188
|
+
router.use((0, actionLogs_1.ActionLogRouter)(uagProject));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
42
191
|
async router() {
|
|
192
|
+
await this.connectDatabase();
|
|
43
193
|
const router = await super.router();
|
|
44
|
-
const uagProject =
|
|
194
|
+
const uagProject = this.uagProject;
|
|
45
195
|
// Without a project there are no forms to expose, so there is nothing to
|
|
46
196
|
// serve. Say why: this used to throw "Cannot read properties of undefined
|
|
47
197
|
// (reading 'uagRouter')", which told the operator nothing about the
|
|
@@ -49,16 +199,38 @@ class UAGServer extends appserver_1.Server {
|
|
|
49
199
|
if (!uagProject) {
|
|
50
200
|
throw new Error('No Form.io project could be loaded, so the UAG has nothing to serve. Check that PROJECT ' +
|
|
51
201
|
'points at a reachable Form.io deployment, and that it is paired with the right ' +
|
|
52
|
-
'credential — ADMIN_KEY for an Open Source server,
|
|
53
|
-
'
|
|
202
|
+
'credential — ADMIN_KEY for an Open Source server, PROJECT_KEY for a project on Form.io\'s ' +
|
|
203
|
+
'hosted service, or PROJECT_KEY plus a license (LICENSE_KEY) for a self-hosted Enterprise project. ' +
|
|
204
|
+
'Earlier log lines report why the project load failed.');
|
|
205
|
+
}
|
|
206
|
+
// A database of the UAG's own needs the license, on every edition.
|
|
207
|
+
await this.enableDatabase(uagProject);
|
|
208
|
+
// Decide which engines are on before serving anything, so a misconfigured
|
|
209
|
+
// engine stops startup with its name in the error instead of failing the
|
|
210
|
+
// first request.
|
|
211
|
+
uagProject.engines = await (0, engines_1.loadUAGEngines)(uagProject.config || {}, process.env);
|
|
212
|
+
// Say which edition this is, and whether Agentic Formflow will run, so an
|
|
213
|
+
// operator knows before the first request is refused.
|
|
214
|
+
// A failure here only loses the message; it never stops the UAG starting.
|
|
215
|
+
try {
|
|
216
|
+
const formflowState = await (0, formflow_1.formflow)(uagProject).state();
|
|
217
|
+
// A project on Form.io's hosted service runs under Form.io's license.
|
|
218
|
+
const hosted = formflowState.reason === 'hosted' ? ', on Form.io\'s hosted service (no license needed)' : '';
|
|
219
|
+
const reason = formflowState.licensedBy ? `licensed by ${formflowState.licensedBy}` : formflowState.reason;
|
|
220
|
+
console.log(`[formio-uag] ${formflowState.edition === 'enterprise' ? 'Enterprise' : 'Open Source'} edition${hosted}. Agentic Formflow ` +
|
|
221
|
+
(formflowState.allowed ? `is enabled (${reason}).` : 'requires a license (LICENSE_KEY) that includes the UAG, or a project on Form.io\'s hosted service; MCP and conversation forms are unaffected.'));
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
debug(`Could not report the edition: ${err?.message || err}`);
|
|
54
225
|
}
|
|
55
|
-
router.use(uagProject.uagRouter());
|
|
226
|
+
router.use(uagProject.uagRouter(false));
|
|
56
227
|
return router;
|
|
57
228
|
}
|
|
58
229
|
}
|
|
59
230
|
exports.UAGServer = UAGServer;
|
|
60
231
|
__exportStar(require("@formio/appserver"), exports);
|
|
61
232
|
__exportStar(require("./config"), exports);
|
|
233
|
+
__exportStar(require("./agent"), exports);
|
|
62
234
|
__exportStar(require("./tools"), exports);
|
|
63
235
|
__exportStar(require("./router"), exports);
|
|
64
236
|
__exportStar(require("./template"), exports);
|
package/lib/router.js
CHANGED
|
@@ -4,13 +4,11 @@ exports.UAGRouter = UAGRouter;
|
|
|
4
4
|
const express_1 = require("express");
|
|
5
5
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
6
6
|
// Helper function to create JSON RPC error responses
|
|
7
|
-
const createJsonRpcErrorResponse = (code, message) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
13
|
-
};
|
|
7
|
+
const createJsonRpcErrorResponse = (code, message) => ({
|
|
8
|
+
error: { code, message },
|
|
9
|
+
id: null,
|
|
10
|
+
jsonrpc: "2.0",
|
|
11
|
+
});
|
|
14
12
|
/**
|
|
15
13
|
* Handle errors that occur during response processing.
|
|
16
14
|
* @param error The error that occurred.
|
|
@@ -29,7 +27,7 @@ const handleResponseError = (error, res) => {
|
|
|
29
27
|
return true;
|
|
30
28
|
}
|
|
31
29
|
console.error("[formio-uag] error handling request", error);
|
|
32
|
-
res.status(500).json(
|
|
30
|
+
res.status(500).json(createJsonRpcErrorResponse(-32603, "Internal Server Error"));
|
|
33
31
|
return false;
|
|
34
32
|
};
|
|
35
33
|
/**
|
|
@@ -37,8 +35,8 @@ const handleResponseError = (error, res) => {
|
|
|
37
35
|
* when the response closes. A fresh instance per request keeps concurrent requests
|
|
38
36
|
* from overwriting each other's transport.
|
|
39
37
|
*/
|
|
40
|
-
const connectMcpTransport = async (project, res) => {
|
|
41
|
-
const server = await project.buildMcpServer();
|
|
38
|
+
const connectMcpTransport = async (project, res, scope) => {
|
|
39
|
+
const server = await project.buildMcpServer(scope);
|
|
42
40
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
43
41
|
sessionIdGenerator: undefined, // Use "undefined" to trigger stateless mode.
|
|
44
42
|
});
|
|
@@ -51,26 +49,18 @@ const connectMcpTransport = async (project, res) => {
|
|
|
51
49
|
};
|
|
52
50
|
function UAGRouter(project) {
|
|
53
51
|
const router = (0, express_1.Router)();
|
|
54
|
-
// Handles the MCP
|
|
55
|
-
|
|
52
|
+
// Handles the MCP requests: posts carry a JSON-RPC body, gets do not (health checks, etc).
|
|
53
|
+
const handleMcpRequest = (withBody) => async (req, res) => {
|
|
56
54
|
try {
|
|
57
|
-
const transport = await connectMcpTransport(project, res);
|
|
58
|
-
await transport.handleRequest(req, res, req.body);
|
|
55
|
+
const transport = await connectMcpTransport(project, res, req.uagScope);
|
|
56
|
+
await transport.handleRequest(req, res, withBody ? req.body : undefined);
|
|
59
57
|
}
|
|
60
58
|
catch (error) {
|
|
61
59
|
handleResponseError(error, res);
|
|
62
60
|
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
router.get('/',
|
|
66
|
-
try {
|
|
67
|
-
const transport = await connectMcpTransport(project, res);
|
|
68
|
-
await transport.handleRequest(req, res);
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
handleResponseError(err, res);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
61
|
+
};
|
|
62
|
+
router.post('/', handleMcpRequest(true));
|
|
63
|
+
router.get('/', handleMcpRequest(false));
|
|
74
64
|
// Return the router
|
|
75
65
|
return router;
|
|
76
66
|
}
|
package/lib/runner.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { AuthRequest } from '@formio/appserver';
|
|
2
|
+
import type { CallerIdentity } from './formflow';
|
|
3
|
+
import { ActingFor } from './identity';
|
|
4
|
+
import { TaskRunStore } from './runs';
|
|
5
|
+
import type { UAGProjectInterface } from './UAGProjectInterface';
|
|
6
|
+
/** A Run task action's run after the save, on the submission Form.io stored. */
|
|
7
|
+
export type TaskRunJob = {
|
|
8
|
+
/** The form's name. */
|
|
9
|
+
form: string;
|
|
10
|
+
submission: string;
|
|
11
|
+
task: string;
|
|
12
|
+
answer: 'decisions' | 'task';
|
|
13
|
+
/** The user the run acts for: who sent the submission. Never a token. */
|
|
14
|
+
actingFor: ActingFor;
|
|
15
|
+
/**
|
|
16
|
+
* The role the agent acts with at Form.io, on the user's behalf. Such runs
|
|
17
|
+
* can be recorded and resumed: everything they need is in the job.
|
|
18
|
+
*/
|
|
19
|
+
agentRole?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Without an agent role, the caller's own identity (with their token), kept
|
|
22
|
+
* in memory only and never recorded.
|
|
23
|
+
*/
|
|
24
|
+
caller?: AuthRequest;
|
|
25
|
+
/** Without an agent role, who the engines research as (the caller's bearer token, if any). */
|
|
26
|
+
identity?: CallerIdentity;
|
|
27
|
+
/**
|
|
28
|
+
* Without an agent role or the caller's token: act as the user, with their
|
|
29
|
+
* recorded roles and tokens the UAG signs. A run started by the save of a
|
|
30
|
+
* run that acted with an agent role, so it never inherits that role.
|
|
31
|
+
*/
|
|
32
|
+
asUser?: boolean;
|
|
33
|
+
/** How many runner saves led to this run: none for a run a client's request started. */
|
|
34
|
+
chain?: number;
|
|
35
|
+
/** The action that started the run, for the record. */
|
|
36
|
+
action?: string;
|
|
37
|
+
/** For the record: the form's id, and the method that started the run. */
|
|
38
|
+
formId?: string;
|
|
39
|
+
method?: string;
|
|
40
|
+
/** The X-Request-Id of the request that started the run, passed on to the engines. */
|
|
41
|
+
requestId?: string;
|
|
42
|
+
};
|
|
43
|
+
export type TaskRunOutcome = {
|
|
44
|
+
status: 'done' | 'failed';
|
|
45
|
+
/** Paths written, with their values, and paths cleared. Empty when nothing changed. */
|
|
46
|
+
written: Record<string, any>;
|
|
47
|
+
cleared: string[];
|
|
48
|
+
/** The engines and models that answered. */
|
|
49
|
+
engines: {
|
|
50
|
+
kind: string;
|
|
51
|
+
engine: string | null;
|
|
52
|
+
model: string | null;
|
|
53
|
+
}[];
|
|
54
|
+
/** Whether the results were saved (nothing is saved when nothing changed). */
|
|
55
|
+
saved: boolean;
|
|
56
|
+
error?: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The request field that marks the runner's own save, so the form's Run task
|
|
60
|
+
* actions that run before the save skip it: its data is the agent's output.
|
|
61
|
+
*/
|
|
62
|
+
export declare const RUN_TASK_SAVE = "uagRunTaskSave";
|
|
63
|
+
/**
|
|
64
|
+
* The request fields that carry, through the runner's own save, who the run
|
|
65
|
+
* acts for, so a Run task action that the save starts acts for the same user.
|
|
66
|
+
*/
|
|
67
|
+
export declare const RUN_TASK_ACTING_FOR = "uagActingFor";
|
|
68
|
+
export declare const RUN_TASK_CALLER = "uagCaller";
|
|
69
|
+
export declare const RUN_TASK_REQUEST_ID = "uagRequestId";
|
|
70
|
+
export declare const RUN_TASK_IDENTITY = "uagIdentity";
|
|
71
|
+
export declare const RUN_TASK_AS_USER = "uagAsUser";
|
|
72
|
+
export declare const RUN_TASK_CHAIN = "uagChain";
|
|
73
|
+
/**
|
|
74
|
+
* Runs a runner's save may start in a row: a task whose save starts a task
|
|
75
|
+
* that changes something each time would otherwise never stop.
|
|
76
|
+
*/
|
|
77
|
+
export declare const MAX_CHAIN = 5;
|
|
78
|
+
/** How often a replica looks for runs that are pending or lost their replica. */
|
|
79
|
+
export declare const SWEEP_MS: number;
|
|
80
|
+
/**
|
|
81
|
+
* Runs Run task actions' background runs: load the stored submission, run the
|
|
82
|
+
* task, and save only what changed, as an update through the UAG's own
|
|
83
|
+
* pipeline (so the form's update actions, including other Run task actions,
|
|
84
|
+
* run on it). If the submission changes while the task runs, the run starts
|
|
85
|
+
* again once on the new version, then gives up as a conflict.
|
|
86
|
+
*
|
|
87
|
+
* Without the UAG's database, runs are kept in memory, a few at a time, and
|
|
88
|
+
* are lost if the process stops. With it, every run is recorded; a run that
|
|
89
|
+
* acts with an agent role is recorded as pending before the caller's response
|
|
90
|
+
* and taken from the database by one replica at a time, under a lease it
|
|
91
|
+
* renews, so it survives a restart. A sweep at startup and every minute takes
|
|
92
|
+
* runs that are pending or whose replica stopped.
|
|
93
|
+
*/
|
|
94
|
+
export declare class TaskRunner {
|
|
95
|
+
private project;
|
|
96
|
+
private concurrency;
|
|
97
|
+
private queue;
|
|
98
|
+
private active;
|
|
99
|
+
private waiting;
|
|
100
|
+
private store?;
|
|
101
|
+
private sweeper?;
|
|
102
|
+
private sweeping;
|
|
103
|
+
constructor(project: UAGProjectInterface, concurrency?: number);
|
|
104
|
+
/** Whether runs acting with an agent role are recorded and survive a restart. */
|
|
105
|
+
get durable(): boolean;
|
|
106
|
+
/** Record runs in the UAG's database, and take durable runs from it. */
|
|
107
|
+
useStore(store: TaskRunStore): void;
|
|
108
|
+
/** Stop sweeping (the runs in progress finish). */
|
|
109
|
+
stop(): void;
|
|
110
|
+
/** The project's run records, when there is a database. */
|
|
111
|
+
get records(): TaskRunStore | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Start a run. A durable run is recorded as pending before this resolves, so
|
|
114
|
+
* the caller's response is sent only once it can survive a restart.
|
|
115
|
+
*/
|
|
116
|
+
enqueue(job: TaskRunJob): Promise<void>;
|
|
117
|
+
/** Record a run made before the save, once the save gave the submission its id. */
|
|
118
|
+
recordBefore(job: TaskRunJob, outcome: TaskRunOutcome): Promise<void>;
|
|
119
|
+
private newRecord;
|
|
120
|
+
/** Resolves once every queued and running job has finished (for tests and shutdown). */
|
|
121
|
+
idle(): Promise<void>;
|
|
122
|
+
private isIdle;
|
|
123
|
+
private settled;
|
|
124
|
+
private drain;
|
|
125
|
+
/** Take durable runs from the database while there is room. */
|
|
126
|
+
private sweep;
|
|
127
|
+
private runRecord;
|
|
128
|
+
/** Run a job, report it, and record its end. */
|
|
129
|
+
private track;
|
|
130
|
+
protected report(job: TaskRunJob, outcome: TaskRunOutcome): void;
|
|
131
|
+
/**
|
|
132
|
+
* Who the job acts as at Form.io (`act`: a new identity for each request,
|
|
133
|
+
* since signed tokens are short-lived), and who its engines research as.
|
|
134
|
+
* Never the project's key: the agent role's signed token, or the caller's own.
|
|
135
|
+
*/
|
|
136
|
+
private identities;
|
|
137
|
+
/**
|
|
138
|
+
* Why Form.io refused the agent's signed token, when it did: asks Form.io who
|
|
139
|
+
* the token is, so a JWT_SECRET that is not Form.io's is told apart from
|
|
140
|
+
* other failures.
|
|
141
|
+
*/
|
|
142
|
+
private refusal;
|
|
143
|
+
/** Run one job to completion. */
|
|
144
|
+
execute(job: TaskRunJob): Promise<TaskRunOutcome>;
|
|
145
|
+
private run;
|
|
146
|
+
}
|
|
147
|
+
export declare function taskRunner(project: UAGProjectInterface): TaskRunner;
|