@pikku/core 0.12.80 → 0.12.83

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 (237) hide show
  1. package/CHANGELOG.md +345 -0
  2. package/dist/errors/index.d.ts +1 -1
  3. package/dist/errors/index.js +1 -1
  4. package/dist/function/function-runner.js +2 -5
  5. package/dist/function/index.d.ts +1 -1
  6. package/dist/index.d.ts +11 -11
  7. package/dist/index.js +3 -3
  8. package/dist/pikku-state.js +4 -0
  9. package/dist/services/ai-agent-runner-service.d.ts +7 -0
  10. package/dist/services/ai-run-state-service.d.ts +10 -0
  11. package/dist/services/in-memory-ai-run-state-service.d.ts +5 -1
  12. package/dist/services/in-memory-ai-run-state-service.js +9 -0
  13. package/dist/services/index.d.ts +15 -16
  14. package/dist/services/index.js +5 -5
  15. package/dist/services/meta-service.d.ts +2 -1
  16. package/dist/services/scoped-credential-service.d.ts +21 -0
  17. package/dist/services/scoped-credential-service.js +53 -0
  18. package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
  19. package/dist/types/core.types.d.ts +2 -3
  20. package/dist/types/state.types.d.ts +19 -1
  21. package/dist/wirings/actor-flow/index.d.ts +1 -1
  22. package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
  23. package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
  24. package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
  25. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
  26. package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
  27. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
  28. package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
  29. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -0
  30. package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
  31. package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
  32. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
  33. package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
  34. package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
  35. package/dist/wirings/ai-agent/index.d.ts +8 -7
  36. package/dist/wirings/ai-agent/index.js +5 -4
  37. package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
  38. package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
  39. package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
  40. package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
  41. package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
  42. package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
  43. package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
  44. package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
  45. package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
  46. package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
  47. package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
  48. package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
  49. package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
  50. package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
  51. package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
  52. package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
  53. package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
  54. package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
  55. package/dist/wirings/ai-scorer/index.d.ts +6 -0
  56. package/dist/wirings/ai-scorer/index.js +5 -0
  57. package/dist/wirings/channel/index.d.ts +5 -6
  58. package/dist/wirings/channel/index.js +3 -4
  59. package/dist/wirings/channel/local/local-channel-runner.js +8 -1
  60. package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
  61. package/dist/wirings/cli/channel/index.d.ts +1 -2
  62. package/dist/wirings/cli/channel/index.js +0 -1
  63. package/dist/wirings/cli/cli-runner.js +13 -1
  64. package/dist/wirings/credential/index.d.ts +1 -1
  65. package/dist/wirings/gateway/index.d.ts +1 -1
  66. package/dist/wirings/http/http-runner.js +8 -2
  67. package/dist/wirings/http/index.d.ts +1 -2
  68. package/dist/wirings/mcp/index.d.ts +1 -1
  69. package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
  70. package/dist/wirings/mcp/mcp-runner.js +18 -5
  71. package/dist/wirings/persona/index.d.ts +3 -4
  72. package/dist/wirings/persona/index.js +2 -3
  73. package/dist/wirings/queue/index.d.ts +1 -3
  74. package/dist/wirings/queue/index.js +1 -3
  75. package/dist/wirings/rpc/addon-runner.d.ts +8 -0
  76. package/dist/wirings/rpc/addon-runner.js +31 -3
  77. package/dist/wirings/rpc/rpc-runner.js +4 -0
  78. package/dist/wirings/rpc/rpc-types.d.ts +8 -0
  79. package/dist/wirings/rpc/wire-addon.d.ts +25 -0
  80. package/dist/wirings/rpc/wire-addon.js +8 -0
  81. package/dist/wirings/scheduler/index.d.ts +1 -1
  82. package/dist/wirings/trigger/index.d.ts +1 -1
  83. package/dist/wirings/virtual-user/index.d.ts +5 -6
  84. package/dist/wirings/virtual-user/index.js +2 -4
  85. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
  86. package/dist/wirings/workflow/feature.d.ts +2 -1
  87. package/dist/wirings/workflow/index.d.ts +5 -16
  88. package/dist/wirings/workflow/index.js +1 -9
  89. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -7
  90. package/dist/wirings/workflow/pikku-scenario-service.js +48 -13
  91. package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
  92. package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
  93. package/dist/wirings/workflow/scenario.types.d.ts +37 -0
  94. package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
  95. package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
  96. package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
  97. package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
  98. package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
  99. package/dist/wirings/workflow/workflow-approval.js +65 -2
  100. package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
  101. package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
  102. package/dist/wirings/workflow/workflow.types.d.ts +2 -37
  103. package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
  104. package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
  105. package/knowledge/decisions/internals/index.md +30 -3
  106. package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
  107. package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
  108. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
  109. package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
  110. package/knowledge/decisions/security/index.md +3 -1
  111. package/knowledge/questions/index.md +1 -1
  112. package/package.json +3 -2
  113. package/scripts/generate-api-report.mts +143 -18
  114. package/src/api-report.test.ts +2 -2
  115. package/src/errors/index.ts +1 -1
  116. package/src/function/function-runner.test.ts +52 -0
  117. package/src/function/function-runner.ts +5 -9
  118. package/src/function/index.ts +0 -2
  119. package/src/index.ts +0 -35
  120. package/src/pikku-state.ts +5 -0
  121. package/src/public-surface.json +81 -118
  122. package/src/services/ai-agent-runner-service.ts +12 -1
  123. package/src/services/ai-run-state-service.ts +11 -0
  124. package/src/services/in-memory-ai-run-state-service.ts +13 -0
  125. package/src/services/index.ts +7 -58
  126. package/src/services/meta-service.ts +2 -4
  127. package/src/services/scoped-credential-service.test.ts +86 -0
  128. package/src/services/scoped-credential-service.ts +63 -0
  129. package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
  130. package/src/types/core.types.ts +4 -7
  131. package/src/types/state.types.ts +21 -1
  132. package/src/wirings/actor-flow/index.ts +0 -3
  133. package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
  134. package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
  135. package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
  136. package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
  137. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
  138. package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
  139. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -0
  140. package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
  141. package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
  142. package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
  143. package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
  144. package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
  145. package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
  146. package/src/wirings/ai-agent/index.ts +2 -16
  147. package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
  148. package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
  149. package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
  150. package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
  151. package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
  152. package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
  153. package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
  154. package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
  155. package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
  156. package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
  157. package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
  158. package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
  159. package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
  160. package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
  161. package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
  162. package/src/wirings/ai-scorer/index.ts +24 -0
  163. package/src/wirings/channel/index.ts +1 -20
  164. package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
  165. package/src/wirings/channel/local/local-channel-runner.ts +8 -1
  166. package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
  167. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
  168. package/src/wirings/cli/channel/index.ts +0 -7
  169. package/src/wirings/cli/cli-runner.test.ts +68 -0
  170. package/src/wirings/cli/cli-runner.ts +18 -1
  171. package/src/wirings/credential/index.ts +0 -1
  172. package/src/wirings/gateway/index.ts +0 -3
  173. package/src/wirings/http/http-runner.test.ts +66 -0
  174. package/src/wirings/http/http-runner.ts +10 -2
  175. package/src/wirings/http/index.ts +1 -1
  176. package/src/wirings/mcp/index.ts +0 -1
  177. package/src/wirings/mcp/mcp-runner.test.ts +181 -0
  178. package/src/wirings/mcp/mcp-runner.ts +35 -5
  179. package/src/wirings/persona/index.ts +0 -8
  180. package/src/wirings/queue/index.ts +0 -14
  181. package/src/wirings/rpc/addon-runner.ts +62 -3
  182. package/src/wirings/rpc/addon-secrets.test.ts +391 -0
  183. package/src/wirings/rpc/rpc-runner.test.ts +2 -0
  184. package/src/wirings/rpc/rpc-runner.ts +4 -0
  185. package/src/wirings/rpc/rpc-types.ts +8 -0
  186. package/src/wirings/rpc/wire-addon.ts +33 -0
  187. package/src/wirings/scheduler/index.ts +0 -1
  188. package/src/wirings/trigger/index.ts +0 -1
  189. package/src/wirings/virtual-user/index.ts +0 -16
  190. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
  191. package/src/wirings/workflow/feature.ts +2 -5
  192. package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
  193. package/src/wirings/workflow/index.ts +2 -68
  194. package/src/wirings/workflow/pikku-scenario-service.ts +81 -16
  195. package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
  196. package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
  197. package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
  198. package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
  199. package/src/wirings/workflow/scenario-step.types.ts +8 -0
  200. package/src/wirings/workflow/scenario.types.ts +63 -0
  201. package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
  202. package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
  203. package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
  204. package/src/wirings/workflow/workflow-approval.ts +113 -9
  205. package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
  206. package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
  207. package/src/wirings/workflow/workflow.types.ts +1 -63
  208. package/src/wirings-stay-decoupled.test.ts +6 -2
  209. package/tsconfig.tsbuildinfo +1 -1
  210. package/dist/internal.d.ts +0 -3
  211. package/dist/internal.js +0 -2
  212. package/dist/middleware/timeout.d.ts +0 -9
  213. package/dist/middleware/timeout.js +0 -15
  214. package/dist/pikku-response.d.ts +0 -6
  215. package/dist/pikku-response.js +0 -6
  216. package/dist/services/gopass-secrets.d.ts +0 -15
  217. package/dist/services/gopass-secrets.js +0 -76
  218. package/dist/services/http-scenario-actors.d.ts +0 -75
  219. package/dist/services/http-scenario-actors.js +0 -195
  220. package/dist/services/http-user-flow-actors.d.ts +0 -67
  221. package/dist/services/http-user-flow-actors.js +0 -193
  222. package/dist/services/scenario-actors-service.d.ts +0 -127
  223. package/dist/services/scenario-actors-service.js +0 -40
  224. package/dist/services/user-flow-actors-service.d.ts +0 -39
  225. package/dist/wirings/credential/wire-credential.d.ts +0 -48
  226. package/dist/wirings/credential/wire-credential.js +0 -47
  227. package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
  228. package/dist/wirings/oauth2/oauth2-client.js +0 -263
  229. package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
  230. package/dist/wirings/oauth2/oauth2-routes.js +0 -146
  231. package/dist/wirings/scope/wire-scope.d.ts +0 -33
  232. package/dist/wirings/scope/wire-scope.js +0 -32
  233. package/dist/wirings/workflow/dsl/index.d.ts +0 -5
  234. package/dist/wirings/workflow/dsl/index.js +0 -4
  235. package/dist/wirings/workflow/graph/index.d.ts +0 -5
  236. package/dist/wirings/workflow/graph/index.js +0 -4
  237. /package/dist/{services/user-flow-actors-service.js → wirings/workflow/scenario.types.js} +0 -0
