@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
@@ -53,6 +53,19 @@ export interface WorkflowExpectServiceOptions extends WorkflowStepOptions {
53
53
  /** Assert the exact number of matching calls. Default: at least one. */
54
54
  times?: number;
55
55
  }
56
+ /** Options for workflow.expectScore() */
57
+ export interface WorkflowExpectScoreOptions extends WorkflowStepOptions {
58
+ /** Fail below this score. Defaults to 0.5 — a scorer that answered at all. */
59
+ atLeast?: number;
60
+ /** Fail above this score, for a scorer where high is the failure. */
61
+ atMost?: number;
62
+ /**
63
+ * The known-correct answer, which is what a reference-based judge grades
64
+ * against. Live traffic has no answer key, so a judge declared
65
+ * `requiresReference` is only ever reachable from a scenario.
66
+ */
67
+ reference?: string;
68
+ }
56
69
  /**
57
70
  * Type signature for workflow.do() RPC form - used by inspector
58
71
  */
@@ -80,10 +93,50 @@ export type WorkflowWireSleep = (stepName: string, duration: string) => Promise<
80
93
  * loops, like dynamic `do()` step names.
81
94
  */
82
95
  export type WorkflowWireSuspend = (reason: string) => Promise<void>;
96
+ /**
97
+ * Who is allowed to answer an approval gate, relative to the user who started
98
+ * the run.
99
+ *
100
+ * - `any` — anyone the approve entrypoint lets through. The gate is a pause for
101
+ * a decision, not an authorization boundary.
102
+ * - `owner` — only the user who started the run, so a gate can be used to
103
+ * confirm an action against the same person who requested it.
104
+ * - `not-initiator` — anyone *except* the user who started the run: four-eyes.
105
+ * A run with no recorded initiator has nobody to exclude, so this degrades to
106
+ * requiring a signed-in decider.
107
+ */
108
+ export type WorkflowApprovalApprovers = 'any' | 'owner' | 'not-initiator';
109
+ /**
110
+ * Who may answer an approval gate. Declared on the gate rather than the
111
+ * entrypoint because it is a property of the decision being made, and because
112
+ * one workflow can hold several gates with different requirements.
113
+ */
114
+ export interface WorkflowApprovalPolicy {
115
+ /**
116
+ * Defaults to `any`. See {@link WorkflowApprovalApprovers}.
117
+ */
118
+ approvers?: WorkflowApprovalApprovers;
119
+ /**
120
+ * Additionally require the decider's session to hold this scope, so
121
+ * "a second pair of eyes" can be narrowed to "a second pair of *senior*
122
+ * eyes". Combines with `approvers` — both must pass.
123
+ */
124
+ approverScope?: string;
125
+ }
126
+ /**
127
+ * The decider, reduced to the two facts a policy can be expressed in terms of.
128
+ * Recorded alongside the decision so the gate can be judged on replay — the
129
+ * session itself is long gone by then — and carried into the settled outcome so
130
+ * the answer keeps its provenance.
131
+ */
132
+ export interface ApprovalDecider {
133
+ userId?: string;
134
+ scopes?: string[];
135
+ }
83
136
  /**
84
137
  * Options for workflow.approval().
85
138
  */
