@pikku/core 0.12.93 → 0.12.95

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 (56) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/services/email-template.d.ts +43 -0
  3. package/dist/services/email-template.js +139 -0
  4. package/dist/services/http-personas.d.ts +6 -1
  5. package/dist/services/http-personas.js +4 -1
  6. package/dist/services/index.d.ts +1 -0
  7. package/dist/services/index.js +1 -0
  8. package/dist/wirings/agent/agent-prepare.d.ts +14 -0
  9. package/dist/wirings/agent/agent-prepare.js +24 -0
  10. package/dist/wirings/agent/index.d.ts +1 -1
  11. package/dist/wirings/agent/index.js +1 -1
  12. package/dist/wirings/persona/index.d.ts +1 -0
  13. package/dist/wirings/persona/index.js +1 -0
  14. package/dist/wirings/persona/persona-app-scopes.d.ts +41 -0
  15. package/dist/wirings/persona/persona-app-scopes.js +61 -0
  16. package/dist/wirings/scheduler/scheduler-runner.js +0 -1
  17. package/dist/wirings/virtual-user/index.d.ts +1 -0
  18. package/dist/wirings/virtual-user/index.js +1 -0
  19. package/dist/wirings/virtual-user/virtual-user-derive.js +9 -0
  20. package/dist/wirings/virtual-user/virtual-user-scaffold.d.ts +267 -0
  21. package/dist/wirings/virtual-user/virtual-user-scaffold.js +400 -0
  22. package/dist/wirings/workflow/index.d.ts +1 -0
  23. package/dist/wirings/workflow/index.js +1 -0
  24. package/dist/wirings/workflow/pikku-workflow-service.js +3 -9
  25. package/dist/wirings/workflow/workflow-queue-routing.d.ts +18 -0
  26. package/dist/wirings/workflow/workflow-queue-routing.js +35 -0
  27. package/dist/wirings/workflow/workflow-status-stream.d.ts +28 -0
  28. package/dist/wirings/workflow/workflow-status-stream.js +105 -0
  29. package/package.json +1 -1
  30. package/src/public-surface.json +20 -1
  31. package/src/services/email-template.test.ts +311 -0
  32. package/src/services/email-template.ts +254 -0
  33. package/src/services/http-personas.ts +10 -2
  34. package/src/services/index.ts +8 -0
  35. package/src/services/persona-sign-in.test.ts +22 -0
  36. package/src/wirings/agent/agent-helpers.test.ts +63 -0
  37. package/src/wirings/agent/agent-prepare.ts +25 -0
  38. package/src/wirings/agent/index.ts +1 -0
  39. package/src/wirings/persona/index.ts +5 -0
  40. package/src/wirings/persona/persona-app-scopes.test.ts +47 -0
  41. package/src/wirings/persona/persona-app-scopes.ts +74 -0
  42. package/src/wirings/scheduler/scheduler-runner.test.ts +178 -0
  43. package/src/wirings/scheduler/scheduler-runner.ts +0 -1
  44. package/src/wirings/virtual-user/index.ts +20 -0
  45. package/src/wirings/virtual-user/virtual-user-derive.test.ts +28 -0
  46. package/src/wirings/virtual-user/virtual-user-derive.ts +9 -0
  47. package/src/wirings/virtual-user/virtual-user-scaffold.test.ts +795 -0
  48. package/src/wirings/virtual-user/virtual-user-scaffold.ts +634 -0
  49. package/src/wirings/workflow/index.ts +4 -0
  50. package/src/wirings/workflow/pikku-workflow-service.test.ts +71 -2
  51. package/src/wirings/workflow/pikku-workflow-service.ts +5 -11
  52. package/src/wirings/workflow/workflow-child-run-session.test.ts +79 -0
  53. package/src/wirings/workflow/workflow-queue-routing.ts +44 -0
  54. package/src/wirings/workflow/workflow-status-stream.test.ts +354 -0
  55. package/src/wirings/workflow/workflow-status-stream.ts +144 -0
  56. package/tsconfig.tsbuildinfo +1 -1
@@ -213,6 +213,97 @@ describe('runScheduledTask', () => {
213
213
  assert.deepEqual(receivedSession, session)
214
214
  })
215
215
 
