@forestadmin/workflow-executor 1.0.0

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 (123) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +141 -0
  3. package/dist/adapters/activity-log-drainer.d.ts +6 -0
  4. package/dist/adapters/activity-log-drainer.js +21 -0
  5. package/dist/adapters/agent-client-agent-port.d.ts +27 -0
  6. package/dist/adapters/agent-client-agent-port.js +211 -0
  7. package/dist/adapters/ai-client-adapter.d.ts +11 -0
  8. package/dist/adapters/ai-client-adapter.js +38 -0
  9. package/dist/adapters/always-error-ai-model-port.d.ts +8 -0
  10. package/dist/adapters/always-error-ai-model-port.js +23 -0
  11. package/dist/adapters/console-logger.d.ts +7 -0
  12. package/dist/adapters/console-logger.js +15 -0
  13. package/dist/adapters/forest-server-workflow-port.d.ts +25 -0
  14. package/dist/adapters/forest-server-workflow-port.js +163 -0
  15. package/dist/adapters/forestadmin-client-activity-log-port-factory.d.ts +12 -0
  16. package/dist/adapters/forestadmin-client-activity-log-port-factory.js +22 -0
  17. package/dist/adapters/forestadmin-client-activity-log-port.d.ts +15 -0
  18. package/dist/adapters/forestadmin-client-activity-log-port.js +78 -0
  19. package/dist/adapters/pretty-logger.d.ts +9 -0
  20. package/dist/adapters/pretty-logger.js +37 -0
  21. package/dist/adapters/record-id-serializer.d.ts +4 -0
  22. package/dist/adapters/record-id-serializer.js +20 -0
  23. package/dist/adapters/run-to-available-step-mapper.d.ts +4 -0
  24. package/dist/adapters/run-to-available-step-mapper.js +137 -0
  25. package/dist/adapters/server-ai-adapter.d.ts +16 -0
  26. package/dist/adapters/server-ai-adapter.js +60 -0
  27. package/dist/adapters/server-types.d.ts +181 -0
  28. package/dist/adapters/server-types.js +35 -0
  29. package/dist/adapters/step-definition-mapper.d.ts +4 -0
  30. package/dist/adapters/step-definition-mapper.js +68 -0
  31. package/dist/adapters/step-outcome-to-update-step-mapper.d.ts +4 -0
  32. package/dist/adapters/step-outcome-to-update-step-mapper.js +34 -0
  33. package/dist/adapters/with-retry.d.ts +6 -0
  34. package/dist/adapters/with-retry.js +40 -0
  35. package/dist/build-workflow-executor.d.ts +35 -0
  36. package/dist/build-workflow-executor.js +175 -0
  37. package/dist/cli-core.d.ts +26 -0
  38. package/dist/cli-core.js +228 -0
  39. package/dist/cli.d.ts +3 -0
  40. package/dist/cli.js +17 -0
  41. package/dist/defaults.d.ts +9 -0
  42. package/dist/defaults.js +12 -0
  43. package/dist/errors.d.ts +153 -0
  44. package/dist/errors.js +327 -0
  45. package/dist/executors/activity-log.d.ts +14 -0
  46. package/dist/executors/activity-log.js +33 -0
  47. package/dist/executors/agent-with-log.d.ts +33 -0
  48. package/dist/executors/agent-with-log.js +76 -0
  49. package/dist/executors/base-step-executor.d.ts +40 -0
  50. package/dist/executors/base-step-executor.js +267 -0
  51. package/dist/executors/condition-step-executor.d.ts +15 -0
  52. package/dist/executors/condition-step-executor.js +108 -0
  53. package/dist/executors/guidance-step-executor.d.ts +12 -0
  54. package/dist/executors/guidance-step-executor.js +39 -0
  55. package/dist/executors/load-related-record-step-executor.d.ts +38 -0
  56. package/dist/executors/load-related-record-step-executor.js +478 -0
  57. package/dist/executors/mcp-step-executor.d.ts +22 -0
  58. package/dist/executors/mcp-step-executor.js +188 -0
  59. package/dist/executors/read-record-step-executor.d.ts +10 -0
  60. package/dist/executors/read-record-step-executor.js +100 -0
  61. package/dist/executors/record-step-executor.d.ts +19 -0
  62. package/dist/executors/record-step-executor.js +108 -0
  63. package/dist/executors/step-executor-factory.d.ts +24 -0
  64. package/dist/executors/step-executor-factory.js +99 -0
  65. package/dist/executors/summary/step-execution-formatters.d.ts +8 -0
  66. package/dist/executors/summary/step-execution-formatters.js +49 -0
  67. package/dist/executors/summary/step-summary-builder.d.ts +7 -0
  68. package/dist/executors/summary/step-summary-builder.js +52 -0
  69. package/dist/executors/trigger-record-action-step-executor.d.ts +17 -0
  70. package/dist/executors/trigger-record-action-step-executor.js +169 -0
  71. package/dist/executors/update-record-step-executor.d.ts +13 -0
  72. package/dist/executors/update-record-step-executor.js +245 -0
  73. package/dist/http/executor-http-server.d.ts +25 -0
  74. package/dist/http/executor-http-server.js +170 -0
  75. package/dist/http/pending-data-validators.d.ts +25 -0
  76. package/dist/http/pending-data-validators.js +79 -0
  77. package/dist/http/step-serializer.d.ts +3 -0
  78. package/dist/http/step-serializer.js +47 -0
  79. package/dist/in-flight-run-registry.d.ts +9 -0
  80. package/dist/in-flight-run-registry.js +30 -0
  81. package/dist/index.d.ts +38 -0
  82. package/dist/index.js +88 -0
  83. package/dist/ports/activity-log-port.d.ts +24 -0
  84. package/dist/ports/activity-log-port.js +3 -0
  85. package/dist/ports/agent-port.d.ts +54 -0
  86. package/dist/ports/agent-port.js +3 -0
  87. package/dist/ports/ai-model-port.d.ts +7 -0
  88. package/dist/ports/ai-model-port.js +3 -0
  89. package/dist/ports/logger-port.d.ts +6 -0
  90. package/dist/ports/logger-port.js +3 -0
  91. package/dist/ports/run-store.d.ts +9 -0
  92. package/dist/ports/run-store.js +3 -0
  93. package/dist/ports/workflow-port.d.ts +30 -0
  94. package/dist/ports/workflow-port.js +3 -0
  95. package/dist/remote-tool-fetcher.d.ts +19 -0
  96. package/dist/remote-tool-fetcher.js +56 -0
  97. package/dist/runner.d.ts +50 -0
  98. package/dist/runner.js +317 -0
  99. package/dist/schema-cache.d.ts +11 -0
  100. package/dist/schema-cache.js +37 -0
  101. package/dist/schema-resolver.d.ts +11 -0
  102. package/dist/schema-resolver.js +24 -0
  103. package/dist/stores/build-run-store.d.ts +5 -0
  104. package/dist/stores/build-run-store.js +28 -0
  105. package/dist/stores/database-store.d.ts +17 -0
  106. package/dist/stores/database-store.js +119 -0
  107. package/dist/stores/in-memory-store.d.ts +11 -0
  108. package/dist/stores/in-memory-store.js +48 -0
  109. package/dist/types/execution-context.d.ts +37 -0
  110. package/dist/types/execution-context.js +3 -0
  111. package/dist/types/step-execution-data.d.ts +137 -0
  112. package/dist/types/step-execution-data.js +3 -0
  113. package/dist/types/validated/collection.d.ts +126 -0
  114. package/dist/types/validated/collection.js +96 -0
  115. package/dist/types/validated/execution.d.ts +362 -0
  116. package/dist/types/validated/execution.js +43 -0
  117. package/dist/types/validated/step-definition.d.ts +243 -0
  118. package/dist/types/validated/step-definition.js +128 -0
  119. package/dist/types/validated/step-outcome.d.ts +108 -0
  120. package/dist/types/validated/step-outcome.js +66 -0
  121. package/dist/validate-secrets.d.ts +5 -0
  122. package/dist/validate-secrets.js +14 -0
  123. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # @forestadmin/workflow-executor
