@pikku/core 0.12.70 → 0.12.72

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 (77) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/LICENSE +21 -0
  3. package/dist/function/function-runner.js +7 -2
  4. package/dist/handle-error.d.ts +2 -0
  5. package/dist/handle-error.js +8 -2
  6. package/dist/schema.d.ts +24 -0
  7. package/dist/schema.js +46 -0
  8. package/dist/services/in-memory-queue-service.d.ts +6 -0
  9. package/dist/services/in-memory-queue-service.js +8 -1
  10. package/dist/services/in-memory-workflow-service.d.ts +3 -5
  11. package/dist/services/in-memory-workflow-service.js +10 -19
  12. package/dist/services/workflow-service.d.ts +7 -5
  13. package/dist/types/core.types.d.ts +7 -0
  14. package/dist/wirings/ai-agent/ai-agent-agui.js +0 -8
  15. package/dist/wirings/ai-agent/ai-agent-prepare.js +1 -2
  16. package/dist/wirings/ai-agent/ai-agent.types.d.ts +0 -6
  17. package/dist/wirings/http/http-runner.d.ts +1 -1
  18. package/dist/wirings/http/http-runner.js +4 -2
  19. package/dist/wirings/http/http.types.d.ts +2 -0
  20. package/dist/wirings/http/index.d.ts +2 -1
  21. package/dist/wirings/http/index.js +1 -1
  22. package/dist/wirings/http/pikku-fetch-http-request.d.ts +11 -1
  23. package/dist/wirings/http/pikku-fetch-http-request.js +67 -3
  24. package/dist/wirings/mcp/mcp-runner.d.ts +5 -0
  25. package/dist/wirings/mcp/mcp-runner.js +5 -2
  26. package/dist/wirings/workflow/graph/graph-runner.js +3 -2
  27. package/dist/wirings/workflow/graph/graph-validation.d.ts +0 -2
  28. package/dist/wirings/workflow/graph/graph-validation.js +0 -142
  29. package/dist/wirings/workflow/graph/index.d.ts +1 -1
  30. package/dist/wirings/workflow/graph/index.js +1 -1
  31. package/dist/wirings/workflow/index.d.ts +0 -1
  32. package/dist/wirings/workflow/index.js +0 -2
  33. package/dist/wirings/workflow/pikku-workflow-service.d.ts +69 -15
  34. package/dist/wirings/workflow/pikku-workflow-service.js +260 -164
  35. package/dist/wirings/workflow/workflow.types.d.ts +1 -6
  36. package/package.json +1 -2
  37. package/src/function/function-runner.test.ts +75 -0
  38. package/src/function/function-runner.ts +15 -2
  39. package/src/gopass-secrets-removed.test.ts +51 -0
  40. package/src/handle-error.test.ts +108 -1
  41. package/src/handle-error.ts +10 -2
  42. package/src/schema.test.ts +103 -0
  43. package/src/schema.ts +51 -0
  44. package/src/services/in-memory-queue-service.test.ts +66 -1
  45. package/src/services/in-memory-queue-service.ts +13 -2
  46. package/src/services/in-memory-workflow-service.ts +12 -25
  47. package/src/services/workflow-service.ts +7 -4
  48. package/src/types/core.types.ts +7 -0
  49. package/src/wirings/ai-agent/ai-agent-agui.test.ts +0 -16
  50. package/src/wirings/ai-agent/ai-agent-agui.ts +0 -9
  51. package/src/wirings/ai-agent/ai-agent-prepare.ts +1 -2
  52. package/src/wirings/ai-agent/ai-agent.types.ts +0 -7
  53. package/src/wirings/http/http-runner.ts +4 -1
  54. package/src/wirings/http/http.types.ts +2 -0
  55. package/src/wirings/http/index.ts +5 -1
  56. package/src/wirings/http/pikku-fetch-http-request.test.ts +89 -0
  57. package/src/wirings/http/pikku-fetch-http-request.ts +85 -3
  58. package/src/wirings/mcp/mcp-runner.test.ts +40 -0
  59. package/src/wirings/mcp/mcp-runner.ts +11 -2
  60. package/src/wirings/workflow/graph/graph-runner.ts +3 -2
  61. package/src/wirings/workflow/graph/graph-validation.test.ts +1 -144
  62. package/src/wirings/workflow/graph/graph-validation.ts +0 -196
  63. package/src/wirings/workflow/graph/index.ts +1 -5
  64. package/src/wirings/workflow/index.ts +0 -6
  65. package/src/wirings/workflow/pikku-workflow-service.ts +377 -212
  66. package/src/wirings/workflow/scenario-expectations.test.ts +153 -0
  67. package/src/wirings/workflow/scenario-step.test.ts +1 -1
  68. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  69. package/src/wirings/workflow/workflow-dispatch-payload.test.ts +59 -0
  70. package/src/wirings/workflow/workflow-mirror.test.ts +178 -0
  71. package/src/wirings/workflow/workflow-replay-snapshot.test.ts +139 -0
  72. package/src/wirings/workflow/workflow-run-context.test.ts +177 -0
  73. package/src/wirings/workflow/workflow-run-polling.test.ts +132 -0
  74. package/src/wirings/workflow/workflow-step-ordinal.test.ts +4 -4
  75. package/src/wirings/workflow/workflow.types.ts +1 -4
  76. package/tsconfig.tsbuildinfo +1 -1
  77. package/src/services/gopass-secrets.ts +0 -78
