@pikku/core 0.12.80 → 0.12.82

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 (231) hide show
  1. package/CHANGELOG.md +312 -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 -15
  14. package/dist/services/index.js +5 -5
  15. package/dist/services/scoped-credential-service.d.ts +21 -0
  16. package/dist/services/scoped-credential-service.js +53 -0
  17. package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
  18. package/dist/types/core.types.d.ts +0 -2
  19. package/dist/types/state.types.d.ts +13 -0
  20. package/dist/wirings/actor-flow/index.d.ts +1 -1
  21. package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
  22. package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
  23. package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
  24. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
  25. package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
  26. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
  27. package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
  28. package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -0
  29. package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
  30. package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
  31. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
  32. package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
  33. package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
  34. package/dist/wirings/ai-agent/index.d.ts +8 -7
  35. package/dist/wirings/ai-agent/index.js +5 -4
  36. package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
  37. package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
  38. package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
  39. package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
  40. package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
  41. package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
  42. package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
  43. package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
  44. package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
  45. package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
  46. package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
  47. package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
  48. package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
  49. package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
  50. package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
  51. package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
  52. package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
  53. package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
  54. package/dist/wirings/ai-scorer/index.d.ts +6 -0
  55. package/dist/wirings/ai-scorer/index.js +5 -0
  56. package/dist/wirings/channel/index.d.ts +5 -6
  57. package/dist/wirings/channel/index.js +3 -4
  58. package/dist/wirings/channel/local/local-channel-runner.js +8 -1
  59. package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
  60. package/dist/wirings/cli/channel/index.d.ts +1 -2
  61. package/dist/wirings/cli/channel/index.js +0 -1
  62. package/dist/wirings/cli/cli-runner.js +13 -1
  63. package/dist/wirings/credential/index.d.ts +1 -1
  64. package/dist/wirings/gateway/index.d.ts +1 -1
  65. package/dist/wirings/http/http-runner.js +8 -2
  66. package/dist/wirings/http/index.d.ts +1 -2
  67. package/dist/wirings/mcp/index.d.ts +1 -1
  68. package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
  69. package/dist/wirings/mcp/mcp-runner.js +18 -5
  70. package/dist/wirings/persona/index.d.ts +3 -4
  71. package/dist/wirings/persona/index.js +2 -3
  72. package/dist/wirings/queue/index.d.ts +1 -3
  73. package/dist/wirings/queue/index.js +1 -3
  74. package/dist/wirings/rpc/addon-runner.d.ts +4 -0
  75. package/dist/wirings/rpc/addon-runner.js +19 -3
  76. package/dist/wirings/rpc/rpc-runner.js +2 -0
  77. package/dist/wirings/rpc/rpc-types.d.ts +4 -0
  78. package/dist/wirings/rpc/wire-addon.d.ts +13 -0
  79. package/dist/wirings/rpc/wire-addon.js +4 -0
  80. package/dist/wirings/scheduler/index.d.ts +1 -1
  81. package/dist/wirings/trigger/index.d.ts +1 -1
  82. package/dist/wirings/virtual-user/index.d.ts +5 -6
  83. package/dist/wirings/virtual-user/index.js +2 -4
  84. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
  85. package/dist/wirings/workflow/index.d.ts +6 -6
  86. package/dist/wirings/workflow/index.js +2 -2
  87. package/dist/wirings/workflow/pikku-scenario-service.d.ts +7 -7
  88. package/dist/wirings/workflow/pikku-scenario-service.js +39 -13
  89. package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
  90. package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
  91. package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
  92. package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
  93. package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
  94. package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
  95. package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
  96. package/dist/wirings/workflow/workflow-approval.js +65 -2
  97. package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
  98. package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
  99. package/dist/wirings/workflow/workflow.types.d.ts +1 -1
  100. package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
  101. package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
  102. package/knowledge/decisions/internals/index.md +30 -3
  103. package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
  104. package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
  105. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
  106. package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
  107. package/knowledge/decisions/security/index.md +3 -1
  108. package/knowledge/questions/index.md +1 -1
  109. package/package.json +3 -1
  110. package/scripts/generate-api-report.mts +143 -18
  111. package/src/api-report.test.ts +2 -2
  112. package/src/errors/index.ts +1 -1
  113. package/src/function/function-runner.test.ts +52 -0
  114. package/src/function/function-runner.ts +5 -9
  115. package/src/function/index.ts +0 -2
  116. package/src/index.ts +0 -35
  117. package/src/pikku-state.ts +5 -0
  118. package/src/public-surface.json +70 -94
  119. package/src/services/ai-agent-runner-service.ts +12 -1
  120. package/src/services/ai-run-state-service.ts +11 -0
  121. package/src/services/in-memory-ai-run-state-service.ts +13 -0
  122. package/src/services/index.ts +3 -43
  123. package/src/services/scoped-credential-service.test.ts +86 -0
  124. package/src/services/scoped-credential-service.ts +63 -0
  125. package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
  126. package/src/types/core.types.ts +3 -6
  127. package/src/types/state.types.ts +16 -0
  128. package/src/wirings/actor-flow/index.ts +0 -3
  129. package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
  130. package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
  131. package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
  132. package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
  133. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
  134. package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
  135. package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -0
  136. package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
  137. package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
  138. package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
  139. package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
  140. package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
  141. package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
  142. package/src/wirings/ai-agent/index.ts +2 -16
  143. package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
  144. package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
  145. package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
  146. package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
  147. package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
  148. package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
  149. package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
  150. package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
  151. package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
  152. package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
  153. package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
  154. package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
  155. package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
  156. package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
  157. package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
  158. package/src/wirings/ai-scorer/index.ts +24 -0
  159. package/src/wirings/channel/index.ts +1 -20
  160. package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
  161. package/src/wirings/channel/local/local-channel-runner.ts +8 -1
  162. package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
  163. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
  164. package/src/wirings/cli/channel/index.ts +0 -7
  165. package/src/wirings/cli/cli-runner.test.ts +68 -0
  166. package/src/wirings/cli/cli-runner.ts +18 -1
  167. package/src/wirings/credential/index.ts +0 -1
  168. package/src/wirings/gateway/index.ts +0 -3
  169. package/src/wirings/http/http-runner.test.ts +66 -0
  170. package/src/wirings/http/http-runner.ts +10 -2
  171. package/src/wirings/http/index.ts +1 -1
  172. package/src/wirings/mcp/index.ts +0 -1
  173. package/src/wirings/mcp/mcp-runner.test.ts +181 -0
  174. package/src/wirings/mcp/mcp-runner.ts +35 -5
  175. package/src/wirings/persona/index.ts +0 -8
  176. package/src/wirings/queue/index.ts +0 -14
  177. package/src/wirings/rpc/addon-runner.ts +34 -3
  178. package/src/wirings/rpc/addon-secrets.test.ts +261 -0
  179. package/src/wirings/rpc/rpc-runner.test.ts +2 -0
  180. package/src/wirings/rpc/rpc-runner.ts +2 -0
  181. package/src/wirings/rpc/rpc-types.ts +4 -0
  182. package/src/wirings/rpc/wire-addon.ts +17 -0
  183. package/src/wirings/scheduler/index.ts +0 -1
  184. package/src/wirings/trigger/index.ts +0 -1
  185. package/src/wirings/virtual-user/index.ts +0 -16
  186. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
  187. package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
  188. package/src/wirings/workflow/index.ts +2 -20
  189. package/src/wirings/workflow/pikku-scenario-service.ts +60 -15
  190. package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
  191. package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
  192. package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
  193. package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
  194. package/src/wirings/workflow/scenario-step.types.ts +8 -0
  195. package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
  196. package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
  197. package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
  198. package/src/wirings/workflow/workflow-approval.ts +113 -9
  199. package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
  200. package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
  201. package/src/wirings/workflow/workflow.types.ts +0 -9
  202. package/src/wirings-stay-decoupled.test.ts +6 -2
  203. package/tsconfig.tsbuildinfo +1 -1
  204. package/dist/internal.d.ts +0 -3
  205. package/dist/internal.js +0 -2
  206. package/dist/middleware/timeout.d.ts +0 -9
  207. package/dist/middleware/timeout.js +0 -15
  208. package/dist/pikku-response.d.ts +0 -6
  209. package/dist/pikku-response.js +0 -6
  210. package/dist/services/gopass-secrets.d.ts +0 -15
  211. package/dist/services/gopass-secrets.js +0 -76
  212. package/dist/services/http-scenario-actors.d.ts +0 -75
  213. package/dist/services/http-scenario-actors.js +0 -195
  214. package/dist/services/http-user-flow-actors.d.ts +0 -67
  215. package/dist/services/http-user-flow-actors.js +0 -193
  216. package/dist/services/scenario-actors-service.d.ts +0 -127
  217. package/dist/services/scenario-actors-service.js +0 -40
  218. package/dist/services/user-flow-actors-service.d.ts +0 -39
  219. package/dist/services/user-flow-actors-service.js +0 -1
  220. package/dist/wirings/credential/wire-credential.d.ts +0 -48
  221. package/dist/wirings/credential/wire-credential.js +0 -47
  222. package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
  223. package/dist/wirings/oauth2/oauth2-client.js +0 -263
  224. package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
  225. package/dist/wirings/oauth2/oauth2-routes.js +0 -146
  226. package/dist/wirings/scope/wire-scope.d.ts +0 -33
  227. package/dist/wirings/scope/wire-scope.js +0 -32
  228. package/dist/wirings/workflow/dsl/index.d.ts +0 -5
  229. package/dist/wirings/workflow/dsl/index.js +0 -4
  230. package/dist/wirings/workflow/graph/index.d.ts +0 -5
  231. package/dist/wirings/workflow/graph/index.js +0 -4
