@pikku/cli 0.12.120 → 0.12.121
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.
- package/CHANGELOG.md +192 -0
- package/console-app/assets/{abnfDiagram-N423BO3Z-Df3BCQUl.js → abnfDiagram-N423BO3Z-Dfq94a7u.js} +1 -1
- package/console-app/assets/architecture-TIHT7OUA-CA-Di-O3.js +1 -0
- package/console-app/assets/{architectureDiagram-T3A2C74G-BlV2VA0B.js → architectureDiagram-T3A2C74G-C9MjP0IU.js} +1 -1
- package/console-app/assets/{blockDiagram-VBNYF7ZC-BZ7GFw7a.js → blockDiagram-VBNYF7ZC-CGgHSlOO.js} +1 -1
- package/console-app/assets/{c4Diagram-5PPSVZJV-uXLYnA16.js → c4Diagram-5PPSVZJV-3kzckivq.js} +1 -1
- package/console-app/assets/channel-UOxM22B3.js +1 -0
- package/console-app/assets/{chunk-2GRJ4B5K-CU6Szqvt.js → chunk-2GRJ4B5K-UZ1q29J_.js} +1 -1
- package/console-app/assets/{chunk-4I5QYGJK-Bf8ARw9p.js → chunk-4I5QYGJK-bq4iSS6S.js} +1 -1
- package/console-app/assets/{chunk-5RXB4S5H-CuRjyaQR.js → chunk-5RXB4S5H-BCeU8nXq.js} +1 -1
- package/console-app/assets/{chunk-6Q2QTUOP-DMFott3W.js → chunk-6Q2QTUOP-DpsTVhit.js} +1 -1
- package/console-app/assets/{chunk-7Z6QIM7H-YJMK1J3W.js → chunk-7Z6QIM7H-I6XKkv2A.js} +1 -1
- package/console-app/assets/{chunk-GF5L2VYU-Co4slpk_.js → chunk-GF5L2VYU-aYODFCZ6.js} +1 -1
- package/console-app/assets/{chunk-I66GZJ75-CE6Jk-ML.js → chunk-I66GZJ75-CWdPrY4a.js} +1 -1
- package/console-app/assets/{chunk-JQJVKLGR-BjkITZJq.js → chunk-JQJVKLGR-D5bHP4bt.js} +1 -1
- package/console-app/assets/{chunk-KBJHAD2P-DdHZUbE2.js → chunk-KBJHAD2P-B-Spac9o.js} +1 -1
- package/console-app/assets/{chunk-NSK5VX7P-BB0gp55h.js → chunk-NSK5VX7P-0jZVLBhi.js} +1 -1
- package/console-app/assets/{chunk-QR6OTTB3-BhAzJcX6.js → chunk-QR6OTTB3-BurXFOtV.js} +1 -1
- package/console-app/assets/{chunk-RYQCIY6F-BaRFYCcu.js → chunk-RYQCIY6F-C4zacxhH.js} +1 -1
- package/console-app/assets/{chunk-UBXNYLIW-DN_ayb-W.js → chunk-UBXNYLIW-7RthekQ-.js} +1 -1
- package/console-app/assets/{chunk-W5SLKNZC-DQ0C1K0m.js → chunk-W5SLKNZC-rq3UcMTy.js} +1 -1
- package/console-app/assets/{chunk-WRU74C26-Dej4aDDK.js → chunk-WRU74C26-DUX-oZjQ.js} +1 -1
- package/console-app/assets/chunk-XXDRQBXY-gIL1rRSY.js +1 -0
- package/console-app/assets/classDiagram-JCYQIIEL-BwV1w5At.js +1 -0
- package/console-app/assets/classDiagram-v2-OCEON4UE-BwV1w5At.js +1 -0
- package/console-app/assets/{cose-bilkent-JH36ORCC-CaCBDIGn.js → cose-bilkent-JH36ORCC-DZn35HFs.js} +1 -1
- package/console-app/assets/{cynefin-VYW2F7L2-DvNj3Ktd.js → cynefin-VYW2F7L2-mZlOMKNi.js} +1 -1
- package/console-app/assets/{cynefinDiagram-MW4NZA55-2M1I4KVc.js → cynefinDiagram-MW4NZA55-uUAfUoag.js} +1 -1
- package/console-app/assets/{dagre-BX6AcUT6.js → dagre-DfTmIhGN.js} +1 -1
- package/console-app/assets/{dagre-VZM6K2ZE-CeiMExQ0.js → dagre-VZM6K2ZE-7oTddCss.js} +1 -1
- package/console-app/assets/{diagram-7IWD3JNH-DFb1QCit.js → diagram-7IWD3JNH-D6TR4TlN.js} +1 -1
- package/console-app/assets/{diagram-B4RE2ZJO-CcmxcWm0.js → diagram-B4RE2ZJO-BZwUFLzC.js} +1 -1
- package/console-app/assets/{diagram-LBJQPF4R-DpxWXRoq.js → diagram-LBJQPF4R-B4Ug2xRo.js} +1 -1
- package/console-app/assets/{diagram-Q27KOJAE-Cu_MNubg.js → diagram-Q27KOJAE-Fpr_Cahs.js} +1 -1
- package/console-app/assets/{diagram-UB23O5K3-B-R4JZVn.js → diagram-UB23O5K3-d0X7PA-5.js} +1 -1
- package/console-app/assets/{ebnfDiagram-BXEA7PRR-C26G-5vz.js → ebnfDiagram-BXEA7PRR-DExP954c.js} +1 -1
- package/console-app/assets/{erDiagram-JOGREHBK-DJaz9wds.js → erDiagram-JOGREHBK-WdnXmG8f.js} +1 -1
- package/console-app/assets/eventmodeling-45OFAUF4-DXnDrAdV.js +1 -0
- package/console-app/assets/flowDiagram-UKHOOZJN-UixAvy5Z.js +1 -0
- package/console-app/assets/{ganttDiagram-PKOTCBZU-BmHvLxmw.js → ganttDiagram-PKOTCBZU-ADJHCyvl.js} +1 -1
- package/console-app/assets/{gitGraph-TEB2WS4Q-gjOfZX1j.js → gitGraph-TEB2WS4Q-BPcXt1vK.js} +1 -1
- package/console-app/assets/{gitGraphDiagram-DS77QQ5N-Cn2aQfgw.js → gitGraphDiagram-DS77QQ5N-DFhenfUX.js} +1 -1
- package/console-app/assets/{index-Cx39I_BR.js → index-BwP6EcJr.js} +3 -3
- package/console-app/assets/{info-DKCQHKI2-Dxvvr3Du.js → info-DKCQHKI2-BsE7egna.js} +1 -1
- package/console-app/assets/{infoDiagram-6WML65LV-CU2DYprj.js → infoDiagram-6WML65LV-BmVqXMw0.js} +1 -1
- package/console-app/assets/{ishikawaDiagram-WSZJBQD7-CEIhGAXe.js → ishikawaDiagram-WSZJBQD7-CdFTPIY5.js} +1 -1
- package/console-app/assets/{journeyDiagram-NVQOT4AX-vDLEtm6u.js → journeyDiagram-NVQOT4AX-DC8CKdJk.js} +1 -1
- package/console-app/assets/{kanban-definition-27J2QSJJ-rfZdVBY0.js → kanban-definition-27J2QSJJ-Bo7kRcLN.js} +1 -1
- package/console-app/assets/{line-CK7XHoXo.js → line-5nUqh22u.js} +1 -1
- package/console-app/assets/{linear-DeA9Np_F.js → linear-82KoPtTH.js} +1 -1
- package/console-app/assets/{mermaid-parser.core-Bkwm3osM.js → mermaid-parser.core-R240ylPw.js} +3 -3
- package/console-app/assets/{mermaid.core-DvVwOMEV.js → mermaid.core-BZ3jSM6D.js} +4 -4
- package/console-app/assets/{mindmap-definition-FAOFIHXS-DYMAQz7X.js → mindmap-definition-FAOFIHXS-x1K54VeO.js} +1 -1
- package/console-app/assets/{packet-7NZHBO7P-CtoDJZ2L.js → packet-7NZHBO7P-CE2Lun0t.js} +1 -1
- package/console-app/assets/{pegDiagram-VL7TDLO6-BRxy7Znf.js → pegDiagram-VL7TDLO6-CpGZYdUz.js} +1 -1
- package/console-app/assets/{pie-RZYD4A2V-NEqmtqwU.js → pie-RZYD4A2V-Hf5MOyY8.js} +1 -1
- package/console-app/assets/{pieDiagram-7S7Q4E2Y-eekm9APm.js → pieDiagram-7S7Q4E2Y-0J5H7r7g.js} +1 -1
- package/console-app/assets/{quadrantDiagram-CIZ2JOQS-BSew_APH.js → quadrantDiagram-CIZ2JOQS-DWtYi8O-.js} +1 -1
- package/console-app/assets/{radar-I7S5WNFK-BgvnQqhv.js → radar-I7S5WNFK-BgtvrX4w.js} +1 -1
- package/console-app/assets/{railroad-3IZDKUUU-DqG1o6ll.js → railroad-3IZDKUUU-CeR0NhLL.js} +1 -1
- package/console-app/assets/railroad-abnf-AHOZXSZD-BEVU16-y.js +1 -0
- package/console-app/assets/railroad-ebnf-EBAXGLYW-WT4rrGXf.js +1 -0
- package/console-app/assets/railroad-peg-LSFZ7HO6-BnObCMjn.js +1 -0
- package/console-app/assets/{railroadDiagram-AXF67PYL-Rb5V_7ZT.js → railroadDiagram-AXF67PYL-CUIn_gPf.js} +1 -1
- package/console-app/assets/{requirementDiagram-LRYGKXZP-BONrWtQh.js → requirementDiagram-LRYGKXZP-Ds9Aqcts.js} +1 -1
- package/console-app/assets/{sankeyDiagram-W5VNT64P-CUPWhFm-.js → sankeyDiagram-W5VNT64P-CKhuya4d.js} +1 -1
- package/console-app/assets/{sequenceDiagram-SI44F4Z6-DXwluQXT.js → sequenceDiagram-SI44F4Z6-DKgOmdzz.js} +1 -1
- package/console-app/assets/{src-CyNQB37R.js → src-DwHVY--Z.js} +1 -1
- package/console-app/assets/{stateDiagram-OKZ733FA-DXopCa6U.js → stateDiagram-OKZ733FA-CPIfaZie.js} +1 -1
- package/console-app/assets/stateDiagram-v2-UEYNNEHI-DHcCmJ1C.js +1 -0
- package/console-app/assets/{swimlanes-SLNWSIFB-C_oImvSp.js → swimlanes-SLNWSIFB-8q3lK4RS.js} +1 -1
- package/console-app/assets/swimlanesDiagram-ULZ7WXOC-B1uT0NiL.js +8 -0
- package/console-app/assets/{timeline-definition-Z64GVDOM-BEx4FmJK.js → timeline-definition-Z64GVDOM-CHHi2Rpq.js} +1 -1
- package/console-app/assets/{treeView-QDETBFTQ-DIQFLe_6.js → treeView-QDETBFTQ-DrLHlsII.js} +1 -1
- package/console-app/assets/{treemap-6X3UGDF4-Dz8_vGjV.js → treemap-6X3UGDF4-C3saYKuK.js} +1 -1
- package/console-app/assets/{vennDiagram-T6HMQDX7-_sMSuxYe.js → vennDiagram-T6HMQDX7-CdyzNiAu.js} +1 -1
- package/console-app/assets/{wardley-OPB4EBWU-BkFD9f0r.js → wardley-OPB4EBWU-BSrymyse.js} +1 -1
- package/console-app/assets/{wardleyDiagram-T6FBY63Y-DhqBDV2O.js → wardleyDiagram-T6FBY63Y-CCW6rhEo.js} +1 -1
- package/console-app/assets/{xychartDiagram-ELKLHX3M-D8Jav5f0.js → xychartDiagram-ELKLHX3M-BQvPaxzg.js} +1 -1
- package/console-app/index.html +1 -1
- package/dist/.pikku/addon/index.d.ts +1 -1
- package/dist/.pikku/addon/index.js +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.js +1 -1
- package/dist/.pikku/agent/index.d.ts +1 -1
- package/dist/.pikku/agent/index.js +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/agent/pikku-model-aliases.gen.js +1 -1
- package/dist/.pikku/auth/index.d.ts +1 -1
- package/dist/.pikku/auth/index.js +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.d.ts +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.js +1 -1
- package/dist/.pikku/channel/index.d.ts +1 -1
- package/dist/.pikku/channel/index.js +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/index.d.ts +1 -1
- package/dist/.pikku/cli/index.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/error/index.d.ts +1 -1
- package/dist/.pikku/error/index.js +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.d.ts +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.js +1 -1
- package/dist/.pikku/function/index.d.ts +1 -1
- package/dist/.pikku/function/index.js +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +1 -1
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/gateway/index.d.ts +1 -1
- package/dist/.pikku/gateway/index.js +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.d.ts +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.js +1 -1
- package/dist/.pikku/http/index.d.ts +1 -1
- package/dist/.pikku/http/index.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/mcp/index.d.ts +1 -1
- package/dist/.pikku/mcp/index.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/middleware/index.d.ts +1 -1
- package/dist/.pikku/middleware/index.js +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.d.ts +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/queue/index.d.ts +1 -1
- package/dist/.pikku/queue/index.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/scenarios/index.d.ts +1 -1
- package/dist/.pikku/scenarios/index.js +1 -1
- package/dist/.pikku/scenarios/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-types.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.js +1 -1
- package/dist/.pikku/scheduler/index.d.ts +1 -1
- package/dist/.pikku/scheduler/index.js +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +1 -1
- package/dist/.pikku/scopes/index.d.ts +1 -1
- package/dist/.pikku/scopes/index.js +1 -1
- package/dist/.pikku/scopes/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-roles.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/secrets/index.d.ts +1 -1
- package/dist/.pikku/secrets/index.js +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/services/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/services/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/setup/index.d.ts +1 -1
- package/dist/.pikku/setup/index.js +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.d.ts +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.js +1 -1
- package/dist/.pikku/trigger/index.d.ts +1 -1
- package/dist/.pikku/trigger/index.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/index.d.ts +1 -1
- package/dist/.pikku/variables/index.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/index.d.ts +1 -1
- package/dist/.pikku/workflow/index.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/fabric/functions/validate.function.js +22 -2
- package/dist/src/functions/wirings/agent/serialize-public-agent.js +7 -19
- package/dist/src/functions/wirings/console/serialize-console-functions.js +3 -12
- package/dist/src/functions/wirings/emails/serialize-emails.d.ts +10 -0
- package/dist/src/functions/wirings/emails/serialize-emails.js +30 -173
- package/dist/src/functions/wirings/virtual-user/serialize-virtual-user-functions.js +158 -438
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +7 -7
- package/dist/src/functions/wirings/workflow/serialize-workflow-routes.js +11 -148
- package/dist/src/utils/pikku-cli-config.d.ts +18 -0
- package/dist/src/utils/pikku-cli-config.js +39 -0
- package/package.json +6 -6
- package/surface.json +1 -1
- package/console-app/assets/architecture-TIHT7OUA-i2kvkWiG.js +0 -1
- package/console-app/assets/channel-BjjIBjrV.js +0 -1
- package/console-app/assets/chunk-XXDRQBXY-COHoQA_g.js +0 -1
- package/console-app/assets/classDiagram-JCYQIIEL-BkQzFYJq.js +0 -1
- package/console-app/assets/classDiagram-v2-OCEON4UE-BkQzFYJq.js +0 -1
- package/console-app/assets/eventmodeling-45OFAUF4-Cge15uFd.js +0 -1
- package/console-app/assets/flowDiagram-UKHOOZJN-DN8OJeAP.js +0 -1
- package/console-app/assets/railroad-abnf-AHOZXSZD-DQHwBMhW.js +0 -1
- package/console-app/assets/railroad-ebnf-EBAXGLYW-cu_kDg2r.js +0 -1
- package/console-app/assets/railroad-peg-LSFZ7HO6-CJh8d3dM.js +0 -1
- package/console-app/assets/stateDiagram-v2-UEYNNEHI-BCi_SQzZ.js +0 -1
- package/console-app/assets/swimlanesDiagram-ULZ7WXOC-C01TBFdM.js +0 -8
|
@@ -69,6 +69,15 @@ export const RunVirtualUserInput = z.object({
|
|
|
69
69
|
budget: Budget.optional(),
|
|
70
70
|
/** Fixed seed, so a run replays into the same finding. */
|
|
71
71
|
seed: z.number().int().optional(),
|
|
72
|
+
/**
|
|
73
|
+
* A short-lived Fabric operator token, handed in by whoever starts the run.
|
|
74
|
+
*
|
|
75
|
+
* Handed in rather than held: a deployed stage that could ask for a token
|
|
76
|
+
* would be holding a credential able to mint admin sessions for itself, for
|
|
77
|
+
* as long as the box lives. This way it holds one receipt, for one run, and
|
|
78
|
+
* the receipt expires on its own. It is never written to the run record.
|
|
79
|
+
*/
|
|
80
|
+
operatorToken: z.string().min(1).optional(),
|
|
72
81
|
})
|
|
73
82
|
|
|
74
83
|
export const RunVirtualUserOutput = z.object({
|
|
@@ -242,6 +251,8 @@ export const ExecuteVirtualUserRunInput = z.object({
|
|
|
242
251
|
})
|
|
243
252
|
.optional(),
|
|
244
253
|
seed: z.number(),
|
|
254
|
+
/** Carried from the caller for the duration of the dispatch, never stored. */
|
|
255
|
+
operatorToken: z.string().optional(),
|
|
245
256
|
})
|
|
246
257
|
|
|
247
258
|
export const ExecuteVirtualUserRunOutput = z.object({
|
|
@@ -252,16 +263,20 @@ export const ExecuteVirtualUserRunOutput = z.object({
|
|
|
252
263
|
* Auto-generated virtual user functions
|
|
253
264
|
* Do not edit manually - regenerate with 'npx pikku'
|
|
254
265
|
*/
|
|
255
|
-
import { pikkuFunc, pikkuSessionlessFunc } from '${leaf('function')}'
|
|
266
|
+
import { pikkuFunc, pikkuSessionlessFunc, type Session } from '${leaf('function')}'
|
|
267
|
+
import { pikkuMiddleware } from '${leaf('middleware')}'
|
|
256
268
|
import { defineScope } from '${leaf('scopes')}'
|
|
257
|
-
import { personaVirtualUserTarget, runVirtualUser as runVirtualUserEngine, type SchemaMap, type VirtualUserDisposition } from '@pikku/core/virtual-user'
|
|
258
269
|
import {
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
executeVirtualUserRun as executeRun,
|
|
271
|
+
logVirtualUserTick,
|
|
272
|
+
requireVirtualUserRunStore,
|
|
273
|
+
serializeVirtualUserRun,
|
|
274
|
+
serializeVirtualUserSchedule,
|
|
275
|
+
serializeVirtualUserSteps,
|
|
276
|
+
startVirtualUserRun,
|
|
261
277
|
tickVirtualUserSchedules as tick,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
type VirtualUserScheduleRecord,
|
|
278
|
+
virtualUserScheduleRunInput,
|
|
279
|
+
writeVirtualUserSchedule,
|
|
265
280
|
} from '@pikku/core/virtual-user'
|
|
266
281
|
import { createPersonas, personaConfigs } from '${pathToPersonas}'
|
|
267
282
|
import {
|
|
@@ -280,6 +295,14 @@ import {
|
|
|
280
295
|
SetVirtualUserScheduleOutput,
|
|
281
296
|
} from './virtual-user.schemas.gen.js'
|
|
282
297
|
|
|
298
|
+
/**
|
|
299
|
+
* The wirings, and only the wirings.
|
|
300
|
+
*
|
|
301
|
+
* Every body below is one call into \`@pikku/core/virtual-user\`, because none of
|
|
302
|
+
* this work varies by application — what is generated is the part that does:
|
|
303
|
+
* the scopes this project declares, the schemas its RPCs are checked against,
|
|
304
|
+
* and the \`rpc.invoke\` calls typed off its own RPC map.
|
|
305
|
+
*/
|
|
283
306
|
defineScope({
|
|
284
307
|
virtualUser: {
|
|
285
308
|
displayName: 'Virtual Users',
|
|
@@ -294,168 +317,6 @@ defineScope({
|
|
|
294
317
|
},
|
|
295
318
|
})
|
|
296
319
|
|
|
297
|
-
/**
|
|
298
|
-
* Where the virtual user signs in. Its own variable rather than a guess at the
|
|
299
|
-
* host's origin: a run drives real traffic through the real front door, and a
|
|
300
|
-
* server that cannot name its own public URL would be signing in somewhere it
|
|
301
|
-
* only assumed was itself.
|
|
302
|
-
*/
|
|
303
|
-
const API_URL_VARIABLE = 'VIRTUAL_USER_API_URL'
|
|
304
|
-
const SECRET_VARIABLE = 'SCENARIO_ACTOR_SECRET'
|
|
305
|
-
const MODEL_VARIABLE = 'VIRTUAL_USER_MODEL'
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* The same two variables a scenario run reads, because a virtual user signs in
|
|
309
|
-
* and calls through exactly the doors a scenario does. An app that mounts auth
|
|
310
|
-
* somewhere other than the root — \`/api/auth\` is the common one — has no other
|
|
311
|
-
* way to say so, and without them the run signs in against a 404 and spends its
|
|
312
|
-
* whole budget thinking about why nothing works.
|
|
313
|
-
*/
|
|
314
|
-
const SIGN_IN_PATH_VARIABLE = 'SCENARIO_SIGN_IN_PATH'
|
|
315
|
-
const RPC_PATH_VARIABLE = 'SCENARIO_RPC_PATH'
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* One run on the wire. Findings and intents are free-form by design — the
|
|
319
|
-
* engine records what it noticed, not a fixed row shape — so they cross as the
|
|
320
|
-
* schema's open objects rather than being narrowed to whatever kinds exist
|
|
321
|
-
* today.
|
|
322
|
-
*/
|
|
323
|
-
const serializeRun = (run: VirtualUserRunRecord) => ({
|
|
324
|
-
runId: run.runId,
|
|
325
|
-
persona: run.persona,
|
|
326
|
-
disposition: run.disposition,
|
|
327
|
-
seed: run.seed,
|
|
328
|
-
status: run.status,
|
|
329
|
-
goals: run.goals,
|
|
330
|
-
memory: run.memory,
|
|
331
|
-
findings: run.findings.map((finding) => ({
|
|
332
|
-
kind: finding.kind as string,
|
|
333
|
-
detail: finding.detail,
|
|
334
|
-
rpcName: finding.rpcName,
|
|
335
|
-
status: finding.status,
|
|
336
|
-
intentId: finding.intentId,
|
|
337
|
-
step: finding.step,
|
|
338
|
-
})),
|
|
339
|
-
intents: run.intents.map((intent) => ({
|
|
340
|
-
id: intent.id,
|
|
341
|
-
sourceId: intent.sourceId,
|
|
342
|
-
title: intent.title,
|
|
343
|
-
status: intent.status as string,
|
|
344
|
-
steps: intent.steps,
|
|
345
|
-
suspensions: intent.suspensions,
|
|
346
|
-
summary: intent.summary,
|
|
347
|
-
})),
|
|
348
|
-
tally: (run.tally ?? null) as Record<string, unknown> | null,
|
|
349
|
-
stoppedBy: run.stoppedBy,
|
|
350
|
-
error: run.error,
|
|
351
|
-
createdAt: run.createdAt.toISOString(),
|
|
352
|
-
finishedAt: run.finishedAt ? run.finishedAt.toISOString() : null,
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Resolves a persona down to a run and kicks it off, for the two callers that
|
|
357
|
-
* start one: a person with the scope, and the tick acting on a schedule.
|
|
358
|
-
*
|
|
359
|
-
* Shared rather than duplicated because the checks are the point — an acted-upon
|
|
360
|
-
* persona has no session, and only one disposition may ever touch production.
|
|
361
|
-
* A second copy of that would eventually disagree with this one.
|
|
362
|
-
*/
|
|
363
|
-
const startVirtualUserRun = async (
|
|
364
|
-
services: {
|
|
365
|
-
virtualUserRunStore?: VirtualUserRunStore
|
|
366
|
-
// \`unknown\` because an application's Config is its own interface and need
|
|
367
|
-
// not declare nodeEnv at all. A structural \`{ nodeEnv?: string }\` shares no
|
|
368
|
-
// property with such a config and TypeScript rejects the whole call.
|
|
369
|
-
config: unknown
|
|
370
|
-
},
|
|
371
|
-
// Named literally rather than as \`string\`: a project's generated rpc.invoke
|
|
372
|
-
// is generic over its own map's keys, and \`string\` is not one of them.
|
|
373
|
-
rpc: {
|
|
374
|
-
invoke: (name: 'executeVirtualUserRun', data: any) => Promise<unknown>
|
|
375
|
-
},
|
|
376
|
-
input: {
|
|
377
|
-
persona: string
|
|
378
|
-
disposition?: string
|
|
379
|
-
goals?: string[]
|
|
380
|
-
memory?: Record<string, string>
|
|
381
|
-
budget?: { steps?: number; mutations?: number; durationMs?: number }
|
|
382
|
-
seed?: number
|
|
383
|
-
startedBy?: string | null
|
|
384
|
-
}
|
|
385
|
-
): Promise<string> => {
|
|
386
|
-
const { virtualUserRunStore, config } = services
|
|
387
|
-
if (!virtualUserRunStore) {
|
|
388
|
-
throw new Error(
|
|
389
|
-
'No virtualUserRunStore is wired — a run has nowhere to be recorded. ' +
|
|
390
|
-
'Wire KyselyVirtualUserRunStore from @pikku/kysely, or your own implementation of VirtualUserRunStore.'
|
|
391
|
-
)
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const persona = personaConfigs[input.persona as keyof typeof personaConfigs] as
|
|
395
|
-
| { id: string; runnable: boolean; disposition?: string }
|
|
396
|
-
| undefined
|
|
397
|
-
if (!persona) {
|
|
398
|
-
throw new Error(
|
|
399
|
-
\`Unknown persona "\${input.persona}" — declare it with definePersonas()\`
|
|
400
|
-
)
|
|
401
|
-
}
|
|
402
|
-
// An acted-upon persona has no session of its own, and running one would
|
|
403
|
-
// race whatever scenario acts on it.
|
|
404
|
-
if (!persona.runnable) {
|
|
405
|
-
throw new Error(
|
|
406
|
-
\`Persona "\${input.persona}" is declared as acted upon, never run\`
|
|
407
|
-
)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const disposition = (input.disposition ??
|
|
411
|
-
persona.disposition ??
|
|
412
|
-
'realistic') as VirtualUserDisposition
|
|
413
|
-
// Every disposition other than this one exists to find out what the product
|
|
414
|
-
// does wrong, which is not a thing to do to real customers' data. Checked
|
|
415
|
-
// against the effective disposition, so the override cannot smuggle one in.
|
|
416
|
-
if (
|
|
417
|
-
(config as { nodeEnv?: string } | undefined)?.nodeEnv === 'production' &&
|
|
418
|
-
disposition !== PRODUCTION_DISPOSITION
|
|
419
|
-
) {
|
|
420
|
-
throw new Error(
|
|
421
|
-
\`Only the '\${PRODUCTION_DISPOSITION}' disposition may run against production; "\${input.persona}" is \${disposition}\`
|
|
422
|
-
)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Seeded here rather than inside the engine so the record carries the seed
|
|
426
|
-
// even if the run dies before returning — an unreproducible crash costs the
|
|
427
|
-
// most.
|
|
428
|
-
const seed = input.seed ?? Math.floor(Math.random() * 2_147_483_647)
|
|
429
|
-
|
|
430
|
-
const runId = await virtualUserRunStore.start({
|
|
431
|
-
persona: persona.id,
|
|
432
|
-
disposition,
|
|
433
|
-
seed,
|
|
434
|
-
goals: input.goals ?? [],
|
|
435
|
-
memory: input.memory ?? {},
|
|
436
|
-
startedBy: input.startedBy ?? null,
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
// Deliberately not awaited: a run takes minutes, and a held-open request
|
|
440
|
-
// survives neither a rollout nor a proxy timeout. executeVirtualUserRun
|
|
441
|
-
// writes both outcomes to the record itself, so the only thing left to
|
|
442
|
-
// handle here is a rejection escaping the promise — without the catch it
|
|
443
|
-
// becomes an unhandled rejection and takes the process with it.
|
|
444
|
-
void rpc
|
|
445
|
-
.invoke('executeVirtualUserRun', {
|
|
446
|
-
runId,
|
|
447
|
-
persona: persona.id,
|
|
448
|
-
disposition,
|
|
449
|
-
goals: input.goals ?? [],
|
|
450
|
-
memory: input.memory ?? {},
|
|
451
|
-
budget: input.budget,
|
|
452
|
-
seed,
|
|
453
|
-
})
|
|
454
|
-
.catch(() => {})
|
|
455
|
-
|
|
456
|
-
return runId
|
|
457
|
-
}
|
|
458
|
-
|
|
459
320
|
export const runVirtualUser = pikkuFunc({
|
|
460
321
|
tags: ['pikku'],
|
|
461
322
|
title: 'Run a Virtual User',
|
|
@@ -466,11 +327,42 @@ export const runVirtualUser = pikkuFunc({
|
|
|
466
327
|
input: RunVirtualUserInput,
|
|
467
328
|
output: RunVirtualUserOutput,
|
|
468
329
|
func: async ({ virtualUserRunStore, config }, input, { session, rpc }) => {
|
|
469
|
-
const runId
|
|
470
|
-
{
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
330
|
+
const { runId, persona, disposition, goals, memory, seed } =
|
|
331
|
+
await startVirtualUserRun({
|
|
332
|
+
store: virtualUserRunStore,
|
|
333
|
+
personas: personaConfigs,
|
|
334
|
+
// Cast because an application's Config is its own interface and need
|
|
335
|
+
// not declare nodeEnv at all.
|
|
336
|
+
config: config as { nodeEnv?: string } | undefined,
|
|
337
|
+
persona: input.persona,
|
|
338
|
+
disposition: input.disposition,
|
|
339
|
+
seed: input.seed,
|
|
340
|
+
goals: input.goals,
|
|
341
|
+
memory: input.memory,
|
|
342
|
+
startedBy: session?.userId ?? null,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// Deliberately not awaited: a run takes minutes, and a held-open request
|
|
346
|
+
// survives neither a rollout nor a proxy timeout. executeVirtualUserRun
|
|
347
|
+
// writes both outcomes to the record itself, so the only thing left to
|
|
348
|
+
// handle here is a rejection escaping the promise — without the catch it
|
|
349
|
+
// becomes an unhandled rejection and takes the process with it.
|
|
350
|
+
void rpc!
|
|
351
|
+
.invoke('executeVirtualUserRun', {
|
|
352
|
+
runId,
|
|
353
|
+
persona,
|
|
354
|
+
disposition,
|
|
355
|
+
goals,
|
|
356
|
+
memory,
|
|
357
|
+
seed,
|
|
358
|
+
budget: input.budget,
|
|
359
|
+
// Rides the dispatch and nothing else. The run record is deliberately
|
|
360
|
+
// not given it: a record outlives the run, and a credential in a row
|
|
361
|
+
// somebody can read back is a credential leak.
|
|
362
|
+
operatorToken: input.operatorToken,
|
|
363
|
+
})
|
|
364
|
+
.catch(() => {})
|
|
365
|
+
|
|
474
366
|
return { runId }
|
|
475
367
|
},
|
|
476
368
|
})
|
|
@@ -488,14 +380,14 @@ export const getVirtualUserRun = pikkuFunc({
|
|
|
488
380
|
input: GetVirtualUserRunInput,
|
|
489
381
|
output: GetVirtualUserRunOutput,
|
|
490
382
|
func: async ({ virtualUserRunStore }, { runId }) => {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
383
|
+
const run = await requireVirtualUserRunStore(
|
|
384
|
+
virtualUserRunStore,
|
|
385
|
+
true
|
|
386
|
+
).get(runId)
|
|
495
387
|
if (!run) {
|
|
496
388
|
throw new Error(\`No virtual user run \${runId}\`)
|
|
497
389
|
}
|
|
498
|
-
return
|
|
390
|
+
return serializeVirtualUserRun(run)
|
|
499
391
|
},
|
|
500
392
|
})
|
|
501
393
|
|
|
@@ -509,11 +401,11 @@ export const listVirtualUserRuns = pikkuFunc({
|
|
|
509
401
|
input: ListVirtualUserRunsInput,
|
|
510
402
|
output: ListVirtualUserRunsOutput,
|
|
511
403
|
func: async ({ virtualUserRunStore }, { persona, limit, offset }) => {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return { runs: runs.map(
|
|
404
|
+
const runs = await requireVirtualUserRunStore(
|
|
405
|
+
virtualUserRunStore,
|
|
406
|
+
true
|
|
407
|
+
).list({ persona, limit, offset })
|
|
408
|
+
return { runs: runs.map(serializeVirtualUserRun) }
|
|
517
409
|
},
|
|
518
410
|
})
|
|
519
411
|
|
|
@@ -530,64 +422,14 @@ export const getVirtualUserRunSteps = pikkuFunc({
|
|
|
530
422
|
input: GetVirtualUserRunStepsInput,
|
|
531
423
|
output: GetVirtualUserRunStepsOutput,
|
|
532
424
|
func: async ({ virtualUserRunStore }, { runId, limit, offset }) => {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
return {
|
|
538
|
-
steps: steps.map((step) => ({
|
|
539
|
-
index: step.index,
|
|
540
|
-
intentId: step.intentId,
|
|
541
|
-
action: step.action as unknown as Record<string, unknown>,
|
|
542
|
-
status: step.status,
|
|
543
|
-
ok: step.ok,
|
|
544
|
-
response: step.response,
|
|
545
|
-
findingKinds: step.findingKinds as string[] | undefined,
|
|
546
|
-
tokensIn: step.tokensIn,
|
|
547
|
-
tokensOut: step.tokensOut,
|
|
548
|
-
})),
|
|
549
|
-
}
|
|
425
|
+
const steps = await requireVirtualUserRunStore(
|
|
426
|
+
virtualUserRunStore,
|
|
427
|
+
true
|
|
428
|
+
).steps(runId, { limit, offset })
|
|
429
|
+
return { steps: serializeVirtualUserSteps(steps) }
|
|
550
430
|
},
|
|
551
431
|
})
|
|
552
432
|
|
|
553
|
-
/**
|
|
554
|
-
* One schedule on the wire. The budget crosses as \`durationMs\` because that is
|
|
555
|
-
* what every other call here takes; the engine's own duration also accepts
|
|
556
|
-
* \`'30m'\`, which nothing on this side ever writes.
|
|
557
|
-
*/
|
|
558
|
-
const serializeSchedule = (schedule: VirtualUserScheduleRecord) => {
|
|
559
|
-
const persona = personaConfigs[
|
|
560
|
-
schedule.persona as keyof typeof personaConfigs
|
|
561
|
-
] as { goals?: string[]; disposition?: string } | undefined
|
|
562
|
-
const declared = {
|
|
563
|
-
disposition: (persona?.disposition ??
|
|
564
|
-
'realistic') as VirtualUserDisposition,
|
|
565
|
-
goals: persona?.goals ?? [],
|
|
566
|
-
}
|
|
567
|
-
return {
|
|
568
|
-
persona: schedule.persona,
|
|
569
|
-
enabled: schedule.enabled,
|
|
570
|
-
disposition: schedule.disposition,
|
|
571
|
-
goals: schedule.goals,
|
|
572
|
-
budget: schedule.budget
|
|
573
|
-
? {
|
|
574
|
-
steps: schedule.budget.steps,
|
|
575
|
-
mutations: schedule.budget.mutations,
|
|
576
|
-
durationMs:
|
|
577
|
-
typeof schedule.budget.duration === 'number'
|
|
578
|
-
? schedule.budget.duration
|
|
579
|
-
: undefined,
|
|
580
|
-
}
|
|
581
|
-
: null,
|
|
582
|
-
minIntervalMs: schedule.minIntervalMs,
|
|
583
|
-
maxIntervalMs: schedule.maxIntervalMs,
|
|
584
|
-
nextRunAt: schedule.nextRunAt.toISOString(),
|
|
585
|
-
lastRunId: schedule.lastRunId,
|
|
586
|
-
lastRunAt: schedule.lastRunAt ? schedule.lastRunAt.toISOString() : null,
|
|
587
|
-
declared,
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
433
|
export const setVirtualUserSchedule = pikkuFunc({
|
|
592
434
|
tags: ['pikku'],
|
|
593
435
|
title: 'Put a Virtual User on a Clock',
|
|
@@ -601,48 +443,12 @@ export const setVirtualUserSchedule = pikkuFunc({
|
|
|
601
443
|
input: SetVirtualUserScheduleInput,
|
|
602
444
|
output: SetVirtualUserScheduleOutput,
|
|
603
445
|
func: async ({ virtualUserScheduleStore }, input) => {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
)
|
|
609
|
-
}
|
|
610
|
-
const persona = personaConfigs[
|
|
611
|
-
input.persona as keyof typeof personaConfigs
|
|
612
|
-
] as { runnable: boolean } | undefined
|
|
613
|
-
if (!persona) {
|
|
614
|
-
throw new Error(
|
|
615
|
-
\`Unknown persona "\${input.persona}" — declare it with definePersonas()\`
|
|
616
|
-
)
|
|
617
|
-
}
|
|
618
|
-
// The same rule \`runVirtualUser\` enforces, applied at the point the row is
|
|
619
|
-
// written rather than every hour afterwards: an acted-upon persona has no
|
|
620
|
-
// session, so a cadence for one is a tick that can only ever fail to start.
|
|
621
|
-
if (!persona.runnable) {
|
|
622
|
-
throw new Error(
|
|
623
|
-
\`Persona "\${input.persona}" is declared as acted upon, never run\`
|
|
624
|
-
)
|
|
625
|
-
}
|
|
626
|
-
const schedule = await virtualUserScheduleStore.set({
|
|
627
|
-
persona: input.persona,
|
|
628
|
-
enabled: input.enabled,
|
|
629
|
-
disposition: input.disposition as VirtualUserDisposition | undefined,
|
|
630
|
-
goals: input.goals,
|
|
631
|
-
budget:
|
|
632
|
-
input.budget === undefined
|
|
633
|
-
? undefined
|
|
634
|
-
: input.budget === null
|
|
635
|
-
? null
|
|
636
|
-
: {
|
|
637
|
-
steps: input.budget.steps,
|
|
638
|
-
mutations: input.budget.mutations,
|
|
639
|
-
duration: input.budget.durationMs,
|
|
640
|
-
},
|
|
641
|
-
minIntervalMs: input.minIntervalMs,
|
|
642
|
-
maxIntervalMs: input.maxIntervalMs,
|
|
643
|
-
nextRunAt: input.nextRunAt ? new Date(input.nextRunAt) : undefined,
|
|
446
|
+
const schedule = await writeVirtualUserSchedule({
|
|
447
|
+
store: virtualUserScheduleStore,
|
|
448
|
+
personas: personaConfigs,
|
|
449
|
+
...input,
|
|
644
450
|
})
|
|
645
|
-
return
|
|
451
|
+
return serializeVirtualUserSchedule(schedule, personaConfigs)
|
|
646
452
|
},
|
|
647
453
|
})
|
|
648
454
|
|
|
@@ -659,10 +465,44 @@ export const listVirtualUserSchedules = pikkuFunc({
|
|
|
659
465
|
return { schedules: [] }
|
|
660
466
|
}
|
|
661
467
|
const schedules = await virtualUserScheduleStore.list()
|
|
662
|
-
return {
|
|
468
|
+
return {
|
|
469
|
+
schedules: schedules.map((schedule) =>
|
|
470
|
+
serializeVirtualUserSchedule(schedule, personaConfigs)
|
|
471
|
+
),
|
|
472
|
+
}
|
|
663
473
|
},
|
|
664
474
|
})
|
|
665
475
|
|
|
476
|
+
/**
|
|
477
|
+
* The identity a scheduled tick runs as.
|
|
478
|
+
*
|
|
479
|
+
* \`pikku-platform\` is the platform's own principal, and it already exists for
|
|
480
|
+
* exactly this: a reserved user row created with no credential account of any
|
|
481
|
+
* kind, so no sign-in method can resolve it, and one the user directory already
|
|
482
|
+
* filters out — so unlike a seeded service account it costs no phantom member
|
|
483
|
+
* in any list, seat count or bill.
|
|
484
|
+
*
|
|
485
|
+
* Not parameterised, because it is not a decision an application makes: work
|
|
486
|
+
* this scaffold starts on its own clock is the platform's work, and the scope
|
|
487
|
+
* is the one \`runVirtualUser\` gates on.
|
|
488
|
+
*
|
|
489
|
+
* Attached to \`wireScheduler\`'s \`middleware\` rather than declared as tag
|
|
490
|
+
* middleware over \`/rpc\`, which cannot set a session at all: \`runScheduledTask\`
|
|
491
|
+
* builds its wire with a \`sessionService\`, so the session set here is the one
|
|
492
|
+
* the function is frozen with.
|
|
493
|
+
*/
|
|
494
|
+
const PLATFORM_USER_ID = 'pikku-platform'
|
|
495
|
+
|
|
496
|
+
export const virtualUserPlatformSession = pikkuMiddleware(
|
|
497
|
+
async (_services, { setSession }, next) => {
|
|
498
|
+
await setSession?.({
|
|
499
|
+
userId: PLATFORM_USER_ID,
|
|
500
|
+
scopes: ['virtualUser:run'],
|
|
501
|
+
} as Session)
|
|
502
|
+
return next()
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
|
|
666
506
|
/**
|
|
667
507
|
* Acts on whichever personas are due, once.
|
|
668
508
|
*
|
|
@@ -675,13 +515,21 @@ export const listVirtualUserSchedules = pikkuFunc({
|
|
|
675
515
|
* wireScheduler({
|
|
676
516
|
* name: 'virtualUsers',
|
|
677
517
|
* schedule: '0 * * * *',
|
|
518
|
+
* middleware: [virtualUserPlatformSession],
|
|
678
519
|
* func: tickVirtualUserSchedules,
|
|
679
520
|
* })
|
|
680
521
|
* \`\`\`
|
|
681
522
|
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
523
|
+
* The middleware is not decoration: the tick starts a run through
|
|
524
|
+
* \`runVirtualUser\`, the same gated entry point a person calls, so without a
|
|
525
|
+
* session carrying \`virtualUser:run\` every dispatch is refused. Going through
|
|
526
|
+
* that front door rather than a shared helper is the point — the persona
|
|
527
|
+
* checks, the production-disposition rule and the record all live in one place,
|
|
528
|
+
* and a second copy of them would eventually disagree with the first.
|
|
529
|
+
*
|
|
530
|
+
* An hourly tick is plenty for intervals measured in hours: the tick decides
|
|
531
|
+
* nothing except which rows are already due, so running it more often costs a
|
|
532
|
+
* query and changes no cadence.
|
|
685
533
|
*/
|
|
686
534
|
export const tickVirtualUserSchedules = pikkuSessionlessFunc<void, void>({
|
|
687
535
|
tags: ['pikku'],
|
|
@@ -690,50 +538,26 @@ export const tickVirtualUserSchedules = pikkuSessionlessFunc<void, void>({
|
|
|
690
538
|
if (!virtualUserScheduleStore || !virtualUserRunStore) {
|
|
691
539
|
return
|
|
692
540
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
? schedule.budget.duration
|
|
708
|
-
: undefined,
|
|
709
|
-
}
|
|
710
|
-
: undefined,
|
|
711
|
-
}),
|
|
712
|
-
})
|
|
713
|
-
// Logged rather than returned: the caller is a cron, and a run this started
|
|
714
|
-
// is otherwise the only trace that a persona is still out there working.
|
|
715
|
-
for (const { persona, runId } of result.dispatched) {
|
|
716
|
-
logger.info(\`Virtual user \${persona} started run \${runId} on schedule\`)
|
|
717
|
-
}
|
|
718
|
-
for (const runId of result.reaped) {
|
|
719
|
-
logger.warn(
|
|
720
|
-
\`Virtual user run \${runId} was abandoned — marked failed so its persona can run again\`
|
|
721
|
-
)
|
|
722
|
-
}
|
|
723
|
-
for (const { persona, reason } of result.skipped) {
|
|
724
|
-
logger.info(\`Virtual user \${persona} skipped this tick: \${reason}\`)
|
|
725
|
-
}
|
|
541
|
+
logVirtualUserTick(
|
|
542
|
+
logger,
|
|
543
|
+
await tick({
|
|
544
|
+
schedules: virtualUserScheduleStore,
|
|
545
|
+
runs: virtualUserRunStore,
|
|
546
|
+
dispatch: async (schedule) => {
|
|
547
|
+
const { runId } = await rpc!.invoke(
|
|
548
|
+
'runVirtualUser',
|
|
549
|
+
virtualUserScheduleRunInput(schedule)
|
|
550
|
+
)
|
|
551
|
+
return runId
|
|
552
|
+
},
|
|
553
|
+
})
|
|
554
|
+
)
|
|
726
555
|
},
|
|
727
556
|
})
|
|
728
557
|
|
|
729
558
|
/**
|
|
730
559
|
* The run itself. Not exposed: it is dispatched by \`runVirtualUser\` and has no
|
|
731
560
|
* caller of its own.
|
|
732
|
-
*
|
|
733
|
-
* Everything it needs is derived through \`metaService\` and the generated
|
|
734
|
-
* personas — the same public surface any consumer has. Nothing reaches into
|
|
735
|
-
* pikku's internals, because an app could not, and a feature built on what only
|
|
736
|
-
* the framework can see would not be this feature.
|
|
737
561
|
*/
|
|
738
562
|
export const executeVirtualUserRun = pikkuSessionlessFunc({
|
|
739
563
|
tags: ['pikku'],
|
|
@@ -741,122 +565,18 @@ export const executeVirtualUserRun = pikkuSessionlessFunc({
|
|
|
741
565
|
output: ExecuteVirtualUserRunOutput,
|
|
742
566
|
func: async (
|
|
743
567
|
{ virtualUserRunStore, metaService, agentRunner, variables, logger },
|
|
744
|
-
|
|
745
|
-
) =>
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const apiUrl = await variables.get(API_URL_VARIABLE)
|
|
758
|
-
if (!apiUrl) {
|
|
759
|
-
throw new Error(
|
|
760
|
-
\`\${API_URL_VARIABLE} is not set — a virtual user has no address to sign in at.\`
|
|
761
|
-
)
|
|
762
|
-
}
|
|
763
|
-
const secret = await variables.get(SECRET_VARIABLE)
|
|
764
|
-
if (!secret) {
|
|
765
|
-
throw new Error(
|
|
766
|
-
\`\${SECRET_VARIABLE} is not set — actor sign-in is disabled, so there is nobody for the virtual user to be.\`
|
|
767
|
-
)
|
|
768
|
-
}
|
|
769
|
-
const model = await variables.get(MODEL_VARIABLE)
|
|
770
|
-
if (!model) {
|
|
771
|
-
throw new Error(\`\${MODEL_VARIABLE} is not set — no model to think with.\`)
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const functionsMeta = await metaService.getFunctionsMeta()
|
|
775
|
-
// Only the schemas the catalogue can actually refer to, so a large app
|
|
776
|
-
// does not pull every schema it has ever generated into one run.
|
|
777
|
-
const schemaNames = [
|
|
778
|
-
...new Set(
|
|
779
|
-
Object.values(functionsMeta).flatMap((meta: any) =>
|
|
780
|
-
[meta.inputSchemaName, meta.outputSchemaName].filter(
|
|
781
|
-
(name: unknown): name is string => !!name
|
|
782
|
-
)
|
|
783
|
-
)
|
|
784
|
-
),
|
|
785
|
-
]
|
|
786
|
-
const schemas = (await metaService.getSchemas(schemaNames)) as SchemaMap
|
|
787
|
-
|
|
788
|
-
const persona = personaConfigs[personaId as keyof typeof personaConfigs]
|
|
789
|
-
if (!persona) {
|
|
790
|
-
throw new Error(\`Persona "\${personaId}" is no longer declared\`)
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const { catalogue, intents, agents } = prepareVirtualUserRun({
|
|
794
|
-
persona,
|
|
795
|
-
functionsMeta,
|
|
796
|
-
schemas,
|
|
797
|
-
workflowsMeta: await metaService.getWorkflowMeta(),
|
|
798
|
-
systemRoles: await metaService.getSystemRolesMeta(),
|
|
799
|
-
agentsMeta: await metaService.getAgentsMeta(),
|
|
800
|
-
})
|
|
801
|
-
|
|
802
|
-
const signedIn = createPersonas({
|
|
803
|
-
apiUrl,
|
|
804
|
-
secret,
|
|
805
|
-
model,
|
|
806
|
-
signInPath: (await variables.get(SIGN_IN_PATH_VARIABLE)) ?? undefined,
|
|
807
|
-
rpcPath: (await variables.get(RPC_PATH_VARIABLE)) ?? undefined,
|
|
808
|
-
})
|
|
809
|
-
const target = signedIn[personaId as keyof typeof signedIn]
|
|
810
|
-
if (!target) {
|
|
811
|
-
throw new Error(\`Persona "\${personaId}" cannot sign in\`)
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const result = await runVirtualUserEngine({
|
|
815
|
-
persona,
|
|
816
|
-
personaId,
|
|
817
|
-
disposition: disposition as VirtualUserDisposition,
|
|
818
|
-
catalogue,
|
|
819
|
-
intents,
|
|
820
|
-
goals,
|
|
821
|
-
memory,
|
|
822
|
-
seed,
|
|
823
|
-
agents,
|
|
824
|
-
target: personaVirtualUserTarget(target, {
|
|
825
|
-
model,
|
|
826
|
-
agents: agents.map((agent) => agent.name),
|
|
827
|
-
}),
|
|
828
|
-
// \`AgentRunnerService.run\` IS the engine's \`ActorLLM\` — same params,
|
|
829
|
-
// same result — so a virtual user thinks through the same runner every
|
|
830
|
-
// agent in the app does, provider quirks and all.
|
|
831
|
-
llm: (params) => agentRunner.run(params),
|
|
832
|
-
model,
|
|
833
|
-
budget: {
|
|
834
|
-
steps: budget?.steps,
|
|
835
|
-
mutations: budget?.mutations,
|
|
836
|
-
duration: budget?.durationMs,
|
|
837
|
-
},
|
|
838
|
-
})
|
|
839
|
-
|
|
840
|
-
await virtualUserRunStore.complete(runId, {
|
|
841
|
-
findings: result.findings,
|
|
842
|
-
tally: result.tally,
|
|
843
|
-
memory: result.memory,
|
|
844
|
-
stoppedBy: result.stoppedBy ?? null,
|
|
845
|
-
intents: result.intents,
|
|
846
|
-
steps: result.steps,
|
|
847
|
-
})
|
|
848
|
-
|
|
849
|
-
return { findings: result.findings.length }
|
|
850
|
-
} catch (error) {
|
|
851
|
-
// A crashed run and a run that found nothing are different states, and the
|
|
852
|
-
// record is the only place that distinction survives — leaving it at
|
|
853
|
-
// 'running' forever is what \`fail\` exists to prevent.
|
|
854
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
855
|
-
logger.error(\`Virtual user run \${runId} (\${personaId}) failed: \${message}\`)
|
|
856
|
-
await virtualUserRunStore.fail(runId, message)
|
|
857
|
-
throw error
|
|
858
|
-
}
|
|
859
|
-
},
|
|
568
|
+
input
|
|
569
|
+
) =>
|
|
570
|
+
executeRun({
|
|
571
|
+
runStore: virtualUserRunStore,
|
|
572
|
+
metaService,
|
|
573
|
+
agentRunner,
|
|
574
|
+
variables,
|
|
575
|
+
logger,
|
|
576
|
+
personas: personaConfigs,
|
|
577
|
+
createPersonas,
|
|
578
|
+
...input,
|
|
579
|
+
}),
|
|
860
580
|
})
|
|
861
581
|
`;
|
|
862
582
|
return { schemas, functions };
|