@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
@@ -0,0 +1,76 @@
1
+ import type {
2
+ PikkuAIScorer,
3
+ ScorerInput,
4
+ ScorerOutput,
5
+ } from './ai-scorer.types.js'
6
+
7
+ const assertSampleRate = (name: string, sampleRate: number | undefined) => {
8
+ if (sampleRate === undefined) return 1
9
+ if (sampleRate < 0 || sampleRate > 1) {
10
+ throw new Error(
11
+ `Scorer '${name}' has a sampleRate of ${sampleRate} — it is a fraction of live runs to grade, so it must be between 0 and 1`
12
+ )
13
+ }
14
+ return sampleRate
15
+ }
16
+
17
+ /**
18
+ * A heuristic scorer: pure code over the finished run, no model call, so it
19
+ * grades on the fast lane.
20
+ */
21
+ export const pikkuAIScorer = <Services = any>(config: {
22
+ name: string
23
+ description: string
24
+ /** 0..1 fraction of live runs to grade. Defaults to all of them. */
25
+ sampleRate?: number
26
+ /**
27
+ * Grades against a known-correct answer. Such a scorer is test-only — live
28
+ * traffic has no answer key, so the runtime never samples it.
29
+ */
30
+ requiresReference?: boolean
31
+ score: (
32
+ input: ScorerInput,
33
+ services: Services
34
+ ) => ScorerOutput | Promise<ScorerOutput>
35
+ }): PikkuAIScorer<Services> => ({
36
+ name: config.name,
37
+ description: config.description,
38
+ lane: 'fast',
39
+ sampleRate: assertSampleRate(config.name, config.sampleRate),
40
+ requiresReference: config.requiresReference ?? false,
41
+ score: config.score,
42
+ })
43
+
44
+ /**
45
+ * An LLM-judge scorer: the runtime makes the model call and forces a structured
46
+ * `{ score, reason }`, so a judge is a rubric rather than a prompt to parse.
47
+ *
48
+ * The rubric field is `goal`, matching `pikkuAIAgent`'s prompt vocabulary — a
49
+ * judge is a degenerate agent, and should use the same word for the same thing.
50
+ * `prompt` is the escape hatch for non-standard framing.
51
+ */
52
+ export const pikkuAIJudge = <Services = any>(config: {
53
+ name: string
54
+ description: string
55
+ /** 0..1 fraction of live runs to grade. Defaults to all of them. */
56
+ sampleRate?: number
57
+ /**
58
+ * Grades against a known-correct answer. Such a judge is test-only — live
59
+ * traffic has no answer key, so the runtime never samples it.
60
+ */
61
+ requiresReference?: boolean
62
+ model: string
63
+ goal: string
64
+ prompt?: (input: ScorerInput) => string
65
+ }): PikkuAIScorer<Services> => ({
66
+ name: config.name,
67
+ description: config.description,
68
+ lane: 'slow',
69
+ sampleRate: assertSampleRate(config.name, config.sampleRate),
70
+ requiresReference: config.requiresReference ?? false,
71
+ judge: {
72
+ model: config.model,
73
+ goal: config.goal,
74
+ ...(config.prompt ? { prompt: config.prompt } : {}),
75
+ },
76
+ })
@@ -0,0 +1,107 @@
1
+ /**
2
+ * What a scorer is shown about a finished run.
3
+ *
4
+ * One snapshot, taken after the output middleware has resolved, so a scorer and
5
+ * the persisted run record see the same thing — including the redactions.
6
+ */
7
+ export interface ScorerInput {
8
+ runId: string
9
+ agentName: string
10
+ threadId?: string
11
+ resourceId?: string
12
+ /** The user prompt the run answered. */
13
+ input: string
14
+ /** The agent's final text answer. */
15
+ output: string
16
+ /**
17
+ * The known-correct answer. Supplied by a scenario for a reference-based
18
+ * judge; never set on live traffic, which has no answer key.
19
+ */
20
+ reference?: string
21
+ toolCalls: {
22
+ name: string
23
+ args: unknown
24
+ result?: unknown
25
+ error?: string
26
+ }[]
27
+ usage: {
28
+ inputTokens: number
29
+ outputTokens: number
30
+ model?: string
31
+ }
32
+ }
33
+
34
+ export interface ScorerOutput {
35
+ /** 0..1, so grades are comparable across scorers. */
36
+ score: number
37
+ reason?: string
38
+ metadata?: Record<string, unknown>
39
+ }
40
+
41
+ /**
42
+ * The queue a scorer's jobs go to. Two lanes exist only so a flood of slow
43
+ * LLM-judge jobs cannot starve the cheap heuristic ones; within a lane it is
44
+ * plain FIFO.
45
+ */
46
+ export type ScorerLane = 'fast' | 'slow'
47
+
48
+ export const SCORER_LANE_QUEUES: Record<ScorerLane, string> = {
49
+ fast: 'ai-score-fast',
50
+ slow: 'ai-score-slow',
51
+ }
52
+
53
+ /**
54
+ * A judge's model call. Present only on a scorer built with `pikkuAIJudge`;
55
+ * `score` is present only on one built with `pikkuAIScorer`. Exactly one of the
56
+ * two is set, which is what the two constructors exist to guarantee.
57
+ */
58
+ export type ScorerJudgeConfig = {
59
+ model: string
60
+ goal: string
61
+ prompt?: (input: ScorerInput) => string
62
+ }
63
+
64
+ export type PikkuAIScorer<Services = any> = {
65
+ name: string
66
+ description: string
67
+ lane: ScorerLane
68
+ /** 0..1 fraction of live runs to grade. */
69
+ sampleRate: number
70
+ /**
71
+ * Grades against a known-correct answer, so it is test-only: live traffic has
72
+ * no answer key and the runtime never samples it.
73
+ */
74
+ requiresReference: boolean
75
+ score?: (
76
+ input: ScorerInput,
77
+ services: Services
78
+ ) => ScorerOutput | Promise<ScorerOutput>
79
+ judge?: ScorerJudgeConfig
80
+ }
81
+
82
+ export type ScorerMeta = Record<
83
+ string,
84
+ {
85
+ name: string
86
+ description: string
87
+ lane: ScorerLane
88
+ sampleRate: number
89
+ requiresReference: boolean
90
+ sourceFile?: string
91
+ exportedName?: string
92
+ }
93
+ >
94
+
95
+ /** A single scorer's job on a lane queue. */
96
+ export type ScoreJob = ScorerInput & {
97
+ scorerName: string
98
+ }
99
+
100
+ export type AIRunScore = {
101
+ runId: string
102
+ scorerName: string
103
+ score: number
104
+ reason?: string
105
+ metadata?: Record<string, unknown>
106
+ createdAt: Date
107
+ }
@@ -0,0 +1,24 @@
1
+ export { pikkuAIScorer, pikkuAIJudge } from './ai-scorer.js'
2
+ export {
3
+ addAIScorer,
4
+ getAIScorers,
5
+ getAIScorersMeta,
6
+ } from './ai-scorer-registry.js'
7
+ export { gradeRun } from './ai-scorer-grade.js'
8
+ export {
9
+ enableScoreSnapshots,
10
+ getScoreSnapshot,
11
+ } from './ai-scorer-snapshots.js'
12
+ export {
13
+ wireAIScorerQueueWorkers,
14
+ } from './ai-scorer-worker.js'
15
+ export type {
16
+ AIRunScore,
17
+ PikkuAIScorer,
18
+ ScoreJob,
19
+ ScorerInput,
20
+ ScorerJudgeConfig,
21
+ ScorerLane,
22
+ ScorerMeta,
23
+ ScorerOutput,
24
+ } from './ai-scorer.types.js'
@@ -1,8 +1,7 @@
1
- export { wireChannel, openChannel } from './channel-runner.js'
1
+ export { wireChannel } from './channel-runner.js'
2
2
  export {
3
3
  addChannelMiddleware,
4
4
  combineChannelMiddleware,
5
- wrapChannelWithMiddleware,
6
5
  } from './channel-middleware-runner.js'