86
- export interface WorkflowApprovalOptions<TSchema extends StandardSchemaV1 = StandardSchemaV1> {
139
+ export interface WorkflowApprovalOptions<TSchema extends StandardSchemaV1 = StandardSchemaV1> extends WorkflowApprovalPolicy {
87
140
  /**
88
141
  * Schema the decision payload is validated against. This is a VALUE, not a
89
142
  * type generic: the payload arrives from an untrusted caller over the approve
@@ -107,6 +160,14 @@ export interface WorkflowApprovalOptions<TSchema extends StandardSchemaV1 = Stan
107
160
  export type ApprovalOutcome<T> = {
108
161
  status: 'decided';
109
162
  data: T;
163
+ /**
164
+ * Who answered, and when. Run state holds the decision only while the gate
165
+ * is open — it is overwritten by the next write and cleared outright when a
166
+ * decision is refused — so the settled answer carries its own provenance
167
+ * into the step result, which is append-only.
168
+ */
169
+ decidedBy?: ApprovalDecider;
170
+ decidedAt?: string;
110
171
  } | {
111
172
  status: 'expired';
112
173
  };
@@ -386,6 +447,10 @@ export interface ApprovalStepMeta {
386
447
  outputVar?: string;
387
448
  /** Expiry duration, when one was given */
388
449
  expiry?: string | number;
450
+ /** Who may answer the gate, when the default was not used */
451
+ approvers?: WorkflowApprovalApprovers;
452
+ /** Scope the decider must hold, when one was required */
453
+ approverScope?: string;
389
454
  }
390
455
  /**
391
456
  * Filter step metadata (array.filter)
@@ -476,25 +541,21 @@ export interface PikkuWorkflowWire {
476
541
  /**
477
542
  * What a scenario has accumulated so far, shared by its body and its hooks.
478
543
  *
479
- * Typed as a partial of the scenario's own output because a scenario can fail
480
- * at any step: the shape describes what a *completed* run produces, and the
481
- * context holds however much of it this run reached. A scenario declaring no
482
- * object output gets an open record rather than `Partial<never>`.
544
+ * `Partial` because a scenario can fail at any step: `Out` describes what a
545
+ * *completed* run produces, and the context holds however much of it this run
546
+ * reached.
483
547
  *
484
- * `any` collapses to `any`, not to the union of both branches. `PikkuWire`'s
485
- * default depends on that: the wire type is also used as a generic *constraint*,
486
- * and a constraint carrying a concrete context would reject every wire whose
487
- * scenario output differs from it.
548
+ * The `0 extends 1 & Out` branch keeps `any` collapsing to `any` rather than to
549
+ * the union of the other two. `PikkuWire` and `PikkuFunctionConfig` both use
550
+ * this type as a generic *constraint*, and a constraint carrying a concrete
551
+ * context would reject every wire whose scenario output differs from it.
488
552
  */
489
553
  export type ScenarioContext<Out = unknown> = 0 extends 1 & Out ? any : Out extends object ? Partial<Out> : Record<string, unknown>;
490
554
  export interface PikkuScenarioWire<Out = unknown> extends PikkuWorkflowWire {
491
555
  /**
492
- * Scratch the body writes and the `before`/`after` hooks read.
493
- *
494
- * A hook is a separate function, so it cannot see the body's locals — which
495
- * is why teardown had nowhere to learn the ids the body minted. Assigning
496
- * them here (`scenario.context.projectId = project.projectId`) hands them to
497
- * `after`, which runs in a `finally` and so cleans up on a failed run too.
556
+ * Scratch the body writes and the `before`/`after` hooks read. A hook is a
557
+ * separate function and cannot see the body's locals, so this is how teardown
558
+ * learns the ids the body minted.
498
559
  *
499
560
  * Deliberately *not* a world: it is scoped to one run, and scenario steps
500
561
  * cannot reach it. Steps stay pure functions of their declared inputs, so the
@@ -510,6 +571,15 @@ export interface PikkuScenarioWire<Out = unknown> extends PikkuWorkflowWire {
510
571
  expectError: <TInput = any>(stepName: string, rpcName: string, data: TInput, options?: WorkflowExpectErrorOptions) => Promise<string>;
511
572
  /** Stub-assertion step: asserts `service.method` was called on the target server */
512
573
  expectService: (stepName: string, serviceMethod: string, options?: WorkflowExpectServiceOptions) => Promise<void>;
574
+ /**
575
+ * Grade-assertion step: runs one declared scorer against a finished agent run
576
+ * and asserts the score. Returns the grade, so a scenario can report the
577
+ * reason a judge gave rather than only that it fell short.
578
+ */
579
+ expectScore: (stepName: string, runId: string, scorerName: string, options?: WorkflowExpectScoreOptions) => Promise<{
580
+ score: number;
581
+ reason?: string;
582
+ }>;
513
583
  /**
514
584
  * Run a registered scenario step, as the setup the scenario starts from.
515
585
  *
@@ -1,4 +1,5 @@
1
- import type { CoreFeature, CoreWorkflow, FeaturePlanEntry } from './workflow.types.js';
1
+ import type { CoreWorkflow } from './workflow.types.js';
2
+ import type { CoreFeature, FeaturePlanEntry } from './scenario.types.js';
2
3
  export declare const addFeature: (featureId: string, feature: CoreFeature, packageName?: string | null) => void;
3
4
  export declare const resolveFeatureScenarios: (features: Map<string, CoreFeature>, registrations: Map<string, CoreWorkflow>) => {
4
5
  entries: FeaturePlanEntry[];
@@ -2,27 +2,16 @@ export { PikkuWorkflowService } from './pikku-workflow-service.js';
2
2
  export { WorkflowCancelledException, WorkflowSuspendedException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunNotFoundError, WorkflowApprovalResolvedError, WorkflowStepFunctionMismatchError, } from './workflow-errors.js';
3
3
  export { DEFAULT_STEP_RETRIES } from './workflow-constants.js';
4
4
  export { assertWorkflowRunOwner, WorkflowRunForbiddenError, } from './workflow-run-ownership.js';
5
- export type { RunLifecycleContext, WorkflowRunEngine, WorkflowRunExtension, } from './workflow-run-engine.types.js';
5
+ export { WorkflowApprovalForbiddenError } from './workflow-approval-policy.js';
6
+ export type { WorkflowRunEngine, WorkflowRunExtension, } from './workflow-run-engine.types.js';
6
7
  export { deriveInvocationId, uuidv5 } from './workflow-invocation-id.js';
7
8
  export { isRef } from './graph/workflow-graph.types.js';
8
9
  export type { RefValue } from './graph/workflow-graph.types.js';
9
10
  export { buildRunTimeline, reconstructStateAt, reconstructFinalState, } from './run-timeline.js';
10
- export type { RunTimeline, RunTimelineEvent, ReconstructedRunState, ReconstructedStep, RunPhase, } from './run-timeline.js';
11
+ export type { RunTimeline, ReconstructedRunState } from './run-timeline.js';
11
12
  export { addWorkflow } from './dsl/workflow-runner.js';
12
- export { addFeature, resolveFeatureScenarios } from './feature.js';
13
13
  export { template, type TemplateString } from './graph/template.js';
14
14
  export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGraphResult, } from './graph/wire-workflow-graph.js';
15
- export { pikkuWorkflowWorkerFunc, pikkuWorkflowOrchestratorFunc, pikkuWorkflowSleeperFunc, } from './workflow-queue-workers.js';
16
15
  export type { WorkflowStepInput as WorkflowStepQueueInput, PikkuWorkflowOrchestratorInput, PikkuWorkflowSleeperInput, } from './workflow-queue-workers.js';
17
- export type { WorkflowService, WorkflowQueueOptions, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, CoreFeature, CoreFeatureScenario, FeatureMeta, FeatureMetaEntry, FeaturesMeta, FeaturePlanEntry, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, } from './workflow.types.js';
18
- export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowWireDoInline, WorkflowWireSleep, WorkflowWireSuspend, WorkflowWireApproval, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, SimpleCondition, Condition, BranchCase, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, ScenarioStepInvocation, ScenarioStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, PikkuScenarioWire, } from './workflow.types.js';
19
- export type { ScenarioStepPhase, ScenarioStepKind, ScenarioStepOptions, PikkuScenarioStepWire, ScenarioEnvironment, ScenarioSurface, ScenarioSurfaceResolution, } from './scenario-step.types.js';
20
- export { SCENARIO_SURFACES } from './scenario-step.types.js';
21
- export { resolveScenarioSurfaces, witnessesAgree } from './scenario-surface.js';
22
- export { requireActor, requireScenarioEnv } from './scenario-step-guards.js';
23
- export { pollUntil, type PollOptions } from './scenario-poll.js';
24
- export { createCookieJar } from './scenario-cookie-jar.js';
25
- export type { ScenarioCookieJar } from './scenario-cookie-jar.js';
26
- export type { ScenarioHttpResponse, ScenarioJsonRequest, } from '../../services/personas-service.js';
27
- export type { PikkuBrowserWire, TestIdSelector, ScenarioBrowserProvider, ScenarioBrowserFailure, } from './scenario-step.types.js';
28
- export { composeStepProse, renderStepTemplate } from './scenario-prose.js';
16
+ export type { WorkflowService, WorkflowQueueOptions, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowsRuntimeMeta, } from './workflow.types.js';
17
+ export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, Condition, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './workflow.types.js';
@@ -2,18 +2,10 @@ export { PikkuWorkflowService } from './pikku-workflow-service.js';
2
2
  export { WorkflowCancelledException, WorkflowSuspendedException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunNotFoundError, WorkflowApprovalResolvedError, WorkflowStepFunctionMismatchError, } from './workflow-errors.js';
3
3
  export { DEFAULT_STEP_RETRIES } from './workflow-constants.js';
4
4
  export { assertWorkflowRunOwner, WorkflowRunForbiddenError, } from './workflow-run-ownership.js';
5
+ export { WorkflowApprovalForbiddenError } from './workflow-approval-policy.js';
5
6
  export { deriveInvocationId, uuidv5 } from './workflow-invocation-id.js';
6
7
  export { isRef } from './graph/workflow-graph.types.js';
7
8
  export { buildRunTimeline, reconstructStateAt, reconstructFinalState, } from './run-timeline.js';
8
9
  export { addWorkflow } from './dsl/workflow-runner.js';
9
- export { addFeature, resolveFeatureScenarios } from './feature.js';
10
10
  export { template } from './graph/template.js';
11
11
  export { pikkuWorkflowGraph, } from './graph/wire-workflow-graph.js';
12
- export { pikkuWorkflowWorkerFunc, pikkuWorkflowOrchestratorFunc, pikkuWorkflowSleeperFunc, } from './workflow-queue-workers.js';
13
- export { SCENARIO_SURFACES } from './scenario-step.types.js';
14
- // Which of a step's bindings run: one for an action, every witness for a `then`
15
- export { resolveScenarioSurfaces, witnessesAgree } from './scenario-surface.js';
16
- export { requireActor, requireScenarioEnv } from './scenario-step-guards.js';
17
- export { pollUntil } from './scenario-poll.js';
18
- export { createCookieJar } from './scenario-cookie-jar.js';
19
- export { composeStepProse, renderStepTemplate } from './scenario-prose.js';
@@ -5,6 +5,16 @@ import type { PikkuRawWire } from '../../types/core.types.js';
5
5
  import type { ScenarioPersonas } from '../../services/personas-service.js';
6
6
  import type { ScenarioBrowserProvider, ScenarioEnvironment, ScenarioSurface } from './scenario-step.types.js';
7
7
  import type { PikkuWorkflowWire, WorkflowQueueOptions } from './workflow.types.js';
8
+ export { addFeature, resolveFeatureScenarios } from './feature.js';
9
+ export type * from './scenario.types.js';
10
+ export { SCENARIO_SURFACES } from './scenario-step.types.js';
11
+ export { resolveScenarioSurfaces } from './scenario-surface.js';
12
+ export { pollUntil, type PollOptions } from './scenario-poll.js';
13
+ export { createCookieJar } from './scenario-cookie-jar.js';
14
+ export type { ScenarioCookieJar } from './scenario-cookie-jar.js';
15
+ export { composeStepProse, renderStepTemplate } from './scenario-prose.js';
16
+ export { requireActor, requireScenarioEnv } from './scenario-step-guards.js';
17
+ export type { ScenarioHttpResponse, ScenarioJsonRequest, } from '../../services/personas-service.js';
8
18
  /**
9
19
  * A workflow service with the scenario capability attached — the two lines
10
20
  * `pikku scenario run` needs, in one call so no caller has to remember that the
@@ -156,16 +166,16 @@ export declare class PikkuScenarioService implements WorkflowRunExtension {
156
166
  }): Promise<void>;
157
167
  detachRunContext(runId: string): void;
158
168
  /**
159
- * What the run has accumulated, for a reporter that wants to say what a
160
- * failed scenario left behind. Undefined for a plain workflow.
169
+ * What the run has accumulated.
170
+ *
171
+ * Undefined only for a run whose wire was never decorated — decoration calls
172
+ * `contextForRun` unconditionally, so a plain workflow that has reached a
173
+ * step holds an empty context rather than none.
161
174
  */
162
175
  getRunContext(runId: string): Record<string, unknown> | undefined;
163
176
  /**
164
- * The run's context, created on demand.
165
- *
166
- * `attachRunContext` seeds it for an ordinary scenario run, but the wire is
167
- * also built on paths that skip attach; without this the body would be
168
- * writing to an `undefined` and teardown would read nothing.
177
+ * The run's context, created on demand because the wire is also built on
178
+ * paths that skip `attachRunContext`.
169
179
  */
170
180
  private contextForRun;
171
181
  decorateRunWire(wire: PikkuRawWire, context: {
@@ -7,6 +7,15 @@ import { InMemoryWorkflowService } from '../../services/in-memory-workflow-servi
7
7
  import { runScheduledTask } from '../scheduler/scheduler-runner.js';
8
8
  import { WorkflowStepNameNotString } from './workflow-errors.js';
9
9
  import { resolveScenarioSurfaces, witnessesAgree } from './scenario-surface.js';
10
+ export { addFeature, resolveFeatureScenarios } from './feature.js';
11
+ export { SCENARIO_SURFACES } from './scenario-step.types.js';
12
+ // Which of a step's bindings run: one for an action, every witness for a `then`
13
+ export { resolveScenarioSurfaces } from './scenario-surface.js';
14
+ export { pollUntil } from './scenario-poll.js';
15
+ export { createCookieJar } from './scenario-cookie-jar.js';
16
+ export { composeStepProse, renderStepTemplate } from './scenario-prose.js';
17
+ export { requireActor, requireScenarioEnv } from './scenario-step-guards.js';
18
+ // The readers themselves live on `@pikku/core/persona`
10
19
  /**
11
20
  * A workflow service with the scenario capability attached — the two lines
12
21
  * `pikku scenario run` needs, in one call so no caller has to remember that the
@@ -176,9 +185,9 @@ export class PikkuScenarioService {
176
185
  // Scenario actors per run: live authenticated clients (cookie jars) are
177
186
  // process-local by nature, so they ride this map, never the persisted wire.
178
187
  runActors = new Map();
179
- // What each run has accumulated so far. Process-local like the actors above:
180
- // a scenario is a single in-process run, so the body and its hooks share one
181
- // object rather than reading it back off the persisted wire.
188
+ // What each run has accumulated so far — process-local like the actors above,
189
+ // so the body and its hooks share one object rather than reading it back off
190
+ // the persisted wire.
182
191
  runContexts = new Map();
183
192
  scenarioBrowserProvider;
184
193
  scenarioEnvironment;
@@ -235,18 +244,18 @@ export class PikkuScenarioService {
235
244
  this.runContexts.delete(runId);
236
245
  }
237
246
  /**
238
- * What the run has accumulated, for a reporter that wants to say what a
239
- * failed scenario left behind. Undefined for a plain workflow.
247
+ * What the run has accumulated.
248
+ *
249
+ * Undefined only for a run whose wire was never decorated — decoration calls
250
+ * `contextForRun` unconditionally, so a plain workflow that has reached a
251
+ * step holds an empty context rather than none.
240
252
  */
241
253
  getRunContext(runId) {
242
254
  return this.runContexts.get(runId);
243
255
  }
244
256
  /**
245
- * The run's context, created on demand.
246
- *
247
- * `attachRunContext` seeds it for an ordinary scenario run, but the wire is
248
- * also built on paths that skip attach; without this the body would be
249
- * writing to an `undefined` and teardown would read nothing.
257
+ * The run's context, created on demand because the wire is also built on
258
+ * paths that skip `attachRunContext`.
250
259
  */
251
260
  contextForRun(runId) {
252
261
  let runContext = this.runContexts.get(runId);
@@ -397,9 +406,6 @@ export class PikkuScenarioService {
397
406
  rpcService,
398
407
  });
399
408
  Object.assign(workflowWire, {
400
- // One object per run, resolved through the map rather than created here,
401
- // so the body and its before/after hooks share the same scratch even
402
- // though the hooks are separate functions that cannot see its locals.
403
409
  context: this.contextForRun(runId),
404
410
  // Durable polling step: invoke an RPC (as an actor when options.as is
405
411
  // set) until the predicate passes or `within` elapses. The whole poll is
@@ -485,6 +491,35 @@ export class PikkuScenarioService {
485
491
  }
486
492
  }, options);
487
493
  },
494
+ expectScore: async (stepName, agentRunId, scorerName, options) => {
495
+ this.engine.verifyStepName(stepName);
496
+ return await this.engine.inlineStep(runId, stepName, async () => {
497
+ const rpcName = 'pikkuScenarioGradeRun';
498
+ const data = {
499
+ runId: agentRunId,
500
+ scorer: scorerName,
501
+ ...(options?.reference !== undefined
502
+ ? { reference: options.reference }
503
+ : {}),
504
+ };
505
+ const grade = options?.actor
506
+ ? await options.actor.invoke(rpcName, data)
507
+ : await rpcService.rpcWithWire(rpcName, data, {});
508
+ // A scorer that graded is a scorer that answered, so an unstated
509
+ // bound still fails a zero rather than passing anything at all.
510
+ const atLeast = options?.atLeast ?? 0.5;
511
+ const failed = grade.score < atLeast ||
512
+ (options?.atMost !== undefined && grade.score > options.atMost);
513
+ if (failed) {
514
+ const bound = options?.atMost !== undefined
515
+ ? `between ${atLeast} and ${options.atMost}`
516
+ : `at least ${atLeast}`;
517
+ throw new Error(`[workflow] expectScore '${stepName}' expected '${scorerName}' to grade run ${agentRunId} ${bound}, got ${grade.score}` +
518
+ `${grade.reason ? `: ${grade.reason}` : ''}`);
519
+ }
520
+ return grade;
521
+ }, options);
522
+ },
488
523
  // knowledge: decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md
489
524
  given: (stepName, stepFunc, data, options) => this.scenarioStep('given', scenarioStepContext(), stepName, stepFunc, data, options),
490
525
  when: (stepName, stepFunc, data, options) => this.scenarioStep('when', scenarioStepContext(), stepName, stepFunc, data, options),
@@ -6,7 +6,7 @@ import { isExpectedError } from '../../errors/error-handler.js';
6
6
  import { PikkuMissingMetaError } from '../../errors/errors.js';
7
7
  import { RPCNotFoundError } from '../rpc/rpc-runner.js';
8
8
  import { deriveInvocationId } from './workflow-invocation-id.js';
9
- import { assertWorkflowRunOwner } from './workflow-run-ownership.js';
9
+ import { approvalDeciderFrom } from './workflow-approval-policy.js';
10
10
  import { buildRunTimeline, reconstructStateAt, } from './run-timeline.js';
11
11
  import { DEFAULT_STEP_RETRIES, WORKFLOW_CHILD_POLL_MAX_MS, WORKFLOW_END_STATES, WORKFLOW_POLL_FACTOR, WORKFLOW_POLL_MIN_MS, WORKFLOW_TERMINAL_STATES, } from './workflow-constants.js';
12
12
  import { WorkflowAsyncException, WorkflowCancelledException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotFoundError, WorkflowStepFunctionMismatchError, WorkflowStepNameNotString, WorkflowSuspendedException, } from './workflow-errors.js';
@@ -14,6 +14,7 @@ import { resolveWorkflowMeta } from './workflow-meta-resolver.js';
14
14
  import { jobGroupFor, orchestratorQueueName, resolveWorkflowConfig, stepJobOptions, stepWorkerQueueName, } from './workflow-queue-routing.js';
15
15
  import { wireWorkflowQueueWorkers } from './workflow-queue-wiring.js';
16
16
  import { approvalStepNameFor, evaluateApprovalStep, recordApprovalDecision, } from './workflow-approval.js';
17
+ import { auditApprovalDecision } from './workflow-approval-audit.js';
17
18
  import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js';
18
19
  import { RedispatchBackoff, sweepStalledRuns, sweepUndispatchedSteps, } from './workflow-recovery.js';
19
20
  export class PikkuWorkflowService {
@@ -371,6 +372,18 @@ export class PikkuWorkflowService {
371
372
  throw new WorkflowNotFoundError(name);
372
373
  }
373
374
  if (workflowMeta.source === 'graph') {
375
+ // A caller-supplied startNode must be one of the graph's declared entry
376
+ // nodes. startWorkflow is the boundary the public
377
+ // `/workflow/:name/graph/:nodeId` route and triggers enter through, so
378
+ // without this a request could name any dependency-free node — one whose
379
+ // input reads only `trigger` — and fire its RPC directly with
380
+ // attacker-chosen data, skipping every upstream eligibility, validation or
381
+ // approval node. (Internal resume/replay drives runWorkflowGraph directly
382
+ // and is unaffected.)
383
+ if (options?.startNode &&
384
+ !(workflowMeta.entryNodeIds ?? []).includes(options.startNode)) {
385
+ throw new Error(`Workflow graph '${name}': '${options.startNode}' is not a declared entry node`);
386
+ }
374
387
  const shouldInline = options?.inline || !getSingletonServices()?.queueService;
375
388
  return runWorkflowGraph(this, name, input, rpcService, shouldInline, options?.startNode, wire, workflowMeta);
376
389
  }
@@ -1014,11 +1027,12 @@ export class PikkuWorkflowService {
1014
1027
  updateRunState: (runId, key, value) => this.updateRunState(runId, key, value),
1015
1028
  resumeWorkflow: (runId) => this.resumeWorkflow(runId),
1016
1029
  scheduleRunWake: (runId, delay) => this.scheduleRunWake(runId, delay),
1030
+ getRunOwner: async (runId) => (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
1031
+ auditApproval: (event) => auditApprovalDecision(event),
1017
1032
  };
1018
1033
  }
1019
1034
  async approveStep(runId, reason, decision, session) {
1020
- assertWorkflowRunOwner((await this.getRunIdentity(runId))?.wire, session);
1021
- return recordApprovalDecision(this.approvalStore, runId, reason, decision);
1035
+ return recordApprovalDecision(this.approvalStore, runId, reason, decision, approvalDeciderFrom(session));
1022
1036
  }
1023
1037
  async approvalStep(runId, reason, options) {
1024
1038
  const fromStepName = this.lastStepName(runId);
@@ -210,6 +210,14 @@ export interface ScenarioBrowserProvider {
210
210
  * scenario cannot leave the next signed in as somebody else.
211
211
  */
212
212
  reset?(): Promise<void>;
213
+ /**
214
+ * Name the scenario about to run, so anything it captures is filed under it.
215
+ *
216
+ * Called before each scenario. A provider that is never told has to fall
217
+ * back to one shared label, which puts every run's artifacts in a single
218
+ * folder — findable only by timestamp.
219
+ */
220
+ beginScenario?(scenario: string): void;
213
221
  /**
214
222
  * Snapshot every open window for a failed scenario. `label` identifies the
215
223
  * scenario in artifact filenames. Never throws: a failure to capture must
@@ -0,0 +1,37 @@
1
+ import type { CorePikkuFunctionConfig, CorePikkuFunctionHook } from '../../function/functions.types.js';
2
+ export type { ScenarioStepInvocation, ScenarioStepMeta, PikkuScenarioWire, } from './dsl/workflow-dsl.types.js';
3
+ export type { ScenarioStepPhase, ScenarioStepKind, ScenarioStepOptions, PikkuScenarioStepWire, ScenarioEnvironment, ScenarioSurface, ScenarioSurfaceResolution, PikkuBrowserWire, TestIdSelector, ScenarioBrowserProvider, ScenarioBrowserFailure, } from './scenario-step.types.js';
4
+ export type CoreFeatureScenario = CorePikkuFunctionConfig<any, any, any> | {
5
+ scenario: CorePikkuFunctionConfig<any, any, any>;
6
+ data: unknown;
7
+ };
8
+ export type CoreFeature = {
9
+ name: string;
10
+ description?: string;
11
+ tags?: string[];
12
+ scenarios: readonly CoreFeatureScenario[];
13
+ before?: CorePikkuFunctionHook;
14
+ after?: CorePikkuFunctionHook;
15
+ };
16
+ export type FeatureMetaEntry = {
17
+ scenario: string;
18
+ data?: unknown;
19
+ };
20
+ export type FeatureMeta = {
21
+ id: string;
22
+ name: string;
23
+ description?: string;
24
+ tags: string[];
25
+ entries: FeatureMetaEntry[];
26
+ unresolvedEntries: number;
27
+ hasBefore: boolean;
28
+ hasAfter: boolean;
29
+ };
30
+ export type FeaturesMeta = Record<string, FeatureMeta>;
31
+ export type FeaturePlanEntry = {
32
+ featureId: string;
33
+ featureName: string;
34
+ scenarioName: string;
35
+ data?: unknown;
36
+ tags: string[];
37
+ };
@@ -0,0 +1,16 @@
1
+ import type { ApprovalAuditEvent } from './workflow-approval.js';
2
+ /** The audit type an answer to an approval gate is recorded under. */
3
+ export declare const APPROVAL_AUDIT_TYPE = "workflow.approval.decided";
4
+ /**
5
+ * Record an answer to an approval gate where it outlives the run.
6
+ *
7
+ * The step result carries the settled decision, but it is deleted with the run
8
+ * — `deleteRun` cascades to steps and to history — and a refused attempt never
9
+ * reaches a step at all. An approval is asked for precisely so it can be
10
+ * answered for afterwards, so the answer also goes to the audit sink, which
11
+ * holds no foreign key to the run.
12
+ *
13
+ * A project with no sink wired records nothing, and a sink that fails is logged
14
+ * rather than thrown: the trail must not be the reason a decision is lost.
15
+ */
16
+ export declare const auditApprovalDecision: (event: ApprovalAuditEvent) => Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { getSingletonServices } from '../../pikku-state.js';
2
+ /** The audit type an answer to an approval gate is recorded under. */
3
+ export const APPROVAL_AUDIT_TYPE = 'workflow.approval.decided';
4
+ /**
5
+ * Record an answer to an approval gate where it outlives the run.
6
+ *
7
+ * The step result carries the settled decision, but it is deleted with the run
8
+ * — `deleteRun` cascades to steps and to history — and a refused attempt never
9
+ * reaches a step at all. An approval is asked for precisely so it can be
10
+ * answered for afterwards, so the answer also goes to the audit sink, which
11
+ * holds no foreign key to the run.
12
+ *
13
+ * A project with no sink wired records nothing, and a sink that fails is logged
14
+ * rather than thrown: the trail must not be the reason a decision is lost.
15
+ */
16
+ export const auditApprovalDecision = async (event) => {
17
+ const services = getSingletonServices();
18
+ if (!services?.audit) {
19
+ return;
20
+ }
21
+ try {
22
+ await services.audit.audit({
23
+ type: APPROVAL_AUDIT_TYPE,
24
+ source: 'explicit',
25
+ outcome: event.outcome,
26
+ occurredAt: new Date().toISOString(),
27
+ wireType: 'workflow',
28
+ userIdentity: { pikkuUserId: event.decidedBy?.userId },
29
+ metadata: {
30
+ runId: event.runId,
31
+ reason: event.reason,
32
+ scopes: event.decidedBy?.scopes,
33
+ refusal: event.refusal,
34
+ },
35
+ });
36
+ }
37
+ catch (error) {
38
+ services.logger?.warn(`Failed to audit the decision on approval '${event.reason}' for run ${event.runId}`, error);
39
+ }
40
+ };
@@ -0,0 +1,20 @@
1
+ import { ForbiddenError } from '../../errors/errors.js';
2
+ import type { CoreUserSession } from '../../types/core.types.js';
3
+ import type { ApprovalDecider, WorkflowApprovalPolicy } from './dsl/workflow-dsl.types.js';
4
+ export declare class WorkflowApprovalForbiddenError extends ForbiddenError {
5
+ payload: {
6
+ reason: string;
7
+ detail: string;
8
+ };
9
+ constructor(reason: string, detail: string);
10
+ }
11
+ export declare const approvalDeciderFrom: (session: CoreUserSession | undefined) => ApprovalDecider | undefined;
12
+ /**
13
+ * Judge a decision against the gate's declared policy, returning the reason it
14
+ * is refused or `undefined` if it stands.
15
+ *
16
+ * Returns a message rather than throwing because the same judgement is needed
17
+ * in two places with different outcomes: refusing a live submission with a 403,
18
+ * and clearing an already-recorded decision on replay.
19
+ */
20
+ export declare const approvalPolicyRefusal: (policy: WorkflowApprovalPolicy, owner: string | undefined, decider: ApprovalDecider | undefined) => string | undefined;
@@ -0,0 +1,48 @@
1
+ import { ForbiddenError } from '../../errors/errors.js';
2
+ import { addError } from '../../errors/error-handler.js';
3
+ export class WorkflowApprovalForbiddenError extends ForbiddenError {
4
+ payload;
5
+ constructor(reason, detail) {
6
+ super(detail);
7
+ this.payload = { reason, detail };
8
+ }
9
+ }
10
+ addError(WorkflowApprovalForbiddenError, {
11
+ status: 403,
12
+ message: 'Not authorized to answer this approval.',
13
+ });
14
+ export const approvalDeciderFrom = (session) => session ? { userId: session.userId, scopes: session.scopes } : undefined;
15
+ /**
16
+ * Judge a decision against the gate's declared policy, returning the reason it
17
+ * is refused or `undefined` if it stands.
18
+ *
19
+ * Returns a message rather than throwing because the same judgement is needed
20
+ * in two places with different outcomes: refusing a live submission with a 403,
21
+ * and clearing an already-recorded decision on replay.
22
+ */
23
+ export const approvalPolicyRefusal = (policy, owner, decider) => {
24
+ if (policy.approverScope &&
25
+ !decider?.scopes?.includes(policy.approverScope)) {
26
+ return `Answering this approval requires the '${policy.approverScope}' scope`;
27
+ }
28
+ switch (policy.approvers ?? 'any') {
29
+ case 'owner':
30
+ if (!owner) {
31
+ return undefined;
32
+ }
33
+ if (!decider?.userId || decider.userId !== owner) {
34
+ return 'Only the user who started this run may answer this approval';
35
+ }
36
+ return undefined;
37
+ case 'not-initiator':
38
+ if (!decider?.userId) {
39
+ return 'Answering this approval requires a signed-in user';
40
+ }
41
+ if (owner && decider.userId === owner) {
42
+ return 'The user who started this run may not answer this approval';
43
+ }
44
+ return undefined;
45
+ case 'any':
46
+ return undefined;
47
+ }
48
+ };