package/CHANGELOG.md CHANGED
@@ -1,3 +1,146 @@
1
+ ## 0.12.72
2
+
3
+ ### Patch Changes
4
+
5
+ - 384e484: Apply schema defaults, which nothing was ever filling in
6
+
7
+ A `default` on an input property reaches the generated JSON Schema and keeps
8
+ that property out of `required`, so a call that omits it validates. Nothing
9
+ then filled it in: JSON Schema validators are pure by specification, and
10
+ neither `@cfworker/json-schema` nor Ajv (without `useDefaults`) annotates the
11
+ instance being checked. The function received `undefined` for a property its
12
+ own generated type declares as present.
13
+
14
+ That is the worst shape the mismatch can take. Validation permits the omission,
15
+ the type promises a value, and the body reads `undefined` — so it surfaces far
16
+ from its cause, as `const offset = (page - 1) * limit` evaluating to `NaN` and
17
+ `.limit(undefined)` reaching the database on a paginated call made with no
18
+ arguments.
19
+
20
+ Defaults are now filled in before validation, on every path. Deliberately not
21
+ gated on `coerceDataFromSchema`, the flag guarding the neighbouring coercion
22
+ step: that flag is about decoding transport-encoded values (a query string's
23
+ `"1,2"` into an array) and is absent on a direct RPC invocation. A default
24
+ belongs to the schema rather than to the transport a call arrived on, so
25
+ gating it there would fill defaults over HTTP and skip them on RPC.
26
+
27
+ Filling is by presence rather than truthiness, so a supplied `0` or `false`
28
+ survives, and a call made with no arguments at all still gets its defaults.
29
+ Values are cloned, because an object or array default would otherwise be a
30
+ single mutable instance shared by every request in the process — one request's
31
+ `push` showing up in the next.
32
+
33
+ Nothing needs to change in generated types or call sites: both were already
34
+ written as though defaults worked. This makes them true.
35
+
36
+ - b5a73fb: fix: stop leaking internal error detail and bound the request body size
37
+
38
+ HTTP error responses no longer forward an error's `payload` or its raw `message` for
39
+ registered 5xx errors — those responses carry the registered error message instead, so an
40
+ internal error that happens to hold a `payload` cannot leak it to the client. Errors
41
+ registered with a 4xx status keep their message and payload, and `exposeErrors` still
42
+ surfaces the full detail outside production.
43
+
44
+ `PikkuFetchHTTPRequest` now caps how much of a request body it buffers, rejecting the
45
+ declared `content-length` up front and measuring the stream as it arrives so a lying or
46
+ absent header cannot exhaust memory. Exceeding the limit throws `PayloadTooLargeError`
47
+ (413). The ceiling defaults to 10MB and is configurable via the new `maxBodySize` option on
48
+ the constructor and on `RunHTTPWiringOptions`.
49
+
50
+ - 6be5ab0: Security hardening: removed the gopass secret service and stopped MCP internal errors leaking stack traces.
51
+
52
+ **Breaking:** `GopassSecretService` and the `@pikku/core/services/gopass-secrets` subpath export are gone. The service shelled out to the `gopass` binary and its key validation accepted `../`, so a caller-supplied key could traverse out of the configured prefix namespace and read secrets outside it. Rather than harden a shell-out that few projects used, the service is removed. Anyone importing it should implement `SecretService` against their own secret backend. Pre-0.13 breaking changes still ship as a patch.
53
+
54
+ MCP internal errors (JSON-RPC `-32603`) previously always attached `data: { message, stack }`, handing any MCP client an internal stack trace. That payload is now gated on `exposeErrors`, which defaults to `!isProduction()` — the same convention `handleHTTPError` already uses. In production a client receives a bare `Internal error` with no `message` and no `stack`. `RunMCPEndpointParams` accepts an explicit `exposeErrors` to suppress the detail outside production as well; it cannot force the detail on in production, because the check is `exposeErrors && !isProduction()` — again matching `handleHTTPError`.
55
+
56
+ ## 0.12.71
57
+
58
+ ### Patch Changes
59
+
60
+ - 8a2c993: Make the workflow service cheaper to run, and fix two ways it lost state.
61
+
62
+ The SQL workflow tables had no indexes at all, so every step read, every
63
+ history walk and every orchestrator tick was a sequential scan; five indexes
64
+ now cover the columns the engine actually queries by. A replay used to ask for
65
+ each step's row individually — O(N) reads per replay, O(N^2) over a run — and
66
+ now takes one read of the run's steps and serves the walk from it. A step
67
+ transition wrote the step row and its history row as two separate statements,
68
+ so a crash between them left a step saying `succeeded` whose history still said
69
+ `running`; both halves are now one transaction, and the history row is found by
70
+ attempt number rather than by sorting on `created_at`, which two attempts can
71
+ share. Resolving a dynamic workflow read and parsed every AI-generated workflow
72
+ in the deployment to `.find()` one by name; it is a point lookup now.
73
+
74
+ Waiting on a run no longer polls at a fixed interval. `pollIntervalMs` became a
75
+ ceiling rather than a cadence: polling starts at 10ms and widens towards it, so
76
+ a workflow that finishes in milliseconds is no longer held for a full second,
77
+ and a long-running one is not read at full rate for its whole life.
78
+
79
+ Two backend-specific defects: Redis kept a run's state as one JSON blob and
80
+ read-modified-wrote it, so parallel branches setting different variables
81
+ overwrote each other — state is a field per variable now, with the old blob
82
+ still read underneath so runs in flight keep what they had. Mongo's
83
+ `setStepScheduled` never wrote history, leaving a queued step reading as never
84
+ dispatched.
85
+
86
+ Also: dispatch no longer JSON round-trips every step payload before handing it
87
+ to a queue that serialises it anyway — the in-process dev queue, which is the
88
+ only one that was relying on it, does it itself now.
89
+
90
+ Two more defects. A transition whose step had no live attempt wrote its status
91
+ to the step row and silently nothing to history — the exact divergence the
92
+ transaction exists to prevent — and now repairs the step and writes the
93
+ missing row. And resolving a dynamic workflow was non-deterministic on all
94
+ three backends: a name can hold several active versions, and none of them
95
+ ordered the candidates, so which one ran could change between two calls
96
+ reading identical data. The newest version wins, with the graph hash breaking
97
+ a tie.
98
+
99
+ The two attempt columns and the five indexes are declared in the workflow
100
+ schema, so a fresh database gets them at boot. An existing one gets them from
101
+ a migration — `pikku db generate` writes the declaration down — rather than
102
+ from DDL issued at boot.
103
+
104
+ - a261006: **Breaking:** removed dynamic workflows — runtime-defined workflow graphs stored in the database and resolved by name instead of by codegen.
105
+
106
+ The feature was already half-gone. Its authoring surface (`createAgentWorkflow`, `saveAgentWorkflow`, `listAgentWorkflows`, `executeAgentWorkflow`, and the AI-agent instruction builder) was deleted in April 2026 along with its entire e2e suite, and nothing has written a dynamic workflow since. What remained could not execute one either: `executeAgentWorkflow` gated on `pikkuState('workflows', 'meta')`, which only codegen ever populates, so a graph that existed solely in the database was never findable. The two backend families had also drifted onto different `source` sentinels (`'ai-agent'` vs `'dynamic-workflow'`), and the two Redis implementations disagreed on key escaping — so at least one of them matched nothing. Rather than keep shipping plumbing for a path no caller could complete, it is removed until it can be reintroduced deliberately.
107
+
108
+ Removed:
109
+ - `getAIGeneratedWorkflows` from `WorkflowService` and `WorkflowRunService`, and from every backend (in-memory, Redis, MongoDB, Kysely, and the Cloudflare Durable Object service and client — the last two were already a `return []` stub and a rejection).
110
+ - The database-lookup fallbacks in `startWorkflow` and `runWorkflowJob` that resolved a workflow name against stored graphs when static meta had no match.
111
+ - `'dynamic-workflow'` from the `WorkflowRuntimeMeta['source']` union.
112
+ - `validateWorkflowWiring` and `computeEntryNodeIds` from `@pikku/core/workflow`. These validated AI-authored graphs and had no callers in core; the inspector keeps its own private entry-node computation for static graph wiring, which is unaffected.
113
+ - The `workflow-created` AI stream event and its AG-UI `pikku:workflow-created` custom event. Its only emitter went with the April deletion, so it could never fire.
114
+ - The console's `console:getAIWorkflows` RPC, the `useAIWorkflows` hook, the "Dynamic" workflow filter and badge, and the trigger-schema scraper that derived an input form from a stored graph's `$ref` bindings.
115
+
116
+ Kept, because static graph workflows depend on them and this is not a change to versioning:
117
+ - `upsertWorkflowVersion`, `getWorkflowVersion`, `updateWorkflowVersionStatus`, and the `workflowVersions` storage in every backend. These back version-mismatch replay: when a deployed graph's hash changes, in-flight runs continue against the exact graph they started on. No schema migration is needed — the table, its columns, and its `(workflowName, graphHash)` upsert key are unchanged.
118
+ - `generateMermaidDiagram`, which renders any workflow graph and is not specific to dynamic ones.
119
+
120
+ Static `pikkuWorkflowGraph` and DSL workflows are entirely unaffected: they resolve from codegen'd meta, which was always the only path that worked.
121
+
122
+ To revive this post-MVP, the deleted authoring code is recoverable in full — its prompt engineering (a compact tool table upfront, full schemas with flattened dotted output paths returned only after a validation failure) is worth reading before rewriting:
123
+
124
+ ```
125
+ git show f52f3308b^:packages/core/src/wirings/ai-agent/agent-dynamic-workflow.ts
126
+ git show f52f3308b^:packages/core/src/wirings/workflow/graph/graph-validation.ts
127
+ git show f52f3308b --stat # the April removal, incl. the three e2e feature files
128
+ ```
129
+
130
+ Note that reviving it needs more than restoring those files: the queued-step path (`executeWorkflowStep`), `onError` compensation, and sub-workflow resolution all read static meta only and would need a fallback for a graph that exists solely in the database.
131
+
132
+ - 09973b9: Scenarios, features and steps no longer reach a deployment.
133
+
134
+ Steps were already held back from the app bootstrap, so a deployed server never imported a step body. Everything _about_ a scenario still travelled with the application: a `pikkuScenario(...)` is a function, so its name, schemas and hashes sat in the app function meta; the schemas it and its steps validate against sat in the app's `register.gen.ts` — on one project 458 of the 582 registered schemas belonged to tests; its name sat in the internal RPC meta; and because a scenario is _also_ a workflow, the inspector synthesised a `wf-orchestrator-<scenario>` queue worker for each one. The deploy analyzer, which reads inspector state rather than the partitioned codegen output, then read all of it back as application code: a unit per scenario, a `WorkflowDefinition` per scenario, and a real queue per scenario. A 13-scenario suite turned into 13 production queues named after tests, waiting for a provider to create them.
135
+
136
+ The existing scenario/app partition is now applied everywhere it was missing. `FunctionRuntimeMeta` gains a `scenario` marker (the counterpart of `scenarioStep`) so a scenario body is recognisable without walking the workflow graph; scenario bodies join their steps on the scenario side of the function-meta and registration split; schemas only a scenario or step needs are written and registered under `.pikku/scenarios/schemas/` and imported by the scenario bootstrap alone; scenario names are dropped from the internal RPC meta; no orchestrator queue worker is synthesised for a scenario; and the deploy analyzer drops both scenario functions and scenario workflows before it decides what a deployment contains.
137
+
138
+ The MCP metas are keyed by wiring rather than by function, so a scenario wired as an MCP tool, resource or prompt was the one id that still reached the manifest after the function and workflow filters — as an endpoint on the gateway plus a gateway dependency on a unit that was never emitted. Those ids are now filtered too.
139
+
140
+ `scenarioSchemaDirectory` is rejected when it resolves to the same directory as `schemaDirectory`. A schema write owns its directory — it emits `register.gen.ts` and prunes every schema file its own required-set does not name — so sharing one would replace the application register with the scenario-only one and delete the app's schema files, which nothing downstream can detect.
141
+
142
+ Nothing changes for `pikku scenario run` — the scenario bootstrap still registers every scenario, feature, step, meta and schema. What changes is that a bundle stops carrying them.
143
+
1
144
  ## 0.12.70