216
+ test('a task middleware can set the session the task runs as', async () => {
217
+ let frozenSession: CoreUserSession | undefined
218
+ const machineSession: CoreUserSession = {
219
+ userId: 'cron:machine-session-task',
220
+ }
221
+
222
+ const mockTask: CoreScheduledTask = {
223
+ name: 'machine-session-task',
224
+ schedule: '0 0 * * *',
225
+ middleware: [
226
+ async (_services: any, wire: any, next: any) => {
227
+ wire.setSession(machineSession)
228
+ return next()
229
+ },
230
+ ] as any,
231
+ func: {
232
+ func: async (_services: any, _data: any, wire: any) => {
233
+ frozenSession = wire.session
234
+ },
235
+ auth: false,
236
+ },
237
+ }
238
+
239
+ pikkuState(null, 'scheduler', 'meta')['machine-session-task'] = {
240
+ pikkuFuncId: 'scheduler_machine-session-task',
241
+ name: 'machine-session-task',
242
+ schedule: '0 0 * * *',
243
+ }
244
+ pikkuState(null, 'function', 'meta')['scheduler_machine-session-task'] = {
245
+ pikkuFuncId: 'scheduler_machine-session-task',
246
+ inputSchemaName: null,
247
+ outputSchemaName: null,
248
+ sessionless: true,
249
+ }
250
+ wireScheduler(mockTask)
251
+
252
+ pikkuState(null, 'package', 'singletonServices', {
253
+ logger: createMockLogger(),
254
+ } as any)
255
+
256
+ await runScheduledTask({ name: 'machine-session-task' })
257
+
258
+ assert.deepEqual(frozenSession, machineSession)
259
+ })
260
+
261
+ test('a session-taking task does not warn that auth was disabled', async () => {
262
+ const mockTask: CoreScheduledTask = {
263
+ name: 'identified-task',
264
+ schedule: '0 0 * * *',
265
+ middleware: [
266
+ async (_services: any, wire: any, next: any) => {
267
+ wire.setSession({ userId: 'cron:identified-task' })
268
+ return next()
269
+ },
270
+ ] as any,
271
+ func: {
272
+ func: async () => {},
273
+ },
274
+ }
275
+
276
+ pikkuState(null, 'scheduler', 'meta')['identified-task'] = {
277
+ pikkuFuncId: 'scheduler_identified-task',
278
+ name: 'identified-task',
279
+ schedule: '0 0 * * *',
280
+ }
281
+ pikkuState(null, 'function', 'meta')['scheduler_identified-task'] = {
282
+ pikkuFuncId: 'scheduler_identified-task',
283
+ inputSchemaName: null,
284
+ outputSchemaName: null,
285
+ sessionless: false,
286
+ }
287
+ wireScheduler(mockTask)
288
+
289
+ const mockLogger = createMockLogger()
290
+ pikkuState(null, 'package', 'singletonServices', {
291
+ logger: mockLogger,
292
+ } as any)
293
+
294
+ await runScheduledTask({ name: 'identified-task' })
295
+
296
+ assert.deepEqual(
297
+ mockLogger
298
+ .getLogs()
299
+ .filter(
300
+ (log) =>
301
+ log.level === 'warn' && /auth was explicitly disabled/.test(log.message)
302
+ ),
303
+ []
304
+ )
305
+ })
306
+
216
307
  test('should throw ScheduledTaskNotFoundError when task not found', async () => {
217
308
  const mockLogger = createMockLogger()
218
309
  pikkuState(null, 'package', 'singletonServices', {
@@ -655,3 +746,90 @@ describe('getScheduledTasks', () => {
655
746
  assert.equal(tasks.size, 0)
656
747
  })
657
748
  })
749
+
750
+ /**
751
+ * A cron has no caller to authenticate, so the only thing that can give it an
752
+ * identity is its own wiring. These cover the mechanism the virtual-user
753
+ * scaffold's `virtualUserPlatformSession` relies on: middleware on the task
754
+ * sets the session, and the scope gate on the function it drives is enforced
755
+ * against exactly that session — a tick with no identity, or one holding the
756
+ * wrong scope, is refused the same way a person would be.
757
+ */
758
+ describe('a scheduled task authorizes on the session its own middleware sets', () => {
759
+ const wireGatedTask = (
760
+ name: string,
761
+ middleware?: Array<
762
+ (services: any, wire: any, next: any) => Promise<void> | void
763
+ >
764
+ ) => {
765
+ let seen: CoreUserSession | undefined
766
+ const task: CoreScheduledTask = {
767
+ name,
768
+ schedule: '0 * * * *',
769
+ func: {
770
+ func: async (_services: any, _data: any, wire: any) => {
771
+ seen = await wire.getSession()
772
+ },
773
+ } as any,
774
+ middleware,
775
+ }
776
+ pikkuState(null, 'scheduler', 'meta')[name] = {
777
+ pikkuFuncId: `scheduler_${name}`,
778
+ name,
779
+ schedule: '0 * * * *',
780
+ }
781
+ pikkuState(null, 'function', 'meta')[`scheduler_${name}`] = {
782
+ pikkuFuncId: `scheduler_${name}`,
783
+ inputSchemaName: null,
784
+ outputSchemaName: null,
785
+ sessionless: false,
786
+ scopes: ['virtualUser:run'],
787
+ }
788
+ wireScheduler(task)
789
+ pikkuState(null, 'package', 'singletonServices', {
790
+ logger: createMockLogger(),
791
+ } as any)
792
+ return () => seen
793
+ }
794
+
795
+ const settingSession = (session: CoreUserSession) => [
796
+ async (_services: any, wire: any, next: any) => {
797
+ await wire.setSession(session)
798
+ return next()
799
+ },
800
+ ]
801
+
802
+ test('is refused outright when nothing gives the tick an identity', async () => {
803
+ wireGatedTask('unidentified-tick')
804
+ await assert.rejects(
805
+ runScheduledTask({ name: 'unidentified-tick' }),
806
+ /Authentication required/
807
+ )
808
+ })
809
+
810
+ test('is refused when the identity it sets holds the wrong scope', async () => {
811
+ wireGatedTask(
812
+ 'misscoped-tick',
813
+ settingSession({
814
+ userId: 'pikku-platform',
815
+ scopes: ['admin'],
816
+ } as CoreUserSession)
817
+ )
818
+ await assert.rejects(
819
+ runScheduledTask({ name: 'misscoped-tick' }),
820
+ /virtualUser:run/
821
+ )
822
+ })
823
+
824
+ test('runs as the platform user its middleware set', async () => {
825
+ const seen = wireGatedTask(
826
+ 'platform-tick',
827
+ settingSession({
828
+ userId: 'pikku-platform',
829
+ scopes: ['virtualUser:run'],
830
+ } as CoreUserSession)
831
+ )
832
+ await runScheduledTask({ name: 'platform-tick' })
833
+ assert.equal(seen()?.userId, 'pikku-platform')
834
+ })
835
+ })
@@ -116,7 +116,6 @@ export async function runScheduledTask({
116
116
  await runPikkuFunc('scheduler', meta.name, meta.pikkuFuncId, {
117
117
  singletonServices,
118
118
  createWireServices,
119
- auth: false,
120
119
  data: () => undefined,
121
120
  inheritedMiddleware: meta.middleware,
122
121
  wireMiddleware: task.middleware,
@@ -85,3 +85,23 @@ export {
85
85
  personaVirtualUserTarget,
86
86
  type PersonaTargetOptions,
87
87
  } from './virtual-user-target.js'
88
+ export {
89
+ executeVirtualUserRun,
90
+ logVirtualUserTick,
91
+ requireVirtualUserRunStore,
92
+ requireVirtualUserScheduleStore,
93
+ runnablePersona,
94
+ serializeVirtualUserRun,
95
+ serializeVirtualUserSchedule,
96
+ serializeVirtualUserSteps,
97
+ signInPathFor,
98
+ startVirtualUserRun,
99
+ VIRTUAL_USER_VARIABLES,
100
+ virtualUserScheduleRunInput,
101
+ writeVirtualUserSchedule,
102
+ type ExecuteVirtualUserRunParams,
103
+ type ScaffoldPersonas,
104
+ type StartedVirtualUserRun,
105
+ type StartVirtualUserRunParams,
106
+ type WriteVirtualUserScheduleParams,
107
+ } from './virtual-user-scaffold.js'
@@ -396,3 +396,31 @@ describe('driving a virtual user through a signed-in actor', () => {
396
396
  assert.match(verdict.reasoning, /no assistant called 'concierge'/)
397
397
  })
398
398
  })
399
+
400
+ // An adversarial run's transcript is working exploits against this same app,
401
+ // and a schedule outlives the run that wrote it. Neither belongs in the hands
402
+ // of the thing being run.
403
+ test('a virtual user is never offered the machinery that runs virtual users', () => {
404
+ const catalogue = deriveCatalogue({
405
+ runVirtualUser: {
406
+ name: 'runVirtualUser',
407
+ expose: true,
408
+ scopes: ['virtualUser:run'],
409
+ },
410
+ getVirtualUserRunSteps: {
411
+ name: 'getVirtualUserRunSteps',
412
+ expose: true,
413
+ scopes: ['virtualUser:read'],
414
+ },
415
+ listBookings: {
416
+ name: 'listBookings',
417
+ expose: true,
418
+ scopes: ['bookings:read'],
419
+ },
420
+ } as any)
421
+
422
+ assert.deepEqual(
423
+ catalogue.map((entry) => entry.name),
424
+ ['listBookings']
425
+ )
426
+ })
@@ -35,6 +35,15 @@ export const deriveCatalogue = (
35
35
  if (meta.scenario || meta.scenarioStep || meta.scenarioStepKind) continue
36
36
  // knowledge: decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md
37
37
  if (meta.expose !== true) continue
38
+ // A virtual user is not offered the machinery that runs virtual users. A
39
+ // persona whose role carries `virtualUser:*` would otherwise be able to
40
+ // start further runs, read every run's findings — an adversarial run's
41
+ // transcript is working exploits against this same app — and put a persona
42
+ // on a schedule that outlives it. Same reasoning as the scenario-step rule
43
+ // above: the tool is about the run, not about the product being explored.
44
+ if (meta.scopes?.some((scope) => scope.split(':')[0] === 'virtualUser')) {
45
+ continue
46
+ }
38
47
 
39
48
  const inputSchema = meta.inputSchemaName
40
49
  ? schemas[meta.inputSchemaName]