@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
@@ -4,6 +4,8 @@ import type {
4
4
  CoreSingletonServices,
5
5
  } from '../../types/core.types.js'
6
6
  import type { SecretService } from '../../services/secret-service.js'
7
+ import { ScopedSecretService } from '../../services/scoped-secret-service.js'
8
+ import { ScopedCredentialService } from '../../services/scoped-credential-service.js'
7
9
  import type { VariablesService } from '../../services/variables-service.js'
8
10
 
9
11
  export type AddonInstance = {
@@ -11,8 +13,32 @@ export type AddonInstance = {
11
13
  secretOverrides?: Record<string, string>
12
14
  variableOverrides?: Record<string, string>
13
15
  credentialOverrides?: Record<string, string>
16
+ /** Set by the consuming app: secrets it lends this instance, as the addon names them. */
17
+ secretGrants?: string[]
18
+ /** Set by the consuming app: credentials it lends this instance, as the addon names them. */
19
+ credentialGrants?: string[]
20
+ /** Set by the consuming app to opt this instance out of secret scoping. */
21
+ globalSecrets?: string
22
+ /** Set by the consuming app to opt this instance out of credential scoping. */
23
+ globalCredentials?: string
14
24
  }
15
25
 
26
+ /**
27
+ * What the addon declared, plus what the host lent it. Scoping runs outside the
28
+ * aliaser, so every name here is the one the addon reads — which is why an
29
+ * override's *key* grants, and its value does not.
30
+ */
31
+ const allowedNames = (
32
+ declared: string[] | null | undefined,
33
+ grants: string[] | undefined,
34
+ overrides: Record<string, string> | undefined
35
+ ): Set<string> =>
36
+ new Set([
37
+ ...(declared ?? []),
38
+ ...(grants ?? []),
39
+ ...Object.keys(overrides ?? {}),
40
+ ])
41
+
16
42
  const aliasSecretService = (
17
43
  secrets: SecretService,
18
44
  overrides: Record<string, string>
@@ -111,9 +137,6 @@ export const getOrCreatePackageSingletonServices = async (
111
137
  }
112
138
 
113
139
  const factories = pikkuState(packageName, 'package', 'factories')
114
- if (!factories || !factories.createSingletonServices) {
115
- return parentServices
116
- }
117
140
 
118
141
  let existingServices = parentServices
119
142
  if (addonInstance?.secretOverrides && parentServices.secrets) {
@@ -134,6 +157,38 @@ export const getOrCreatePackageSingletonServices = async (
134
157
  ),
135
158
  }
136
159
  }
160
+ if (!addonInstance?.globalSecrets && existingServices.secrets) {
161
+ existingServices = {
162
+ ...existingServices,
163
+ secrets: new ScopedSecretService(
164
+ existingServices.secrets,
165
+ allowedNames(
166
+ pikkuState(packageName, 'package', 'declaredSecrets'),
167
+ addonInstance?.secretGrants,
168
+ addonInstance?.secretOverrides
169
+ )
170
+ ),
171
+ }
172
+ }
173
+ if (!addonInstance?.globalCredentials && existingServices.credentialService) {
174
+ existingServices = {
175
+ ...existingServices,
176
+ credentialService: new ScopedCredentialService(
177
+ existingServices.credentialService,
178
+ allowedNames(
179
+ Object.keys(
180
+ pikkuState(packageName, 'package', 'credentialsMeta') ?? {}
181
+ ),
182
+ addonInstance?.credentialGrants,
183
+ addonInstance?.credentialOverrides
184
+ )
185
+ ),
186
+ }
187
+ }
188
+
189
+ if (!factories || !factories.createSingletonServices) {
190
+ return existingServices
191
+ }
137
192
 
138
193
  let config: CoreConfig = existingServices.config
139
194
  if (factories.createConfig) {
@@ -173,5 +228,9 @@ export const addonInstanceForNamespace = (
173
228
  secretOverrides: cfg.secretOverrides,
174
229
  variableOverrides: cfg.variableOverrides,
175
230
  credentialOverrides: cfg.credentialOverrides,
231
+ secretGrants: cfg.secretGrants,
232
+ credentialGrants: cfg.credentialGrants,
233
+ globalSecrets: cfg.globalSecrets,
234
+ globalCredentials: cfg.globalCredentials,
176
235
  }
177
236
  }
@@ -0,0 +1,391 @@
1
+ import { beforeEach, describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
5
+ import { getOrCreatePackageSingletonServices } from './addon-runner.js'
6
+ import { wireAddon } from './wire-addon.js'
7
+ import type { SecretService } from '../../services/secret-service.js'
8
+ import type { CredentialService } from '../../services/credential-service.js'
9
+ import type { CoreSingletonServices } from '../../types/core.types.js'
10
+
11
+ const ADDON_PACKAGE = '@addon/example'
12
+
13
+ const createSecretService = (): SecretService =>
14
+ ({
15
+ getSecret: async (key: string) => `value-of-${key}` as never,
16
+ hasSecret: async () => true,
17
+ setSecret: async () => {},
18
+ deleteSecret: async () => {},
19
+ getSecrets: async (keys: string[]) =>
20
+ Object.fromEntries(keys.map((k) => [k, `value-of-${k}`])) as never,
21
+ }) as SecretService
22
+
23
+ const createCredentialService = (): CredentialService =>
24
+ ({
25
+ get: async (name: string) => `credential-of-${name}`,
26
+ set: async () => {},
27
+ delete: async () => {},
28
+ has: async () => true,
29
+ getAll: async () => ({ slack: 'a', stripe: 'b' }),
30
+ getUsersWithCredential: async () => ['user-1'],
31
+ getAllUsers: async () => ['user-1'],
32
+ }) as unknown as CredentialService
33
+
34
+ const createParentServices = () =>
35
+ ({
36
+ config: {},
37
+ logger: {
38
+ debug: () => {},
39
+ info: () => {},
40
+ warn: () => {},
41
+ error: () => {},
42
+ },
43
+ secrets: createSecretService(),
44
+ credentialService: createCredentialService(),
45
+ }) as unknown as CoreSingletonServices
46
+
47
+ /**
48
+ * Registers an addon package whose singleton factory hands back whatever
49
+ * secrets service the host decided it should see.
50
+ */
51
+ const registerAddon = (
52
+ declaredSecrets: string[] | null,
53
+ declaredCredentials?: string[]
54
+ ) => {
55
+ pikkuState(ADDON_PACKAGE, 'package', 'factories', {
56
+ createSingletonServices: (async (_config: unknown, parent: any) => ({
57
+ ...parent,
58
+ secretsSeenByAddon: parent.secrets,
59
+ credentialsSeenByAddon: parent.credentialService,
60
+ })) as never,
61
+ })
62
+ if (declaredSecrets) {
63
+ pikkuState(ADDON_PACKAGE, 'package', 'declaredSecrets', declaredSecrets)
64
+ }
65
+ if (declaredCredentials) {
66
+ pikkuState(
67
+ ADDON_PACKAGE,
68
+ 'package',
69
+ 'credentialsMeta',
70
+ Object.fromEntries(
71
+ declaredCredentials.map((name) => [
72
+ name,
73
+ { name, displayName: name, type: 'singleton' },
74
+ ])
75
+ )
76
+ )
77
+ }
78
+ }
79
+
80
+ const servicesHandedToAddon = async (namespace: string) =>
81
+ (await getOrCreatePackageSingletonServices(
82
+ ADDON_PACKAGE,
83
+ createParentServices(),
84
+ pikkuState(null, 'addons', 'packages').get(namespace) as never
85
+ )) as any
86
+
87
+ const secretsHandedToAddon = async (namespace: string) =>
88
+ (await servicesHandedToAddon(namespace)).secretsSeenByAddon as SecretService
89
+
90
+ const credentialsHandedToAddon = async (namespace: string) =>
91
+ (await servicesHandedToAddon(namespace))
92
+ .credentialsSeenByAddon as CredentialService
93
+
94
+ describe('addon secrets are scoped to what the addon declared', () => {
95
+ beforeEach(() => {
96
+ resetPikkuState()
97
+ })
98
+
99
+ test('a declared secret is readable', async () => {
100
+ registerAddon(['ADDON_KEY'])
101
+ wireAddon({
102
+ name: 'example',
103
+ package: ADDON_PACKAGE,
104
+ })
105
+
106
+ const secrets = await secretsHandedToAddon('example')
107
+ assert.equal(await secrets.getSecret('ADDON_KEY'), 'value-of-ADDON_KEY')
108
+ })
109
+
110
+ test('an undeclared secret is denied', async () => {
111
+ registerAddon(['ADDON_KEY'])
112
+ wireAddon({
113
+ name: 'example',
114
+ package: ADDON_PACKAGE,
115
+ })
116
+
117
+ const secrets = await secretsHandedToAddon('example')
118
+ await assert.rejects(
119
+ () => secrets.getSecret('SOMEONE_ELSES_KEY'),
120
+ /denied/i
121
+ )
122
+ })
123
+
124
+ test('a scoped addon cannot write secrets', async () => {
125
+ registerAddon(['ADDON_KEY'])
126
+ wireAddon({
127
+ name: 'example',
128
+ package: ADDON_PACKAGE,
129
+ })
130
+
131
+ const secrets = await secretsHandedToAddon('example')
132
+ await assert.rejects(() => secrets.setSecret('ADDON_KEY', 'x'))
133
+ })
134
+
135
+ test('an addon that declares nothing is scoped to nothing', async () => {
136
+ registerAddon([])
137
+ wireAddon({
138
+ name: 'example',
139
+ package: ADDON_PACKAGE,
140
+ })
141
+
142
+ const secrets = await secretsHandedToAddon('example')
143
+ await assert.rejects(() => secrets.getSecret('ADDON_KEY'), /denied/i)
144
+ })
145
+
146
+ test('globalSecrets, set by the host, hands over the whole service', async () => {
147
+ registerAddon(['ADDON_KEY'])
148
+ wireAddon({
149
+ name: 'example',
150
+ package: ADDON_PACKAGE,
151
+ globalSecrets: 'administers secrets named by the operator at runtime',
152
+ })
153
+
154
+ const secrets = await secretsHandedToAddon('example')
155
+ assert.equal(
156
+ await secrets.getSecret('SOMEONE_ELSES_KEY'),
157
+ 'value-of-SOMEONE_ELSES_KEY'
158
+ )
159
+ await assert.doesNotReject(() => secrets.setSecret('ADDON_KEY', 'x'))
160
+ })
161
+
162
+ test('an addon with no singleton factory is still scoped', async () => {
163
+ pikkuState(ADDON_PACKAGE, 'package', 'declaredSecrets', ['ADDON_KEY'])
164
+ wireAddon({
165
+ name: 'example',
166
+ package: ADDON_PACKAGE,
167
+ })
168
+
169
+ const services = await getOrCreatePackageSingletonServices(
170
+ ADDON_PACKAGE,
171
+ createParentServices(),
172
+ pikkuState(null, 'addons', 'packages').get('example') as never
173
+ )
174
+
175
+ assert.equal(
176
+ await services.secrets.getSecret('ADDON_KEY'),
177
+ 'value-of-ADDON_KEY'
178
+ )
179
+ await assert.rejects(
180
+ () => services.secrets.getSecret('SOMEONE_ELSES_KEY'),
181
+ /denied/i
182
+ )
183
+ })
184
+
185
+ test('scoping applies to the key the addon asks for, not the aliased one', async () => {
186
+ registerAddon(['ADDON_KEY'])
187
+ wireAddon({
188
+ name: 'example',
189
+ package: ADDON_PACKAGE,
190
+ secretOverrides: { ADDON_KEY: 'PROD_ADDON_KEY' },
191
+ })
192
+
193
+ const secrets = await secretsHandedToAddon('example')
194
+ assert.equal(
195
+ await secrets.getSecret('ADDON_KEY'),
196
+ 'value-of-PROD_ADDON_KEY'
197
+ )
198
+ await assert.rejects(() => secrets.getSecret('PROD_ADDON_KEY'), /denied/i)
199
+ })
200
+ })
201
+
202
+ describe('addon credentials are scoped to what the addon declared', () => {
203
+ beforeEach(() => {
204
+ resetPikkuState()
205
+ })
206
+
207
+ test('a declared credential is reachable', async () => {
208
+ registerAddon(['ADDON_KEY'], ['slack'])
209
+ wireAddon({ name: 'example', package: ADDON_PACKAGE })
210
+
211
+ const credentials = await credentialsHandedToAddon('example')
212
+ assert.equal(await credentials.get('slack'), 'credential-of-slack')
213
+ })
214
+
215
+ test('an undeclared credential is denied', async () => {
216
+ registerAddon(['ADDON_KEY'], ['slack'])
217
+ wireAddon({ name: 'example', package: ADDON_PACKAGE })
218
+
219
+ const credentials = await credentialsHandedToAddon('example')
220
+ await assert.rejects(() => credentials.get('stripe'), /denied/i)
221
+ })
222
+
223
+ test('a scoped addon cannot enumerate the app users', async () => {
224
+ registerAddon(['ADDON_KEY'], ['slack'])
225
+ wireAddon({ name: 'example', package: ADDON_PACKAGE })
226
+
227
+ const credentials = await credentialsHandedToAddon('example')
228
+ await assert.rejects(() => credentials.getAllUsers(), /denied/i)
229
+ })
230
+
231
+ test('an addon with no singleton factory is still scoped', async () => {
232
+ pikkuState(ADDON_PACKAGE, 'package', 'credentialsMeta', {
233
+ slack: { name: 'slack', displayName: 'slack', type: 'singleton' },
234
+ } as never)
235
+ wireAddon({ name: 'example', package: ADDON_PACKAGE })
236
+
237
+ const services = await getOrCreatePackageSingletonServices(
238
+ ADDON_PACKAGE,
239
+ createParentServices(),
240
+ pikkuState(null, 'addons', 'packages').get('example') as never
241
+ )
242
+ const credentials = services.credentialService as CredentialService
243
+
244
+ assert.equal(await credentials.get('slack'), 'credential-of-slack')
245
+ await assert.rejects(() => credentials.get('stripe'), /denied/i)
246
+ await assert.rejects(() => credentials.getAllUsers(), /denied/i)
247
+ })
248
+
249
+ test('globalCredentials, set by the host, hands over the whole service', async () => {
250
+ registerAddon(['ADDON_KEY'], ['slack'])
251
+ wireAddon({
252
+ name: 'example',
253
+ package: ADDON_PACKAGE,
254
+ globalCredentials: 'links credentials an operator names at runtime',
255
+ })
256
+
257
+ const credentials = await credentialsHandedToAddon('example')
258
+ assert.equal(await credentials.get('stripe'), 'credential-of-stripe')
259
+ assert.deepEqual(await credentials.getAllUsers(), ['user-1'])
260
+ })
261
+ })
262
+
263
+ /**
264
+ * An addon whose secret names come off its input rather than its own source
265
+ * declares nothing, so the host lends it names it could not have derived.
266
+ *
267
+ * knowledge: decisions/security/decision-secretless-functions.md
268
+ */
269
+ describe('the host grants an addon secrets it could not declare', () => {
270
+ beforeEach(() => {
271
+ resetPikkuState()
272
+ })
273
+
274
+ test('a granted secret is readable', async () => {
275
+ registerAddon([])
276
+ wireAddon({
277
+ name: 'example',
278
+ package: ADDON_PACKAGE,
279
+ secretGrants: ['STRIPE_KEY'],
280
+ })
281
+
282
+ const secrets = await secretsHandedToAddon('example')
283
+ assert.equal(await secrets.getSecret('STRIPE_KEY'), 'value-of-STRIPE_KEY')
284
+ })
285
+
286
+ test('a secret the host did not grant is still denied', async () => {
287
+ registerAddon([])
288
+ wireAddon({
289
+ name: 'example',
290
+ package: ADDON_PACKAGE,
291
+ secretGrants: ['STRIPE_KEY'],
292
+ })
293
+
294
+ const secrets = await secretsHandedToAddon('example')
295
+ await assert.rejects(() => secrets.getSecret('DATABASE_URL'), /denied/i)
296
+ })
297
+
298
+ test('a grant adds to what the addon declared rather than replacing it', async () => {
299
+ registerAddon(['ADDON_KEY'])
300
+ wireAddon({
301
+ name: 'example',
302
+ package: ADDON_PACKAGE,
303
+ secretGrants: ['STRIPE_KEY'],
304
+ })
305
+
306
+ const secrets = await secretsHandedToAddon('example')
307
+ assert.equal(await secrets.getSecret('ADDON_KEY'), 'value-of-ADDON_KEY')
308
+ assert.equal(await secrets.getSecret('STRIPE_KEY'), 'value-of-STRIPE_KEY')
309
+ })
310
+
311
+ test('an overridden secret is granted by the override itself', async () => {
312
+ registerAddon([])
313
+ wireAddon({
314
+ name: 'example',
315
+ package: ADDON_PACKAGE,
316
+ secretOverrides: { MAILGUN_KEY: 'PROD_EMAIL_KEY' },
317
+ })
318
+
319
+ const secrets = await secretsHandedToAddon('example')
320
+ assert.equal(
321
+ await secrets.getSecret('MAILGUN_KEY'),
322
+ 'value-of-PROD_EMAIL_KEY'
323
+ )
324
+ })
325
+
326
+ test('a grant is checked before the override renames it', async () => {
327
+ registerAddon([])
328
+ wireAddon({
329
+ name: 'example',
330
+ package: ADDON_PACKAGE,
331
+ secretGrants: ['MAILGUN_KEY'],
332
+ secretOverrides: { MAILGUN_KEY: 'PROD_EMAIL_KEY' },
333
+ })
334
+
335
+ const secrets = await secretsHandedToAddon('example')
336
+ assert.equal(
337
+ await secrets.getSecret('MAILGUN_KEY'),
338
+ 'value-of-PROD_EMAIL_KEY'
339
+ )
340
+ await assert.rejects(() => secrets.getSecret('PROD_EMAIL_KEY'), /denied/i)
341
+ })
342
+
343
+ test('a granted credential is readable', async () => {
344
+ registerAddon([], [])
345
+ wireAddon({
346
+ name: 'example',
347
+ package: ADDON_PACKAGE,
348
+ credentialGrants: ['stripe'],
349
+ })
350
+
351
+ const credentials = await credentialsHandedToAddon('example')
352
+ assert.equal(await credentials.get('stripe'), 'credential-of-stripe')
353
+ await assert.rejects(() => credentials.get('slack'), /denied/i)
354
+ })
355
+
356
+ test('a granted credential does not widen the rest of the service', async () => {
357
+ registerAddon([], [])
358
+ wireAddon({
359
+ name: 'example',
360
+ package: ADDON_PACKAGE,
361
+ credentialGrants: ['stripe'],
362
+ })
363
+
364
+ const credentials = await credentialsHandedToAddon('example')
365
+ await assert.rejects(() => credentials.getAllUsers(), /denied/i)
366
+ })
367
+
368
+ test('an addon without a singleton factory is granted too', async () => {
369
+ pikkuState(ADDON_PACKAGE, 'package', 'declaredSecrets', [])
370
+ wireAddon({
371
+ name: 'example',
372
+ package: ADDON_PACKAGE,
373
+ secretGrants: ['STRIPE_KEY'],
374
+ })
375
+
376
+ const services = await getOrCreatePackageSingletonServices(
377
+ ADDON_PACKAGE,
378
+ createParentServices(),
379
+ pikkuState(null, 'addons', 'packages').get('example') as never
380
+ )
381
+
382
+ assert.equal(
383
+ await services.secrets.getSecret('STRIPE_KEY'),
384
+ 'value-of-STRIPE_KEY'
385
+ )
386
+ await assert.rejects(
387
+ () => services.secrets.getSecret('DATABASE_URL'),
388
+ /denied/i
389
+ )
390
+ })
391
+ })
@@ -708,6 +708,7 @@ describe('multi-instance addons', () => {
708
708
  return svc
709
709
  },
710
710
  } as never)
711
+ pikkuState('@addon/slack', 'package', 'declaredSecrets', ['slackToken'])
711
712
  registerFunction(
712
713
  'send',
713
714
  async (services: any) => ({ token: services.token }),
@@ -753,6 +754,7 @@ describe('multi-instance addons', () => {
753
754
  }
754
755
  },
755
756
  } as never)