2
145
 
3
146
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 - present Yasser Fadl and Pikku contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -2,7 +2,7 @@ import { runMiddleware, combineMiddleware } from '../middleware-runner.js';
2
2
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../wirings/channel/channel-middleware-runner.js';
3
3
  import { runPermissions } from '../permissions.js';
4
4
  import { pikkuState } from '../pikku-state.js';
5
- import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js';
5
+ import { applyDefaultsFromSchema, coerceTopLevelDataFromSchema, validateSchema, } from '../schema.js';
6
6
  import { parseVersionedId } from '../version.js';
7
7
  import { PikkuSessionService } from '../services/user-session-service.js';
8
8
  import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js';
@@ -166,10 +166,15 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
166
166
  // evaluates the function's own OR-groups against request data.
167
167
  verifyScopes(funcConfig.scopes ?? funcMeta.scopes, session);
168
168
  // Evaluate the data from the lazy function
169
- const actualData = await data();
169
+ let actualData = await data();
170
170
  // Validate and coerce data if schema is defined
171
171
  const inputSchemaName = funcMeta.inputSchemaName;
172
172
  if (inputSchemaName) {
173
+ // Fill in schema defaults before anything reads the data. Unconditional:
174
+ // a default belongs to the schema, not to the transport the call arrived
175
+ // on. Runs before coercion so a defaulted value and a supplied one are
176
+ // treated identically from here on.
177
+ actualData = applyDefaultsFromSchema(inputSchemaName, actualData, packageName);
173
178
  // Coerce (top level) data types before validation (e.g. string→array, string→date)
174
179
  if (coerceDataFromSchema) {
175
180
  coerceTopLevelDataFromSchema(inputSchemaName, actualData, packageName);
@@ -10,5 +10,7 @@ import type { PikkuHTTP } from './wirings/http/http.types.js';
10
10
  * @param {number[]} logWarningsForStatusCodes - HTTP status codes to log as warnings
11
11
  * @param {boolean} respondWith404 - Whether to respond with 404 for NotFoundError
12
12
  * @param {boolean} bubbleError - Whether to throw the error after handling
13
+ * @param {boolean} exposeErrors - Whether to include internal error details (message, stack and
14
+ * payload of 5xx errors) in the response body. Ignored in production.
13
15
  */
14
16
  export declare const handleHTTPError: (e: any, http: PikkuHTTP | undefined, traceId: string | undefined, logger: Logger, logWarningsForStatusCodes: number[], respondWith404: boolean, bubbleError: boolean, exposeErrors?: boolean) => void;
@@ -11,6 +11,8 @@ import { NotFoundError } from './errors/errors.js';
11
11
  * @param {number[]} logWarningsForStatusCodes - HTTP status codes to log as warnings
12
12
  * @param {boolean} respondWith404 - Whether to respond with 404 for NotFoundError
13
13
  * @param {boolean} bubbleError - Whether to throw the error after handling
14
+ * @param {boolean} exposeErrors - Whether to include internal error details (message, stack and
15
+ * payload of 5xx errors) in the response body. Ignored in production.
14
16
  */
15
17
  export const handleHTTPError = (e, http, traceId, logger, logWarningsForStatusCodes, respondWith404, bubbleError, exposeErrors = false) => {
16
18
  // Skip 404 handling if configured to do so
@@ -20,14 +22,18 @@ export const handleHTTPError = (e, http, traceId, logger, logWarningsForStatusCo
20
22
  // Get appropriate error response
21
23
  const errorResponse = getErrorResponse(e);
22
24
  if (errorResponse != null) {
25
+ const clientFacing = errorResponse.status < 500 || (exposeErrors && !isProduction());
23
26
  // Set status and response body
24
27
  http?.response?.status(errorResponse.status);
25
28
  http?.response?.json({
26
29
  name: e instanceof Error ? e.name : undefined,
27
- message: e instanceof Error && e.message && e.message !== 'An error occurred'
30
+ message: clientFacing &&
31
+ e instanceof Error &&
32
+ e.message &&
33
+ e.message !== 'An error occurred'
28
34
  ? e.message
29
35
  : errorResponse.message,
30
- payload: e.payload,
36
+ payload: clientFacing ? e.payload : undefined,
31
37
  errorId: traceId,
32
38
  });
33
39
  // Log certain status codes as warnings
package/dist/schema.d.ts CHANGED
@@ -21,5 +21,29 @@ export declare const getSchema: (name: string, packageName?: string | null) => R
21
21
  * @param logger - A logger for logging information.
22
22
  */
23
23
  export declare const compileAllSchemas: (logger: Logger, schemaService?: SchemaService) => void;
24
+ /**
25
+ * Fill in absent top-level properties from their schema `default`.
26
+ *
27
+ * A `default` reaches the generated JSON Schema and keeps the property out of
28
+ * `required`, so omitting it validates — but nothing was ever filling it in.
29
+ * JSON Schema validators are pure by specification and none of the ones Pikku
30
+ * ships with (`@cfworker/json-schema`, and Ajv unless `useDefaults` is set)
31
+ * annotate the instance, so the function received `undefined` for a property
32
+ * its generated type declares as present. That is the worst shape a mismatch
33
+ * can take: validation permits the omission, the type says the value is there,
34
+ * and the body reads `undefined`.
35
+ *
36
+ * Applied unconditionally rather than alongside `coerceTopLevelDataFromSchema`,
37
+ * whose `coerceDataFromSchema` flag is about decoding transport-encoded values
38
+ * (a query string's `"1,2"` into an array). Defaults are a property of the
39
+ * schema, not of how the call arrived, so gating them on that flag would apply
40
+ * them over HTTP and skip them on a direct RPC invocation.
41
+ *
42
+ * Returns the data to use, which is a new object only when defaults had to be
43
+ * added to a nullish input — a call made with no arguments at all still gets
44
+ * them. Values are cloned so an object or array default (`[]`, `{}`) is never
45
+ * shared as one mutable instance across every request.
46
+ */
47
+ export declare const applyDefaultsFromSchema: (schemaName: string, data: any, packageName?: string | null) => any;
24
48
  export declare const coerceTopLevelDataFromSchema: (schemaName: string, data: any, packageName?: string | null) => void;
25
49
  export declare const validateSchema: (logger: Logger, schemaService: SchemaService | undefined, schemaName: string | undefined | null, data: any, packageName?: string | null) => Promise<void>;
package/dist/schema.js CHANGED
@@ -65,6 +65,52 @@ const validateAllSchemasLoaded = (logger, schemaService) => {
65
65
  logger.info('All schemas loaded');
66
66
  }
67
67
  };
68
+ /**
69
+ * Fill in absent top-level properties from their schema `default`.
70
+ *
71
+ * A `default` reaches the generated JSON Schema and keeps the property out of
72
+ * `required`, so omitting it validates — but nothing was ever filling it in.
73
+ * JSON Schema validators are pure by specification and none of the ones Pikku
74
+ * ships with (`@cfworker/json-schema`, and Ajv unless `useDefaults` is set)
75
+ * annotate the instance, so the function received `undefined` for a property
76
+ * its generated type declares as present. That is the worst shape a mismatch
77
+ * can take: validation permits the omission, the type says the value is there,
78
+ * and the body reads `undefined`.
79
+ *
80
+ * Applied unconditionally rather than alongside `coerceTopLevelDataFromSchema`,
81
+ * whose `coerceDataFromSchema` flag is about decoding transport-encoded values
82
+ * (a query string's `"1,2"` into an array). Defaults are a property of the
83
+ * schema, not of how the call arrived, so gating them on that flag would apply
84
+ * them over HTTP and skip them on a direct RPC invocation.
85
+ *
86
+ * Returns the data to use, which is a new object only when defaults had to be
87
+ * added to a nullish input — a call made with no arguments at all still gets
88
+ * them. Values are cloned so an object or array default (`[]`, `{}`) is never
89
+ * shared as one mutable instance across every request.
90
+ */
91
+ export const applyDefaultsFromSchema = (schemaName, data, packageName = null) => {
92
+ const schema = pikkuState(packageName, 'misc', 'schemas').get(schemaName);
93
+ if (!schema?.properties)
94
+ return data;
95
+ // A primitive body cannot carry named properties; leave it for the validator
96
+ // to reject rather than reshaping it into something that would pass.
97
+ if (data != null && typeof data !== 'object')
98
+ return data;
99
+ let result = data;
100
+ for (const key in schema.properties) {
101
+ const property = schema.properties[key];
102
+ if (typeof property === 'boolean' || !('default' in property)) {
103
+ continue;
104
+ }
105
+ // Allocated only once a default is actually found, so a schema without any
106
+ // leaves the caller's data (and its absence) exactly as it was.
107
+ result ??= {};
108
+ if (result[key] === undefined) {
109
+ result[key] = structuredClone(property.default);
110
+ }
111
+ }
112
+ return result;
113
+ };
68
114
  export const coerceTopLevelDataFromSchema = (schemaName, data, packageName = null) => {
69
115
  const schema = pikkuState(packageName, 'misc', 'schemas').get(schemaName);
70
116
  if (!schema?.properties)
@@ -5,6 +5,12 @@ import type { QueueService, JobOptions } from '../wirings/queue/queue.types.js';
5
5
  * a real queue — and redelivers a failed job up to `options.attempts` times with
6
6
  * backoff, so a transiently-failing workflow step recovers exactly as it would
7
7
  * on pg-boss/bullmq instead of being silently dropped on its first error.
8
+ *
9
+ * Payloads are JSON round-tripped on the way in, because every real backend
10
+ * puts the job on a wire (SQS body, Redis value, jsonb column) and the worker
11
+ * therefore never sees the caller's live object. Doing it here keeps dev
12
+ * behaviour honest, and keeps the callers — who cannot know which backend they
13
+ * are talking to — from having to serialise defensively.
8
14
  */
9
15
  export declare class InMemoryQueueService implements QueueService {
10
16
  readonly supportsResults = false;
@@ -5,6 +5,12 @@ import { runQueueJob } from '../wirings/queue/queue-runner.js';
5
5
  * a real queue — and redelivers a failed job up to `options.attempts` times with
6
6
  * backoff, so a transiently-failing workflow step recovers exactly as it would
7
7
  * on pg-boss/bullmq instead of being silently dropped on its first error.
8
+ *
9
+ * Payloads are JSON round-tripped on the way in, because every real backend
10
+ * puts the job on a wire (SQS body, Redis value, jsonb column) and the worker
11
+ * therefore never sees the caller's live object. Doing it here keeps dev
12
+ * behaviour honest, and keeps the callers — who cannot know which backend they
13
+ * are talking to — from having to serialise defensively.
8
14
  */
9
15
  export class InMemoryQueueService {
10
16
  supportsResults = false;
@@ -14,12 +20,13 @@ export class InMemoryQueueService {
14
20
  const maxAttempts = Math.max(1, options?.attempts ?? 1);
15
21
  let attemptsMade = 0;
16
22
  const createdAt = new Date();
23
+ const payload = data === undefined ? data : JSON.parse(JSON.stringify(data));
17
24
  const runAttempt = async () => {
18
25
  attemptsMade++;
19
26
  const job = {
20
27
  id: jobId,
21
28
  queueName,
22
- data,
29
+ data: payload,
23
30
  status: () => 'active',
24
31
  metadata: () => ({ attemptsMade, maxAttempts, createdAt }),
25
32
  pikkuUserId: options?.pikkuUserId,
@@ -66,6 +66,9 @@ export declare class InMemoryWorkflowService extends PikkuWorkflowService implem
66
66
  branchKeys: Record<string, string>;
67
67
  }>;
68
68
  getNodesWithoutSteps(runId: string, nodeIds: string[]): Promise<string[]>;
69
+ protected listStepStates(runId: string): Promise<Array<StepState & {
70
+ stepName: string;
71
+ }>>;
69
72
  getStepInstances(runId: string): Promise<Array<{
70
73
  stepName: string;
71
74
  status: StepStatus;
@@ -82,9 +85,4 @@ export declare class InMemoryWorkflowService extends PikkuWorkflowService implem
82
85
  graph: any;
83
86
  source: string;
84
87
  } | null>;
85
- getAIGeneratedWorkflows(agentName?: string): Promise<Array<{
86
- workflowName: string;
87
- graphHash: string;
88
- graph: any;
89
- }>>;
90
88
  }
@@ -324,6 +324,16 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
324
324
  }
325
325
  return nodeIds.filter((id) => !existingSteps.has(id));
326
326
  }
327
+ async listStepStates(runId) {
328
+ const prefix = `${runId}:`;
329
+ const steps = [];
330
+ for (const [key, step] of this.steps.entries()) {
331
+ if (!key.startsWith(prefix))
332
+ continue;
333
+ steps.push({ ...step, stepName: key.substring(prefix.length) });
334
+ }
335
+ return steps;
336
+ }
327
337
  async getStepInstances(runId) {
328
338
  const prefix = `${runId}:`;
329
339
  const instances = [];
@@ -387,23 +397,4 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
387
397
  return null;
388
398
  return { graph: version.graph, source: version.source };
389
399
  }
390
- async getAIGeneratedWorkflows(agentName) {
391
- const results = [];
392
- const prefix = agentName ? `ai:${agentName}:` : 'ai:';
393
- for (const [key, value] of this.workflowVersions) {
394
- if (value.source !== 'ai-agent' || value.status !== 'active')
395
- continue;
396
- const separatorIdx = key.lastIndexOf(':');
397
- const wfName = key.substring(0, separatorIdx);
398
- const hash = key.substring(separatorIdx + 1);
399
- if (wfName.startsWith(prefix)) {
400
- results.push({
401
- workflowName: wfName,
402
- graphHash: hash,
403
- graph: value.graph,
404
- });
405
- }
406
- }
407
- return results;
408
- }
409
400
  }
@@ -35,6 +35,13 @@ export interface WorkflowService {
35
35
  }): Promise<{
36
36
  runId: string;
37
37
  }>;
38
+ /**
39
+ * Start a run and wait for it to end.
40
+ *
41
+ * `pollIntervalMs` is the ceiling on the wait between reads of the run, not a
42
+ * fixed cadence: polling starts far shorter than this and widens towards it,
43
+ * so a run that finishes quickly is not held for a whole interval.
44
+ */
38
45
  runToCompletion<I>(name: string, input: I, rpcService: any, options?: {
39
46
  pollIntervalMs?: number;
40
47
  wire?: WorkflowRunWire;
@@ -60,9 +67,4 @@ export interface WorkflowService {
60
67
  graph: any;
61
68
  source: string;
62
69
  } | null>;
63
- getAIGeneratedWorkflows(agentName?: string): Promise<Array<{
64
- workflowName: string;
65
- graphHash: string;
66
- graph: any;
67
- }>>;
68
70
  }
@@ -95,6 +95,13 @@ export type FunctionRuntimeMeta = {
95
95
  * may drive a browser or assert against fixtures.
96
96
  */
97
97
  scenarioStep?: boolean;
98
+ /**
99
+ * The function behind a `pikkuScenario(...)` — a scenario's own body, as
100
+ * opposed to the steps it calls. Marked for the same reason as
101
+ * `scenarioStep`: a scenario is only ever run by `pikku scenario run`, so it
102
+ * has to be held back from the app bootstrap and from every deployed unit.
103
+ */
104
+ scenario?: boolean;
98
105
  mcp?: boolean;
99
106
  readonly?: boolean;
100
107
  deploy?: 'serverless' | 'server' | 'auto';
@@ -256,14 +256,6 @@ export function wrapChannelWithAGUI(inner, options) {
256
256
  });
257
257
  break;
258
258
  }
259
- case 'workflow-created': {
260
- send({
261
- type: 'CUSTOM',
262
- name: 'pikku:workflow-created',
263
- value: { workflowName: event.workflowName, graph: event.graph },
264
- });
265
- break;
266
- }
267
259
  case 'agent-call': {
268
260
  send({
269
261
  type: 'CUSTOM',
@@ -312,8 +312,7 @@ export function createScopedChannel(parent, agentName, session) {
312
312
  event.type === 'tool-call' ||
313
313
  event.type === 'tool-result' ||
314
314
  event.type === 'usage' ||
315
- event.type === 'error' ||
316
- event.type === 'workflow-created') {
315
+ event.type === 'error') {
317
316
  parent.send({ ...event, agent: agentName, session });
318
317
  }
319
318
  else {
@@ -346,12 +346,6 @@ export type AIStreamEvent = {
346
346
  type: 'audio-done';
347
347
  agent?: string;
348
348
  session?: string;
349
- } | {
350
- type: 'workflow-created';
351
- workflowName: string;
352
- graph: any;
353
- agent?: string;
354
- session?: string;
355
349
  } | {
356
350
  type: 'data';
357
351
  name: string;
@@ -116,4 +116,4 @@ export declare const pikkuFetch: <In, Out>(request: Request | PikkuHTTPRequest,
116
116
  * @param {RunHTTPWiringOptions} options - Options such as singleton services, session handling, and error configuration.
117
117
  * @returns {Promise<Out | void>} The output from the route handler or void if an error occurred.
118
118
  */
119
- export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, exposeErrors, generateRequestId, traceId: externalTraceId, }?: RunHTTPWiringOptions) => Promise<Out | void>;
119
+ export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, exposeErrors, generateRequestId, traceId: externalTraceId, maxBodySize, }?: RunHTTPWiringOptions) => Promise<Out | void>;
@@ -386,13 +386,15 @@ export const pikkuFetch = async (request, params = {}) => {
386
386
  */
387
387
  export const fetchData = async (request, response, { skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceDataFromSchema = true, bubbleErrors = false,
388
388
  // Surface the error message + stack on unexpected 500s unless in production.
389
- exposeErrors = !isProduction(), generateRequestId, traceId: externalTraceId, } = {}) => {
389
+ exposeErrors = !isProduction(), generateRequestId, traceId: externalTraceId, maxBodySize, } = {}) => {
390
390
  const singletonServices = getSingletonServices();
391
391
  const createWireServices = getCreateWireServices();
392
392
  let wireServices;
393
393
  let result;
394
394
  // Combine the request and response into one wire object
395
- const pikkuRequest = request instanceof Request ? new PikkuFetchHTTPRequest(request) : request;
395
+ const pikkuRequest = request instanceof Request
396
+ ? new PikkuFetchHTTPRequest(request, { maxBodySize })
397
+ : request;
396
398
  // Resolve traceId: external (e.g. CF-Ray) > x-request-id header > generated
397
399
  let requestId = externalTraceId ?? null;
398
400
  if (!requestId) {
@@ -19,6 +19,8 @@ export type RunHTTPWiringOptions = Partial<{
19
19
  generateRequestId: () => string;
20
20
  /** Pre-resolved trace ID (e.g. CF-Ray). Falls back to x-request-id header or generated ID. */
21
21
  traceId: string;
22
+ /** Maximum request body size in bytes, applied when pikku wraps a fetch `Request`. */
23
+ maxBodySize: number;
22
24
  }>;
23
25
  /**
24
26
  * Represents the HTTP methods supported for API HTTP wirings.
@@ -1,4 +1,5 @@
1
- export { PikkuFetchHTTPRequest } from './pikku-fetch-http-request.js';
1
+ export { PikkuFetchHTTPRequest, DEFAULT_MAX_BODY_SIZE, } from './pikku-fetch-http-request.js';
2
+ export type { PikkuFetchHTTPRequestOptions } from './pikku-fetch-http-request.js';
2
3
  export { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js';
3
4
  export { logRoutes } from './log-http-routes.js';
4
5
  export { fetch, fetchData, wireHTTP, addHTTPMiddleware, addHTTPPermission, } from './http-runner.js';
@@ -1,4 +1,4 @@
1
- export { PikkuFetchHTTPRequest } from './pikku-fetch-http-request.js';
1
+ export { PikkuFetchHTTPRequest, DEFAULT_MAX_BODY_SIZE, } from './pikku-fetch-http-request.js';
2
2
  export { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js';
3
3
  export { logRoutes } from './log-http-routes.js';
4
4
  export { fetch, fetchData, wireHTTP, addHTTPMiddleware, addHTTPPermission, } from './http-runner.js';
@@ -1,4 +1,14 @@
1
1
  import type { HTTPMethod, PikkuHTTPRequest, PikkuQuery } from './http.types.js';
2
+ /**
3
+ * The largest request body read into memory when no limit is configured. Ample
4
+ * for JSON APIs and typical uploads while keeping a single request's memory
5
+ * footprint bounded.
6
+ */
7
+ export declare const DEFAULT_MAX_BODY_SIZE: number;
8
+ export type PikkuFetchHTTPRequestOptions = Partial<{
9
+ /** Maximum request body size in bytes. Defaults to {@link DEFAULT_MAX_BODY_SIZE}. */
10
+ maxBodySize: number;
11
+ }>;
2
12
  /**
3
13
  * Abstract class representing a pikku request.
4
14
  * @template In - The type of the request body.
@@ -7,7 +17,7 @@ import type { HTTPMethod, PikkuHTTPRequest, PikkuQuery } from './http.types.js';
7
17
  export declare class PikkuFetchHTTPRequest<In = unknown> implements PikkuHTTPRequest<In> {
8
18
  #private;
9
19
  private request;
10
- constructor(request: Request);
20
+ constructor(request: Request, { maxBodySize }?: PikkuFetchHTTPRequestOptions);
11
21
  method(): HTTPMethod;
12
22
  path(): string;
13
23
  /**