@formio/uag 1.13.2 → 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 (66) 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 -33
  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/collectData.js +1 -1
  54. package/lib/tools/confirmSubmission.js +1 -1
  55. package/lib/tools/fetchExternalData.js +8 -17
  56. package/lib/tools/findSubmission.js +62 -56
  57. package/lib/tools/getFieldInfo.js +1 -1
  58. package/lib/tools/getFormFields.js +1 -1
  59. package/lib/tools/getForms.js +1 -1
  60. package/lib/tools/index.d.ts +3 -1
  61. package/lib/tools/index.js +21 -14
  62. package/lib/tools/submissionUpdate.js +1 -1
  63. package/lib/tools/submitForm.js +1 -1
  64. package/package.json +41 -18
  65. package/lib/tools/agentProvideData.d.ts +0 -3
  66. package/lib/tools/agentProvideData.js +0 -62
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StubAgentEngine = exports.StubDecisionEngine = void 0;
4
+ const uag_sdk_1 = require("@formio/uag-sdk");
5
+ const formflow_1 = require("../formflow");
6
+ /**
7
+ * Stand-in engines for tests and local development. They answer instantly with
8
+ * predictable values, so the Agent API can be exercised end to end without
9
+ * model credentials: `UAG_ENGINES=stub-decision,stub-agent`. Core creates
10
+ * them, so they keep the standing that lets them run without a license under
11
+ * any instance name.
12
+ */
13
+ exports.StubDecisionEngine = (0, uag_sdk_1.defineEngine)({
14
+ type: 'stub-decision',
15
+ kinds: ['decision'],
16
+ apiVersion: uag_sdk_1.ENGINE_API_VERSION,
17
+ description: 'Answers every decision with a fixed, predictable value. For tests and local development.',
18
+ create: ({ name }) => (0, formflow_1.stubDecisionEngine)({ name }),
19
+ });
20
+ exports.StubAgentEngine = (0, uag_sdk_1.defineEngine)({
21
+ type: 'stub-agent',
22
+ kinds: ['agent'],
23
+ apiVersion: uag_sdk_1.ENGINE_API_VERSION,
24
+ description: 'Extracts nothing. For tests and local development.',
25
+ create: ({ name }) => (0, formflow_1.stubAgentEngine)({ name }),
26
+ });
@@ -0,0 +1,10 @@
1
+ import { FormflowTool } from "../formflow";
2
+ import type { UAGProjectInterface } from "../UAGProjectInterface";
3
+ import { ToolInfo } from "../tools/utils";
4
+ /** Every tool the UAG serves, including custom tools from configuration. */
5
+ export declare function allTools(project: UAGProjectInterface): Promise<ToolInfo[]>;
6
+ /**
7
+ * An MCP tool as an Agentic Formflow tool: its schema as JSON, its arguments
8
+ * validated with its own schema, and read-only only when annotated so.
9
+ */
10
+ export declare function formflowTool(tool: ToolInfo): FormflowTool;
@@ -0,0 +1,33 @@
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.allTools = allTools;
7
+ exports.formflowTool = formflowTool;
8
+ const zod_1 = __importDefault(require("zod"));
9
+ const tools_1 = require("../tools");
10
+ /** Every tool the UAG serves, including custom tools from configuration. */
11
+ async function allTools(project) {
12
+ return [...await (0, tools_1.getTools)(project), ...(project.config?.tools || [])].filter((tool) => !!tool?.name);
13
+ }
14
+ /**
15
+ * An MCP tool as an Agentic Formflow tool: its schema as JSON, its arguments
16
+ * validated with its own schema, and read-only only when annotated so.
17
+ */
18
+ function formflowTool(tool) {
19
+ const schema = zod_1.default.object(tool.inputSchema || {});
20
+ const inputSchema = zod_1.default.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' });
21
+ delete inputSchema.$schema;
22
+ return {
23
+ name: tool.name,
24
+ description: tool.description || tool.title || tool.name,
25
+ readOnly: tool.annotations?.readOnlyHint === true,
26
+ inputSchema,
27
+ parse: (args) => {
28
+ const parsed = schema.safeParse(args || {});
29
+ return parsed.success ? { ok: true, value: parsed.data } : { ok: false, error: parsed.error.message };
30
+ },
31
+ execute: async (args, auth) => tool.execute(args, { authInfo: auth })
32
+ };
33
+ }
package/lib/config.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { ServerConfig } from '@formio/appserver';
2
+ import type { FormflowAgentApiConfig, FormflowRateLimit, FormflowRoutingConfig } from './formflow';
3
+ import type { EngineInstanceSpec, EngineTypeDefinition } from '@formio/uag-sdk';
2
4
  import { ToolInfo } from './tools/utils';