757
+ pikkuState('@addon/slack', 'package', 'declaredSecrets', ['slackToken'])
756
758
 
757
759
  registerFunction(
758
760
  'leaf',
@@ -284,6 +284,10 @@ export class ContextAwareRPCService {
284
284
  secretOverrides: resolved.addonConfig?.secretOverrides,
285
285
  variableOverrides: resolved.addonConfig?.variableOverrides,
286
286
  credentialOverrides: resolved.addonConfig?.credentialOverrides,
287
+ secretGrants: resolved.addonConfig?.secretGrants,
288
+ credentialGrants: resolved.addonConfig?.credentialGrants,
289
+ globalSecrets: resolved.addonConfig?.globalSecrets,
290
+ globalCredentials: resolved.addonConfig?.globalCredentials,
287
291
  },
288
292
  }
289
293
  }
@@ -67,6 +67,14 @@ 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: secrets it lends this instance, as the addon names them */
71
+ secretGrants?: string[]
72
+ /** Set by the consuming app: credentials it lends this instance, as the addon names them */
73
+ credentialGrants?: string[]
74
+ /** Set by the consuming app: hand this instance the unscoped `SecretService` */
75
+ globalSecrets?: string
76
+ /** Set by the consuming app: hand this instance the unscoped `CredentialService` */
77
+ globalCredentials?: string
70
78
  /** Set by `wireRemoteAddon`: dispatch this namespace's RPCs over HTTP */