@@ -284,6 +284,8 @@ export class ContextAwareRPCService {
284
284
  secretOverrides: resolved.addonConfig?.secretOverrides,
285
285
  variableOverrides: resolved.addonConfig?.variableOverrides,
286
286
  credentialOverrides: resolved.addonConfig?.credentialOverrides,
287
+ globalSecrets: resolved.addonConfig?.globalSecrets,
288
+ globalCredentials: resolved.addonConfig?.globalCredentials,
287
289
  },
288
290
  }
289
291
  }
@@ -67,6 +67,10 @@ export interface ResolvedFunction {
67
67
  secretOverrides?: Record<string, string>
68
68
  variableOverrides?: Record<string, string>
69
69
  credentialOverrides?: Record<string, string>
70
+ /** Set by the consuming app: hand this instance the unscoped `SecretService` */
71
+ globalSecrets?: string
72
+ /** Set by the consuming app: hand this instance the unscoped `CredentialService` */
73
+ globalCredentials?: string
70
74
  /** Set by `wireRemoteAddon`: dispatch this namespace's RPCs over HTTP */
71
75
  remote?: boolean
72
76
  serverUrl?: string | ((services: any) => string | Promise<string>)
@@ -14,6 +14,19 @@ export type WireAddonConfig = {
14
14
  secretOverrides?: Record<string, string>
15
15
  variableOverrides?: Record<string, string>
16
16
  credentialOverrides?: Record<string, string>
17
+ /**
18
+ * Hands this instance the whole `SecretService` instead of one scoped to the
19
+ * secrets it declared. The value is the reason, recorded in the deploy
20
+ * manifest — an addon that names secrets at runtime cannot be scoped, and
21
+ * only the consuming app, never the addon, can grant it.
22
+ */
23
+ globalSecrets?: string
24
+ /**
25
+ * Hands this instance the whole `CredentialService` instead of one narrowed
26
+ * to the credentials it declared. The value is the reason, recorded in the
27
+ * deploy manifest, and only the consuming app can grant it.
28
+ */
29
+ globalCredentials?: string
17
30
  }
18
31
 
19
32
  export const wireAddon = (config: WireAddonConfig): void => {
@@ -32,6 +45,10 @@ export const wireAddon = (config: WireAddonConfig): void => {
32
45
  ...(config.credentialOverrides
33
46
  ? { credentialOverrides: config.credentialOverrides }
34
47
  : {}),
48
+ ...(config.globalSecrets ? { globalSecrets: config.globalSecrets } : {}),
49
+ ...(config.globalCredentials
50
+ ? { globalCredentials: config.globalCredentials }
51
+ : {}),
35
52
  })
36
53
  }
