@pikku/core 0.12.63 → 0.12.64
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/CHANGELOG.md +147 -0
- package/dist/dev/hot-reload.js +13 -55
- package/dist/dev/module-runner.d.ts +14 -0
- package/dist/dev/module-runner.js +59 -0
- package/dist/dev/reload-meta.d.ts +13 -0
- package/dist/dev/reload-meta.js +22 -0
- package/dist/errors/errors.d.ts +12 -0
- package/dist/errors/errors.js +19 -0
- package/dist/function/function-runner.d.ts +5 -5
- package/dist/function/function-runner.js +39 -111
- package/dist/function/functions.types.d.ts +17 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/middleware/auth-apikey.d.ts +1 -0
- package/dist/middleware/auth-bearer.d.ts +1 -0
- package/dist/middleware/auth-cookie.d.ts +1 -0
- package/dist/middleware/cors.d.ts +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.d.ts +1 -0
- package/dist/middleware/telemetry.d.ts +2 -0
- package/dist/middleware/timeout.d.ts +1 -0
- package/dist/permissions.d.ts +32 -25
- package/dist/permissions.js +104 -185
- package/dist/pikku-state.js +0 -2
- package/dist/scopes.d.ts +17 -0
- package/dist/scopes.js +59 -0
- package/dist/services/ai-embedding-service.d.ts +31 -0
- package/dist/services/ai-embedding-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +3 -1
- package/dist/services/credential-wire-service.js +10 -4
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +2 -0
- package/dist/services/meta-service.d.ts +15 -2
- package/dist/services/meta-service.js +51 -1
- package/dist/services/queue-webhook-service.d.ts +55 -0
- package/dist/services/queue-webhook-service.js +136 -0
- package/dist/services/scope-service.d.ts +67 -0
- package/dist/services/scope-service.js +1 -0
- package/dist/services/typed-secret-service.d.ts +8 -0
- package/dist/services/typed-secret-service.js +39 -4
- package/dist/services/webhook-service.d.ts +140 -0
- package/dist/services/webhook-service.js +44 -0
- package/dist/services/workflow-service.d.ts +7 -0
- package/dist/types/core.types.d.ts +34 -6
- package/dist/types/state.types.d.ts +20 -2
- package/dist/utils/hmac.d.ts +16 -0
- package/dist/utils/hmac.js +26 -0
- package/dist/utils/safe-fetch.d.ts +51 -0
- package/dist/utils/safe-fetch.js +192 -0
- package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +41 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +146 -6
- package/dist/wirings/ai-agent/ai-agent-runner.js +6 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +61 -10
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +27 -1
- package/dist/wirings/ai-agent/index.d.ts +1 -0
- package/dist/wirings/ai-agent/index.js +1 -0
- package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
- package/dist/wirings/ai-agent/voice-input.js +2 -38
- package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
- package/dist/wirings/channel/channel-common.js +0 -1
- package/dist/wirings/channel/channel-handler.js +1 -4
- package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
- package/dist/wirings/channel/channel-middleware-runner.js +34 -19
- package/dist/wirings/channel/channel.types.d.ts +2 -6
- package/dist/wirings/cli/cli-runner.js +0 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -2
- package/dist/wirings/http/http-routes.js +0 -3
- package/dist/wirings/http/http-runner.d.ts +9 -29
- package/dist/wirings/http/http-runner.js +9 -34
- package/dist/wirings/http/http.types.d.ts +1 -9
- package/dist/wirings/mcp/mcp-runner.js +0 -2
- package/dist/wirings/mcp/mcp.types.d.ts +5 -11
- package/dist/wirings/oauth2/index.d.ts +0 -3
- package/dist/wirings/oauth2/index.js +1 -2
- package/dist/wirings/rpc/addon-runner.d.ts +28 -0
- package/dist/wirings/rpc/addon-runner.js +173 -0
- package/dist/wirings/rpc/index.d.ts +5 -1
- package/dist/wirings/rpc/index.js +3 -1
- package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
- package/dist/wirings/rpc/remote-addon-auth.js +43 -0
- package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
- package/dist/wirings/rpc/rpc-runner.js +109 -3
- package/dist/wirings/rpc/rpc-types.d.ts +15 -1
- package/dist/wirings/rpc/wire-addon.js +9 -0
- package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
- package/dist/wirings/rpc/wire-remote-addon.js +19 -0
- package/dist/wirings/scope/index.d.ts +3 -0
- package/dist/wirings/scope/index.js +2 -0
- package/dist/wirings/scope/scope.types.d.ts +39 -0
- package/dist/wirings/scope/scope.types.js +1 -0
- package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
- package/dist/wirings/scope/validate-scope-definitions.js +76 -0
- package/dist/wirings/scope/wire-scope.d.ts +33 -0
- package/dist/wirings/scope/wire-scope.js +32 -0
- package/dist/wirings/workflow/dsl/index.d.ts +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +53 -1
- package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
- package/dist/wirings/workflow/graph/graph-runner.js +3 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
- package/dist/wirings/workflow/index.d.ts +2 -2
- package/dist/wirings/workflow/index.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +51 -1
- package/dist/wirings/workflow/pikku-workflow-service.js +171 -0
- package/dist/wirings/workflow/workflow.types.d.ts +1 -1
- package/package.json +5 -1
- package/run-tests.sh +1 -0
- package/src/dev/hot-reload.ts +13 -68
- package/src/dev/module-runner.test.ts +169 -0
- package/src/dev/module-runner.ts +103 -0
- package/src/dev/reload-meta.test.ts +31 -2
- package/src/dev/reload-meta.ts +26 -0
- package/src/errors/errors.ts +24 -0
- package/src/function/function-runner.test.ts +276 -142
- package/src/function/function-runner.ts +52 -145
- package/src/function/functions.types.ts +28 -38
- package/src/index.ts +8 -6
- package/src/middleware/index.ts +1 -5
- package/src/permissions.test.ts +153 -372
- package/src/permissions.ts +139 -259
- package/src/pikku-state.ts +0 -2
- package/src/scopes.test.ts +167 -0
- package/src/scopes.ts +69 -0
- package/src/services/ai-embedding-service.ts +31 -0
- package/src/services/credential-wire-service.ts +10 -4
- package/src/services/index.ts +20 -0
- package/src/services/meta-service.ts +68 -3
- package/src/services/queue-webhook-service.test.ts +408 -0
- package/src/services/queue-webhook-service.ts +182 -0
- package/src/services/scope-service.ts +75 -0
- package/src/services/typed-secret-service.test.ts +35 -0
- package/src/services/typed-secret-service.ts +39 -4
- package/src/services/webhook-service.ts +180 -0
- package/src/services/workflow-service.ts +7 -0
- package/src/types/core.types.ts +42 -14
- package/src/types/state.types.ts +23 -3
- package/src/utils/hmac.ts +27 -0
- package/src/utils/safe-fetch.test.ts +373 -0
- package/src/utils/safe-fetch.ts +213 -0
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +308 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +192 -8
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +21 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +198 -10
- package/src/wirings/ai-agent/ai-agent-stream.ts +84 -10
- package/src/wirings/ai-agent/ai-agent.types.ts +28 -0
- package/src/wirings/ai-agent/index.ts +1 -0
- package/src/wirings/ai-agent/voice-input.ts +6 -35
- package/src/wirings/channel/channel-common.ts +0 -1
- package/src/wirings/channel/channel-handler.ts +1 -9
- package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
- package/src/wirings/channel/channel-middleware-runner.ts +39 -27
- package/src/wirings/channel/channel.types.ts +0 -6
- package/src/wirings/cli/cli-runner.ts +0 -2
- package/src/wirings/cli/cli.types.ts +0 -2
- package/src/wirings/http/http-routes.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +1 -11
- package/src/wirings/http/http-runner.ts +16 -39
- package/src/wirings/http/http.types.ts +1 -10
- package/src/wirings/mcp/mcp-runner.ts +0 -2
- package/src/wirings/mcp/mcp.types.ts +3 -11
- package/src/wirings/oauth2/index.ts +0 -3
- package/src/wirings/rpc/addon-runner.ts +233 -0
- package/src/wirings/rpc/index.ts +17 -1
- package/src/wirings/rpc/remote-addon-auth.ts +69 -0
- package/src/wirings/rpc/rpc-runner.test.ts +319 -16
- package/src/wirings/rpc/rpc-runner.ts +155 -3
- package/src/wirings/rpc/rpc-types.ts +16 -5
- package/src/wirings/rpc/wire-addon.test.ts +6 -3
- package/src/wirings/rpc/wire-addon.ts +9 -0
- package/src/wirings/rpc/wire-remote-addon.ts +57 -0
- package/src/wirings/scope/index.ts +14 -0
- package/src/wirings/scope/scope.test.ts +135 -0
- package/src/wirings/scope/scope.types.ts +44 -0
- package/src/wirings/scope/validate-scope-definitions.ts +110 -0
- package/src/wirings/scope/wire-scope.ts +34 -0
- package/src/wirings/workflow/dsl/index.ts +4 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +61 -0
- package/src/wirings/workflow/graph/graph-node.ts +2 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
- package/src/wirings/workflow/graph/graph-runner.ts +3 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
- package/src/wirings/workflow/index.ts +5 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +236 -0
- package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
- package/src/wirings/workflow/workflow.types.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
- package/src/wirings/oauth2/oauth2-client.ts +0 -331
- package/src/wirings/oauth2/oauth2-routes.ts +0 -234
|
@@ -48,9 +48,11 @@ import type { PikkuWire, SerializedError } from '../../types/core.types.js'
|
|
|
48
48
|
import type { QueueService } from '../queue/queue.types.js'
|
|
49
49
|
import { runScheduledTask } from '../scheduler/scheduler-runner.js'
|
|
50
50
|
import type {
|
|
51
|
+
ApprovalOutcome,
|
|
51
52
|
PikkuScenarioWire,
|
|
52
53
|
StepState,
|
|
53
54
|
StepStatus,
|
|
55
|
+
WorkflowApprovalOptions,
|
|
54
56
|
WorkflowPlannedStep,
|
|
55
57
|
WorkflowRun,
|
|
56
58
|
WorkflowRunMirror,
|
|
@@ -203,6 +205,27 @@ addError(WorkflowRunCancelledError, {
|
|
|
203
205
|
message: 'Workflow was cancelled.',
|
|
204
206
|
})
|
|
205
207
|
|
|
208
|
+
/**
|
|
209
|
+
* A decision arrived for an approval gate that has already resolved. The gate
|
|
210
|
+
* caches its outcome as the step result and never re-reads run state, so the
|
|
211
|
+
* decision could not take effect — it is rejected rather than accepted and
|
|
212
|
+
* dropped.
|
|
213
|
+
*/
|
|
214
|
+
export class WorkflowApprovalResolvedError extends PikkuError {
|
|
215
|
+
public payload: {
|
|
216
|
+
reason: string
|
|
217
|
+
outcome: ApprovalOutcome<unknown>['status']
|
|
218
|
+
}
|
|
219
|
+
constructor(reason: string, outcome: ApprovalOutcome<unknown>['status']) {
|
|
220
|
+
super(`Approval already ${outcome}: ${reason}`)
|
|
221
|
+
this.payload = { reason, outcome }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
addError(WorkflowApprovalResolvedError, {
|
|
225
|
+
status: 409,
|
|
226
|
+
message: 'Approval has already been resolved.',
|
|
227
|
+
})
|
|
228
|
+
|
|
206
229
|
export class WorkflowServiceNotInitialized extends Error {}
|
|
207
230
|
export class WorkflowStepNameNotString extends Error {
|
|
208
231
|
constructor(stepName: any) {
|
|
@@ -2148,6 +2171,214 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2148
2171
|
})
|
|
2149
2172
|
}
|
|
2150
2173
|
|
|
2174
|
+
/**
|
|
2175
|
+
* Wake a run later by enqueuing a delayed orchestrator pass. Deliberately NOT
|
|
2176
|
+
* {@link scheduleSleep}: that resolves the step it is given, which for an
|
|
2177
|
+
* approval would resolve the gate itself. This only nudges the run to replay
|
|
2178
|
+
* and re-evaluate — the gate stays the sole judge of its own outcome.
|
|
2179
|
+
*
|
|
2180
|
+
* Best-effort by design. Expiry is decided from the recorded deadline on
|
|
2181
|
+
* replay, so losing this wake costs liveness (the run sits until something
|
|
2182
|
+
* else resumes it), never correctness.
|
|
2183
|
+
*/
|
|
2184
|
+
private async scheduleRunWake(runId: string, delay: number): Promise<void> {
|
|
2185
|
+
try {
|
|
2186
|
+
const queueService = this.verifyQueueService()
|
|
2187
|
+
const run = await this.getRun(runId)
|
|
2188
|
+
if (!run?.workflow) return
|
|
2189
|
+
await queueService.add(
|
|
2190
|
+
this.getOrchestratorQueueName(run.workflow),
|
|
2191
|
+
{ runId },
|
|
2192
|
+
{ ...this.resolveStepJobOptions(), delay }
|
|
2193
|
+
)
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
this.logger?.warn(
|
|
2196
|
+
`Failed to schedule approval expiry wake for run ${runId}; expiry will still resolve on the next replay`,
|
|
2197
|
+
error
|
|
2198
|
+
)
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Durable step name for an approval gate. Namespaced separately from suspend
|
|
2204
|
+
* so the two can't collide, and derived from `reason` for the same reason
|
|
2205
|
+
* {@link getSuspendStepName} is: it must be stable across replays.
|
|
2206
|
+
*/
|
|
2207
|
+
private getApprovalStepName(reason: string): string {
|
|
2208
|
+
return `__workflow_approval:${reason}`
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/**
|
|
2212
|
+
* Run-state key holding an approval gate's record. Hex-encoded because the
|
|
2213
|
+
* Mongo backend restricts state keys to `/^[a-zA-Z0-9_]+$/` and a `reason` is
|
|
2214
|
+
* arbitrary human text. One key per gate, so two gates resolving concurrently
|
|
2215
|
+
* can't clobber each other through a read-modify-write.
|
|
2216
|
+
*/
|
|
2217
|
+
private approvalStateKey(stepName: string): string {
|
|
2218
|
+
let hex = ''
|
|
2219
|
+
for (const byte of new TextEncoder().encode(stepName)) {
|
|
2220
|
+
hex += byte.toString(16).padStart(2, '0')
|
|
2221
|
+
}
|
|
2222
|
+
return `__approval_${hex}`
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/**
|
|
2226
|
+
* Record a decision against an approval gate and wake the run. Called from
|
|
2227
|
+
* outside the workflow (an HTTP route, an RPC), so the schema value is NOT in
|
|
2228
|
+
* scope here — the payload is stored raw and validated on replay inside the
|
|
2229
|
+
* workflow body, which is the only place the schema exists. An invalid payload
|
|
2230
|
+
* therefore leaves the gate closed rather than failing the run.
|
|
2231
|
+
*
|
|
2232
|
+
* `reason` addresses the first reach of that gate. An approval reached more
|
|
2233
|
+
* than once under the same reason (e.g. in a loop) gets `#N`-suffixed step
|
|
2234
|
+
* rows that this cannot currently target.
|
|
2235
|
+
*/
|
|
2236
|
+
public async approveStep(
|
|
2237
|
+
runId: string,
|
|
2238
|
+
reason: string,
|
|
2239
|
+
decision: unknown
|
|
2240
|
+
): Promise<void> {
|
|
2241
|
+
const stepName = this.getApprovalStepName(reason)
|
|
2242
|
+
const stateKey = this.approvalStateKey(stepName)
|
|
2243
|
+
|
|
2244
|
+
// A resolved gate returns its cached step result and never re-reads state,
|
|
2245
|
+
// so a decision recorded now would be silently discarded — most obviously
|
|
2246
|
+
// when it loses the race with expiry. Reject instead, so the approver
|
|
2247
|
+
// learns their decision did not land.
|
|
2248
|
+
let resolved: StepState | undefined
|
|
2249
|
+
try {
|
|
2250
|
+
resolved = await this.getStepState(runId, stepName)
|
|
2251
|
+
} catch {
|
|
2252
|
+
// No step row yet: the run has not reached the gate. Recording a decision
|
|
2253
|
+
// ahead of it is legitimate — the gate picks it up on arrival.
|
|
2254
|
+
}
|
|
2255
|
+
if (resolved?.stepId && resolved.status === 'succeeded') {
|
|
2256
|
+
const outcome = resolved.result as ApprovalOutcome<unknown> | undefined
|
|
2257
|
+
throw new WorkflowApprovalResolvedError(
|
|
2258
|
+
reason,
|
|
2259
|
+
outcome?.status ?? 'decided'
|
|
2260
|
+
)
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
const state = await this.getRunState(runId)
|
|
2264
|
+
const record = (state[stateKey] ?? {}) as Record<string, unknown>
|
|
2265
|
+
await this.updateRunState(runId, stateKey, {
|
|
2266
|
+
...record,
|
|
2267
|
+
decision,
|
|
2268
|
+
decidedAt: new Date().toISOString(),
|
|
2269
|
+
error: undefined,
|
|
2270
|
+
})
|
|
2271
|
+
await this.resumeWorkflow(runId)
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
private async approvalStep(
|
|
2275
|
+
runId: string,
|
|
2276
|
+
reason: string,
|
|
2277
|
+
options: WorkflowApprovalOptions
|
|
2278
|
+
): Promise<ApprovalOutcome<unknown>> {
|
|
2279
|
+
const fromStepName = this.lastStepName(runId)
|
|
2280
|
+
const approvalStepName = this.nextStepKey(
|
|
2281
|
+
runId,
|
|
2282
|
+
this.getApprovalStepName(reason)
|
|
2283
|
+
)
|
|
2284
|
+
return await this.withStepLock(runId, approvalStepName, async () => {
|
|
2285
|
+
let stepState: StepState
|
|
2286
|
+
try {
|
|
2287
|
+
stepState = await this.getStepState(runId, approvalStepName)
|
|
2288
|
+
} catch {
|
|
2289
|
+
stepState = await this.insertStepState(
|
|
2290
|
+
runId,
|
|
2291
|
+
approvalStepName,
|
|
2292
|
+
'pikkuWorkflowApproval',
|
|
2293
|
+
{ reason, expiry: options.expiry },
|
|
2294
|
+
undefined,
|
|
2295
|
+
fromStepName
|
|
2296
|
+
)
|
|
2297
|
+
}
|
|
2298
|
+
if (!stepState.stepId) {
|
|
2299
|
+
stepState = await this.insertStepState(
|
|
2300
|
+
runId,
|
|
2301
|
+
approvalStepName,
|
|
2302
|
+
'pikkuWorkflowApproval',
|
|
2303
|
+
{ reason, expiry: options.expiry },
|
|
2304
|
+
undefined,
|
|
2305
|
+
fromStepName
|
|
2306
|
+
)
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// Unlike suspend, `succeeded` here means a decision (or expiry) was
|
|
2310
|
+
// actually resolved, and the step result IS the return channel.
|
|
2311
|
+
if (stepState.status === 'succeeded') {
|
|
2312
|
+
return stepState.result as ApprovalOutcome<unknown>
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
const stateKey = this.approvalStateKey(approvalStepName)
|
|
2316
|
+
let record = ((await this.getRunState(runId))[stateKey] ?? {}) as {
|
|
2317
|
+
decision?: unknown
|
|
2318
|
+
decidedAt?: string
|
|
2319
|
+
expiresAt?: string
|
|
2320
|
+
error?: unknown
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
if (stepState.status === 'pending') {
|
|
2324
|
+
await this.setStepRunning(stepState.stepId)
|
|
2325
|
+
// First reach: stamp the deadline and nudge the run awake when it
|
|
2326
|
+
// passes. The deadline is what's authoritative — see below.
|
|
2327
|
+
if (options.expiry !== undefined && !record.expiresAt) {
|
|
2328
|
+
const expiresAt = new Date(
|
|
2329
|
+
Date.now() + getDurationInMilliseconds(options.expiry)
|
|
2330
|
+
).toISOString()
|
|
2331
|
+
record = { ...record, expiresAt }
|
|
2332
|
+
await this.updateRunState(runId, stateKey, record)
|
|
2333
|
+
await this.scheduleRunWake(
|
|
2334
|
+
runId,
|
|
2335
|
+
getDurationInMilliseconds(options.expiry)
|
|
2336
|
+
)
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
if (record.decision !== undefined) {
|
|
2341
|
+
const validation = await options.schema['~standard'].validate(
|
|
2342
|
+
record.decision
|
|
2343
|
+
)
|
|
2344
|
+
if (validation.issues) {
|
|
2345
|
+
// Drop the bad decision and re-close the gate, leaving the failure
|
|
2346
|
+
// legible to whoever tries next. Failing the run instead would let any
|
|
2347
|
+
// caller kill a workflow with a malformed payload.
|
|
2348
|
+
await this.updateRunState(runId, stateKey, {
|
|
2349
|
+
...record,
|
|
2350
|
+
decision: undefined,
|
|
2351
|
+
decidedAt: undefined,
|
|
2352
|
+
error: validation.issues.map((issue) => ({
|
|
2353
|
+
message: issue.message,
|
|
2354
|
+
path: issue.path?.map((segment) =>
|
|
2355
|
+
typeof segment === 'object' ? segment.key : segment
|
|
2356
|
+
),
|
|
2357
|
+
})),
|
|
2358
|
+
})
|
|
2359
|
+
throw new WorkflowSuspendedException(runId, reason)
|
|
2360
|
+
}
|
|
2361
|
+
const outcome: ApprovalOutcome<unknown> = {
|
|
2362
|
+
status: 'decided',
|
|
2363
|
+
data: validation.value,
|
|
2364
|
+
}
|
|
2365
|
+
await this.setStepResult(stepState.stepId, outcome)
|
|
2366
|
+
return outcome
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// Expiry is decided by comparing against the recorded deadline rather than
|
|
2370
|
+
// by the timer having fired, so a duplicate, late, or dropped timer all
|
|
2371
|
+
// produce the same answer.
|
|
2372
|
+
if (record.expiresAt && Date.now() >= Date.parse(record.expiresAt)) {
|
|
2373
|
+
const outcome: ApprovalOutcome<unknown> = { status: 'expired' }
|
|
2374
|
+
await this.setStepResult(stepState.stepId, outcome)
|
|
2375
|
+
return outcome
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
throw new WorkflowSuspendedException(runId, reason)
|
|
2379
|
+
})
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2151
2382
|
public createWorkflowWire(
|
|
2152
2383
|
name: string,
|
|
2153
2384
|
runId: string,
|
|
@@ -2345,6 +2576,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2345
2576
|
await this.suspendStep(runId, reason)
|
|
2346
2577
|
},
|
|
2347
2578
|
|
|
2579
|
+
approval: (async (reason: string, options: WorkflowApprovalOptions) => {
|
|
2580
|
+
this.verifyStepName(reason)
|
|
2581
|
+
return await this.approvalStep(runId, reason, options)
|
|
2582
|
+
}) as PikkuScenarioWire['approval'],
|
|
2583
|
+
|
|
2348
2584
|
runScheduledTask: async (taskName: string) => {
|
|
2349
2585
|
await runScheduledTask({ name: taskName })
|
|
2350
2586
|
},
|
|
@@ -31,7 +31,9 @@ function capturingRpcService(): {
|
|
|
31
31
|
|
|
32
32
|
describe('queued workflow steps carry the run wire pikkuUserId', () => {
|
|
33
33
|
test('invokeStepRpc merges run.wire.pikkuUserId into the step wire override', async () => {
|
|
34
|
-
pikkuState(null, 'package', 'singletonServices', {
|
|
34
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
35
|
+
logger: silentLogger,
|
|
36
|
+
} as any)
|
|
35
37
|
|
|
36
38
|
const ws = new InMemoryWorkflowService()
|
|
37
39
|
const runId = await ws.createRun('flow', {}, false, 'hash', {
|
|
@@ -56,19 +58,33 @@ describe('queued workflow steps carry the run wire pikkuUserId', () => {
|
|
|
56
58
|
'user-abc',
|
|
57
59
|
'the step wire override carries the run wire pikkuUserId so authed steps rehydrate'
|
|
58
60
|
)
|
|
59
|
-
assert.ok(
|
|
61
|
+
assert.ok(
|
|
62
|
+
rpc.wire().workflowStep,
|
|
63
|
+
'workflowStep provenance is still present'
|
|
64
|
+
)
|
|
60
65
|
})
|
|
61
66
|
|
|
62
67
|
test('no pikkuUserId key is injected when the run wire has none', async () => {
|
|
63
|
-
pikkuState(null, 'package', 'singletonServices', {
|
|
68
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
69
|
+
logger: silentLogger,
|
|
70
|
+
} as any)
|
|
64
71
|
|
|
65
72
|
const ws = new InMemoryWorkflowService()
|
|
66
|
-
const runId = await ws.createRun('flow', {}, false, 'hash', {
|
|
73
|
+
const runId = await ws.createRun('flow', {}, false, 'hash', {
|
|
74
|
+
type: 'test',
|
|
75
|
+
})
|
|
67
76
|
await ws.insertStepState(runId, 'noauth', 'someFn', {})
|
|
68
77
|
const stepState = await ws.getStepState(runId, 'noauth')
|
|
69
78
|
|
|
70
79
|
const rpc = capturingRpcService()
|
|
71
|
-
await (ws as any).invokeStepRpc(
|
|
80
|
+
await (ws as any).invokeStepRpc(
|
|
81
|
+
runId,
|
|
82
|
+
'noauth',
|
|
83
|
+
stepState,
|
|
84
|
+
'someFn',
|
|
85
|
+
{},
|
|
86
|
+
rpc.service
|
|
87
|
+
)
|
|
72
88
|
|
|
73
89
|
assert.ok(
|
|
74
90
|
!('pikkuUserId' in rpc.wire()),
|
|
@@ -14,6 +14,9 @@ export type {
|
|
|
14
14
|
WorkflowWireDoInline,
|
|
15
15
|
WorkflowWireSleep,
|
|
16
16
|
WorkflowWireSuspend,
|
|
17
|
+
WorkflowWireApproval,
|
|
18
|
+
WorkflowApprovalOptions,
|
|
19
|
+
ApprovalOutcome,
|
|
17
20
|
InputSource,
|
|
18
21
|
OutputBinding,
|
|
19
22
|
RpcStepMeta,
|
|
@@ -28,6 +31,7 @@ export type {
|
|
|
28
31
|
SleepStepMeta,
|
|
29
32
|
CancelStepMeta,
|
|
30
33
|
SuspendStepMeta,
|
|
34
|
+
ApprovalStepMeta,
|
|
31
35
|
SetStepMeta,
|
|
32
36
|
SwitchCaseMeta,
|
|
33
37
|
SwitchStepMeta,
|