2
+
3
+ Run Forest Admin workflow steps on your own infrastructure.
4
+
5
+ The executor polls the Forest orchestrator for pending steps, runs them locally
6
+ (with access to your data via the Forest agent), and reports results back. No
7
+ client data ever leaves your infrastructure.
8
+
9
+ ## Running in production
10
+
11
+ ### Install
12
+
13
+ ```bash
14
+ npm install -g @forestadmin/workflow-executor
15
+ ```
16
+
17
+ ### Configure via environment
18
+
19
+ | Variable | Required | Default | Description |
20
+ |----------|----------|---------|-------------|
21
+ | `FOREST_ENV_SECRET` | ✓ | — | Forest Admin project environment secret |
22
+ | `FOREST_AUTH_SECRET` | ✓ | — | JWT signing secret (shared with your agent) |
23
+ | `AGENT_URL` | ✓ | — | URL of your running Forest Admin agent |
24
+ | `DATABASE_URL` | ✓* | — | Postgres connection string (*not needed with `--in-memory`) |
25
+ | `HTTP_PORT` | — | `3400` | Port for the executor HTTP server |
26
+ | `FOREST_SERVER_URL` | — | `https://api.forestadmin.com` | Orchestrator URL |
27
+ | `POLLING_INTERVAL_MS` | — | `5000` | Poll cadence for pending steps |
28
+ | `STOP_TIMEOUT_MS` | — | `30000` | Graceful shutdown deadline |
29
+
30
+ Optional AI configuration (all-or-nothing — falls back to server AI if any is missing):
31
+
32
+ | Variable | Description |
33
+ |----------|-------------|
34
+ | `AI_PROVIDER` | `anthropic` or `openai` |
35
+ | `AI_MODEL` | Model name (e.g. `claude-sonnet-4-6`) |
36
+ | `AI_API_KEY` | Provider API key |
37
+
38
+ ### Run
39
+
40
+ ```bash
41
+ forest-workflow-executor
42
+ ```
43
+
44
+ You should see (pretty format when stdout is a TTY):
45
+
46
+ ```
47
+ 13:33:42 info Workflow executor starting mode="database" forestServerUrl="https://api.forestadmin.com" agentUrl="http://localhost:3351" httpPort=3400 pollingIntervalMs=5000 aiConfig="server fallback"
48
+ 13:33:42 info Workflow executor ready url="http://localhost:3400"
49
+ 13:33:47 info Poll cycle completed fetched=0 dispatching=0
50
+ ```
51
+
52
+ When stdout is piped, redirected or inside a container, logs are emitted as
53
+ structured JSON instead — ready to be ingested by Datadog, CloudWatch, Loki, etc.:
54
+
55
+ ```json
56
+ {"message":"Workflow executor ready","timestamp":"2026-04-20T13:33:42.000Z","url":"http://localhost:3400"}
57
+ {"message":"Poll cycle completed","timestamp":"2026-04-20T13:33:47.000Z","fetched":0,"dispatching":0}
58
+ ```
59
+
60
+ ### Log format overrides
61
+
62
+ | Flag | Behavior |
63
+ |------|----------|
64
+ | `--pretty` | Force colorized human-readable logs |
65
+ | `--json` | Force structured JSON logs |
66
+ | (none) | Auto-detect: pretty when stdout is a TTY, JSON otherwise |
67
+
68
+ Setting `NO_COLOR=1` disables ANSI codes while keeping the pretty format.
69
+
70
+ ### Health check
71
+
72
+ ```bash
73
+ curl http://localhost:3400/health
74
+ # → {"state":"running"}
75
+ ```
76
+
77
+ ### Graceful shutdown
78
+
79
+ Send `SIGTERM` or `SIGINT`. The executor drains in-flight steps, closes the HTTP
80
+ server, and exits with code `0`. Steps that don't drain within `STOP_TIMEOUT_MS`
81
+ are force-killed and the process exits with code `1`.
82
+
83
+ ### Exit codes
84
+
85
+ | Code | Meaning |
86
+ |------|---------|
87
+ | `0` | Graceful shutdown |
88
+ | `1` | Startup error (missing env, invalid config) or forced shutdown |
89
+
90
+ ### In-memory mode (dev only)
91
+
92
+ ```bash
93
+ forest-workflow-executor --in-memory
94
+ ```
95
+
96
+ No Postgres needed. State is lost on restart — **not for production**.
97
+
98
+ ### All flags
99
+
100
+ ```bash
101
+ forest-workflow-executor --help
102
+ ```
103
+
104
+ ## Programmatic use
105
+
106
+ If you prefer embedding the executor in your own Node entry point:
107
+
108
+ ```ts
109
+ import { buildDatabaseExecutor } from '@forestadmin/workflow-executor';
110
+
111
+ const executor = buildDatabaseExecutor({
112
+ envSecret: process.env.FOREST_ENV_SECRET!,
113
+ authSecret: process.env.FOREST_AUTH_SECRET!,
114
+ agentUrl: process.env.AGENT_URL!,
115
+ httpPort: 3400,
116
+ database: { uri: process.env.DATABASE_URL! },
117
+ });
118
+
119
+ await executor.start();
120
+ // SIGTERM / SIGINT handling is built in
121
+ ```
122
+
123
+ See `src/build-workflow-executor.ts` for the full options surface.
124
+
125
+ ## Dev with the example scaffold
126
+
127
+ The `example/` folder contains a docker-compose setup with Postgres + a ready
128
+ `index.ts` entrypoint that loads `.env` via `dotenv`. Use it for local development
129
+ only — not for production deployments.
130
+
131
+ ```bash
132
+ cd example
133
+ docker compose up -d
134
+ cp .env.example .env # fill in your secrets
135
+ npx tsx index.ts
136
+ ```
137
+
138
+ ## Architecture
139
+
140
+ See [CLAUDE.md](./CLAUDE.md) for the full package layout, architectural
141
+ principles, privacy boundaries, and extension points.
@@ -0,0 +1,6 @@
1
+ export default class ActivityLogDrainer {
2
+ private readonly inFlight;
3
+ track<T>(fn: () => Promise<T>): Promise<T>;
4
+ drain(): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=activity-log-drainer.d.ts.map
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class ActivityLogDrainer {
4
+ constructor() {
5
+ this.inFlight = new Set();
6
+ }
7
+ track(fn) {
8
+ const promise = fn();
9
+ this.inFlight.add(promise);
10
+ // Swallow rejections on the cleanup chain so tracking a rejecting promise
11
+ // doesn't cause UnhandledPromiseRejection. The original promise returned
12
+ // to the caller still rejects normally.
13
+ promise.finally(() => this.inFlight.delete(promise)).catch(() => { });
14
+ return promise;
15
+ }
16
+ async drain() {
17
+ await Promise.allSettled([...this.inFlight]);
18
+ }
19
+ }
20
+ exports.default = ActivityLogDrainer;
21
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWN0aXZpdHktbG9nLWRyYWluZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYWRhcHRlcnMvYWN0aXZpdHktbG9nLWRyYWluZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxNQUFxQixrQkFBa0I7SUFBdkM7UUFDbUIsYUFBUSxHQUFHLElBQUksR0FBRyxFQUFvQixDQUFDO0lBZ0IxRCxDQUFDO0lBZEMsS0FBSyxDQUFJLEVBQW9CO1FBQzNCLE1BQU0sT0FBTyxHQUFHLEVBQUUsRUFBRSxDQUFDO1FBQ3JCLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQzNCLDBFQUEwRTtRQUMxRSx5RUFBeUU7UUFDekUsd0NBQXdDO1FBQ3hDLE9BQU8sQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLEdBQUUsQ0FBQyxDQUFDLENBQUM7UUFFckUsT0FBTyxPQUFPLENBQUM7SUFDakIsQ0FBQztJQUVELEtBQUssQ0FBQyxLQUFLO1FBQ1QsTUFBTSxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUMsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUMvQyxDQUFDO0NBQ0Y7QUFqQkQscUNBaUJDIn0=
@@ -0,0 +1,27 @@
1
+ import type { AgentPort, ExecuteActionQuery, GetActionFormInfoQuery, GetRecordQuery, GetRelatedDataQuery, GetSingleRelatedDataQuery, UpdateRecordQuery } from '../ports/agent-port';
2
+ import type SchemaCache from '../schema-cache';
3
+ import type { StepUser } from '../types/execution-context';
4
+ import type { RecordData } from '../types/validated/collection';
5
+ export default class AgentClientAgentPort implements AgentPort {
6
+ private readonly agentUrl;
7
+ private readonly authSecret;
8
+ private readonly schemaCache;
9
+ constructor(params: {
10
+ agentUrl: string;
11
+ authSecret: string;
12
+ schemaCache: SchemaCache;
13
+ });
14
+ getRecord({ collection, id, fields }: GetRecordQuery, user: StepUser): Promise<RecordData>;
15
+ updateRecord({ collection, id, values }: UpdateRecordQuery, user: StepUser): Promise<RecordData>;
16
+ getRelatedData({ collection, id, relation, relatedSchema, limit, fields }: GetRelatedDataQuery, user: StepUser): Promise<RecordData[]>;
17
+ getSingleRelatedData({ collection, id, relation, relatedSchema, fields }: GetSingleRelatedDataQuery, user: StepUser): Promise<RecordData | null>;
18
+ executeAction({ collection, action, id }: ExecuteActionQuery, user: StepUser): Promise<unknown>;
19
+ getActionFormInfo({ collection, action, id }: GetActionFormInfoQuery, user: StepUser): Promise<{
20
+ hasForm: boolean;
21
+ }>;
22
+ private callAgent;
23
+ private createClient;
24
+ probe(): Promise<void>;
25
+ private buildActionEndpoints;
26
+ }
27
+ //# sourceMappingURL=agent-client-agent-port.d.ts.map
@@ -0,0 +1,211 @@
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
+ const agent_client_1 = require("@forestadmin/agent-client");
7
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
8
+ const errors_1 = require("../errors");
9
+ function toCamelCase(name) {
10
+ return name.replace(/_([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
11
+ }
12
+ // The agent-client HTTP layer deserializes JSON:API responses with camelCase keys.
13
+ // Field names in the schema and in GetRecordQuery.fields use the original format (e.g. snake_case).
14
+ // This function restores the original field names so callers can look up values by schema fieldName.
15
+ function restoreFieldNames(values, originalFieldNames) {
16
+ if (!originalFieldNames?.length)
17
+ return values;
18
+ const camelToOriginal = {};
19
+ for (const name of originalFieldNames) {
20
+ camelToOriginal[toCamelCase(name)] = name;
21
+ }
22
+ return Object.fromEntries(Object.entries(values).map(([k, v]) => [camelToOriginal[k] ?? k, v]));
23
+ }
24
+ class AgentClientAgentPort {
25
+ constructor(params) {
26
+ this.agentUrl = params.agentUrl;
27
+ this.authSecret = params.authSecret;
28
+ this.schemaCache = params.schemaCache;
29
+ }
30
+ async getRecord({ collection, id, fields }, user) {
31
+ return this.callAgent('getRecord', async () => {
32
+ const client = this.createClient(user);
33
+ // Fetch by id through the agent's by-id route (like update/delete): the recordId is an
34
+ // opaque ordered token and the agent — the only party that knows the primary key column
35
+ // order — does the matching. No primaryKeyFields ordering assumption here.
36
+ let record;
37
+ try {
38
+ record = await client
39
+ .collection(collection)
40
+ .getOne(id, { ...(fields?.length && { fields }) });
41
+ }
42
+ catch (error) {
43
+ if (agent_client_1.HttpRequester.is404Error(error)) {
44
+ throw new errors_1.RecordNotFoundError(collection, id);
45
+ }
46
+ throw error;
47
+ }
48
+ // Some agents answer a missing composite-key record with a 200 + empty body instead of 404.
49
+ if (!record || Object.keys(record).length === 0) {
50
+ throw new errors_1.RecordNotFoundError(collection, id);
51
+ }
52
+ return {
53
+ collectionName: collection,
54
+ recordId: id,
55
+ values: restoreFieldNames(record, fields),
56
+ };
57
+ });
58
+ }
59
+ async updateRecord({ collection, id, values }, user) {
60
+ return this.callAgent('updateRecord', async () => {
61
+ const client = this.createClient(user);
62
+ const updatedRecord = await client
63
+ .collection(collection)
64
+ .update(id, values);
65
+ return {
66
+ collectionName: collection,
67
+ recordId: id,
68
+ values: restoreFieldNames(updatedRecord, Object.keys(values)),
69
+ };
70
+ });
71
+ }
72
+ async getRelatedData({ collection, id, relation, relatedSchema, limit, fields }, user) {
73
+ return this.callAgent('getRelatedData', async () => {
74
+ const client = this.createClient(user);
75
+ const rows = await client
76
+ .collection(collection)
77
+ .relation(relation, id)
78
+ .list({
79
+ ...(limit !== null && { pagination: { size: limit, number: 1 } }),
80
+ ...(fields?.length && { fields }),
81
+ });
82
+ return rows.map(row => {
83
+ const restored = restoreFieldNames(row, relatedSchema.fields.map(f => f.fieldName));
84
+ // For composite PKs, rebuilding the id from primaryKeyFields assumes the schema's
85
+ // (alphabetical) order matches the agent's column order — it may not, which would
86
+ // mis-pair the key. Use the agent's opaque record id (pipe-joined when composite),
87
+ // like getSingleRelatedData, so it round-trips through the by-id route.
88
+ const recordId = relatedSchema.primaryKeyFields.length > 1
89
+ ? String(row.id).split('|')
90
+ : relatedSchema.primaryKeyFields.map(f => restored[f]);
91
+ return {
92
+ collectionName: relatedSchema.collectionName,
93
+ recordId,
94
+ values: restored,
95
+ };
96
+ });
97
+ });
98
+ }
99
+ // xToOne relations have no /relationships/<relation> route on the agent. We read the
100
+ // parent record with a `<relation>@@@<field>` projection and unpack the relation linkage
101
+ // jsonapi-serializer emits as a nested object on the parent (with the related PK packed
102
+ // under "id" when composite).
103
+ async getSingleRelatedData({ collection, id, relation, relatedSchema, fields }, user) {
104
+ return this.callAgent('getSingleRelatedData', async () => {
105
+ // The agent can't parse multiple sub-fields on one relation in a single projection
106
+ // (`fields[store]=id,name` is read as a single field name → ValidationError). The linkage
107
+ // `id` carries the (packed) related PK regardless of projection, so project at most ONE
108
+ // field: the requested reference field for display, else a single PK field just to pull the
109
+ // relation into the response.
110
+ const projectedField = fields?.[0] ?? relatedSchema.primaryKeyFields[0];
111
+ const parent = await this.getRecord({
112
+ collection,
113
+ id,
114
+ fields: [`${relation}@@@${projectedField}`],
115
+ }, user);
116
+ // agent-client camelCases relation keys; look the linkage up under the camelCased name.
117
+ const linkage = parent.values[toCamelCase(relation)];
118
+ const packedId = linkage?.id;
119
+ if (!linkage || !packedId)
120
+ return null;
121
+ const restored = restoreFieldNames(linkage, [projectedField]);
122
+ return {
123
+ collectionName: relatedSchema.collectionName,
124
+ recordId: packedId.split('|'),
125
+ values: restored,
126
+ };
127
+ });
128
+ }
129
+ async executeAction({ collection, action, id }, user) {
130
+ return this.callAgent('executeAction', async () => {
131
+ const client = this.createClient(user);
132
+ const recordIds = id?.length ? [id] : [];
133
+ const act = await client.collection(collection).action(action, { recordIds });
134
+ return act.execute();
135
+ });
136
+ }
137
+ async getActionFormInfo({ collection, action, id }, user) {
138
+ return this.callAgent('getActionFormInfo', async () => {
139
+ const client = this.createClient(user);
140
+ const act = await client.collection(collection).action(action, { recordIds: [id] });
141
+ return { hasForm: act.getFields().length > 0 };
142
+ });
143
+ }
144
+ async callAgent(operation, fn) {
145
+ try {
146
+ return await fn();
147
+ }
148
+ catch (cause) {
149
+ if (cause instanceof errors_1.WorkflowExecutorError)
150
+ throw cause;
151
+ throw new errors_1.AgentPortError(operation, cause);
152
+ }
153
+ }
154
+ createClient(user) {
155
+ // snake_case aliases: Ruby/Python agents splat JWT claims into Caller.new (snake_case kwargs).
156
+ const token = jsonwebtoken_1.default.sign({
157
+ ...user,
158
+ first_name: user.firstName,
159
+ last_name: user.lastName,
160
+ rendering_id: user.renderingId,
161
+ permission_level: user.permissionLevel,
162
+ scope: 'step-execution',
163
+ }, this.authSecret, { expiresIn: '5m' });
164
+ return (0, agent_client_1.createRemoteAgentClient)({
165
+ url: this.agentUrl,
166
+ token,
167
+ actionEndpoints: this.buildActionEndpoints(),
168
+ });
169
+ }
170
+ // Hits GET /forest/ (public, no auth required across all agent versions). A 4xx here means
171
+ // the URL points to something that isn't a Forest agent. JWT is validated naturally on first step.
172
+ async probe() {
173
+ const url = `${this.agentUrl.replace(/\/+$/, '')}/forest/`;
174
+ let response;
175
+ try {
176
+ response = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(5000) });
177
+ }
178
+ catch (error) {
179
+ const isTimeout = error instanceof Error && error.name === 'TimeoutError';
180
+ const reason = isTimeout ? 'timeout after 5000ms' : (0, errors_1.extractErrorMessage)(error);
181
+ throw new errors_1.AgentProbeError(`cannot reach ${this.agentUrl} (${reason})`, { cause: error });
182
+ }
183
+ if (!response.ok) {
184
+ throw new errors_1.AgentProbeError(`${this.agentUrl} responded with ${response.status} ${response.statusText}`);
185
+ }
186
+ }
187
+ buildActionEndpoints() {
188
+ const endpoints = {};
189
+ for (const [collectionName, schema] of this.schemaCache) {
190
+ endpoints[collectionName] = {};
191
+ for (const action of schema.actions) {
192
+ // agent-client POSTs /hooks/load unconditionally; `hooks.load` tells it whether a 404
193
+ // there is expected (Ruby agent, swallowed → fallback to the static `fields` below) or
194
+ // a real error. Both `hooks` and `fields` must mirror the agent's real schema for form
195
+ // detection to work on Ruby agents.
196
+ endpoints[collectionName][action.name] = {
197
+ id: action.name,
198
+ name: action.name,
199
+ endpoint: action.endpoint,
200
+ hooks: action.hooks ?? { load: false, change: [] },
201
+ // Zod envelope-validates `fields` as an array of opaque objects. Inner widget/parameters
202
+ // shape is owned by @forestadmin/forestadmin-client and consumed by agent-client below.
203
+ fields: (action.fields ?? []),
204
+ };
205
+ }
206
+ }
207
+ return endpoints;
208
+ }
209
+ }
210
+ exports.default = AgentClientAgentPort;
211
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWdlbnQtY2xpZW50LWFnZW50LXBvcnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYWRhcHRlcnMvYWdlbnQtY2xpZW50LWFnZW50LXBvcnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFjQSw0REFBbUY7QUFDbkYsZ0VBQXdDO0FBRXhDLHNDQU1tQjtBQUVuQixTQUFTLFdBQVcsQ0FBQyxJQUFZO0lBQy9CLE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFTLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0FBQzVFLENBQUM7QUFFRCxtRkFBbUY7QUFDbkYsb0dBQW9HO0FBQ3BHLHFHQUFxRztBQUNyRyxTQUFTLGlCQUFpQixDQUN4QixNQUErQixFQUMvQixrQkFBd0M7SUFFeEMsSUFBSSxDQUFDLGtCQUFrQixFQUFFLE1BQU07UUFBRSxPQUFPLE1BQU0sQ0FBQztJQUUvQyxNQUFNLGVBQWUsR0FBMkIsRUFBRSxDQUFDO0lBRW5ELEtBQUssTUFBTSxJQUFJLElBQUksa0JBQWtCLEVBQUUsQ0FBQztRQUN0QyxlQUFlLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsSUFBSSxDQUFDO0lBQzVDLENBQUM7SUFFRCxPQUFPLE1BQU0sQ0FBQyxXQUFXLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxlQUFlLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUNsRyxDQUFDO0FBRUQsTUFBcUIsb0JBQW9CO0lBS3ZDLFlBQVksTUFBMEU7UUFDcEYsSUFBSSxDQUFDLFFBQVEsR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDO1FBQ2hDLElBQUks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
@@ -0,0 +1,11 @@
1
+ import type { AiModelPort } from '../ports/ai-model-port';
2
+ import type { AiConfiguration, BaseChatModel, RemoteTool, ToolConfig } from '@forestadmin/ai-proxy';
3
+ export default class AiClientAdapter implements AiModelPort {
4
+ private readonly aiClient;
5
+ constructor(aiConfigurations: AiConfiguration[]);
6
+ getModel(aiConfigName?: string): BaseChatModel;
7
+ loadRemoteTools(configs: Record<string, ToolConfig>): Promise<RemoteTool[]>;
8
+ closeConnections(): Promise<void>;
9
+ private callPort;
10
+ }
11
+ //# sourceMappingURL=ai-client-adapter.d.ts.map
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ai_proxy_1 = require("@forestadmin/ai-proxy");
4
+ const errors_1 = require("../errors");
5
+ class AiClientAdapter {
6
+ constructor(aiConfigurations) {
7
+ const withRetries = aiConfigurations.map(c => ({ maxRetries: 2, ...c }));
8
+ this.aiClient = new ai_proxy_1.AiClient({ aiConfigurations: withRetries });
9
+ }
10
+ getModel(aiConfigName) {
11
+ try {
12
+ return this.aiClient.getModel(aiConfigName);
13
+ }
14
+ catch (cause) {
15
+ if (cause instanceof errors_1.WorkflowExecutorError)
16
+ throw cause;
17
+ throw new errors_1.AiModelPortError('getModel', cause);
18
+ }
19
+ }
20
+ loadRemoteTools(configs) {
21
+ return this.callPort('loadRemoteTools', () => this.aiClient.loadRemoteTools(configs));
22
+ }
23
+ closeConnections() {
24
+ return this.callPort('closeConnections', () => this.aiClient.closeConnections());
25
+ }
26
+ async callPort(operation, fn) {
27
+ try {
28
+ return await fn();
29
+ }
30
+ catch (cause) {
31
+ if (cause instanceof errors_1.WorkflowExecutorError)
32
+ throw cause;
33
+ throw new errors_1.AiModelPortError(operation, cause);
34
+ }
35
+ }
36
+ }
37
+ exports.default = AiClientAdapter;
38
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWktY2xpZW50LWFkYXB0ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYWRhcHRlcnMvYWktY2xpZW50LWFkYXB0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFHQSxvREFBaUQ7QUFFakQsc0NBQW9FO0FBRXBFLE1BQXFCLGVBQWU7SUFHbEMsWUFBWSxnQkFBbUM7UUFDN0MsTUFBTSxXQUFXLEdBQUcsZ0JBQWdCLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLFVBQVUsRUFBRSxDQUFDLEVBQUUsR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUM7UUFDekUsSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLG1CQUFRLENBQUMsRUFBRSxnQkFBZ0IsRUFBRSxXQUFnQyxFQUFFLENBQUMsQ0FBQztJQUN2RixDQUFDO0lBRUQsUUFBUSxDQUFDLFlBQXFCO1FBQzVCLElBQUksQ0FBQztZQUNILE9BQU8sSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFDLENBQUM7UUFDOUMsQ0FBQztRQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7WUFDZixJQUFJLEtBQUssWUFBWSw4QkFBcUI7Z0JBQUUsTUFBTSxLQUFLLENBQUM7WUFDeEQsTUFBTSxJQUFJLHlCQUFnQixDQUFDLFVBQVUsRUFBRSxLQUFLLENBQUMsQ0FBQztRQUNoRCxDQUFDO0lBQ0gsQ0FBQztJQUVELGVBQWUsQ0FBQyxPQUFtQztRQUNqRCxPQUFPLElBQUksQ0FBQyxRQUFRLENBQUMsaUJBQWlCLEVBQUUsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxlQUFlLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztJQUN4RixDQUFDO0lBRUQsZ0JBQWdCO1FBQ2QsT0FBTyxJQUFJLENBQUMsUUFBUSxDQUFDLGtCQUFrQixFQUFFLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQyxDQUFDO0lBQ25GLENBQUM7SUFFTyxLQUFLLENBQUMsUUFBUSxDQUFJLFNBQWlCLEVBQUUsRUFBb0I7UUFDL0QsSUFBSSxDQUFDO1lBQ0gsT0FBTyxNQUFNLEVBQUUsRUFBRSxDQUFDO1FBQ3BCLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsSUFBSSxLQUFLLFlBQVksOEJBQXFCO2dCQUFFLE1BQU0sS0FBSyxDQUFDO1lBQ3hELE1BQU0sSUFBSSx5QkFBZ0IsQ0FBQyxTQUFTLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDL0MsQ0FBQztJQUNILENBQUM7Q0FDRjtBQWpDRCxrQ0FpQ0MifQ==
@@ -0,0 +1,8 @@
1
+ import type { AiModelPort } from '../ports/ai-model-port';
2
+ import type { BaseChatModel, RemoteTool, ToolConfig } from '@forestadmin/ai-proxy';
3
+ export default class AlwaysErrorAiModelPort implements AiModelPort {
4
+ getModel(): BaseChatModel;
5
+ loadRemoteTools(_configs: Record<string, ToolConfig>): Promise<RemoteTool[]>;
6
+ closeConnections(): Promise<void>;
7
+ }
8
+ //# sourceMappingURL=always-error-ai-model-port.d.ts.map
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const errors_1 = require("../errors");
4
+ // Dev-only adapter: every AI call throws immediately so devs can reproduce AI error paths
5
+ // without a real AI service. Activated via FORCE_AI_ERROR=true.
6
+ class AlwaysErrorAiModelPort {
7
+ getModel() {
8
+ return {
9
+ bindTools: () => ({
10
+ invoke: () => Promise.reject(new errors_1.AiModelPortError('invoke', new Error('[dev] AI forced to always error'))),
11
+ }),
12
+ };
13
+ }
14
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
+ loadRemoteTools(_configs) {
16
+ return Promise.resolve([]);
17
+ }
18
+ closeConnections() {
19
+ return Promise.resolve();
20
+ }
21
+ }
22
+ exports.default = AlwaysErrorAiModelPort;
23
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWx3YXlzLWVycm9yLWFpLW1vZGVsLXBvcnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYWRhcHRlcnMvYWx3YXlzLWVycm9yLWFpLW1vZGVsLXBvcnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFHQSxzQ0FBNkM7QUFFN0MsMEZBQTBGO0FBQzFGLGdFQUFnRTtBQUNoRSxNQUFxQixzQkFBc0I7SUFDekMsUUFBUTtRQUNOLE9BQU87WUFDTCxTQUFTLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQztnQkFDaEIsTUFBTSxFQUFFLEdBQUcsRUFBRSxDQUNYLE9BQU8sQ0FBQyxNQUFNLENBQ1osSUFBSSx5QkFBZ0IsQ0FBQyxRQUFRLEVBQUUsSUFBSSxLQUFLLENBQUMsaUNBQWlDLENBQUMsQ0FBQyxDQUM3RTthQUNKLENBQUM7U0FDeUIsQ0FBQztJQUNoQyxDQUFDO0lBRUQsNkRBQTZEO0lBQzdELGVBQWUsQ0FBQyxRQUFvQztRQUNsRCxPQUFPLE9BQU8sQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDN0IsQ0FBQztJQUVELGdCQUFnQjtRQUNkLE9BQU8sT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQzNCLENBQUM7Q0FDRjtBQXBCRCx5Q0FvQkMifQ==
@@ -0,0 +1,7 @@
1
+ import type { Logger } from '../ports/logger-port';
2
+ export default class ConsoleLogger implements Logger {
3
+ error(message: string, context: Record<string, unknown>): void;
4
+ warn(message: string, context: Record<string, unknown>): void;
5
+ info(message: string, context: Record<string, unknown>): void;
6
+ }
7
+ //# sourceMappingURL=console-logger.d.ts.map
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class ConsoleLogger {
4
+ error(message, context) {
5
+ console.error(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
6
+ }
7
+ warn(message, context) {
8
+ console.warn(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
9
+ }
10
+ info(message, context) {
11
+ console.info(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context }));
12
+ }
13
+ }
14
+ exports.default = ConsoleLogger;
15
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc29sZS1sb2dnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYWRhcHRlcnMvY29uc29sZS1sb2dnZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFFQSxNQUFxQixhQUFhO0lBQ2hDLEtBQUssQ0FBQyxPQUFlLEVBQUUsT0FBZ0M7UUFDckQsT0FBTyxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxFQUFFLEdBQUcsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBQzlGLENBQUM7SUFFRCxJQUFJLENBQUMsT0FBZSxFQUFFLE9BQWdDO1FBQ3BELE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxFQUFFLE9BQU8sRUFBRSxTQUFTLEVBQUUsSUFBSSxJQUFJLEVBQUUsQ0FBQyxXQUFXLEVBQUUsRUFBRSxHQUFHLE9BQU8sRUFBRSxDQUFDLENBQUMsQ0FBQztJQUM3RixDQUFDO0lBRUQsSUFBSSxDQUFDLE9BQWUsRUFBRSxPQUFnQztRQUNwRCxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLElBQUksSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLEVBQUUsR0FBRyxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDN0YsQ0FBQztDQUNGO0FBWkQsZ0NBWUMifQ==
@@ -0,0 +1,25 @@
1
+ import type { Logger } from '../ports/logger-port';
2
+ import type { AvailableRunDispatch, AvailableRunsBatch, WorkflowPort } from '../ports/workflow-port';
3
+ import type { StepUser } from '../types/execution-context';
4
+ import type { CollectionSchema } from '../types/validated/collection';
5
+ import type { StepOutcome } from '../types/validated/step-outcome';
6
+ import type { ToolConfig } from '@forestadmin/ai-proxy';
7
+ export default class ForestServerWorkflowPort implements WorkflowPort {
8
+ private readonly options;
9
+ private readonly logger;
10
+ constructor(params: {
11
+ envSecret: string;
12
+ forestServerUrl: string;
13
+ logger?: Logger;
14
+ });
15
+ getAvailableRuns(): Promise<AvailableRunsBatch>;
16
+ getAvailableRun(runId: string): Promise<AvailableRunDispatch | null>;
17
+ private toDispatch;
18
+ private toMalformedInfo;
19
+ updateStepExecution(runId: string, stepOutcome: StepOutcome): Promise<AvailableRunDispatch | null>;
20
+ getCollectionSchema(collectionName: string, runId: string): Promise<CollectionSchema>;
21
+ getMcpServerConfigs(): Promise<Record<string, ToolConfig>>;
22
+ hasRunAccess(runId: string, user: StepUser): Promise<boolean>;
23
+ private callPort;
24
+ }
25
+ //# sourceMappingURL=forest-server-workflow-port.d.ts.map