37
54
 
@@ -7,5 +7,4 @@ export { logSchedulers } from './log-schedulers.js'
7
7
  export type {
8
8
  CoreScheduledTask,
9
9
  ScheduledTasksMeta,
10
- PikkuScheduledTask,
11
10
  } from './scheduler.types.js'
@@ -6,5 +6,4 @@ export type {
6
6
  CorePikkuTriggerFunction,
7
7
  CorePikkuTriggerFunctionConfig,
8
8
  CoreTrigger,
9
- PikkuTrigger,
10
9
  } from './trigger.types.js'
@@ -18,25 +18,16 @@
18
18
  */
19
19
  export type {
20
20
  ApiCatalogueEntry,
21
- IntentRecord,
22
21
  IntentSource,
23
- IntentStatus,
24
- StepRecord,
25
- VirtualUserAction,
26
- VirtualUserBudget,
27
22
  VirtualUserDisposition,
28
23
  VirtualUserFinding,
29
- VirtualUserFindingKind,
30
24
  VirtualUserRunResult,
31
- VirtualUserTally,
32
25
  VirtualUserTarget,
33
26
  } from './virtual-user.types.js'
34
27
  export { PRODUCTION_DISPOSITION } from './virtual-user.types.js'
35
28
  export {
36
29
  runVirtualUser,
37
- rememberIds,
38
30
  type RunVirtualUserParams,
39
- type VirtualUserCallContext,
40
31
  } from './run-virtual-user.js'