@@ -65,6 +65,20 @@ export interface WorkflowExpectServiceOptions extends WorkflowStepOptions {
65
65
  times?: number
66
66
  }
67
67
 
68
+ /** Options for workflow.expectScore() */
69
+ export interface WorkflowExpectScoreOptions extends WorkflowStepOptions {
70
+ /** Fail below this score. Defaults to 0.5 — a scorer that answered at all. */
71
+ atLeast?: number
72
+ /** Fail above this score, for a scorer where high is the failure. */
73
+ atMost?: number
74
+ /**
75
+ * The known-correct answer, which is what a reference-based judge grades
76
+ * against. Live traffic has no answer key, so a judge declared
77
+ * `requiresReference` is only ever reachable from a scenario.
78
+ */
79
+ reference?: string
80
+ }
81
+
68
82
  /**
69
83
  * Type signature for workflow.do() RPC form - used by inspector
70
84
  */
@@ -114,12 +128,55 @@ export type WorkflowWireSleep = (
114
128
  */
115
129
  export type WorkflowWireSuspend = (reason: string) => Promise<void>
116
130
 
131
+ /**
132
+ * Who is allowed to answer an approval gate, relative to the user who started
133
+ * the run.
134
+ *
135
+ * - `any` — anyone the approve entrypoint lets through. The gate is a pause for
136
+ * a decision, not an authorization boundary.
137
+ * - `owner` — only the user who started the run, so a gate can be used to
138
+ * confirm an action against the same person who requested it.
139
+ * - `not-initiator` — anyone *except* the user who started the run: four-eyes.
140
+ * A run with no recorded initiator has nobody to exclude, so this degrades to
141
+ * requiring a signed-in decider.
142
+ */
143
+ export type WorkflowApprovalApprovers = 'any' | 'owner' | 'not-initiator'
144
+
145
+ /**
146
+ * Who may answer an approval gate. Declared on the gate rather than the
147
+ * entrypoint because it is a property of the decision being made, and because
148
+ * one workflow can hold several gates with different requirements.
149
+ */
150
+ export interface WorkflowApprovalPolicy {
151
+ /**
152
+ * Defaults to `any`. See {@link WorkflowApprovalApprovers}.
153
+ */
154
+ approvers?: WorkflowApprovalApprovers
155
+ /**
156
+ * Additionally require the decider's session to hold this scope, so
157
+ * "a second pair of eyes" can be narrowed to "a second pair of *senior*
158
+ * eyes". Combines with `approvers` — both must pass.
159
+ */
160
+ approverScope?: string
161
+ }
162
+
163
+ /**
164
+ * The decider, reduced to the two facts a policy can be expressed in terms of.
165
+ * Recorded alongside the decision so the gate can be judged on replay — the
166
+ * session itself is long gone by then — and carried into the settled outcome so
167
+ * the answer keeps its provenance.
168
+ */
169
+ export interface ApprovalDecider {
170
+ userId?: string
171
+ scopes?: string[]
172
+ }
173
+
117
174
  /**
118
175
  * Options for workflow.approval().
119
176
  */
120
177
  export interface WorkflowApprovalOptions<
121
178
  TSchema extends StandardSchemaV1 = StandardSchemaV1,
122
- > {
179
+ > extends WorkflowApprovalPolicy {
123
180
  /**
124
181
  * Schema the decision payload is validated against. This is a VALUE, not a
125
182
  * type generic: the payload arrives from an untrusted caller over the approve
@@ -142,7 +199,18 @@ export interface WorkflowApprovalOptions<
142
199
  * carried in `data` and is the application's business, not the framework's.
143
200
  */
144
201
  export type ApprovalOutcome<T> =
145
- | { status: 'decided'; data: T }
202
+ | {
203
+ status: 'decided'
204
+ data: T
205
+ /**
206
+ * Who answered, and when. Run state holds the decision only while the gate
207
+ * is open — it is overwritten by the next write and cleared outright when a
208
+ * decision is refused — so the settled answer carries its own provenance
209
+ * into the step result, which is append-only.
210
+ */
211
+ decidedBy?: ApprovalDecider
212
+ decidedAt?: string
213
+ }
146
214
  | { status: 'expired' }
147
215
 
148
216
  /**
@@ -416,6 +484,10 @@ export interface ApprovalStepMeta {
416
484
  outputVar?: string
417
485
  /** Expiry duration, when one was given */
418
486
  expiry?: string | number
487
+ /** Who may answer the gate, when the default was not used */
488
+ approvers?: WorkflowApprovalApprovers
489
+ /** Scope the decider must hold, when one was required */
490
+ approverScope?: string
419
491
  }
420
492
 
421
493
  /**
@@ -531,15 +603,14 @@ export interface PikkuWorkflowWire {
531
603
  /**
532
604
  * What a scenario has accumulated so far, shared by its body and its hooks.
533
605
  *
534
- * Typed as a partial of the scenario's own output because a scenario can fail
535
- * at any step: the shape describes what a *completed* run produces, and the
536
- * context holds however much of it this run reached. A scenario declaring no
537
- * object output gets an open record rather than `Partial<never>`.
606
+ * `Partial` because a scenario can fail at any step: `Out` describes what a
607
+ * *completed* run produces, and the context holds however much of it this run
608
+ * reached.
538
609
  *
539
- * `any` collapses to `any`, not to the union of both branches. `PikkuWire`'s
540
- * default depends on that: the wire type is also used as a generic *constraint*,
541
- * and a constraint carrying a concrete context would reject every wire whose
542
- * scenario output differs from it.
610
+ * The `0 extends 1 & Out` branch keeps `any` collapsing to `any` rather than to
611
+ * the union of the other two. `PikkuWire` and `PikkuFunctionConfig` both use
612
+ * this type as a generic *constraint*, and a constraint carrying a concrete
613
+ * context would reject every wire whose scenario output differs from it.
543
614
  */
544
615
  export type ScenarioContext<Out = unknown> = 0 extends 1 & Out
545
616
  ? any
@@ -549,12 +620,9 @@ export type ScenarioContext<Out = unknown> = 0 extends 1 & Out
549
620
 
550
621
  export interface PikkuScenarioWire<Out = unknown> extends PikkuWorkflowWire {
551
622
  /**
552
- * Scratch the body writes and the `before`/`after` hooks read.
553
- *
554
- * A hook is a separate function, so it cannot see the body's locals — which
555
- * is why teardown had nowhere to learn the ids the body minted. Assigning
556
- * them here (`scenario.context.projectId = project.projectId`) hands them to
557
- * `after`, which runs in a `finally` and so cleans up on a failed run too.
623
+ * Scratch the body writes and the `before`/`after` hooks read. A hook is a
624
+ * separate function and cannot see the body's locals, so this is how teardown
625
+ * learns the ids the body minted.
558
626
  *
559
627
  * Deliberately *not* a world: it is scoped to one run, and scenario steps
560
628
  * cannot reach it. Steps stay pure functions of their declared inputs, so the
@@ -589,6 +657,18 @@ export interface PikkuScenarioWire<Out = unknown> extends PikkuWorkflowWire {
589
657
  options?: WorkflowExpectServiceOptions
590
658
  ) => Promise<void>
591
659
 
660
+ /**
661
+ * Grade-assertion step: runs one declared scorer against a finished agent run
662
+ * and asserts the score. Returns the grade, so a scenario can report the
663
+ * reason a judge gave rather than only that it fell short.
664
+ */
665
+ expectScore: (
666
+ stepName: string,
667
+ runId: string,
668
+ scorerName: string,
669
+ options?: WorkflowExpectScoreOptions
670
+ ) => Promise<{ score: number; reason?: string }>
671
+
592
672
  /**
593
673
  * Run a registered scenario step, as the setup the scenario starts from.
594
674
  *
@@ -1,9 +1,6 @@
1
1
  import { pikkuState } from '../../pikku-state.js'
2
- import type {
3
- CoreFeature,
4
- CoreWorkflow,
5
- FeaturePlanEntry,
6
- } from './workflow.types.js'
2
+ import type { CoreWorkflow } from './workflow.types.js'
3
+ import type { CoreFeature, FeaturePlanEntry } from './scenario.types.js'
7
4
 
8
5
  export const addFeature = (
9
6
  featureId: string,
@@ -54,6 +54,78 @@ describe('graph-runner bugs', () => {
54
54
  assert.equal(run?.status, 'running')
55
55
  })
56
56
 
57
+ test('startWorkflow rejects a startNode that is not a declared entry node', async () => {
58
+ const ws = new InMemoryWorkflowService()
59
+ const metaState = pikkuState(null, 'workflows', 'meta')
60
+ metaState['testStartNodeGate'] = {
61
+ name: 'testStartNodeGate',
62
+ pikkuFuncId: 'testStartNodeGate',
63
+ source: 'graph',
64
+ entryNodeIds: ['gate'],
65
+ graphHash: 'startnode-hash',
66
+ nodes: {
67
+ // 'charge' reads only trigger input, so it is dependency-free and would
68
+ // otherwise be directly startable — skipping the 'gate' entry node.
69
+ gate: { nodeId: 'gate', rpcName: 'checkEligibility', next: 'charge' },
70
+ charge: { nodeId: 'charge', rpcName: 'capturePayment' },
71
+ },
72
+ }
73
+
74
+ await assert.rejects(
75
+ () =>
76
+ ws.startWorkflow(
77
+ 'testStartNodeGate',
78
+ {},
79
+ { type: 'test' },
80
+ { rpcWithWire: async () => ({}) },
81
+ { startNode: 'charge', inline: true }
82
+ ),
83
+ /not a declared entry node/
84
+ )
85
+ assert.equal(
86
+ [...(ws as any).runs.keys()].length,
87
+ 0,
88
+ 'no run should be created for a non-entry startNode'
89
+ )
90
+ delete metaState['testStartNodeGate']
91
+ })
92
+
93
+ test('startWorkflow accepts a startNode that is a declared entry node', async () => {
94
+ const ws = new InMemoryWorkflowService()
95
+ const metaState = pikkuState(null, 'workflows', 'meta')
96
+ // 'b' is a declared entry but carries an unknown input ref, so it clears the
97
+ // startNode gate and then throws at reference validation — proving the gate
98
+ // admitted it, without dispatching real work.
99
+ metaState['testStartNodeAllowed'] = {
100
+ name: 'testStartNodeAllowed',
101
+ pikkuFuncId: 'testStartNodeAllowed',
102
+ source: 'graph',
103
+ entryNodeIds: ['a', 'b'],
104
+ graphHash: 'startnode-ok-hash',
105
+ nodes: {
106
+ a: { nodeId: 'a', rpcName: 'doA' },
107
+ b: {
108
+ nodeId: 'b',
109
+ rpcName: 'doB',
110
+ input: { dep: { $ref: 'missingNode', path: 'value' } },
111
+ },
112
+ },
113
+ }
114
+ await assert.rejects(
115
+ () =>
116
+ ws.startWorkflow(
117
+ 'testStartNodeAllowed',
118
+ {},
119
+ { type: 'test' },
120
+ { rpcWithWire: async () => ({}) },
121
+ { startNode: 'b', inline: true }
122
+ ),
123
+ /references unknown node 'missingNode'/,
124
+ 'a declared entry node must pass the startNode gate (reaching ref validation)'
125
+ )
126
+ delete metaState['testStartNodeAllowed']
127
+ })
128
+
57
129
  test('runWorkflowGraph should throw and not create a run for unknown input refs', async () => {
58
130
  const ws = new InMemoryWorkflowService()
59
131
 
@@ -13,8 +13,8 @@ export {
13
13
  assertWorkflowRunOwner,
14
14
  WorkflowRunForbiddenError,
15
15
  } from './workflow-run-ownership.js'
16
+ export { WorkflowApprovalForbiddenError } from './workflow-approval-policy.js'
16
17
  export type {
17
- RunLifecycleContext,
18
18
  WorkflowRunEngine,
19
19
  WorkflowRunExtension,
20
20
  } from './workflow-run-engine.types.js'
@@ -28,16 +28,9 @@ export {
28
28
  reconstructStateAt,
29
29
  reconstructFinalState,
30
30
  } from './run-timeline.js'
31
- export type {
32
- RunTimeline,
33
- RunTimelineEvent,
34
- ReconstructedRunState,
35
- ReconstructedStep,
36
- RunPhase,
37
- } from './run-timeline.js'
31
+ export type { RunTimeline, ReconstructedRunState } from './run-timeline.js'
38
32
 
39
33
  export { addWorkflow } from './dsl/workflow-runner.js'
40
- export { addFeature, resolveFeatureScenarios } from './feature.js'
41
34
 
42
35
  export { template, type TemplateString } from './graph/template.js'
43
36
  export {
@@ -45,12 +38,6 @@ export {
45
38
  type PikkuWorkflowGraphConfig,
46
39
  type PikkuWorkflowGraphResult,
47
40
  } from './graph/wire-workflow-graph.js'
48
-
49
- export {
50
- pikkuWorkflowWorkerFunc,
51
- pikkuWorkflowOrchestratorFunc,
52
- pikkuWorkflowSleeperFunc,
53
- } from './workflow-queue-workers.js'
54
41
  export type {
55
42
  WorkflowStepInput as WorkflowStepQueueInput,
56
43
  PikkuWorkflowOrchestratorInput,
@@ -72,35 +59,22 @@ export type {
72
59
  WorkflowRunService,
73
60
  WorkflowRunMirror,
74
61
  CoreWorkflow,
75
- CoreFeature,
76
- CoreFeatureScenario,
77
- FeatureMeta,
78
- FeatureMetaEntry,
79
- FeaturesMeta,
80
- FeaturePlanEntry,
81
62
  PikkuWorkflow,
82
63
  ContextVariable,
83
64
  WorkflowContext,
84
65
  WorkflowsMeta,
85
- WorkflowRuntimeMeta,
86
66
  WorkflowsRuntimeMeta,
87
67
  } from './workflow.types.js'
88
68
 
89
69
  export type {
90
70
  WorkflowStepOptions,
91
71
  WorkflowWireDoRPC,
92
- WorkflowWireDoInline,
93
- WorkflowWireSleep,
94
- WorkflowWireSuspend,
95
- WorkflowWireApproval,
96
72
  WorkflowApprovalOptions,
97
73
  ApprovalOutcome,
98
74
  InputSource,
99
75
  OutputBinding,
100
76
  RpcStepMeta,
101
- SimpleCondition,
102
77
  Condition,
103
- BranchCase,
104
78
  BranchStepMeta,
105
79
  ParallelGroupStepMeta,
106
80
  FanoutStepMeta,
@@ -115,47 +89,7 @@ export type {
115
89
  SwitchStepMeta,
116
90
  FilterStepMeta,
117
91
  ArrayPredicateStepMeta,
118
- ScenarioStepInvocation,
119
- ScenarioStepMeta,
120
92
  WorkflowStepMeta,
121
93
  WorkflowStepWire,
122
94
  PikkuWorkflowWire,
123
- PikkuScenarioWire,
124
95
  } from './workflow.types.js'
125
-
126
- export type {
127
- ScenarioStepPhase,
128
- ScenarioStepKind,
129
- ScenarioStepOptions,
130
- PikkuScenarioStepWire,
131
- ScenarioEnvironment,
132
- ScenarioSurface,
133
- ScenarioSurfaceResolution,
134
- } from './scenario-step.types.js'
135
- export { SCENARIO_SURFACES } from './scenario-step.types.js'
136
-
137
- // Which of a step's bindings run: one for an action, every witness for a `then`
138
- export { resolveScenarioSurfaces, witnessesAgree } from './scenario-surface.js'
139
-
140
- export { requireActor, requireScenarioEnv } from './scenario-step-guards.js'
141
-
142
- export { pollUntil, type PollOptions } from './scenario-poll.js'
143
-
144
- export { createCookieJar } from './scenario-cookie-jar.js'
145
- export type { ScenarioCookieJar } from './scenario-cookie-jar.js'
146
-
147
- export type {
148
- ScenarioHttpResponse,
149
- ScenarioJsonRequest,
150
- } from '../../services/personas-service.js'
151
- // The readers themselves live on `@pikku/core/persona`; workflow is a production
152
- // wiring and must not pull scenario runtime in behind it.
153
-
154
- export type {
155
- PikkuBrowserWire,
156
- TestIdSelector,
157
- ScenarioBrowserProvider,
158
- ScenarioBrowserFailure,
159
- } from './scenario-step.types.js'
160
-
161
- export { composeStepProse, renderStepTemplate } from './scenario-prose.js'
@@ -27,13 +27,36 @@ import type {
27
27
  } from './scenario-step.types.js'
28
28
  import { resolveScenarioSurfaces, witnessesAgree } from './scenario-surface.js'
29
29
  import type {
30
- PikkuScenarioWire,
31
30
  PikkuWorkflowWire,
31
+ WorkflowQueueOptions,
32
+ } from './workflow.types.js'
33
+ import type { PikkuScenarioWire } from './scenario.types.js'
34
+ import type {
32
35
  WorkflowExpectEventuallyOptions,
33
36
  WorkflowExpectErrorOptions,
37
+ WorkflowExpectScoreOptions,
34
38
  WorkflowExpectServiceOptions,
35
- WorkflowQueueOptions,
36
- } from './workflow.types.js'
39
+ } from './dsl/workflow-dsl.types.js'
40
+
41
+ export { addFeature, resolveFeatureScenarios } from './feature.js'
42
+ export type * from './scenario.types.js'
43
+ export { SCENARIO_SURFACES } from './scenario-step.types.js'
44
+
45
+ // Which of a step's bindings run: one for an action, every witness for a `then`
46
+ export { resolveScenarioSurfaces } from './scenario-surface.js'
47
+
48
+ export { pollUntil, type PollOptions } from './scenario-poll.js'
49
+ export { createCookieJar } from './scenario-cookie-jar.js'
50
+ export type { ScenarioCookieJar } from './scenario-cookie-jar.js'
51
+ export { composeStepProse, renderStepTemplate } from './scenario-prose.js'
52
+
53
+ export { requireActor, requireScenarioEnv } from './scenario-step-guards.js'
54
+
55
+ export type {
56
+ ScenarioHttpResponse,
57
+ ScenarioJsonRequest,
58
+ } from '../../services/personas-service.js'
59
+ // The readers themselves live on `@pikku/core/persona`
37
60
 
38
61
  /**
39
62
  * A workflow service with the scenario capability attached — the two lines
@@ -245,9 +268,9 @@ export class PikkuScenarioService implements WorkflowRunExtension {
245
268
  // Scenario actors per run: live authenticated clients (cookie jars) are
246
269
  // process-local by nature, so they ride this map, never the persisted wire.
247
270
  private runActors = new Map<string, ScenarioPersonas>()
248
- // What each run has accumulated so far. Process-local like the actors above:
249
- // a scenario is a single in-process run, so the body and its hooks share one
250
- // object rather than reading it back off the persisted wire.
271
+ // What each run has accumulated so far — process-local like the actors above,
272
+ // so the body and its hooks share one object rather than reading it back off
273
+ // the persisted wire.
251
274
  private runContexts = new Map<string, Record<string, unknown>>()
252
275
  private scenarioBrowserProvider?: ScenarioBrowserProvider
253
276
  private scenarioEnvironment?: ScenarioEnvironment
@@ -319,19 +342,19 @@ export class PikkuScenarioService implements WorkflowRunExtension {
319
342
  }
320
343
 
321
344
  /**
322
- * What the run has accumulated, for a reporter that wants to say what a
323
- * failed scenario left behind. Undefined for a plain workflow.
345
+ * What the run has accumulated.
346
+ *
347
+ * Undefined only for a run whose wire was never decorated — decoration calls
348
+ * `contextForRun` unconditionally, so a plain workflow that has reached a
349
+ * step holds an empty context rather than none.
324
350
  */
325
351
  public getRunContext(runId: string): Record<string, unknown> | undefined {
326
352
  return this.runContexts.get(runId)
327
353
  }
328
354
 
329
355
  /**
330
- * The run's context, created on demand.
331
- *
332
- * `attachRunContext` seeds it for an ordinary scenario run, but the wire is
333
- * also built on paths that skip attach; without this the body would be
334
- * writing to an `undefined` and teardown would read nothing.
356
+ * The run's context, created on demand because the wire is also built on
357
+ * paths that skip `attachRunContext`.
335
358
  */
336
359
  private contextForRun(runId: string): Record<string, unknown> {
337
360
  let runContext = this.runContexts.get(runId)
@@ -539,9 +562,6 @@ export class PikkuScenarioService implements WorkflowRunExtension {
539
562
  rpcService,
540
563
  })
541
564
  Object.assign(workflowWire, {
542
- // One object per run, resolved through the map rather than created here,
543
- // so the body and its before/after hooks share the same scratch even
544
- // though the hooks are separate functions that cannot see its locals.
545
565
  context: this.contextForRun(runId),
546
566
 
547
567
  // Durable polling step: invoke an RPC (as an actor when options.as is
@@ -684,6 +704,51 @@ export class PikkuScenarioService implements WorkflowRunExtension {
684
704
  )
685
705
  },
686
706
 
707
+ expectScore: async (
708
+ stepName: string,
709
+ agentRunId: string,
710
+ scorerName: string,
711
+ options?: WorkflowExpectScoreOptions
712
+ ) => {
713
+ this.engine.verifyStepName(stepName)
714
+ return await this.engine.inlineStep(
715
+ runId,
716
+ stepName,
717
+ async () => {
718
+ const rpcName = 'pikkuScenarioGradeRun'
719
+ const data = {
720
+ runId: agentRunId,
721
+ scorer: scorerName,
722
+ ...(options?.reference !== undefined
723
+ ? { reference: options.reference }
724
+ : {}),
725
+ }
726
+ const grade: { score: number; reason?: string } = options?.actor
727
+ ? await options.actor.invoke(rpcName, data)
728
+ : await rpcService.rpcWithWire(rpcName, data, {})
729
+
730
+ // A scorer that graded is a scorer that answered, so an unstated
731
+ // bound still fails a zero rather than passing anything at all.
732
+ const atLeast = options?.atLeast ?? 0.5
733
+ const failed =
734
+ grade.score < atLeast ||
735
+ (options?.atMost !== undefined && grade.score > options.atMost)
736
+ if (failed) {
737
+ const bound =
738
+ options?.atMost !== undefined
739
+ ? `between ${atLeast} and ${options.atMost}`
740
+ : `at least ${atLeast}`
741
+ throw new Error(
742
+ `[workflow] expectScore '${stepName}' expected '${scorerName}' to grade run ${agentRunId} ${bound}, got ${grade.score}` +
743
+ `${grade.reason ? `: ${grade.reason}` : ''}`
744
+ )
745
+ }
746
+ return grade
747
+ },
748
+ options
749
+ )
750
+ },
751
+
687
752
  // knowledge: decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md
688
753
  given: (stepName, stepFunc, data, options) =>
689
754
  this.scenarioStep(
@@ -661,12 +661,9 @@ describe('pikku-workflow-service approval', () => {
661
661
 
662
662
  const run = await ws.getRun(runId)
663
663
  assert.equal(run?.status, 'completed')
664
- assert.deepEqual(run?.output, {
665
- decision: {
666
- status: 'decided',
667
- data: { approved: true, comment: 'lgtm' },
668
- },
669
- })
664
+ const decision = (run?.output as any)?.decision
665
+ assert.equal(decision.status, 'decided')
666
+ assert.deepEqual(decision.data, { approved: true, comment: 'lgtm' })
670
667
 
671
668
  cleanup()
672
669
  })
@@ -714,9 +711,9 @@ describe('pikku-workflow-service approval', () => {
714
711
  await ws.runWorkflowJob(runId, {})
715
712
  const run = await ws.getRun(runId)
716
713
  assert.equal(run?.status, 'completed')
717
- assert.deepEqual(run?.output, {
718
- decision: { status: 'decided', data: { approved: false } },
719
- })
714
+ const decision = (run?.output as any)?.decision
715
+ assert.equal(decision.status, 'decided')
716
+ assert.deepEqual(decision.data, { approved: false })
720
717
 
721
718
  cleanup()
722
719
  })
@@ -834,9 +831,13 @@ describe('pikku-workflow-service approval', () => {
834
831
 
835
832
  const run = await ws.getRun(runId)
836
833
  assert.equal(run?.status, 'completed')
837
- assert.deepEqual(run?.output, {
838
- decision: { status: 'decided', data: { approved: true } },
839
- })
834
+ const decision = (run?.output as any)?.decision
835
+ assert.equal(decision.status, 'decided')
836
+ assert.deepEqual(decision.data, { approved: true })
837
+ // Answered with no session, so there is nobody to name — but when the
838
+ // answer arrived is recorded either way.
839
+ assert.equal(decision.decidedBy, undefined)
840
+ assert.ok(!Number.isNaN(Date.parse(decision.decidedAt)))
840
841
 
841
842
  cleanup()
842
843
  })
@@ -47,7 +47,7 @@ import { PikkuMissingMetaError } from '../../errors/errors.js'
47
47
  import { RPCNotFoundError } from '../rpc/rpc-runner.js'
48
48
  import type { PikkuRPC } from '../rpc/rpc-types.js'
49
49
  import { deriveInvocationId } from './workflow-invocation-id.js'
50
- import { assertWorkflowRunOwner } from './workflow-run-ownership.js'
50
+ import { approvalDeciderFrom } from './workflow-approval-policy.js'
51
51
  import {
52
52
  buildRunTimeline,
53
53
  reconstructStateAt,
@@ -95,6 +95,7 @@ import {
95
95
  recordApprovalDecision,
96
96
  type ApprovalStore,
97
97
  } from './workflow-approval.js'
98
+ import { auditApprovalDecision } from './workflow-approval-audit.js'
98
99
  import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js'
99
100
  import {
100
101
  RedispatchBackoff,
@@ -844,6 +845,22 @@ export abstract class PikkuWorkflowService implements WorkflowService {
844
845
  }
845
846
 
846
847
  if (workflowMeta.source === 'graph') {
848
+ // A caller-supplied startNode must be one of the graph's declared entry
849
+ // nodes. startWorkflow is the boundary the public
850
+ // `/workflow/:name/graph/:nodeId` route and triggers enter through, so
851
+ // without this a request could name any dependency-free node — one whose
852
+ // input reads only `trigger` — and fire its RPC directly with
853
+ // attacker-chosen data, skipping every upstream eligibility, validation or
854
+ // approval node. (Internal resume/replay drives runWorkflowGraph directly
855
+ // and is unaffected.)
856
+ if (
857
+ options?.startNode &&
858
+ !(workflowMeta.entryNodeIds ?? []).includes(options.startNode)
859
+ ) {
860
+ throw new Error(
861
+ `Workflow graph '${name}': '${options.startNode}' is not a declared entry node`
862
+ )
863
+ }
847
864
  const shouldInline =
848
865
  options?.inline || !getSingletonServices()?.queueService
849
866
  return runWorkflowGraph(
@@ -1840,6 +1857,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1840
1857
  this.updateRunState(runId, key, value),
1841
1858
  resumeWorkflow: (runId) => this.resumeWorkflow(runId),
1842
1859
  scheduleRunWake: (runId, delay) => this.scheduleRunWake(runId, delay),
1860
+ getRunOwner: async (runId) =>
1861
+ (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
1862
+ auditApproval: (event) => auditApprovalDecision(event),
1843
1863
  }
1844
1864
  }
1845
1865
 
@@ -1849,9 +1869,13 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1849
1869
  decision: unknown,
1850
1870
  session?: CoreUserSession
1851
1871
  ): Promise<void> {
1852
- assertWorkflowRunOwner((await this.getRunIdentity(runId))?.wire, session)
1853
-
1854
- return recordApprovalDecision(this.approvalStore, runId, reason, decision)
1872
+ return recordApprovalDecision(
1873
+ this.approvalStore,
1874
+ runId,
1875
+ reason,
1876
+ decision,
1877
+ approvalDeciderFrom(session)
1878
+ )
1855
1879
  }
1856
1880
 
1857
1881
  private async approvalStep(