7
6
  export { logChannels } from './log-channels.js'
8
7
  export { PikkuAbstractChannelHandler } from './pikku-abstract-channel-handler.js'
@@ -18,25 +17,14 @@ export type {
18
17
  CorePikkuChannelMiddlewareFactory,
19
18
  ChannelMessageMeta,
20
19
  ChannelMeta,
21
- ChannelRemote,
22
20
  PikkuChannel,
23
21
  PikkuChannelHandlerFactory,
24
22
  } from './channel.types.js'
25
23
  export { defineChannelRoutes } from './define-channel-routes.js'
26
24
  export {
27
- CHANNEL_RPC_PENDING,
28
- CHANNEL_RPC_REQUEST,
29
- CHANNEL_RPC_RESPONSE,
30
25
  ChannelDeploymentService,
31
26
  ChannelRPCError,
32
27
  ChannelRPCRegistry,
33
- createChannelRPCResponder,
34
- createChannelRPCInputValidator,
35
- createChannelRPCResultValidator,
36
- isChannelRPCPending,
37
- isChannelRPCRequest,
38
- isChannelRPCResponse,
39
- resolveCapability,
40
28
  unsupportedChannelRemote,
41
29
  } from './channel-rpc.js'
42
30
  export type {
@@ -44,16 +32,9 @@ export type {
44
32
  ApprovalRequester,
45
33
  Capabilities,
46
34
  Capability,
47
- CapabilityDef,
48
35
  CapabilityHandler,
49
36
  ChannelRPCPending,
50
37
  ChannelRPCRequest,
51
38
  ChannelRPCResponse,
52
39
  ChannelRPCValidator,
53
40
  } from './channel-rpc.js'
54
- export {
55
- channelRemote,
56
- getChannelHostRPC,
57
- handleChannelRPCResponse,
58
- releaseChannelHostRPC,
59
- } from './channel-host-rpc.js'
@@ -3,6 +3,7 @@ import * as assert from 'node:assert/strict'
3
3
  import { runLocalChannel } from './local-channel-runner.js'
4
4
  import { pikkuState, resetPikkuState } from '../../../pikku-state.js'
5
5
  import { wireChannel } from '../channel-runner.js'
6
+ import { addFunction } from '../../../function/function-runner.js'
6
7
  import { addHTTPMiddleware } from '../../http/http-runner.js'
7
8
  import type {
8
9
  HTTPMethod,
@@ -270,3 +271,70 @@ test('runChannel should run HTTP middleware on websocket upgrade and establish s
270
271
  await result.message(JSON.stringify({ action: 'ping' }))
271
272
  assert.deepEqual(sent, { action: 'ping' })
272
273
  })
274
+
275
+ test('a message handler that returns nothing does not attempt to send', async () => {
276
+ // The connect path already guards this; the message path did not, so a handler
277
+ // with nothing to say produced `send requires a non-empty message` on every
278
+ // message. Gateway websockets hit it every time — their generated message
279
+ // handler returns undefined by design — which is why a chat gateway could
280
+ // accept a connection and never deliver anything to its handler.
281
+ resetPikkuState()
282
+ pikkuState(null, 'package', 'singletonServices', mockSingletonServices as any)
283
+ pikkuState(null, 'package', 'factories', {
284
+ createWireServices: mockCreateWireServices,
285
+ } as any)
286
+
287
+ const sent: unknown[] = []
288
+ let handled = 0
289
+
290
+ pikkuState(null, 'channel', 'meta', {
291
+ quiet: {
292
+ name: 'quiet',
293
+ route: '/quiet-channel',
294
+ message: { pikkuFuncId: 'quietMessage' },
295
+ messageWirings: {},
296
+ },
297
+ } as any)
298
+ pikkuState(null, 'function', 'meta')['quietMessage'] = {
299
+ pikkuFuncId: 'quietMessage',
300
+ inputSchemaName: null,
301
+ outputSchemaName: null,
302
+ sessionless: true,
303
+ } as any
304
+ const quietMessage = {
305
+ auth: false,
306
+ func: async () => {
307
+ handled++
308
+ return undefined
309
+ },
310
+ }
311
+ wireChannel({
312
+ name: 'quiet',
313
+ route: '/quiet-channel',
314
+ auth: false,
315
+ onMessage: quietMessage as any,
316
+ } as any)
317
+
318
+ httpRouter.initialize()
319
+
320
+ const handler = await runLocalChannel({
321
+ channelId: 'quiet-channel-id',
322
+ request: new PikkuMockRequest('/quiet-channel', 'get'),
323
+ response: new PikkuMockResponse(),
324
+ route: '/quiet-channel',
325
+ })
326
+ assert.ok(handler)
327
+ handler.registerOnSend(async (message: unknown) => {
328
+ sent.push(message)
329
+ })
330
+ handler.open()
331
+
332
+ await handler.message(JSON.stringify({ text: 'hello' }))
333
+
334
+ assert.equal(handled, 1, 'the handler should have run')
335
+ assert.deepEqual(
336
+ sent,
337
+ [],
338
+ 'nothing should have been sent for an empty result'
339
+ )
340
+ })
@@ -213,7 +213,14 @@ export const runLocalChannel = async ({
213
213
  channelHandler.registerOnMessage(async (data) => {
214
214
  try {
215
215
  const result = await onMessage(data)
216
- await channel.send(result)
216
+ // Guarded exactly as the connect path above is. A handler with nothing
217
+ // to say is normal — a gateway websocket's generated message handler
218
+ // always returns undefined — and sending that raised `send requires a
219
+ // non-empty message`, which surfaced as a channel that accepted a
220
+ // connection and then silently delivered nothing.
221
+ if (result !== undefined) {
222
+ await channel.send(result)
223
+ }
217
224
  } catch (e: any) {
218
225
  singletonServices.logger.error(e)
219
226
  const errorResponse = getErrorResponse(e)
@@ -103,6 +103,29 @@ describe('handleRawCLI', () => {
103
103
  assert.match(result.error!, /deploy plan is red/)
104
104
  })
105
105
 
106
+ test('masks the raw error message to the client in production', async () => {
107
+ const prior = process.env.NODE_ENV
108
+ process.env.NODE_ENV = 'production'
109
+ try {
110
+ wireTestCLI(() => {
111
+ throw new Error('connection to internal-db:5432 failed: bad password')
112
+ })
113
+
114
+ const result = await handleRawCLI({
115
+ programName: 'fabric',
116
+ args: ['deploy'],
117
+ singletonServices,
118
+ })
119
+
120
+ assert.equal(result.exitCode, 1)
121
+ assert.equal(result.error, 'Command failed')
122
+ assert.doesNotMatch(result.error!, /internal-db|password/)
123
+ } finally {
124
+ if (prior === undefined) delete process.env.NODE_ENV
125
+ else process.env.NODE_ENV = prior
126
+ }
127
+ })
128
+
106
129
  test('exits 0 on help and 1 on an unknown command', async () => {
107
130
  wireTestCLI(() => ({}))
108
131
 
@@ -1,4 +1,5 @@
1
1
  import { pikkuState } from '../../../pikku-state.js'
2
+ import { isProduction } from '../../../env.js'
2
3
  import type { CLIMeta } from '../cli.types.js'
3
4
  import { parseCLIArguments, generateCommandHelp } from '../command-parser.js'
4
5
  import { runCLICommand } from '../cli-runner.js'
@@ -132,6 +133,16 @@ export async function handleRawCLI({
132
133
  // A command can throw anything; `.message` off a string is undefined, and
133
134
  // the run would then exit 1 saying nothing at all.
134
135
  const message = e instanceof Error ? e.message : String(e)
135
- return { error: message || 'Command failed', exitCode: 1, commandId }
136
+ // The raw message can carry internals (a stack, a DB error, a path). This
137
+ // runs over a channel that may be remote, so return a generic error in
138
+ // production and keep the detail server-side. Dev keeps the message inline.
139
+ singletonServices.logger.error?.(
140
+ `CLI channel command failed: ${message}`,
141
+ e
142
+ )
143
+ const clientError = isProduction()
144
+ ? 'Command failed'
145
+ : message || 'Command failed'
146
+ return { error: clientError, exitCode: 1, commandId }
136
147
  }
137
148
  }
@@ -3,13 +3,6 @@ export { handleRawCLI } from './cli-raw-channel-runner.js'
3
3
  export type { RawCLIFrame, RawCLIResult } from './cli-raw-channel-runner.js'
4
4
  export { executeRawCLIViaChannel } from './cli-raw-client-runner.js'
5
5
  export type {
6
- ClientCLIRenderServices,
7
6
  CorePikkuCLIClientRender,
8
7
  } from './cli-raw-client-runner.js'
9
- export {
10
- APPROVAL_FLAGS,
11
- approverForMode,
12
- createTerminalApprover,
13
- takeApprovalFlags,
14
- } from './cli-approval.js'
15
8
  export type { ApprovalMode } from './cli-approval.js'
@@ -396,6 +396,74 @@ describe('CLI Runner', () => {
396
396
  const programs = pikkuState(null, 'cli', 'programs')
397
397
  assert.strictEqual(programs['nonexistent'], undefined)
398
398
  })
399
+
400
+ test('enforces command-level permissions declared on a CLI command', async () => {
401
+ let functionRan = false
402
+
403
+ pikkuState(null, 'cli', 'meta', {
404
+ programs: {
405
+ 'sec-cli': {
406
+ program: 'sec-cli',
407
+ commands: {
408
+ secret: {
409
+ command: 'secret',
410
+ pikkuFuncId: 'secretFunc',
411
+ positionals: [],
412
+ options: {},
413
+ },
414
+ },
415
+ options: {},
416
+ },
417
+ },
418
+ renderers: {},
419
+ })
420
+ pikkuState(null, 'function', 'meta', {
421
+ secretFunc: {
422
+ pikkuFuncId: 'secretFunc',
423
+ inputSchemaName: null,
424
+ outputSchemaName: null,
425
+ sessionless: true,
426
+ },
427
+ })
428
+
429
+ wireCLI({
430
+ program: 'sec-cli',
431
+ commands: {
432
+ // command.func is a function-config object, so unwrapFunc reads the
433
+ // inner func's auth/permissions and previously dropped the
434
+ // command-level ones — the branch this fix restores.
435
+ secret: {
436
+ func: {
437
+ func: async () => {
438
+ functionRan = true
439
+ return {}
440
+ },
441
+ },
442
+ // A permission that always denies.
443
+ permissions: { deny: async () => false },
444
+ } as any,
445
+ },
446
+ })
447
+
448
+ const result = await runCLICommand({
449
+ program: 'sec-cli',
450
+ commandPath: ['secret'],
451
+ data: {},
452
+ singletonServices,
453
+ createWireServices,
454
+ }).catch((error) => ({ success: false, error }))
455
+
456
+ assert.equal(
457
+ functionRan,
458
+ false,
459
+ 'the function must not run when a declared permission denies'
460
+ )
461
+ assert.notEqual(
462
+ (result as any).success,
463
+ true,
464
+ 'the command must not report success when permission is denied'
465
+ )
466
+ })
399
467
  })
400
468
 
401
469
  describe('pikkuCLIRender', () => {
@@ -169,7 +169,24 @@ function registerCLICommands(
169
169
  }
170
170
  }
171
171
 
172
- addFunction(funcName, unwrapFunc(command), currentMeta?.packageName)
172
+ // Merge the command-level auth/permissions into the registered config so
173
+ // they are actually enforced by the function runner. They were accepted by
174
+ // the types but dropped here, making a command's declared access control a
175
+ // silent no-op. Command-level wins over the handler's own, then falls back
176
+ // to it.
177
+ const unwrapped = unwrapFunc(command)
178
+ const commandAuth = typeof command === 'object' ? command.auth : undefined
179
+ const commandPermissions =
180
+ typeof command === 'object' ? command.permissions : undefined
181
+ addFunction(
182
+ funcName,
183
+ {
184
+ ...unwrapped,
185
+ auth: commandAuth ?? unwrapped.auth,
186
+ permissions: commandPermissions ?? unwrapped.permissions,
187
+ },
188
+ currentMeta?.packageName
189
+ )
173
190
 
174
191
  if (typeof command === 'object' && command.render) {
175
192
  if (programs[program]) {
@@ -2,7 +2,6 @@ export { defineCredential } from './define-credential.js'
2
2
  export { validateAndBuildCredentialDefinitionsMeta } from './validate-credential-definitions.js'
3
3
  export type {
4
4
  CoreCredential,
5
- CredentialDefinitionMeta,
6
5
  CredentialDefinitionsMeta,
7
6
  CredentialDefinitions,
8
7
  } from './credential.types.js'
@@ -6,13 +6,10 @@ export {
6
6
  export type {
7
7
  GatewayAdapter,
8
8
  GatewayAdapterFactory,
9
- GatewayAttachment,
10
9
  GatewayInboundMessage,
11
10
  GatewayOutboundMessage,
12
- GatewayMeta,
13
11
  GatewaysMeta,
14
12
  GatewayTransportType,
15
13
  CoreGateway,
16
- PikkuGateway,
17
14
  WebhookVerificationResult,
18
15
  } from './gateway.types.js'
@@ -70,6 +70,11 @@ class TestResponse extends PikkuMockResponse {
70
70
  return this
71
71
  }
72
72
 
73
+ redirect(location: string, status: number = 302): this {
74
+ this.status(status)
75
+ return this.header('Location', location)
76
+ }
77
+
73
78
  setMode(mode: 'stream') {
74
79
  this.mode = mode
75
80
  }
@@ -365,6 +370,67 @@ describe('http-runner helpers', () => {
365
370
  assert.equal(response.statusCode, 204)
366
371
  })
367
372
 
373
+ test('fetchData keeps a redirect status when the route returns undefined', async () => {
374
+ setRouteMeta('/redirect')
375
+ addFunction('pikku_func_name', {
376
+ func: async (_services: any, _data: any, { http }: any) => {
377
+ http?.response?.redirect('https://example.com/done', 302)
378
+ },
379
+ })
380
+ wireHTTP({
381
+ route: '/redirect',
382
+ method: 'get',
383
+ auth: false,
384
+ func: {
385
+ func: async (_services: any, _data: any, { http }: any) => {
386
+ http?.response?.redirect('https://example.com/done', 302)
387
+ },
388
+ },
389
+ })
390
+ httpRouter.initialize()
391
+
392
+ const request = new TestRequest('/redirect', 'get')
393
+ const response = new TestResponse()
394
+
395
+ await fetchData(request, response)
396
+
397
+ assert.equal(response.statusCode, 302)
398
+ assert.equal(
399
+ response.headersMap.get('Location'),
400
+ 'https://example.com/done'
401
+ )
402
+ })
403
+
404
+ test('fetchData keeps a status the route set alongside a body', async () => {
405
+ setRouteMeta('/created')
406
+ addFunction('pikku_func_name', {
407
+ func: async (_services: any, _data: any, { http }: any) => {
408
+ http?.response?.status(201)
409
+ return { id: 'thing-1' }
410
+ },
411
+ })
412
+ wireHTTP({
413
+ route: '/created',
414
+ method: 'get',
415
+ auth: false,
416
+ func: {
417
+ func: async (_services: any, _data: any, { http }: any) => {
418
+ http?.response?.status(201)
419
+ return { id: 'thing-1' }
420
+ },
421
+ },
422
+ })
423
+ httpRouter.initialize()
424
+
425
+ const request = new TestRequest('/created', 'get')
426
+ const response = new TestResponse()
427
+
428
+ await fetchData(request, response)
429
+
430
+ assert.equal(response.statusCode, 201)
431
+ assert.deepEqual(response.jsonBody, { id: 'thing-1' })
432
+ })
433
+
368
434
  test('fetchData writes binary responses when returnsJSON is false', async () => {
369
435
  setRouteMeta('/binary')
370
436
  wireHTTP({
@@ -283,6 +283,8 @@ const executeRoute = async (
283
283
  hasSessionChanged: () => userSession.sessionChanged,
284
284
  }
285
285
 
286
+ const statusBeforeRoute = http?.response?.statusCode
287
+
286
288
  try {
287
289
  result = await runPikkuFunc(
288
290
  'http',
@@ -323,14 +325,20 @@ const executeRoute = async (
323
325
  if (matchedRoute.route.sse) {
324
326
  http?.response?.flushHeaders?.()
325
327
  } else {
328
+ const statusSetByRoute = http?.response?.statusCode !== statusBeforeRoute
329
+
326
330
  if (result instanceof Response) {
327
331
  await applyWebResponse(http!.response!, result)
328
332
  } else if (result === undefined || result === null) {
329
- http?.response?.status(204)
333
+ if (!statusSetByRoute) {
334
+ http?.response?.status(204)
335
+ }
330
336
  } else if (route.returnsJSON === false) {
331
337
  http?.response?.arrayBuffer(result)
332
338
  } else {
333
- http?.response?.status(200)
339
+ if (!statusSetByRoute) {
340
+ http?.response?.status(200)
341
+ }
334
342
  http?.response?.json(result)
335
343
  }
336
344
  }
@@ -2,7 +2,6 @@ export {
2
2
  PikkuFetchHTTPRequest,
3
3
  DEFAULT_MAX_BODY_SIZE,
4
4
  } from './pikku-fetch-http-request.js'
5
- export type { PikkuFetchHTTPRequestOptions } from './pikku-fetch-http-request.js'
6
5
  export { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
7
6
  export { logRoutes } from './log-http-routes.js'
8
7
 
@@ -19,6 +18,7 @@ export type {
19
18
  HTTPRouteContract,
20
19
  HTTPRouteMap,
21
20
  HTTPWiringsMeta,
21
+ PikkuHTTP,
22
22
  PikkuHTTPRequest,
23
23
  PikkuHTTPResponse,
24
24
  PikkuQuery,
@@ -16,7 +16,6 @@ export type {
16
16
  AssertMCPResourceURIParams,
17
17
  CoreMCPPrompt,
18
18
  CoreMCPResource,
19
- CoreMCPTool,
20
19
  MCPPromptResponse,
21
20
  MCPResourceMeta,
22
21
  MCPResourceResponse,