5
+ import type { UAGDatabaseOptions } from './database';
3
6
  export type UAGForm = {
4
7
  name: string;
5
8
  title: string;
@@ -43,13 +46,77 @@ export type UAGToolOverride = {
43
46
  submit_completed_form?: ToolInfo;
44
47
  submission_update?: ToolInfo;
45
48
  find_submissions?: ToolInfo;
49
+ agent_provide?: ToolInfo;
50
+ /** @deprecated The earlier name of `agent_provide`; also applied to it when `agent_provide` is not set. */
46
51
  agent_provide_data?: ToolInfo;
52
+ agent_decide?: ToolInfo;
47
53
  fetch_external_data?: ToolInfo;
48
54
  };
55
+ /**
56
+ * One engine instance: `"<type>"` or `"<name>:<type>"`, or `{ name, type, options }`, where the
57
+ * type is a type name, a package name, or an engine type definition.
58
+ */
59
+ export type UAGEngineEntry = string | EngineInstanceSpec;
60
+ /** Engine preference lists for each kind of work. See FormflowRoutingConfig. */
61
+ export type UAGRoutingConfig = FormflowRoutingConfig;
62
+ /** A request allowance. See FormflowRateLimit. */
63
+ export type UAGRateLimit = FormflowRateLimit;
64
+ /** Agent API defaults: thresholds, timeouts, tools and rate limits. See FormflowAgentApiConfig. */
65
+ /** Limits on anonymous use of a form's agent operations, per minute in each UAG process. 0 turns anonymous use off. */
66
+ export interface UAGAnonymousConfig {
67
+ /** Requests from one client address (AGENT_ANONYMOUS_RATE_LIMIT, default 10). */
68
+ rateLimit?: number;
69
+ /** Requests from all anonymous callers together (AGENT_ANONYMOUS_TOTAL_RATE_LIMIT, default 100). */
70
+ totalRateLimit?: number;
71
+ }
72
+ /** A form's chat (`{form}/agent/chat`): its engines and the bounds of a turn. */
73
+ export interface UAGChatConfig {
74
+ /** Engine names, or `agent` for every agent engine, in the order to try them. Default: `['agent']`. */
75
+ engines?: string[];
76
+ /** Milliseconds to wait for one engine before trying the next. Default: the engine's routing timeout. */
77
+ timeout?: number;
78
+ /** Rounds of `check_values` calls an engine may make before it answers. Default: 3. */
79
+ maxIterations?: number;
80
+ /** The most messages a turn may carry. Default: 40. */
81
+ maxMessages?: number;
82
+ /** The most characters in one message. Default: 2000. */
83
+ maxMessageLength?: number;
84
+ /** The most characters of conversation sent to the engine; older messages are left out. Default: 24000. */
85
+ maxTranscriptChars?: number;
86
+ }
87
+ export type UAGAgentApiConfig = FormflowAgentApiConfig & {
88
+ anonymous?: UAGAnonymousConfig;
89
+ chat?: UAGChatConfig;
90
+ };
91
+ /**
92
+ * Express's `trust proxy` from TRUST_PROXY: `true` or `false`, a number of
93
+ * proxy hops, or addresses and ranges (`loopback, 10.0.0.0/8`). Unset when
94
+ * TRUST_PROXY is empty. With it, the UAG reads its public URL, and callers'
95
+ * addresses, from the proxy's forwarded headers.
96
+ */
97
+ export declare function trustProxySetting(value: string | undefined): boolean | number | string | undefined;
49
98
  export interface UAGConfig extends ServerConfig {
99
+ /** The UAG's public URL (BASE_URL). Optional: derived from each request when unset. */
50
100
  baseUrl?: string;
51
101
  loginForm?: string;
52
102
  responseTemplates?: UAGTemplateConfig;
53
103
  toolOverrides?: UAGToolOverride;
54
104
  tools?: ToolInfo[];
105
+ /**
106
+ * The engine instances to enable, as in `UAG_ENGINES` (`"jev"`, `"fast:claude"`) or as
107
+ * `{ name, type, options }`. When unset, `UAG_ENGINES` is used; when neither names any, no
108
+ * engine is enabled.
109
+ */
110
+ engines?: UAGEngineEntry[];
111
+ /** Engine types this application provides, by type name, resolved before built-in types and packages. */
112
+ engineTypes?: Record<string, EngineTypeDefinition>;
113
+ routing?: UAGRoutingConfig;
114
+ agentApi?: UAGAgentApiConfig;
115
+ /**
116
+ * The UAG's own database (`MONGO`, `UAG_DATABASE`, `MONGO_CONFIG`,
117
+ * `TASK_RUN_RETENTION_DAYS`), for remote MCP sign-in and durable task runs.
118
+ * It needs a license that includes the UAG, unless the project and the
119
+ * database are both on development hosts.
120
+ */
121
+ database?: UAGDatabaseOptions;
55
122
  }
package/lib/config.js CHANGED
@@ -1,2 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.trustProxySetting = trustProxySetting;
4
+ /**
5
+ * Express's `trust proxy` from TRUST_PROXY: `true` or `false`, a number of
6
+ * proxy hops, or addresses and ranges (`loopback, 10.0.0.0/8`). Unset when
7
+ * TRUST_PROXY is empty. With it, the UAG reads its public URL, and callers'
8
+ * addresses, from the proxy's forwarded headers.
9
+ */
10
+ function trustProxySetting(value) {
11
+ const text = String(value ?? '').trim();
12
+ if (!text)
13
+ return undefined;
14
+ if (text === 'true')
15
+ return true;
16
+ if (text === 'false')
17
+ return false;
18
+ if (/^\d+$/.test(text))
19
+ return parseInt(text, 10);
20
+ return text;
21
+ }
@@ -0,0 +1,75 @@
1
+ import { Collection, Db, MongoClient, MongoClientOptions } from 'mongodb';
2
+ import type { AuthCodeStore } from '@formio/appserver-core';
3
+ /**
4
+ * The UAG's own database (MONGO, with the database named by UAG_DATABASE). It
5
+ * holds only the UAG's state: authorization codes for remote MCP clients, the
6
+ * start of a license server outage, and task-run records. Never submissions.
7
+ *
8
+ * Every collection the UAG uses is named `uag-*`, and nothing else in the
9
+ * database is read or written, so the database may be shared. Before writing
10
+ * anything, the UAG checks that the `uag-*` collections are its own.
11
+ */
12
+ /** The schema version this UAG writes. Older databases are migrated; newer ones refused. */
13
+ export declare const UAG_SCHEMA_VERSION = 1;
14
+ export declare const UAG_COLLECTIONS: Readonly<{
15
+ meta: "uag-meta";
16
+ authCodes: "uag-authcodes";
17
+ taskRuns: "uag-taskruns";
18
+ }>;
19
+ export type UAGDatabaseOptions = {
20
+ /** The connection string (MONGO). Its database, if any, is ignored: UAG_DATABASE names the one used. */
21
+ url: string;
22
+ /** The database to use (UAG_DATABASE). */
23
+ name?: string;
24
+ /** Driver options (MONGO_CONFIG), as JSON or an object. */
25
+ config?: string | MongoClientOptions;
26
+ /** Days a task-run record is kept (TASK_RUN_RETENTION_DAYS); 0 keeps it forever. */
27
+ retentionDays?: number;
28
+ };
29
+ /** Why the UAG may not use a database. Startup stops with this message. */
30
+ export declare class UAGDatabaseError extends Error {
31
+ name: string;
32
+ }
33
+ export declare class UAGDatabase {
34
+ readonly client: MongoClient;
35
+ readonly db: Db;
36
+ readonly name: string;
37
+ readonly retentionDays: number;
38
+ private bootstrapped?;
39
+ private warned;
40
+ constructor(client: MongoClient, db: Db, options: UAGDatabaseOptions);
41
+ /**
42
+ * Connect, and select the database by name on the connection: the
43
+ * connection string's database and `authSource` are left as they are.
44
+ */
45
+ static connect(options: UAGDatabaseOptions): Promise<UAGDatabase>;
46
+ get meta(): Collection<any>;
47
+ get authCodes(): Collection<any>;
48
+ get taskRuns(): Collection<any>;
49
+ /**
50
+ * Check the database without writing anything: whether the UAG may use it,
51
+ * and what bootstrap would do. Refuses (throws) a database whose `uag-*`
52
+ * collections the UAG did not create, or that a newer UAG created.
53
+ */
54
+ inspect(): Promise<'fresh' | 'ready' | 'older'>;
55
+ /**
56
+ * Check the database (see inspect), then create what is missing: the schema
57
+ * marker first, then the collections and their indexes; or migrate an older
58
+ * schema. Safe to call again.
59
+ */
60
+ bootstrap(): Promise<'created' | 'ready' | 'migrated'>;
61
+ /** Create the collections and indexes, as needed; safe to repeat. */
62
+ private ensureCollections;
63
+ /** The task-run records' TTL index, following TASK_RUN_RETENTION_DAYS; none when it is 0. */
64
+ private ensureRetention;
65
+ /** Run database work, reporting a missing privilege with the database's name. */
66
+ private privileged;
67
+ /** Authorization codes for remote MCP clients (MCP_OAUTH), in `uag-authcodes`. */
68
+ authCodeStore(): AuthCodeStore;
69
+ /** When the license server became unreachable, in `uag-meta`, so a restart neither begins nor extends the grace period. */
70
+ licenseGrace(): {
71
+ read(): Promise<number | undefined>;
72
+ write(since?: number): Promise<void>;
73
+ };
74
+ close(): Promise<void>;
75
+ }
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UAGDatabase = exports.UAGDatabaseError = exports.UAG_COLLECTIONS = exports.UAG_SCHEMA_VERSION = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const mongodb_1 = require("mongodb");
6
+ const debug = require('debug')('formio:uag:database');
7
+ /**
8
+ * The UAG's own database (MONGO, with the database named by UAG_DATABASE). It
9
+ * holds only the UAG's state: authorization codes for remote MCP clients, the
10
+ * start of a license server outage, and task-run records. Never submissions.
11
+ *
12
+ * Every collection the UAG uses is named `uag-*`, and nothing else in the
13
+ * database is read or written, so the database may be shared. Before writing
14
+ * anything, the UAG checks that the `uag-*` collections are its own.
15
+ */
16
+ /** The schema version this UAG writes. Older databases are migrated; newer ones refused. */
17
+ exports.UAG_SCHEMA_VERSION = 1;
18
+ exports.UAG_COLLECTIONS = Object.freeze({
19
+ meta: 'uag-meta',
20
+ authCodes: 'uag-authcodes',
21
+ taskRuns: 'uag-taskruns',
22
+ });
23
+ const PREFIX = 'uag-';
24
+ const APP = 'formio-uag';
25
+ const SCHEMA_ID = 'schema';
26
+ const LICENSE_ID = 'license';
27
+ const RETENTION_INDEX = 'retention';
28
+ const DEFAULT_RETENTION_DAYS = 90;
29
+ // MongoDB's "Unauthorized" error.
30
+ const UNAUTHORIZED = 13;
31
+ /** Why the UAG may not use a database. Startup stops with this message. */
32
+ class UAGDatabaseError extends Error {
33
+ constructor() {
34
+ super(...arguments);
35
+ this.name = 'UAGDatabaseError';
36
+ }
37
+ }
38
+ exports.UAGDatabaseError = UAGDatabaseError;
39
+ function driverOptions(config) {
40
+ if (!config)
41
+ return {};
42
+ if (typeof config !== 'string')
43
+ return config;
44
+ try {
45
+ return JSON.parse(config);
46
+ }
47
+ catch (err) {
48
+ throw new UAGDatabaseError(`MONGO_CONFIG is not valid JSON: ${err.message}`);
49
+ }
50
+ }
51
+ /** Authorization codes are stored by their hash, so the database never holds a usable code. */
52
+ const codeKey = (code) => (0, crypto_1.createHash)('sha256').update(code).digest('base64url');
53
+ /**
54
+ * An authorization code's token, sealed with a key only the code gives: the
55
+ * database holds neither the code nor a token it could use.
56
+ */
57
+ const sealKey = (code) => (0, crypto_1.createHash)('sha256').update(`uag-authcode:${code}`).digest();
58
+ function sealToken(code, token) {
59
+ const iv = (0, crypto_1.randomBytes)(12);
60
+ const cipher = (0, crypto_1.createCipheriv)('aes-256-gcm', sealKey(code), iv);
61
+ const sealed = Buffer.concat([cipher.update(token, 'utf8'), cipher.final()]);
62
+ return [iv, cipher.getAuthTag(), sealed].map((part) => part.toString('base64url')).join('.');
63
+ }
64
+ function openToken(code, sealed) {
65
+ try {
66
+ const [iv, tag, data] = sealed.split('.').map((part) => Buffer.from(part, 'base64url'));
67
+ const decipher = (0, crypto_1.createDecipheriv)('aes-256-gcm', sealKey(code), iv);
68
+ decipher.setAuthTag(tag);
69
+ return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ class UAGDatabase {
76
+ constructor(client, db, options) {
77
+ this.client = client;
78
+ this.db = db;
79
+ this.warned = false;
80
+ this.name = db.databaseName;
81
+ const days = options.retentionDays;
82
+ this.retentionDays = days === undefined || Number.isNaN(days) ? DEFAULT_RETENTION_DAYS : Math.max(0, days);
83
+ }
84
+ /**
85
+ * Connect, and select the database by name on the connection: the
86
+ * connection string's database and `authSource` are left as they are.
87
+ */
88
+ static async connect(options) {
89
+ const name = options.name || 'uag';
90
+ const client = new mongodb_1.MongoClient(options.url, driverOptions(options.config));
91
+ try {
92
+ await client.connect();
93
+ }
94
+ catch (err) {
95
+ throw new UAGDatabaseError(`Could not connect to MONGO: ${err.message}`);
96
+ }
97
+ return new UAGDatabase(client, client.db(name), { ...options, name });
98
+ }
99
+ get meta() { return this.db.collection(exports.UAG_COLLECTIONS.meta); }
100
+ get authCodes() { return this.db.collection(exports.UAG_COLLECTIONS.authCodes); }
101
+ get taskRuns() { return this.db.collection(exports.UAG_COLLECTIONS.taskRuns); }
102
+ /**
103
+ * Check the database without writing anything: whether the UAG may use it,
104
+ * and what bootstrap would do. Refuses (throws) a database whose `uag-*`
105
+ * collections the UAG did not create, or that a newer UAG created.
106
+ */
107
+ async inspect() {
108
+ return this.privileged(async () => {
109
+ const names = (await this.db.listCollections({}, { nameOnly: true }).toArray()).map((info) => info.name);
110
+ const ours = names.filter((name) => name.startsWith(PREFIX));
111
+ if (names.length > ours.length && !this.warned) {
112
+ this.warned = true;
113
+ console.warn(`[formio-uag] The database "${this.name}" holds other data too. The UAG uses only its uag-* collections there, ` +
114
+ 'but a database of its own is recommended (UAG_DATABASE).');
115
+ }
116
+ if (!ours.length)
117
+ return 'fresh';
118
+ const marker = ours.includes(exports.UAG_COLLECTIONS.meta) ? await this.meta.findOne({ _id: SCHEMA_ID }) : null;
119
+ if (!marker || marker.app !== APP || typeof marker.version !== 'number') {
120
+ throw new UAGDatabaseError(`The database "${this.name}" holds uag-* collections that the UAG did not create (${ours.join(', ')}). ` +
121
+ 'Nothing was changed. Choose another database with UAG_DATABASE.');
122
+ }
123
+ if (marker.version > exports.UAG_SCHEMA_VERSION) {
124
+ throw new UAGDatabaseError(`The database "${this.name}" was created by a newer UAG (schema version ${marker.version}; ` +
125
+ `this UAG supports ${exports.UAG_SCHEMA_VERSION}). Upgrade the UAG, or choose another database with UAG_DATABASE.`);
126
+ }
127
+ return marker.version < exports.UAG_SCHEMA_VERSION ? 'older' : 'ready';
128
+ });
129
+ }
130
+ /**
131
+ * Check the database (see inspect), then create what is missing: the schema
132
+ * marker first, then the collections and their indexes; or migrate an older
133
+ * schema. Safe to call again.
134
+ */
135
+ bootstrap() {
136
+ this.bootstrapped = this.bootstrapped || this.privileged(async () => {
137
+ const state = await this.inspect();
138
+ if (state === 'fresh') {
139
+ // The marker first: a start interrupted after it finishes the rest next time.
140
+ await this.meta.insertOne({ _id: SCHEMA_ID, app: APP, version: exports.UAG_SCHEMA_VERSION, created: new Date() });
141
+ await this.ensureCollections();
142
+ debug(`Created the UAG's collections in "${this.name}".`);
143
+ return 'created';
144
+ }
145
+ await this.ensureCollections();
146
+ if (state === 'older') {
147
+ // No migrations yet: each schema version adds its steps here.
148
+ await this.meta.updateOne({ _id: SCHEMA_ID }, { $set: { version: exports.UAG_SCHEMA_VERSION, migrated: new Date() } });
149
+ return 'migrated';
150
+ }
151
+ return 'ready';
152
+ });
153
+ // A failed attempt can be tried again.
154
+ this.bootstrapped.catch(() => { this.bootstrapped = undefined; });
155
+ return this.bootstrapped;
156
+ }
157
+ /** Create the collections and indexes, as needed; safe to repeat. */
158
+ async ensureCollections() {
159
+ await this.authCodes.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0, name: 'expires' });
160
+ await this.taskRuns.createIndex({ status: 1, leaseUntil: 1 }, { name: 'claim' });
161
+ await this.taskRuns.createIndex({ project: 1, form: 1, submission: 1, created: -1 }, { name: 'submission' });
162
+ await this.ensureRetention();
163
+ }
164
+ /** The task-run records' TTL index, following TASK_RUN_RETENTION_DAYS; none when it is 0. */
165
+ async ensureRetention() {
166
+ const seconds = this.retentionDays * 24 * 60 * 60;
167
+ const indexes = await this.taskRuns.listIndexes().toArray().catch(() => []);
168
+ const existing = indexes.find((index) => index.name === RETENTION_INDEX);
169
+ if (!seconds) {
170
+ if (existing)
171
+ await this.taskRuns.dropIndex(RETENTION_INDEX);
172
+ return;
173
+ }
174
+ if (!existing) {
175
+ await this.taskRuns.createIndex({ created: 1 }, { expireAfterSeconds: seconds, name: RETENTION_INDEX });
176
+ }
177
+ else if (existing.expireAfterSeconds !== seconds) {
178
+ await this.db.command({ collMod: exports.UAG_COLLECTIONS.taskRuns, index: { name: RETENTION_INDEX, expireAfterSeconds: seconds } });
179
+ }
180
+ }
181
+ /** Run database work, reporting a missing privilege with the database's name. */
182
+ async privileged(work) {
183
+ try {
184
+ return await work();
185
+ }
186
+ catch (err) {
187
+ if (err?.code === UNAUTHORIZED) {
188
+ throw new UAGDatabaseError(`The MONGO user may not use the database "${this.name}" (${err.message}). ` +
189
+ `Grant it the readWrite role on "${this.name}", or choose another database with UAG_DATABASE.`);
190
+ }
191
+ throw err;
192
+ }
193
+ }
194
+ /** Authorization codes for remote MCP clients (MCP_OAUTH), in `uag-authcodes`. */
195
+ authCodeStore() {
196
+ const collection = this.authCodes;
197
+ return {
198
+ async save(code, record, ttl) {
199
+ await collection.insertOne({ _id: codeKey(code), sealed: sealToken(code, record.jwt), codeChallenge: record.codeChallenge, expiresAt: new Date(Date.now() + ttl * 1000) });
200
+ },
201
+ async take(code) {
202
+ // The TTL monitor runs about once a minute, so expiry is checked here too.
203
+ const found = await collection.findOneAndDelete({ _id: codeKey(code), expiresAt: { $gt: new Date() } });
204
+ const jwt = typeof found?.sealed === 'string' ? openToken(code, found.sealed) : null;
205
+ return jwt ? { jwt, codeChallenge: found.codeChallenge } : null;
206
+ }
207
+ };
208
+ }
209
+ /** When the license server became unreachable, in `uag-meta`, so a restart neither begins nor extends the grace period. */
210
+ licenseGrace() {
211
+ const meta = this.meta;
212
+ const database = this;
213
+ return {
214
+ async read() {
215
+ const found = await meta.findOne({ _id: LICENSE_ID });
216
+ return typeof found?.outageSince === 'number' ? found.outageSince : undefined;
217
+ },
218
+ async write(since) {
219
+ // It may write before the license is known, so it never leaves uag-* collections without the marker.
220
+ await database.bootstrap();
221
+ await meta.updateOne({ _id: LICENSE_ID }, since === undefined ? { $unset: { outageSince: '' } } : { $set: { outageSince: since } }, { upsert: true });
222
+ }
223
+ };
224
+ }
225
+ async close() {
226
+ await this.client.close();
227
+ }
228
+ }
229
+ exports.UAGDatabase = UAGDatabase;
@@ -0,0 +1,40 @@
1
+ import { Formflow } from '@formio/appserver-core';
2
+ /**
3
+ * Agentic Formflow from @formio/appserver-core, imported directly rather than
4
+ * through @formio/appserver, under the names the UAG uses. Internal to the UAG:
5
+ * applications reach Formflow as `Formflow` from this package's exports.
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, resolveTaskName: typeof Formflow.resolveTaskName, resultEvent: typeof Formflow.resultEvent, warnLegacy: typeof Formflow.warnLegacy, withoutOwnedValues: typeof Formflow.withoutOwnedValues, stubAgentEngine: typeof Formflow.stubAgentEngine, stubDecisionEngine: typeof Formflow.stubDecisionEngine, toolsForScope: typeof Formflow.toolsForScope, EngineError: typeof Formflow.EngineError, formatReport: typeof Formflow.formatReport;
8
+ export type AgentEngine = Formflow.AgentEngine;
9
+ export type CallerIdentity = Formflow.CallerIdentity;
10
+ export type DecisionEngine = Formflow.DecisionEngine;
11
+ export type DecisionRequest = Formflow.DecisionRequest;
12
+ export type EngineInstance = Formflow.EngineInstance;
13
+ export type EngineRegistry = Formflow.EngineRegistry;
14
+ export type ExtractRequest = Formflow.ExtractRequest;
15
+ export type ExtractResponse = Formflow.ExtractResponse;
16
+ export type FieldSpec = Formflow.FieldSpec;
17
+ export type ToolCallResult = Formflow.ToolCallResult;
18
+ export type ToolDefinition = Formflow.ToolDefinition;
19
+ export type FormflowAgentApiConfig = Formflow.FormflowAgentApiConfig;
20
+ export type FormflowConfig = Formflow.FormflowConfig;
21
+ export type FormflowForm = Formflow.FormflowForm;
22
+ export type FormflowHost = Formflow.FormflowHost;
23
+ export type FormflowRateLimit = Formflow.FormflowRateLimit;
24
+ export type FormflowRoutingConfig = Formflow.FormflowRoutingConfig;
25
+ export type FormflowService = Formflow.FormflowService;
26
+ export type FormflowTask = Formflow.FormflowTask;
27
+ export type FormflowTool = Formflow.FormflowTool;
28
+ export type OperationContext = Formflow.OperationContext;
29
+ export type OperationResult = Formflow.OperationResult;
30
+ export type ScopedTokenClaims = Formflow.ScopedTokenClaims;
31
+ /**
32
+ * Who an operation's research acts as: core's `identityOf`, and for a caller
33
+ * signed in with a Form.io token (`x-jwt-token`), that token, so a scoped token
34
+ * carries the user to /mcp as an `Authorization` bearer token would. A portal
35
+ * team member's remote token is never carried: research for them is refused,
36
+ * as it is for anonymous callers.
37
+ */
38
+ export declare function callerIdentity(auth: any, req: {
39
+ get?: (name: string) => string | undefined;
40
+ }): CallerIdentity;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatReport = exports.EngineError = exports.toolsForScope = exports.stubDecisionEngine = exports.stubAgentEngine = exports.withoutOwnedValues = exports.warnLegacy = exports.resultEvent = exports.resolveTaskName = 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
+ exports.callerIdentity = callerIdentity;
5
+ const appserver_core_1 = require("@formio/appserver-core");
6
+ /**
7
+ * Agentic Formflow from @formio/appserver-core, imported directly rather than
8
+ * through @formio/appserver, under the names the UAG uses. Internal to the UAG:
9
+ * applications reach Formflow as `Formflow` from this package's exports.
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, resolveTaskName: exports.resolveTaskName, resultEvent: exports.resultEvent, warnLegacy: exports.warnLegacy, withoutOwnedValues: exports.withoutOwnedValues, stubAgentEngine: exports.stubAgentEngine, stubDecisionEngine: exports.stubDecisionEngine, toolsForScope: exports.toolsForScope, EngineError: exports.EngineError, formatReport: exports.formatReport } = appserver_core_1.Formflow);
12
+ /**
13
+ * Who an operation's research acts as: core's `identityOf`, and for a caller
14
+ * signed in with a Form.io token (`x-jwt-token`), that token, so a scoped token
15
+ * carries the user to /mcp as an `Authorization` bearer token would. A portal
16
+ * team member's remote token is never carried: research for them is refused,
17
+ * as it is for anonymous callers.
18
+ */
19
+ function callerIdentity(auth, req) {
20
+ const identity = (0, exports.identityOf)(auth, req.get?.('authorization'));
21
+ if (identity.admin || identity.bearer)
22
+ return identity;
23
+ const formioToken = req.get?.('x-jwt-token');
24
+ if (auth?.user && !auth.remoteToken && typeof formioToken === 'string' && formioToken) {
25
+ return { admin: false, bearer: formioToken };
26
+ }
27
+ return identity;
28
+ }
@@ -0,0 +1,53 @@
1
+ import { AuthRequest } from '@formio/appserver';
2
+ import { FormflowError } from './formflow';
3
+ import type { UAGProjectInterface } from './UAGProjectInterface';
4
+ /**
5
+ * Who the UAG acts as at Form.io when it runs a task for someone: the caller,
6
+ * with the token they sent, or a Run task action's agent role, acting on the
7
+ * caller's behalf with a Form.io token the UAG signs.
8
+ *
9
+ * The UAG signs Form.io "external" tokens with JWT_SECRET, which must be Form.io
10
+ * Enterprise's: Form.io then takes the user, and their roles, from the token. A
11
+ * token the UAG signs is short-lived, never marks an administrator, and never
12
+ * carries an admin role.
13
+ */
14
+ /** Seconds a token the UAG signs is valid for. */
15
+ export declare const SIGNED_TOKEN_TTL = 300;
16
+ /** The user the UAG acts for: who sent the submission. No token, ever. */
17
+ export type ActingFor = {
18
+ /** The user's id; none for an anonymous caller. */
19
+ _id?: string;
20
+ /** Their roles when they submitted. */
21
+ roles: string[];
22
+ email?: string;
23
+ };
24
+ /** What a token the UAG signs says about why it was signed (Form.io ignores it). */
25
+ export type AgentClaim = {
26
+ agent: boolean;
27
+ action?: string;
28
+ task?: string;
29
+ };
30
+ /** A token the UAG may not sign, or Form.io would not accept. */
31
+ export declare class AgentIdentityError extends FormflowError {
32
+ constructor(message: string);
33
+ }
34
+ /** The user a request comes from, as the UAG records it. */
35
+ export declare function actingForOf(auth: AuthRequest | undefined, project: UAGProjectInterface): ActingFor;
36
+ /**
37
+ * Sign a Form.io token for `user` with `roles`. Refuses an admin role, and never
38
+ * marks the holder an administrator.
39
+ */
40
+ export declare function signFormioToken(project: UAGProjectInterface, user: ActingFor, roles: string[], claim: AgentClaim): string;
41
+ /** An identity that acts as `role` for `user`, with a newly signed token. */
42
+ export declare function agentAuth(project: UAGProjectInterface, user: ActingFor, role: string, claim: Omit<AgentClaim, 'agent'>): Promise<AuthRequest>;
43
+ /**
44
+ * An identity that acts as `user`, with their recorded roles, for research and
45
+ * for the runs that act as them. A user's admin roles are left out: the UAG
46
+ * never signs a token with one, so the user acts with their other roles.
47
+ */
48
+ export declare function userAuth(project: UAGProjectInterface, user: ActingFor, claim: Omit<AgentClaim, 'agent'>): Promise<AuthRequest>;
49
+ /**
50
+ * Explain a Form.io refusal of a token the UAG signed: almost always a
51
+ * JWT_SECRET that is not Form.io Enterprise's.
52
+ */
53
+ export declare function refusedSignedToken(status: number | undefined): AgentIdentityError | null;