71
79
  remote?: boolean
72
80
  serverUrl?: string | ((services: any) => string | Promise<string>)
@@ -14,6 +14,31 @@ export type WireAddonConfig = {
14
14
  secretOverrides?: Record<string, string>
15
15
  variableOverrides?: Record<string, string>
16
16
  credentialOverrides?: Record<string, string>
17
+ /**
18
+ * Secrets this instance may read on top of the ones it declared, named as the
19
+ * addon reads them — the scope check runs before `secretOverrides` renames
20
+ * them, so an overridden secret is named here by its addon-side key. Listing
21
+ * one in `secretOverrides` grants it too.
22
+ *
23
+ * For an addon whose secret names come off its input rather than its own
24
+ * source, this is how the host lends names the addon could not declare.
25
+ */
26
+ secretGrants?: string[]
27
+ /** Credentials this instance may read on top of the ones it declared. */
28
+ credentialGrants?: string[]
29
+ /**
30
+ * Hands this instance the whole `SecretService` instead of one scoped to the
31
+ * secrets it declared. The value is the reason, recorded in the deploy
32
+ * manifest — an addon that names secrets at runtime cannot be scoped, and
33
+ * only the consuming app, never the addon, can grant it.
34
+ */
35
+ globalSecrets?: string
36
+ /**
37
+ * Hands this instance the whole `CredentialService` instead of one narrowed
38
+ * to the credentials it declared. The value is the reason, recorded in the
39
+ * deploy manifest, and only the consuming app can grant it.
40
+ */
41
+ globalCredentials?: string
17
42
  }
18
43
 
19
44
  export const wireAddon = (config: WireAddonConfig): void => {
@@ -32,6 +57,14 @@ export const wireAddon = (config: WireAddonConfig): void => {
32
57
  ...(config.credentialOverrides
33
58
  ? { credentialOverrides: config.credentialOverrides }
34
59
  : {}),
60
+ ...(config.secretGrants ? { secretGrants: config.secretGrants } : {}),
61
+ ...(config.credentialGrants
62
+ ? { credentialGrants: config.credentialGrants }
63
+ : {}),
64
+ ...(config.globalSecrets ? { globalSecrets: config.globalSecrets } : {}),
65
+ ...(config.globalCredentials
66
+ ? { globalCredentials: config.globalCredentials }
67
+ : {}),
35
68
  })
36
69
  }
37
70
 
@@ -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,