@oomol-lab/open-flow 0.1.0-beta.75 → 0.1.0-beta.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/browser/{ContentIcon-FCpKiwjp.js → ContentIcon-Cm1hbVzW.js} +2 -2
- package/dist/browser/{FlowCanvas-dzRBnlUT.js → FlowCanvas-NzKtla_c.js} +62 -62
- package/dist/browser/{createResourceDialog-BeYDdUl-.js → createResourceDialog-BgdedQqu.js} +3 -3
- package/dist/browser/{dialog-ZtpsFbl4.js → dialog-aSElFFw2.js} +49 -24
- package/dist/browser/{exportSvg-DqXw1yf-.js → exportSvg-Dcz4HTo_.js} +2 -2
- package/dist/browser/{flowChanges-b_5UobFj.js → flowChanges-NZyS7HSp.js} +207 -396
- package/dist/browser/{flowWorkspace-ItojZbO4.js → flowWorkspace-Bl0SDehy.js} +90 -13
- package/dist/browser/preview.css +515 -649
- package/dist/browser/preview.js +454 -545
- package/dist/browser/theme.css +114 -262
- package/dist/browser/{tooltip-wZJRB9oP.js → tooltip-3XMK9jkr.js} +1 -1
- package/dist/browser/{typeScriptSession-CSRwWmZm.js → typeScriptSession-C9sBGOKZ.js} +1 -1
- package/dist/browser/ui.css +354 -460
- package/dist/browser/ui.js +1 -1
- package/dist/browser/workbench.css +515 -649
- package/dist/browser/workbench.js +7 -7
- package/dist/browser/{workbenchSelect-M441Lz0d.js → workbenchSelect-DBGP7Y4Z.js} +2 -2
- package/dist/command/index.js +33 -35
- package/dist/common/authoring.d.ts +3 -3
- package/dist/common/authoringSchema.d.ts +3 -0
- package/dist/common/connector-proxy.d.ts +1 -1
- package/dist/common/control-api-conformance.js +197 -136
- package/dist/common/control-api.d.ts +3 -2
- package/dist/common/control-api.js +192 -131
- package/dist/common/control-requests.js +126 -24
- package/dist/common/cron-trigger.d.ts +2 -1
- package/dist/common/decision.d.ts +4 -4
- package/dist/common/decision.js +5 -2
- package/dist/common/engine-contract.d.ts +5 -1
- package/dist/common/event-source-definition.d.ts +1 -1
- package/dist/common/event-source-view.d.ts +22 -0
- package/dist/common/event-source.d.ts +1 -1
- package/dist/common/event-source.js +24 -65
- package/dist/common/event-sources.d.ts +0 -21
- package/dist/common/flow-authoring.js +40 -18
- package/dist/common/flow-change-schema.d.ts +9 -1
- package/dist/common/flow-change.d.ts +7 -201
- package/dist/common/flow-change.js +46 -22
- package/dist/common/flow-encoding.js +40 -20
- package/dist/common/flow-input-value.d.ts +1 -1
- package/dist/common/flow-model.d.ts +121 -0
- package/dist/common/flow-notifications.d.ts +1 -1
- package/dist/common/flow-retry.d.ts +2 -4
- package/dist/common/flow-semantics.js +83 -86
- package/dist/common/flow-workflowReferences.d.ts +8 -0
- package/dist/common/flowInspection.d.ts +1 -1
- package/dist/common/http-hosts.d.ts +6 -0
- package/dist/common/http-request.d.ts +13 -5
- package/dist/common/http-request.js +23 -5
- package/dist/common/integration-trigger.d.ts +2 -1
- package/dist/common/mcp.js +195 -134
- package/dist/common/openapi.d.ts +4 -4
- package/dist/common/openapi.js +4 -1
- package/dist/common/poll-trigger.d.ts +2 -1
- package/dist/common/runtime-contract.d.ts +23 -1
- package/dist/common/runtime-contract.js +604 -72
- package/dist/common/runtime-nodejs-compat.d.ts +44 -0
- package/dist/common/runtime-nodejs-compat.js +5870 -0
- package/dist/common/scheduler-checkpoint.d.ts +1 -1
- package/dist/common/scheduler-node-execution.d.ts +1 -1
- package/dist/common/scheduler.d.ts +1 -1
- package/dist/common/scheduler.js +25 -4
- package/dist/common/trigger-catalog.d.ts +1 -7
- package/dist/common/trigger-config-options.d.ts +1 -1
- package/dist/common/trigger-config.d.ts +1 -1
- package/dist/common/trigger-contract.d.ts +2 -1
- package/dist/common/trigger-localization.d.ts +8 -2
- package/dist/common/trigger-model.d.ts +101 -0
- package/dist/common/webhook-response.d.ts +1 -1
- package/dist/common/webhook-trigger.d.ts +3 -1
- package/dist/common/workflow.d.ts +3 -12
- package/dist/common/workflow.js +25 -23
- package/dist/index.d.ts +41 -14
- package/package.json +5 -1
- package/src/base/common/json.ts +1 -1
- package/src/connector/common/proxy.ts +1 -1
- package/src/control/common/api.ts +6 -5
- package/src/control/common/authoringSchema.ts +9 -2
- package/src/control/common/authoringTools.ts +1 -1
- package/src/control/common/conformance.ts +1 -1
- package/src/control/common/connectorDecoders.ts +1 -1
- package/src/control/common/eventSources.ts +2 -29
- package/src/control/common/triggerCatalog.ts +1 -8
- package/src/decision/common/decision.ts +15 -7
- package/src/event-source/common/definition.ts +1 -1
- package/src/event-source/common/index.ts +1 -1
- package/src/event-source/common/view.ts +20 -0
- package/src/execution/common/checkpoint.ts +1 -1
- package/src/execution/common/node-execution.ts +1 -1
- package/src/execution/common/scheduler.ts +1 -1
- package/src/flow/common/change.ts +58 -192
- package/src/flow/common/changeSchema.ts +29 -4
- package/src/flow/common/connectionUsage.ts +1 -1
- package/src/flow/common/inputValue.ts +1 -1
- package/src/flow/common/model.ts +130 -0
- package/src/flow/common/modules.ts +1 -1
- package/src/flow/common/portChanges.ts +1 -1
- package/src/flow/common/retry.ts +2 -4
- package/src/flow/common/semantics.ts +11 -4
- package/src/flow/common/webhookResponse.ts +1 -1
- package/src/flow/common/workflowReferences.ts +31 -0
- package/src/http/common/authBindings.ts +6 -9
- package/src/http/common/hosts.ts +13 -0
- package/src/http/common/httpRequest.ts +30 -15
- package/src/openapi/common/openapi.ts +9 -5
- package/src/{execution → runtime}/common/engineContract.ts +20 -15
- package/src/runtime/common/nodejsCompat.ts +275 -0
- package/src/runtime/common/nodejsGuest.generated.json +47 -0
- package/src/runtime/common/runtime.ts +1138 -0
- package/src/trigger/common/config.ts +1 -1
- package/src/trigger/common/configOptions.ts +1 -1
- package/src/trigger/common/contract.ts +2 -1
- package/src/trigger/common/cron.ts +2 -1
- package/src/trigger/common/integration.ts +2 -1
- package/src/trigger/common/model.ts +87 -0
- package/src/trigger/common/poll.ts +2 -1
- package/src/trigger/common/webhook.ts +3 -1
- package/src/trigger/providers/fieldDescriptions.ts +1 -1
- package/src/trigger/providers/localization.ts +9 -2
- package/src/trigger/providers/remote.ts +1 -1
- package/src/types/index.ts +44 -15
- package/src/workflow/common/workflow.ts +3 -44
- package/src/execution/common/runtime.ts +0 -563
- /package/src/{control → connector}/common/providerIconSprite.ts +0 -0
|
@@ -1,563 +0,0 @@
|
|
|
1
|
-
import type { CodeModule, ConnectorCapability, TaskCapability, JsonValue } from '../../flow/common/change.ts'
|
|
2
|
-
|
|
3
|
-
import { dequal } from 'dequal/lite'
|
|
4
|
-
import { currentEngineContract, nodejsEngineContract } from './engineContract.ts'
|
|
5
|
-
|
|
6
|
-
export { currentEngineContract, nodejsEngineContract, findEngineContract, type EngineContract } from './engineContract.ts'
|
|
7
|
-
|
|
8
|
-
export type RuntimeModule = Pick<CodeModule, 'imports' | 'source'>
|
|
9
|
-
|
|
10
|
-
/** Snapshot a Task result before JSON transport can discard or coerce invalid values. */
|
|
11
|
-
export function snapshotRuntimeResult(value: unknown): JsonValue | undefined {
|
|
12
|
-
// This function is also serialized into the isolated runtime; keep it self-contained.
|
|
13
|
-
const parents = new Set<object>()
|
|
14
|
-
const copy = (item: unknown, depth: number): JsonValue => {
|
|
15
|
-
if (depth > 64) throw new Error('Task result exceeds the maximum JSON depth.')
|
|
16
|
-
if (item === null || typeof item == 'string' || typeof item == 'boolean') return item
|
|
17
|
-
if (typeof item == 'number' && Number.isFinite(item)) return item
|
|
18
|
-
if (
|
|
19
|
-
typeof item != 'object' ||
|
|
20
|
-
parents.has(item) ||
|
|
21
|
-
(!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) ||
|
|
22
|
-
Object.getOwnPropertySymbols(item).length > 0
|
|
23
|
-
)
|
|
24
|
-
throw new Error('Task result must contain only JSON values.')
|
|
25
|
-
parents.add(item)
|
|
26
|
-
const result = Array.isArray(item)
|
|
27
|
-
? Array.from(item, (child) => copy(child, depth + 1))
|
|
28
|
-
: Object.fromEntries(
|
|
29
|
-
Object.entries(item)
|
|
30
|
-
.filter(([, child]) => depth != 0 || child !== undefined)
|
|
31
|
-
.map(([key, child]) => [key, copy(child, depth + 1)]),
|
|
32
|
-
)
|
|
33
|
-
parents.delete(item)
|
|
34
|
-
return result
|
|
35
|
-
}
|
|
36
|
-
return value === undefined ? undefined : copy(value, 0)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const nodejsRuntimeConformanceCases: readonly RuntimeConformanceCase[] = [
|
|
40
|
-
{
|
|
41
|
-
name: 'supports Node builtin imports and asynchronous virtual file operations',
|
|
42
|
-
async verify(harness) {
|
|
43
|
-
const value = await harness.invoke({
|
|
44
|
-
capability: async () => ({ body: null, status: 403 }),
|
|
45
|
-
input: null,
|
|
46
|
-
invocationId: 'nodejs-files',
|
|
47
|
-
program: {
|
|
48
|
-
...program(
|
|
49
|
-
harness,
|
|
50
|
-
`import assert from 'node:assert/strict';
|
|
51
|
-
import { Buffer } from 'node:buffer'; import path from 'node:path'; import fs from 'node:fs/promises';
|
|
52
|
-
export default async () => {
|
|
53
|
-
const file = path.join('/tmp', 'conformance.txt');
|
|
54
|
-
await fs.writeFile(file, Buffer.from('你好'));
|
|
55
|
-
const text = await fs.readFile(file, 'utf8');
|
|
56
|
-
assert.equal(text, '你好');
|
|
57
|
-
await fs.unlink(file);
|
|
58
|
-
return { text, bytes: Buffer.byteLength(text) };
|
|
59
|
-
}`,
|
|
60
|
-
),
|
|
61
|
-
engineContract: nodejsEngineContract,
|
|
62
|
-
},
|
|
63
|
-
})
|
|
64
|
-
equal(value, { text: '你好', bytes: 6 }, 'Node virtual files')
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: 'preserves abort reasons and cancels Promise timers',
|
|
69
|
-
async verify(harness) {
|
|
70
|
-
const value = await harness.invoke({
|
|
71
|
-
capability: async () => ({ body: null, status: 403 }),
|
|
72
|
-
input: null,
|
|
73
|
-
invocationId: 'nodejs-abort',
|
|
74
|
-
program: {
|
|
75
|
-
...program(
|
|
76
|
-
harness,
|
|
77
|
-
`import { setTimeout } from 'node:timers/promises';
|
|
78
|
-
export default async () => {
|
|
79
|
-
const controller = new AbortController(); const reason = { message: 'stop' };
|
|
80
|
-
const combined = AbortSignal.any([controller.signal]);
|
|
81
|
-
const pending = setTimeout(60000, 'late', { signal: combined });
|
|
82
|
-
controller.abort(reason); controller.abort('ignored');
|
|
83
|
-
let thrown; try { combined.throwIfAborted(); } catch (error) { thrown = error; }
|
|
84
|
-
try { await pending; return false; } catch (error) {
|
|
85
|
-
return { sameReason: combined.reason === reason && thrown === reason, name: error.name };
|
|
86
|
-
}
|
|
87
|
-
}`,
|
|
88
|
-
),
|
|
89
|
-
engineContract: nodejsEngineContract,
|
|
90
|
-
},
|
|
91
|
-
})
|
|
92
|
-
equal(value, { sameReason: true, name: 'AbortError' }, 'Node cancellation')
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
export interface RuntimeProgram {
|
|
98
|
-
readonly engineContract: string
|
|
99
|
-
readonly engineDigest: string
|
|
100
|
-
readonly entryModuleId: string
|
|
101
|
-
readonly modules: Readonly<Record<string, RuntimeModule>>
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface RuntimeCapabilityCall {
|
|
105
|
-
readonly nodeId?: string
|
|
106
|
-
readonly jobId?: string
|
|
107
|
-
readonly callId: string
|
|
108
|
-
readonly invocationId: string
|
|
109
|
-
readonly kind: string
|
|
110
|
-
readonly payload: JsonValue
|
|
111
|
-
readonly signal: AbortSignal
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface RuntimeCapabilityResponse {
|
|
115
|
-
readonly body: JsonValue
|
|
116
|
-
readonly status: number
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface LlmTaskInvocation {
|
|
120
|
-
readonly input: Readonly<Record<string, JsonValue>>
|
|
121
|
-
readonly invocationId: string
|
|
122
|
-
readonly mode: 'chat' | 'json'
|
|
123
|
-
readonly signal: AbortSignal
|
|
124
|
-
readonly version: 1
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export type LlmTaskResult =
|
|
128
|
-
| { readonly kind: 'completed'; readonly value: JsonValue; readonly version: 1 }
|
|
129
|
-
| {
|
|
130
|
-
readonly retryable?: boolean
|
|
131
|
-
readonly code: 'llm.output-invalid' | 'llm.unavailable'
|
|
132
|
-
readonly kind: 'failed'
|
|
133
|
-
readonly message: string
|
|
134
|
-
readonly version: 1
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export type InvokeLlmTask = (invocation: LlmTaskInvocation) => Promise<LlmTaskResult>
|
|
138
|
-
|
|
139
|
-
export interface AgentCheckpoint {
|
|
140
|
-
readonly state: JsonValue
|
|
141
|
-
readonly callId: string
|
|
142
|
-
readonly toolId: string
|
|
143
|
-
readonly input: Readonly<Record<string, JsonValue>>
|
|
144
|
-
readonly rounds: number
|
|
145
|
-
readonly version: 1
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export type AgentResult = { readonly kind: 'completed'; readonly output: JsonValue } | { readonly kind: 'suspended'; readonly checkpoint: AgentCheckpoint }
|
|
149
|
-
|
|
150
|
-
export interface AgentInvocation {
|
|
151
|
-
readonly input: Readonly<Record<string, JsonValue>>
|
|
152
|
-
readonly invocationId: string
|
|
153
|
-
readonly signal: AbortSignal
|
|
154
|
-
readonly resume?: { readonly action: 'approve' | 'reject'; readonly checkpoint: AgentCheckpoint }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export interface RuntimeInvocation {
|
|
158
|
-
readonly capabilities?: readonly TaskCapability[]
|
|
159
|
-
readonly capability: (call: RuntimeCapabilityCall) => Promise<RuntimeCapabilityResponse>
|
|
160
|
-
readonly input: JsonValue
|
|
161
|
-
readonly invocationId: string
|
|
162
|
-
readonly program: RuntimeProgram
|
|
163
|
-
readonly signal?: AbortSignal
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export interface RuntimeHarness {
|
|
167
|
-
readonly engineDigest: string
|
|
168
|
-
invoke(invocation: RuntimeInvocation): Promise<JsonValue | undefined>
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export interface RuntimeConformanceCase {
|
|
172
|
-
readonly name: string
|
|
173
|
-
verify(harness: RuntimeHarness): Promise<void>
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function program(harness: RuntimeHarness, source: string, modules: Readonly<Record<string, RuntimeModule>> = {}): RuntimeProgram {
|
|
177
|
-
return {
|
|
178
|
-
engineContract: currentEngineContract,
|
|
179
|
-
engineDigest: harness.engineDigest,
|
|
180
|
-
entryModuleId: 'main',
|
|
181
|
-
modules: {
|
|
182
|
-
...modules,
|
|
183
|
-
main: { imports: Object.keys(modules), source },
|
|
184
|
-
},
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function equal(actual: unknown, expected: unknown, message: string): void {
|
|
189
|
-
if (!dequal(actual, expected)) {
|
|
190
|
-
throw new Error(`${message}: expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}.`)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function rejects(operation: Promise<unknown>, pattern: RegExp, message: string): Promise<void> {
|
|
195
|
-
try {
|
|
196
|
-
await operation
|
|
197
|
-
} catch (error) {
|
|
198
|
-
if (pattern.test(error instanceof Error ? error.message : String(error))) return
|
|
199
|
-
throw new Error(`${message}: received ${error instanceof Error ? error.message : String(error)}.`, { cause: error })
|
|
200
|
-
}
|
|
201
|
-
throw new Error(`${message}: operation unexpectedly succeeded.`)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export const runtimeConformanceCases: readonly RuntimeConformanceCase[] = [
|
|
205
|
-
{
|
|
206
|
-
name: 'rejects Task results that JSON transport would discard or coerce',
|
|
207
|
-
async verify(harness) {
|
|
208
|
-
for (const expression of [
|
|
209
|
-
'{ value: () => 42 }',
|
|
210
|
-
'{ value: Symbol("value") }',
|
|
211
|
-
'{ value: NaN }',
|
|
212
|
-
'{ value: Infinity }',
|
|
213
|
-
'{ value: 1n }',
|
|
214
|
-
'{ value: { nested: undefined } }',
|
|
215
|
-
'{ value: [undefined] }',
|
|
216
|
-
'{ value: Array(1) }',
|
|
217
|
-
'{ value: new Date() }',
|
|
218
|
-
'{ value: { toJSON() { return "hidden" } } }',
|
|
219
|
-
'{ [Symbol("key")]: 1 }',
|
|
220
|
-
'(() => { const value = {}; value.self = value; return { value } })()',
|
|
221
|
-
]) {
|
|
222
|
-
await rejects(
|
|
223
|
-
harness.invoke({
|
|
224
|
-
capability: async () => ({ body: null, status: 200 }),
|
|
225
|
-
input: null,
|
|
226
|
-
invocationId: 'invalid-task-result',
|
|
227
|
-
program: program(harness, `export default () => (${expression})`),
|
|
228
|
-
}),
|
|
229
|
-
/Task result must contain only JSON values/,
|
|
230
|
-
`Invalid Task result: ${expression}`,
|
|
231
|
-
)
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
name: 'snapshots JSON results while preserving missing ports and shared values',
|
|
237
|
-
async verify(harness) {
|
|
238
|
-
const value = await harness.invoke({
|
|
239
|
-
capability: async () => ({ body: null, status: 200 }),
|
|
240
|
-
input: null,
|
|
241
|
-
invocationId: 'json-task-result',
|
|
242
|
-
program: program(
|
|
243
|
-
harness,
|
|
244
|
-
`export default () => {
|
|
245
|
-
const shared = { text: 'hello', values: [1, true, null] };
|
|
246
|
-
return { missing: undefined, nullable: null, first: shared, second: shared };
|
|
247
|
-
}`,
|
|
248
|
-
),
|
|
249
|
-
})
|
|
250
|
-
const shared = { text: 'hello', values: [1, true, null] }
|
|
251
|
-
equal(value, { nullable: null, first: shared, second: shared }, 'JSON Task result')
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
name: 'preserves Action error codes and fixed per-call Connection selection',
|
|
256
|
-
async verify(harness) {
|
|
257
|
-
const declarations: readonly ConnectorCapability[] = [
|
|
258
|
-
{
|
|
259
|
-
kind: 'connector',
|
|
260
|
-
action: 'example.echo',
|
|
261
|
-
connections: [{ connectionId: 'work', alias: 'office' }, { connectionId: 'home' }],
|
|
262
|
-
},
|
|
263
|
-
]
|
|
264
|
-
const result = await harness.invoke({
|
|
265
|
-
capabilities: declarations,
|
|
266
|
-
capability: async ({ payload }) => {
|
|
267
|
-
const selected = resolveAction(declarations, payload)
|
|
268
|
-
if (selected.input.fail) throw Object.assign(new Error('Provider unavailable.'), { code: 'connector.unavailable' })
|
|
269
|
-
return { body: selected.connectionId ?? null, status: 200 }
|
|
270
|
-
},
|
|
271
|
-
input: null,
|
|
272
|
-
invocationId: 'action-accounts',
|
|
273
|
-
program: program(
|
|
274
|
-
harness,
|
|
275
|
-
`export default async (_, context) => {
|
|
276
|
-
const values = await Promise.all([
|
|
277
|
-
context.actions.example.echo({}, { connectionAlias: 'office' }),
|
|
278
|
-
context.actions['example.echo']({}, { connectionId: 'home' }),
|
|
279
|
-
])
|
|
280
|
-
try { await context.actions.example.echo({ fail: true }, { connectionId: 'work' }) }
|
|
281
|
-
catch (error) { values.push(error.code) }
|
|
282
|
-
return values
|
|
283
|
-
}`,
|
|
284
|
-
),
|
|
285
|
-
})
|
|
286
|
-
equal(result, ['work', 'home', 'connector.unavailable'], 'Action selection and recovery')
|
|
287
|
-
},
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: 'shares Action entries and assigns independent concurrent call identities',
|
|
291
|
-
async verify(harness) {
|
|
292
|
-
const calls: RuntimeCapabilityCall[] = []
|
|
293
|
-
const value = await harness.invoke({
|
|
294
|
-
capabilities: [{ kind: 'connector', action: 'example.echo', connections: [] }],
|
|
295
|
-
capability: async (call) => {
|
|
296
|
-
calls.push(call)
|
|
297
|
-
return { body: call.payload, status: 200 }
|
|
298
|
-
},
|
|
299
|
-
input: null,
|
|
300
|
-
invocationId: 'action-calls',
|
|
301
|
-
program: program(
|
|
302
|
-
harness,
|
|
303
|
-
`export default async (_input, context) => {
|
|
304
|
-
const call = context.actions.example.echo
|
|
305
|
-
return { same: call === context.actions['example.echo'], values: await Promise.all([call({ value: 1 }), call({ value: 2 }), call({ value: 3 })]) }
|
|
306
|
-
}`,
|
|
307
|
-
),
|
|
308
|
-
})
|
|
309
|
-
equal(value, { same: true, values: [1, 2, 3].map((item) => ({ action: 'example.echo', input: { value: item } })) }, 'Action results')
|
|
310
|
-
equal(new Set(calls.map((call) => call.callId)).size, 3, 'Independent Action identities')
|
|
311
|
-
equal(
|
|
312
|
-
calls.map((call) => call.invocationId),
|
|
313
|
-
['action-calls', 'action-calls', 'action-calls'],
|
|
314
|
-
'Task identity',
|
|
315
|
-
)
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
{
|
|
320
|
-
name: 'loads the fixed ESM closure and platform module',
|
|
321
|
-
async verify(harness) {
|
|
322
|
-
const value = await harness.invoke({
|
|
323
|
-
capability: async () => {
|
|
324
|
-
throw new Error('Capability must not be called.')
|
|
325
|
-
},
|
|
326
|
-
input: { value: 6 },
|
|
327
|
-
invocationId: 'module-closure',
|
|
328
|
-
program: program(
|
|
329
|
-
harness,
|
|
330
|
-
`import { double } from './helper.mjs'
|
|
331
|
-
import { engineContract, identity } from 'open-flow:platform'
|
|
332
|
-
export default (input) => ({ engineContract, value: identity(double(input.value)) })`,
|
|
333
|
-
{ helper: { imports: [], source: 'export const double = (value) => value * 2' } },
|
|
334
|
-
),
|
|
335
|
-
})
|
|
336
|
-
equal(value, { engineContract: currentEngineContract, value: 12 }, 'ESM closure result')
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
name: 'rejects modules outside the fixed closure',
|
|
341
|
-
async verify(harness) {
|
|
342
|
-
await rejects(
|
|
343
|
-
harness.invoke({
|
|
344
|
-
capability: async () => ({ body: null, status: 200 }),
|
|
345
|
-
input: null,
|
|
346
|
-
invocationId: 'closed-linker',
|
|
347
|
-
program: program(
|
|
348
|
-
harness,
|
|
349
|
-
`import fs from 'node:fs'
|
|
350
|
-
export default () => fs.readFileSync('/etc/passwd', 'utf8')`,
|
|
351
|
-
),
|
|
352
|
-
}),
|
|
353
|
-
/invalid|unsupported|cannot import|not part of/i,
|
|
354
|
-
'Closed linker',
|
|
355
|
-
)
|
|
356
|
-
},
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
name: 'copies asynchronous Capability requests and responses',
|
|
360
|
-
async verify(harness) {
|
|
361
|
-
const calls: RuntimeCapabilityCall[] = []
|
|
362
|
-
const value = await harness.invoke({
|
|
363
|
-
capability: async (call) => {
|
|
364
|
-
calls.push(call)
|
|
365
|
-
return { body: { accepted: true, echoed: call.payload }, status: 200 }
|
|
366
|
-
},
|
|
367
|
-
input: { issue: 42 },
|
|
368
|
-
invocationId: 'async-capability',
|
|
369
|
-
program: program(harness, `export default async (input, capability) => capability.artifact.put({ action: 'read', input })`),
|
|
370
|
-
})
|
|
371
|
-
equal(value, { body: { accepted: true, echoed: { action: 'read', input: { issue: 42 } } }, status: 200 }, 'Capability result')
|
|
372
|
-
equal(
|
|
373
|
-
calls.map(({ invocationId, kind, payload }) => ({ invocationId, kind, payload })),
|
|
374
|
-
[{ invocationId: 'async-capability', kind: 'artifact.put', payload: { action: 'read', input: { issue: 42 } } }],
|
|
375
|
-
'Capability request',
|
|
376
|
-
)
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
name: 'returns final outputs without exposing intermediate output capability',
|
|
381
|
-
async verify(harness) {
|
|
382
|
-
const value = await harness.invoke({
|
|
383
|
-
capability: async () => {
|
|
384
|
-
throw new Error('Capability must not be called.')
|
|
385
|
-
},
|
|
386
|
-
input: null,
|
|
387
|
-
invocationId: 'final-outputs',
|
|
388
|
-
program: program(
|
|
389
|
-
harness,
|
|
390
|
-
`export default async (_input, context) => ({ available: typeof context.outputs, previous: await context.getPrevious(), value: 1 })`,
|
|
391
|
-
),
|
|
392
|
-
})
|
|
393
|
-
equal(value, { available: 'undefined', previous: [], value: 1 }, 'Final Task outputs')
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
name: 'cancels a pending Capability and the user invocation',
|
|
398
|
-
async verify(harness) {
|
|
399
|
-
const cancellation = new AbortController()
|
|
400
|
-
let capabilityAborted = false
|
|
401
|
-
let capabilityStarted!: () => void
|
|
402
|
-
const started = new Promise<void>((resolve) => {
|
|
403
|
-
capabilityStarted = resolve
|
|
404
|
-
})
|
|
405
|
-
const invoked = harness.invoke({
|
|
406
|
-
capabilities: [{ kind: 'connector', action: 'example.wait', connections: [] }],
|
|
407
|
-
capability: async ({ signal }) => {
|
|
408
|
-
capabilityStarted()
|
|
409
|
-
return await new Promise<RuntimeCapabilityResponse>((_resolve, reject) =>
|
|
410
|
-
signal.addEventListener(
|
|
411
|
-
'abort',
|
|
412
|
-
() => {
|
|
413
|
-
capabilityAborted = true
|
|
414
|
-
reject(signal.reason)
|
|
415
|
-
},
|
|
416
|
-
{ once: true },
|
|
417
|
-
),
|
|
418
|
-
)
|
|
419
|
-
},
|
|
420
|
-
input: null,
|
|
421
|
-
invocationId: 'canceled-capability',
|
|
422
|
-
program: program(harness, `export default async (_input, context) => context.actions.example.wait({})`),
|
|
423
|
-
signal: cancellation.signal,
|
|
424
|
-
})
|
|
425
|
-
await started
|
|
426
|
-
cancellation.abort(new Error('Invocation canceled.'))
|
|
427
|
-
await rejects(invoked, /cancel|abort|disposed/i, 'Canceled invocation')
|
|
428
|
-
equal(capabilityAborted, true, 'Capability cancellation')
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
]
|
|
432
|
-
|
|
433
|
-
export function createActions(invoke: (payload: JsonValue) => Promise<RuntimeCapabilityResponse>): Readonly<Record<string, unknown>> {
|
|
434
|
-
// Keep validation inside the function serialized into the isolated runtime.
|
|
435
|
-
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
436
|
-
const json = (value: unknown, parents: Set<object>): void => {
|
|
437
|
-
if (value === null || typeof value == 'string' || typeof value == 'boolean' || (typeof value == 'number' && Number.isFinite(value))) return
|
|
438
|
-
if (
|
|
439
|
-
typeof value != 'object' ||
|
|
440
|
-
parents.has(value) ||
|
|
441
|
-
(!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) ||
|
|
442
|
-
Object.getOwnPropertySymbols(value).length > 0
|
|
443
|
-
)
|
|
444
|
-
throw Object.assign(new Error('Action parameters must contain only JSON values.'), { code: 'capability.invalid' })
|
|
445
|
-
parents.add(value)
|
|
446
|
-
for (const child of Array.isArray(value) ? value : Object.values(value)) json(child, parents)
|
|
447
|
-
parents.delete(value)
|
|
448
|
-
}
|
|
449
|
-
const target: Record<string, unknown> = Object.create(null)
|
|
450
|
-
const calls = new Map<string, (input?: JsonValue, options?: JsonValue) => Promise<JsonValue>>()
|
|
451
|
-
const providers = new Map<string, Readonly<Record<string, unknown>>>()
|
|
452
|
-
const actionCall = (action: string): ((input?: JsonValue, options?: JsonValue) => Promise<JsonValue>) => {
|
|
453
|
-
let call = calls.get(action)
|
|
454
|
-
if (call != null) return call
|
|
455
|
-
call = async (input: JsonValue = {}, options?: JsonValue): Promise<JsonValue> => {
|
|
456
|
-
json(input, new Set())
|
|
457
|
-
if (options !== undefined) json(options, new Set())
|
|
458
|
-
const response = await invoke({ action, input, ...(options === undefined ? {} : { options }) })
|
|
459
|
-
return response.body
|
|
460
|
-
}
|
|
461
|
-
calls.set(action, call)
|
|
462
|
-
return call
|
|
463
|
-
}
|
|
464
|
-
target.call = async (action: JsonValue, input: JsonValue = {}, options?: JsonValue): Promise<JsonValue> => {
|
|
465
|
-
if (typeof action != 'string') throw Object.assign(new Error('Action ID must be a string.'), { code: 'capability.invalid' })
|
|
466
|
-
return actionCall(action)(input, options)
|
|
467
|
-
}
|
|
468
|
-
Object.freeze(target)
|
|
469
|
-
return new Proxy(target, {
|
|
470
|
-
get(object, property) {
|
|
471
|
-
if (typeof property != 'string') return Reflect.get(object, property)
|
|
472
|
-
if (Object.hasOwn(object, property)) return object[property]
|
|
473
|
-
if (property.includes('.')) return actionCall(property)
|
|
474
|
-
let methods = providers.get(property)
|
|
475
|
-
if (methods == null) {
|
|
476
|
-
const provider = property
|
|
477
|
-
const providerTarget = Object.freeze(Object.create(null) as Record<string, unknown>)
|
|
478
|
-
methods = new Proxy(providerTarget, {
|
|
479
|
-
get(_object, name) {
|
|
480
|
-
return typeof name == 'string' ? actionCall(`${provider}.${name}`) : undefined
|
|
481
|
-
},
|
|
482
|
-
})
|
|
483
|
-
providers.set(property, methods)
|
|
484
|
-
}
|
|
485
|
-
return methods
|
|
486
|
-
},
|
|
487
|
-
})
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
export function resolveAction(
|
|
491
|
-
declarations: readonly ConnectorCapability[],
|
|
492
|
-
payload: unknown,
|
|
493
|
-
): { readonly action: string; readonly connectionId?: string; readonly input: Readonly<Record<string, JsonValue>> } {
|
|
494
|
-
const invalid = Object.assign(new Error('The Action request is invalid.'), { code: 'capability.invalid' })
|
|
495
|
-
const denied = Object.assign(new Error('The Action or Connection is unavailable for this Code node.'), { code: 'capability.denied' })
|
|
496
|
-
if (payload == null || typeof payload != 'object' || Array.isArray(payload)) throw invalid
|
|
497
|
-
const source = payload as Record<string, JsonValue>
|
|
498
|
-
if (
|
|
499
|
-
Object.keys(source).some((key) => !['action', 'input', 'options'].includes(key)) ||
|
|
500
|
-
typeof source.action != 'string' ||
|
|
501
|
-
!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_.-]+$/.test(source.action) ||
|
|
502
|
-
source.input == null ||
|
|
503
|
-
typeof source.input != 'object' ||
|
|
504
|
-
Array.isArray(source.input)
|
|
505
|
-
)
|
|
506
|
-
throw invalid
|
|
507
|
-
const permissions = declarations.find((declaration) => 'mode' in declaration)
|
|
508
|
-
const allowed = permissions?.mode == 'independent' ? permissions.actions.find((entry) => entry.action == source.action) : undefined
|
|
509
|
-
if (permissions?.mode == 'independent' && allowed == null) throw denied
|
|
510
|
-
const hints = declarations.flatMap((declaration) => {
|
|
511
|
-
if ('mode' in declaration) return []
|
|
512
|
-
if ('action' in declaration) {
|
|
513
|
-
if (declaration.action != source.action) return []
|
|
514
|
-
return [
|
|
515
|
-
...declaration.connections.flatMap((connection) =>
|
|
516
|
-
connection.alias == null ? [] : [{ action: declaration.action, connectionId: connection.connectionId, alias: connection.alias }],
|
|
517
|
-
),
|
|
518
|
-
...(declaration.connectionId == null ? [] : [{ action: declaration.action, connectionId: declaration.connectionId }]),
|
|
519
|
-
]
|
|
520
|
-
}
|
|
521
|
-
return declaration.connectionHints?.filter((hint) => hint.action == source.action) ?? []
|
|
522
|
-
})
|
|
523
|
-
let connectionId = hints.find((hint) => hint.alias == null)?.connectionId
|
|
524
|
-
if (Object.hasOwn(source, 'options')) {
|
|
525
|
-
if (source.options == null || typeof source.options != 'object' || Array.isArray(source.options)) throw invalid
|
|
526
|
-
const options = source.options as Record<string, JsonValue>
|
|
527
|
-
const keys = Object.keys(options)
|
|
528
|
-
if (keys.length != 1) throw invalid
|
|
529
|
-
if (typeof options.connectionId == 'string' && options.connectionId.length > 0 && keys[0] == 'connectionId') {
|
|
530
|
-
connectionId = options.connectionId
|
|
531
|
-
} else if (typeof options.connectionAlias == 'string' && options.connectionAlias.length > 0 && keys[0] == 'connectionAlias') {
|
|
532
|
-
connectionId = hints.find((item) => item.alias == options.connectionAlias)?.connectionId
|
|
533
|
-
if (connectionId == null) throw denied
|
|
534
|
-
} else throw invalid
|
|
535
|
-
}
|
|
536
|
-
if (permissions?.mode == 'independent') {
|
|
537
|
-
if (connectionId != null && connectionId != allowed?.connectionId) throw denied
|
|
538
|
-
connectionId = allowed?.connectionId
|
|
539
|
-
}
|
|
540
|
-
if (permissions?.mode == 'shared' && Object.hasOwn(source, 'options') && connectionId == null) throw denied
|
|
541
|
-
return { action: source.action, input: source.input as Readonly<Record<string, JsonValue>>, ...(connectionId == null ? {} : { connectionId }) }
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export interface DecisionTaskInvocation {
|
|
545
|
-
readonly questions: readonly import('../../decision/common/decision.ts').DecisionQuestion[]
|
|
546
|
-
readonly state: JsonValue
|
|
547
|
-
readonly invocationId: string
|
|
548
|
-
readonly signal: AbortSignal
|
|
549
|
-
readonly version: 1
|
|
550
|
-
}
|
|
551
|
-
export type InvokeDecisionTask = (invocation: DecisionTaskInvocation) => Promise<LlmTaskResult>
|
|
552
|
-
|
|
553
|
-
/** Serialized into the sandbox alongside the strict JSON snapshot helper. */
|
|
554
|
-
export function createWorkflows(invoke: (payload: JsonValue) => Promise<RuntimeCapabilityResponse>) {
|
|
555
|
-
return Object.freeze({
|
|
556
|
-
async call(name: string, input: Readonly<Record<string, JsonValue>> = {}): Promise<JsonValue> {
|
|
557
|
-
if (typeof name != 'string' || input == null || typeof input != 'object' || Array.isArray(input))
|
|
558
|
-
throw Object.assign(new Error('Workflow name and input object are required.'), { code: 'capability.invalid' })
|
|
559
|
-
const value = snapshotRuntimeResult(input)!
|
|
560
|
-
return (await invoke({ name, input: value })).body
|
|
561
|
-
},
|
|
562
|
-
})
|
|
563
|
-
}
|
|
File without changes
|