@formio/uag 1.13.1 → 2.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +235 -535
  2. package/bin/uag-eval.js +95 -0
  3. package/bin/uag.js +73 -0
  4. package/lib/UAGFormInterface.d.ts +60 -2
  5. package/lib/UAGFormInterface.js +172 -46
  6. package/lib/UAGProjectInterface.d.ts +37 -5
  7. package/lib/UAGProjectInterface.js +166 -29
  8. package/lib/actionLogs.d.ts +33 -0
  9. package/lib/actionLogs.js +147 -0
  10. package/lib/actions/runTask.d.ts +43 -0
  11. package/lib/actions/runTask.js +323 -0
  12. package/lib/agent/anonymous.d.ts +34 -0
  13. package/lib/agent/anonymous.js +98 -0
  14. package/lib/agent/chat.d.ts +113 -0
  15. package/lib/agent/chat.js +539 -0
  16. package/lib/agent/engines.d.ts +13 -0
  17. package/lib/agent/engines.js +37 -0
  18. package/lib/agent/index.d.ts +4 -0
  19. package/lib/agent/index.js +18 -0
  20. package/lib/agent/remote.d.ts +67 -0
  21. package/lib/agent/remote.js +229 -0
  22. package/lib/agent/requestId.d.ts +4 -0
  23. package/lib/agent/requestId.js +19 -0
  24. package/lib/agent/router.d.ts +31 -0
  25. package/lib/agent/router.js +295 -0
  26. package/lib/agent/stubs.d.ts +26 -0
  27. package/lib/agent/stubs.js +26 -0
  28. package/lib/agent/tools.d.ts +10 -0
  29. package/lib/agent/tools.js +33 -0
  30. package/lib/config.d.ts +67 -0
  31. package/lib/config.js +19 -0
  32. package/lib/database.d.ts +75 -0
  33. package/lib/database.js +229 -0
  34. package/lib/formflow.d.ts +40 -0
  35. package/lib/formflow.js +28 -0
  36. package/lib/identity.d.ts +53 -0
  37. package/lib/identity.js +107 -0
  38. package/lib/index.d.ts +41 -0
  39. package/lib/index.js +179 -7
  40. package/lib/router.js +15 -25
  41. package/lib/runner.d.ts +147 -0
  42. package/lib/runner.js +345 -0
  43. package/lib/runs.d.ts +89 -0
  44. package/lib/runs.js +74 -0
  45. package/lib/template.js +7 -9
  46. package/lib/templates/uagComponentNotFound.md +5 -5
  47. package/lib/tools/SchemaBuilder.d.ts +7 -2
  48. package/lib/tools/SchemaBuilder.js +11 -3
  49. package/lib/tools/agentDecide.d.ts +8 -0
  50. package/lib/tools/agentDecide.js +57 -0
  51. package/lib/tools/agentProvide.d.ts +13 -0
  52. package/lib/tools/agentProvide.js +130 -0
  53. package/lib/tools/annotations.d.ts +34 -0
  54. package/lib/tools/annotations.js +56 -0
  55. package/lib/tools/collectData.js +3 -1
  56. package/lib/tools/confirmSubmission.js +3 -1
  57. package/lib/tools/fetchExternalData.js +10 -17
  58. package/lib/tools/findSubmission.js +64 -56
  59. package/lib/tools/getFieldInfo.js +3 -1
  60. package/lib/tools/getFormFields.js +3 -1
  61. package/lib/tools/getForms.js +3 -1
  62. package/lib/tools/index.d.ts +4 -1
  63. package/lib/tools/index.js +22 -14
  64. package/lib/tools/submissionUpdate.js +3 -1
  65. package/lib/tools/submitForm.js +3 -1
  66. package/lib/tools/utils.d.ts +2 -0
  67. package/package.json +41 -18
  68. package/lib/tools/agentProvideData.d.ts +0 -3
  69. package/lib/tools/agentProvideData.js +0 -60
@@ -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
- license: (0, lodash_1.get)(process.env, 'UAG_LICENSE', '').toString(),
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 = Object.values(this.projectRouter?.projects || {})[0];
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, or PROJECT_KEY plus UAG_LICENSE for ' +
53
- 'an Enterprise project. Earlier log lines report why the project load failed.');
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
- return JSON.stringify({
9
- error: { code, message },
10
- id: null,
11
- jsonrpc: "2.0",
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(JSON.parse(createJsonRpcErrorResponse(-32603, "Internal Server Error")));
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 post requests.
55
- router.post('/', async (req, res) => {
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
- // Handles the MCP get requests (for health checks, etc).
65
- router.get('/', async (req, res) => {
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
  }
@@ -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;