41
32
  export {
42
33
  personaScopes,
@@ -57,26 +48,19 @@ export {
57
48
  } from './virtual-user-dispositions.js'
58
49
  export {
59
50
  catalogueClassification,
60
- catalogueIndex,
61
51
  catalogueLookup,
62
- describeEntry,
63
52
  isReadOnly,
64
53
  reachableCatalogue,
65
- renderCatalogue,
66
54
  unreachableCatalogue,
67
55
  } from './virtual-user-catalogue.js'
68
56
  export {
69
- reachableAgents,
70
57
  type AgentReachability,
71
58
  type ReachableAgent,
72
59
  } from './virtual-user-agents.js'
73
60
  export {
74
61
  IntentStack,
75
62
  intentsForPersona,
76
- type IntentMove,
77
- type ScheduledTick,
78
63
  } from './virtual-user-intents.js'
79
- export { createRng, type VirtualUserRng } from './virtual-user-rng.js'
80
64
  export {
81
65
  deriveCatalogue,
82
66
  deriveIntents,
@@ -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
  *
@@ -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'
@@ -30,10 +30,7 @@ export {
30
30
  } from './run-timeline.js'
31
31
  export type {
32
32
  RunTimeline,
33
- RunTimelineEvent,
34
33
  ReconstructedRunState,
35
- ReconstructedStep,
36
- RunPhase,
37
34
  } from './run-timeline.js'
38
35
 
39
36
  export { addWorkflow } from './dsl/workflow-runner.js'
@@ -45,12 +42,6 @@ export {
45
42
  type PikkuWorkflowGraphConfig,
46
43
  type PikkuWorkflowGraphResult,
47
44
  } from './graph/wire-workflow-graph.js'
48
-
49
- export {
50
- pikkuWorkflowWorkerFunc,
51
- pikkuWorkflowOrchestratorFunc,
52
- pikkuWorkflowSleeperFunc,
53
- } from './workflow-queue-workers.js'
54
45
  export type {
55
46
  WorkflowStepInput as WorkflowStepQueueInput,
56
47
  PikkuWorkflowOrchestratorInput,
@@ -73,34 +64,25 @@ export type {
73
64
  WorkflowRunMirror,
74
65
  CoreWorkflow,
75
66
  CoreFeature,
76
- CoreFeatureScenario,
77
67
  FeatureMeta,
78
- FeatureMetaEntry,
79
68
  FeaturesMeta,
80
69
  FeaturePlanEntry,
81
70
  PikkuWorkflow,
82
71
  ContextVariable,
83
72
  WorkflowContext,
84
73
  WorkflowsMeta,
85
- WorkflowRuntimeMeta,
86
74
  WorkflowsRuntimeMeta,
87
75
  } from './workflow.types.js'
88
76
 
89
77
  export type {
90
78
  WorkflowStepOptions,
91
79
  WorkflowWireDoRPC,
92
- WorkflowWireDoInline,
93
- WorkflowWireSleep,
94
- WorkflowWireSuspend,
95
- WorkflowWireApproval,
96
80
  WorkflowApprovalOptions,
97
81
  ApprovalOutcome,
98
82
  InputSource,
99
83
  OutputBinding,
100
84
  RpcStepMeta,
101
- SimpleCondition,
102
85
  Condition,
103
- BranchCase,
104
86
  BranchStepMeta,
105
87
  ParallelGroupStepMeta,
106
88
  FanoutStepMeta,
@@ -135,7 +117,7 @@ export type {
135
117
  export { SCENARIO_SURFACES } from './scenario-step.types.js'
136
118
 
137
119
  // Which of a step's bindings run: one for an action, every witness for a `then`
138
- export { resolveScenarioSurfaces, witnessesAgree } from './scenario-surface.js'
120
+ export { resolveScenarioSurfaces } from './scenario-surface.js'
139
121
 
140
122
  export { requireActor, requireScenarioEnv } from './scenario-step-guards.js'
141
123
 
@@ -29,11 +29,14 @@ import { resolveScenarioSurfaces, witnessesAgree } from './scenario-surface.js'
29
29
  import type {
30
30
  PikkuScenarioWire,
31
31
  PikkuWorkflowWire,
32
+ WorkflowQueueOptions,
33
+ } from './workflow.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'
37
40
 
38
41
  /**
39
42
  * A workflow service with the scenario capability attached — the two lines
@@ -245,9 +248,9 @@ export class PikkuScenarioService implements WorkflowRunExtension {
245
248
  // Scenario actors per run: live authenticated clients (cookie jars) are
246
249
  // process-local by nature, so they ride this map, never the persisted wire.
247
250
  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.
251
+ // What each run has accumulated so far — process-local like the actors above,
252
+ // so the body and its hooks share one object rather than reading it back off
253
+ // the persisted wire.
251
254
  private runContexts = new Map<string, Record<string, unknown>>()
252
255
  private scenarioBrowserProvider?: ScenarioBrowserProvider
253
256
  private scenarioEnvironment?: ScenarioEnvironment
@@ -319,19 +322,19 @@ export class PikkuScenarioService implements WorkflowRunExtension {
319
322
  }
320
323
 
321
324
  /**
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.
325
+ * What the run has accumulated.
326
+ *
327
+ * Undefined only for a run whose wire was never decorated — decoration calls
328
+ * `contextForRun` unconditionally, so a plain workflow that has reached a
329
+ * step holds an empty context rather than none.
324
330
  */
325
331
  public getRunContext(runId: string): Record<string, unknown> | undefined {
326
332
  return this.runContexts.get(runId)
327
333
  }
328
334
 
329
335
  /**
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.
336
+ * The run's context, created on demand because the wire is also built on
337
+ * paths that skip `attachRunContext`.
335
338
  */
336
339
  private contextForRun(runId: string): Record<string, unknown> {
337
340
  let runContext = this.runContexts.get(runId)
@@ -539,9 +542,6 @@ export class PikkuScenarioService implements WorkflowRunExtension {
539
542
  rpcService,
540
543
  })
541
544
  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
545
  context: this.contextForRun(runId),
546
546
 
547
547
  // Durable polling step: invoke an RPC (as an actor when options.as is
@@ -684,6 +684,51 @@ export class PikkuScenarioService implements WorkflowRunExtension {
684
684
  )
685
685
  },
686
686
 
687
+ expectScore: async (
688
+ stepName: string,
689
+ agentRunId: string,
690
+ scorerName: string,
691
+ options?: WorkflowExpectScoreOptions
692
+ ) => {
693
+ this.engine.verifyStepName(stepName)
694
+ return await this.engine.inlineStep(
695
+ runId,
696
+ stepName,
697
+ async () => {
698
+ const rpcName = 'pikkuScenarioGradeRun'
699
+ const data = {
700
+ runId: agentRunId,
701
+ scorer: scorerName,
702
+ ...(options?.reference !== undefined
703
+ ? { reference: options.reference }
704
+ : {}),
705
+ }
706
+ const grade: { score: number; reason?: string } = options?.actor
707
+ ? await options.actor.invoke(rpcName, data)
708
+ : await rpcService.rpcWithWire(rpcName, data, {})
709
+
710
+ // A scorer that graded is a scorer that answered, so an unstated
711
+ // bound still fails a zero rather than passing anything at all.
712
+ const atLeast = options?.atLeast ?? 0.5
713
+ const failed =
714
+ grade.score < atLeast ||
715
+ (options?.atMost !== undefined && grade.score > options.atMost)
716
+ if (failed) {
717
+ const bound =
718
+ options?.atMost !== undefined
719
+ ? `between ${atLeast} and ${options.atMost}`
720
+ : `at least ${atLeast}`
721
+ throw new Error(
722
+ `[workflow] expectScore '${stepName}' expected '${scorerName}' to grade run ${agentRunId} ${bound}, got ${grade.score}` +
723
+ `${grade.reason ? `: ${grade.reason}` : ''}`
724
+ )
725
+ }
726
+ return grade
727
+ },
728
+ options
729
+ )
730
+ },
731
+
687
732
  // knowledge: decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md
688
733
  given: (stepName, stepFunc, data, options) =>
689
734
  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(