@pikku/cli 0.12.119 → 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 +225 -0
- package/console-app/assets/{abnfDiagram-N423BO3Z-CiK5F2ml.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-g4Elcs_0.js → architectureDiagram-T3A2C74G-C9MjP0IU.js} +1 -1
- package/console-app/assets/{blockDiagram-VBNYF7ZC-CI6muc3M.js → blockDiagram-VBNYF7ZC-CGgHSlOO.js} +1 -1
- package/console-app/assets/{c4Diagram-5PPSVZJV-CjzL9yGp.js → c4Diagram-5PPSVZJV-3kzckivq.js} +1 -1
- package/console-app/assets/channel-UOxM22B3.js +1 -0
- package/console-app/assets/{chunk-2GRJ4B5K-C68TKOLb.js → chunk-2GRJ4B5K-UZ1q29J_.js} +1 -1
- package/console-app/assets/{chunk-4I5QYGJK-D_DINIPO.js → chunk-4I5QYGJK-bq4iSS6S.js} +1 -1
- package/console-app/assets/{chunk-5RXB4S5H-D8mEuiea.js → chunk-5RXB4S5H-BCeU8nXq.js} +1 -1
- package/console-app/assets/{chunk-6Q2QTUOP-CVNqLnb8.js → chunk-6Q2QTUOP-DpsTVhit.js} +1 -1
- package/console-app/assets/{chunk-7Z6QIM7H-Bnih6HyS.js → chunk-7Z6QIM7H-I6XKkv2A.js} +1 -1
- package/console-app/assets/{chunk-GF5L2VYU-C5efrbFt.js → chunk-GF5L2VYU-aYODFCZ6.js} +1 -1
- package/console-app/assets/{chunk-I66GZJ75-dckv50K3.js → chunk-I66GZJ75-CWdPrY4a.js} +1 -1
- package/console-app/assets/{chunk-JQJVKLGR-HirBo6LQ.js → chunk-JQJVKLGR-D5bHP4bt.js} +1 -1
- package/console-app/assets/{chunk-KBJHAD2P-lknhmPj0.js → chunk-KBJHAD2P-B-Spac9o.js} +1 -1
- package/console-app/assets/{chunk-NSK5VX7P-tGAl7zxF.js → chunk-NSK5VX7P-0jZVLBhi.js} +1 -1
- package/console-app/assets/{chunk-QR6OTTB3-BlSKCrEq.js → chunk-QR6OTTB3-BurXFOtV.js} +1 -1
- package/console-app/assets/{chunk-RYQCIY6F-CwQmwEV-.js → chunk-RYQCIY6F-C4zacxhH.js} +1 -1
- package/console-app/assets/{chunk-UBXNYLIW-D1Dn6ZK0.js → chunk-UBXNYLIW-7RthekQ-.js} +1 -1
- package/console-app/assets/{chunk-W5SLKNZC-DOkZHhST.js → chunk-W5SLKNZC-rq3UcMTy.js} +1 -1
- package/console-app/assets/{chunk-WRU74C26-CB4orIKm.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-stBhaqpU.js → cose-bilkent-JH36ORCC-DZn35HFs.js} +1 -1
- package/console-app/assets/{cynefin-VYW2F7L2-Bb1nmKWZ.js → cynefin-VYW2F7L2-mZlOMKNi.js} +1 -1
- package/console-app/assets/{cynefinDiagram-MW4NZA55-Bp8fMJi-.js → cynefinDiagram-MW4NZA55-uUAfUoag.js} +1 -1
- package/console-app/assets/{dagre-DshmzimC.js → dagre-DfTmIhGN.js} +1 -1
- package/console-app/assets/{dagre-VZM6K2ZE-DIl1klf5.js → dagre-VZM6K2ZE-7oTddCss.js} +1 -1
- package/console-app/assets/{diagram-7IWD3JNH-DdkSOxKT.js → diagram-7IWD3JNH-D6TR4TlN.js} +1 -1
- package/console-app/assets/{diagram-B4RE2ZJO-9Dj9O4L8.js → diagram-B4RE2ZJO-BZwUFLzC.js} +1 -1
- package/console-app/assets/{diagram-LBJQPF4R-CxBb73tU.js → diagram-LBJQPF4R-B4Ug2xRo.js} +1 -1
- package/console-app/assets/{diagram-Q27KOJAE-CPpUmnvb.js → diagram-Q27KOJAE-Fpr_Cahs.js} +1 -1
- package/console-app/assets/{diagram-UB23O5K3-7wV_Db6W.js → diagram-UB23O5K3-d0X7PA-5.js} +1 -1
- package/console-app/assets/{ebnfDiagram-BXEA7PRR-Cxzbf8pk.js → ebnfDiagram-BXEA7PRR-DExP954c.js} +1 -1
- package/console-app/assets/{erDiagram-JOGREHBK-BBPLq83P.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-Erh7WK4H.js → ganttDiagram-PKOTCBZU-ADJHCyvl.js} +1 -1
- package/console-app/assets/{gitGraph-TEB2WS4Q-o5FLRF57.js → gitGraph-TEB2WS4Q-BPcXt1vK.js} +1 -1
- package/console-app/assets/{gitGraphDiagram-DS77QQ5N-BFbsaenX.js → gitGraphDiagram-DS77QQ5N-DFhenfUX.js} +1 -1
- package/console-app/assets/{index-w2ZoJr8X.js → index-BwP6EcJr.js} +184 -184
- package/console-app/assets/{info-DKCQHKI2-Dkcrdr1w.js → info-DKCQHKI2-BsE7egna.js} +1 -1
- package/console-app/assets/{infoDiagram-6WML65LV-iZVC5Wzy.js → infoDiagram-6WML65LV-BmVqXMw0.js} +1 -1
- package/console-app/assets/{ishikawaDiagram-WSZJBQD7-BjV8LLeA.js → ishikawaDiagram-WSZJBQD7-CdFTPIY5.js} +1 -1
- package/console-app/assets/{journeyDiagram-NVQOT4AX-D3rpLjYi.js → journeyDiagram-NVQOT4AX-DC8CKdJk.js} +1 -1
- package/console-app/assets/{kanban-definition-27J2QSJJ-Dds5TFyv.js → kanban-definition-27J2QSJJ-Bo7kRcLN.js} +1 -1
- package/console-app/assets/{line-D1rUAT18.js → line-5nUqh22u.js} +1 -1
- package/console-app/assets/{linear-fQ_6Sc79.js → linear-82KoPtTH.js} +1 -1
- package/console-app/assets/{mermaid-parser.core-B9Q-4IXF.js → mermaid-parser.core-R240ylPw.js} +3 -3
- package/console-app/assets/{mermaid.core-bPhADShw.js → mermaid.core-BZ3jSM6D.js} +4 -4
- package/console-app/assets/{mindmap-definition-FAOFIHXS-CULlU6Fy.js → mindmap-definition-FAOFIHXS-x1K54VeO.js} +1 -1
- package/console-app/assets/{packet-7NZHBO7P-DpwzoQpc.js → packet-7NZHBO7P-CE2Lun0t.js} +1 -1
- package/console-app/assets/{pegDiagram-VL7TDLO6-B9fKKc4F.js → pegDiagram-VL7TDLO6-CpGZYdUz.js} +1 -1
- package/console-app/assets/{pie-RZYD4A2V-DkXpqWAI.js → pie-RZYD4A2V-Hf5MOyY8.js} +1 -1
- package/console-app/assets/{pieDiagram-7S7Q4E2Y-Bcpn2rUM.js → pieDiagram-7S7Q4E2Y-0J5H7r7g.js} +1 -1
- package/console-app/assets/{quadrantDiagram-CIZ2JOQS-Bd5wWFj_.js → quadrantDiagram-CIZ2JOQS-DWtYi8O-.js} +1 -1
- package/console-app/assets/{radar-I7S5WNFK-ylw-u3HI.js → radar-I7S5WNFK-BgtvrX4w.js} +1 -1
- package/console-app/assets/{railroad-3IZDKUUU-D4LMucL-.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-CZukQql9.js → railroadDiagram-AXF67PYL-CUIn_gPf.js} +1 -1
- package/console-app/assets/{requirementDiagram-LRYGKXZP-Ou4nRENK.js → requirementDiagram-LRYGKXZP-Ds9Aqcts.js} +1 -1
- package/console-app/assets/{sankeyDiagram-W5VNT64P-BKdG-c_o.js → sankeyDiagram-W5VNT64P-CKhuya4d.js} +1 -1
- package/console-app/assets/{sequenceDiagram-SI44F4Z6-Dc7d5obe.js → sequenceDiagram-SI44F4Z6-DKgOmdzz.js} +1 -1
- package/console-app/assets/{src-BFb2oeKx.js → src-DwHVY--Z.js} +1 -1
- package/console-app/assets/{stateDiagram-OKZ733FA-BA9fuN7b.js → stateDiagram-OKZ733FA-CPIfaZie.js} +1 -1
- package/console-app/assets/stateDiagram-v2-UEYNNEHI-DHcCmJ1C.js +1 -0
- package/console-app/assets/{swimlanes-SLNWSIFB-l-ETgM83.js → swimlanes-SLNWSIFB-8q3lK4RS.js} +1 -1
- package/console-app/assets/swimlanesDiagram-ULZ7WXOC-B1uT0NiL.js +8 -0
- package/console-app/assets/{timeline-definition-Z64GVDOM-Uj_ByeVr.js → timeline-definition-Z64GVDOM-CHHi2Rpq.js} +1 -1
- package/console-app/assets/{treeView-QDETBFTQ-BvFRkVGY.js → treeView-QDETBFTQ-DrLHlsII.js} +1 -1
- package/console-app/assets/{treemap-6X3UGDF4-BV5T7Dw9.js → treemap-6X3UGDF4-C3saYKuK.js} +1 -1
- package/console-app/assets/{vennDiagram-T6HMQDX7-DAFU8gAo.js → vennDiagram-T6HMQDX7-CdyzNiAu.js} +1 -1
- package/console-app/assets/{wardley-OPB4EBWU-MLNuhuiA.js → wardley-OPB4EBWU-BSrymyse.js} +1 -1
- package/console-app/assets/{wardleyDiagram-T6FBY63Y-B1lCTfi1.js → wardleyDiagram-T6FBY63Y-CCW6rhEo.js} +1 -1
- package/console-app/assets/{xychartDiagram-ELKLHX3M-Bw3zhuWc.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 +171 -405
- 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 +2 -2
- package/console-app/assets/architecture-TIHT7OUA-FMO1RByN.js +0 -1
- package/console-app/assets/channel-Dp8A5NJg.js +0 -1
- package/console-app/assets/chunk-XXDRQBXY-BMybY6E-.js +0 -1
- package/console-app/assets/classDiagram-JCYQIIEL-C-K-UAUR.js +0 -1
- package/console-app/assets/classDiagram-v2-OCEON4UE-C-K-UAUR.js +0 -1
- package/console-app/assets/eventmodeling-45OFAUF4-CLUnygIv.js +0 -1
- package/console-app/assets/flowDiagram-UKHOOZJN-D3mDcWWx.js +0 -1
- package/console-app/assets/railroad-abnf-AHOZXSZD-CRTVzK1g.js +0 -1
- package/console-app/assets/railroad-ebnf-EBAXGLYW-CB13ugAF.js +0 -1
- package/console-app/assets/railroad-peg-LSFZ7HO6-AN8zyQYp.js +0 -1
- package/console-app/assets/stateDiagram-v2-UEYNNEHI-D13a6mUd.js +0 -1
- package/console-app/assets/swimlanesDiagram-ULZ7WXOC-BsC-B2Z9.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({
|
|
@@ -206,6 +215,19 @@ const virtualUserScheduleFields = {
|
|
|
206
215
|
nextRunAt: z.string(),
|
|
207
216
|
lastRunId: z.string().nullable(),
|
|
208
217
|
lastRunAt: z.string().nullable(),
|
|
218
|
+
/**
|
|
219
|
+
* What \`definePersonas()\` says about this persona now, alongside what the
|
|
220
|
+
* row is actually running.
|
|
221
|
+
*
|
|
222
|
+
* A schedule is turned on once and then outlives the declaration it was
|
|
223
|
+
* written from: someone edits \`personas.ts\`, redeploys, and the row keeps
|
|
224
|
+
* running last month's goals with nothing anywhere to say so. Both sides
|
|
225
|
+
* cross the wire so that difference is readable rather than inferred.
|
|
226
|
+
*/
|
|
227
|
+
declared: z.object({
|
|
228
|
+
disposition: Disposition,
|
|
229
|
+
goals: z.array(z.string()),
|
|
230
|
+
}),
|
|
209
231
|
}
|
|
210
232
|
|
|
211
233
|
export const SetVirtualUserScheduleOutput = z.object(virtualUserScheduleFields)
|
|
@@ -229,6 +251,8 @@ export const ExecuteVirtualUserRunInput = z.object({
|
|
|
229
251
|
})
|
|
230
252
|
.optional(),
|
|
231
253
|
seed: z.number(),
|
|
254
|
+
/** Carried from the caller for the duration of the dispatch, never stored. */
|
|
255
|
+
operatorToken: z.string().optional(),
|
|
232
256
|
})
|
|
233
257
|
|
|
234
258
|
export const ExecuteVirtualUserRunOutput = z.object({
|
|
@@ -239,16 +263,20 @@ export const ExecuteVirtualUserRunOutput = z.object({
|
|
|
239
263
|
* Auto-generated virtual user functions
|
|
240
264
|
* Do not edit manually - regenerate with 'npx pikku'
|
|
241
265
|
*/
|
|
242
|
-
import { pikkuFunc, pikkuSessionlessFunc } from '${leaf('function')}'
|
|
266
|
+
import { pikkuFunc, pikkuSessionlessFunc, type Session } from '${leaf('function')}'
|
|
267
|
+
import { pikkuMiddleware } from '${leaf('middleware')}'
|
|
243
268
|
import { defineScope } from '${leaf('scopes')}'
|
|
244
|
-
import { personaVirtualUserTarget, runVirtualUser as runVirtualUserEngine, type SchemaMap, type VirtualUserDisposition } from '@pikku/core/virtual-user'
|
|
245
269
|
import {
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
executeVirtualUserRun as executeRun,
|
|
271
|
+
logVirtualUserTick,
|
|
272
|
+
requireVirtualUserRunStore,
|
|
273
|
+
serializeVirtualUserRun,
|
|
274
|
+
serializeVirtualUserSchedule,
|
|
275
|
+
serializeVirtualUserSteps,
|
|
276
|
+
startVirtualUserRun,
|
|
248
277
|
tickVirtualUserSchedules as tick,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
type VirtualUserScheduleRecord,
|
|
278
|
+
virtualUserScheduleRunInput,
|
|
279
|
+
writeVirtualUserSchedule,
|
|
252
280
|
} from '@pikku/core/virtual-user'
|
|
253
281
|
import { createPersonas, personaConfigs } from '${pathToPersonas}'
|
|
254
282
|
import {
|
|
@@ -267,6 +295,14 @@ import {
|
|
|
267
295
|
SetVirtualUserScheduleOutput,
|
|
268
296
|
} from './virtual-user.schemas.gen.js'
|
|
269
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
|
+
*/
|
|
270
306
|
defineScope({
|
|
271
307
|
virtualUser: {
|
|
272
308
|
displayName: 'Virtual Users',
|
|
@@ -281,151 +317,6 @@ defineScope({
|
|
|
281
317
|
},
|
|
282
318
|
})
|
|
283
319
|
|
|
284
|
-
/**
|
|
285
|
-
* Where the virtual user signs in. Its own variable rather than a guess at the
|
|
286
|
-
* host's origin: a run drives real traffic through the real front door, and a
|
|
287
|
-
* server that cannot name its own public URL would be signing in somewhere it
|
|
288
|
-
* only assumed was itself.
|
|
289
|
-
*/
|
|
290
|
-
const API_URL_VARIABLE = 'VIRTUAL_USER_API_URL'
|
|
291
|
-
const SECRET_VARIABLE = 'SCENARIO_ACTOR_SECRET'
|
|
292
|
-
const MODEL_VARIABLE = 'VIRTUAL_USER_MODEL'
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* One run on the wire. Findings and intents are free-form by design — the
|
|
296
|
-
* engine records what it noticed, not a fixed row shape — so they cross as the
|
|
297
|
-
* schema's open objects rather than being narrowed to whatever kinds exist
|
|
298
|
-
* today.
|
|
299
|
-
*/
|
|
300
|
-
const serializeRun = (run: VirtualUserRunRecord) => ({
|
|
301
|
-
runId: run.runId,
|
|
302
|
-
persona: run.persona,
|
|
303
|
-
disposition: run.disposition,
|
|
304
|
-
seed: run.seed,
|
|
305
|
-
status: run.status,
|
|
306
|
-
goals: run.goals,
|
|
307
|
-
memory: run.memory,
|
|
308
|
-
findings: run.findings.map((finding) => ({
|
|
309
|
-
kind: finding.kind as string,
|
|
310
|
-
detail: finding.detail,
|
|
311
|
-
rpcName: finding.rpcName,
|
|
312
|
-
status: finding.status,
|
|
313
|
-
intentId: finding.intentId,
|
|
314
|
-
step: finding.step,
|
|
315
|
-
})),
|
|
316
|
-
intents: run.intents.map((intent) => ({
|
|
317
|
-
id: intent.id,
|
|
318
|
-
sourceId: intent.sourceId,
|
|
319
|
-
title: intent.title,
|
|
320
|
-
status: intent.status as string,
|
|
321
|
-
steps: intent.steps,
|
|
322
|
-
suspensions: intent.suspensions,
|
|
323
|
-
summary: intent.summary,
|
|
324
|
-
})),
|
|
325
|
-
tally: (run.tally ?? null) as Record<string, unknown> | null,
|
|
326
|
-
stoppedBy: run.stoppedBy,
|
|
327
|
-
error: run.error,
|
|
328
|
-
createdAt: run.createdAt.toISOString(),
|
|
329
|
-
finishedAt: run.finishedAt ? run.finishedAt.toISOString() : null,
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Resolves a persona down to a run and kicks it off, for the two callers that
|
|
334
|
-
* start one: a person with the scope, and the tick acting on a schedule.
|
|
335
|
-
*
|
|
336
|
-
* Shared rather than duplicated because the checks are the point — an acted-upon
|
|
337
|
-
* persona has no session, and only one disposition may ever touch production.
|
|
338
|
-
* A second copy of that would eventually disagree with this one.
|
|
339
|
-
*/
|
|
340
|
-
const startVirtualUserRun = async (
|
|
341
|
-
services: {
|
|
342
|
-
virtualUserRunStore?: VirtualUserRunStore
|
|
343
|
-
config: { nodeEnv?: string }
|
|
344
|
-
},
|
|
345
|
-
rpc: { invoke: (name: string, data: any) => Promise<unknown> },
|
|
346
|
-
input: {
|
|
347
|
-
persona: string
|
|
348
|
-
disposition?: string
|
|
349
|
-
goals?: string[]
|
|
350
|
-
memory?: Record<string, string>
|
|
351
|
-
budget?: { steps?: number; mutations?: number; durationMs?: number }
|
|
352
|
-
seed?: number
|
|
353
|
-
startedBy?: string | null
|
|
354
|
-
}
|
|
355
|
-
): Promise<string> => {
|
|
356
|
-
const { virtualUserRunStore, config } = services
|
|
357
|
-
if (!virtualUserRunStore) {
|
|
358
|
-
throw new Error(
|
|
359
|
-
'No virtualUserRunStore is wired — a run has nowhere to be recorded. ' +
|
|
360
|
-
'Wire KyselyVirtualUserRunStore from @pikku/kysely, or your own implementation of VirtualUserRunStore.'
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const persona = personaConfigs[input.persona as keyof typeof personaConfigs] as
|
|
365
|
-
| { id: string; runnable: boolean; disposition?: string }
|
|
366
|
-
| undefined
|
|
367
|
-
if (!persona) {
|
|
368
|
-
throw new Error(
|
|
369
|
-
\`Unknown persona "\${input.persona}" — declare it with definePersonas()\`
|
|
370
|
-
)
|
|
371
|
-
}
|
|
372
|
-
// An acted-upon persona has no session of its own, and running one would
|
|
373
|
-
// race whatever scenario acts on it.
|
|
374
|
-
if (!persona.runnable) {
|
|
375
|
-
throw new Error(
|
|
376
|
-
\`Persona "\${input.persona}" is declared as acted upon, never run\`
|
|
377
|
-
)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const disposition = (input.disposition ??
|
|
381
|
-
persona.disposition ??
|
|
382
|
-
'realistic') as VirtualUserDisposition
|
|
383
|
-
// Every disposition other than this one exists to find out what the product
|
|
384
|
-
// does wrong, which is not a thing to do to real customers' data. Checked
|
|
385
|
-
// against the effective disposition, so the override cannot smuggle one in.
|
|
386
|
-
if (
|
|
387
|
-
config.nodeEnv === 'production' &&
|
|
388
|
-
disposition !== PRODUCTION_DISPOSITION
|
|
389
|
-
) {
|
|
390
|
-
throw new Error(
|
|
391
|
-
\`Only the '\${PRODUCTION_DISPOSITION}' disposition may run against production; "\${input.persona}" is \${disposition}\`
|
|
392
|
-
)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Seeded here rather than inside the engine so the record carries the seed
|
|
396
|
-
// even if the run dies before returning — an unreproducible crash costs the
|
|
397
|
-
// most.
|
|
398
|
-
const seed = input.seed ?? Math.floor(Math.random() * 2_147_483_647)
|
|
399
|
-
|
|
400
|
-
const runId = await virtualUserRunStore.start({
|
|
401
|
-
persona: persona.id,
|
|
402
|
-
disposition,
|
|
403
|
-
seed,
|
|
404
|
-
goals: input.goals ?? [],
|
|
405
|
-
memory: input.memory ?? {},
|
|
406
|
-
startedBy: input.startedBy ?? null,
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
// Deliberately not awaited: a run takes minutes, and a held-open request
|
|
410
|
-
// survives neither a rollout nor a proxy timeout. executeVirtualUserRun
|
|
411
|
-
// writes both outcomes to the record itself, so the only thing left to
|
|
412
|
-
// handle here is a rejection escaping the promise — without the catch it
|
|
413
|
-
// becomes an unhandled rejection and takes the process with it.
|
|
414
|
-
void rpc
|
|
415
|
-
.invoke('executeVirtualUserRun', {
|
|
416
|
-
runId,
|
|
417
|
-
persona: persona.id,
|
|
418
|
-
disposition,
|
|
419
|
-
goals: input.goals ?? [],
|
|
420
|
-
memory: input.memory ?? {},
|
|
421
|
-
budget: input.budget,
|
|
422
|
-
seed,
|
|
423
|
-
})
|
|
424
|
-
.catch(() => {})
|
|
425
|
-
|
|
426
|
-
return runId
|
|
427
|
-
}
|
|
428
|
-
|
|
429
320
|
export const runVirtualUser = pikkuFunc({
|
|
430
321
|
tags: ['pikku'],
|
|
431
322
|
title: 'Run a Virtual User',
|
|
@@ -436,11 +327,42 @@ export const runVirtualUser = pikkuFunc({
|
|
|
436
327
|
input: RunVirtualUserInput,
|
|
437
328
|
output: RunVirtualUserOutput,
|
|
438
329
|
func: async ({ virtualUserRunStore, config }, input, { session, rpc }) => {
|
|
439
|
-
const runId
|
|
440
|
-
{
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
+
|
|
444
366
|
return { runId }
|
|
445
367
|
},
|
|
446
368
|
})
|
|
@@ -458,14 +380,14 @@ export const getVirtualUserRun = pikkuFunc({
|
|
|
458
380
|
input: GetVirtualUserRunInput,
|
|
459
381
|
output: GetVirtualUserRunOutput,
|
|
460
382
|
func: async ({ virtualUserRunStore }, { runId }) => {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
383
|
+
const run = await requireVirtualUserRunStore(
|
|
384
|
+
virtualUserRunStore,
|
|
385
|
+
true
|
|
386
|
+
).get(runId)
|
|
465
387
|
if (!run) {
|
|
466
388
|
throw new Error(\`No virtual user run \${runId}\`)
|
|
467
389
|
}
|
|
468
|
-
return
|
|
390
|
+
return serializeVirtualUserRun(run)
|
|
469
391
|
},
|
|
470
392
|
})
|
|
471
393
|
|
|
@@ -479,11 +401,11 @@ export const listVirtualUserRuns = pikkuFunc({
|
|
|
479
401
|
input: ListVirtualUserRunsInput,
|
|
480
402
|
output: ListVirtualUserRunsOutput,
|
|
481
403
|
func: async ({ virtualUserRunStore }, { persona, limit, offset }) => {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return { runs: runs.map(
|
|
404
|
+
const runs = await requireVirtualUserRunStore(
|
|
405
|
+
virtualUserRunStore,
|
|
406
|
+
true
|
|
407
|
+
).list({ persona, limit, offset })
|
|
408
|
+
return { runs: runs.map(serializeVirtualUserRun) }
|
|
487
409
|
},
|
|
488
410
|
})
|
|
489
411
|
|
|
@@ -500,53 +422,14 @@ export const getVirtualUserRunSteps = pikkuFunc({
|
|
|
500
422
|
input: GetVirtualUserRunStepsInput,
|
|
501
423
|
output: GetVirtualUserRunStepsOutput,
|
|
502
424
|
func: async ({ virtualUserRunStore }, { runId, limit, offset }) => {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return {
|
|
508
|
-
steps: steps.map((step) => ({
|
|
509
|
-
index: step.index,
|
|
510
|
-
intentId: step.intentId,
|
|
511
|
-
action: step.action as unknown as Record<string, unknown>,
|
|
512
|
-
status: step.status,
|
|
513
|
-
ok: step.ok,
|
|
514
|
-
response: step.response,
|
|
515
|
-
findingKinds: step.findingKinds as string[] | undefined,
|
|
516
|
-
tokensIn: step.tokensIn,
|
|
517
|
-
tokensOut: step.tokensOut,
|
|
518
|
-
})),
|
|
519
|
-
}
|
|
425
|
+
const steps = await requireVirtualUserRunStore(
|
|
426
|
+
virtualUserRunStore,
|
|
427
|
+
true
|
|
428
|
+
).steps(runId, { limit, offset })
|
|
429
|
+
return { steps: serializeVirtualUserSteps(steps) }
|
|
520
430
|
},
|
|
521
431
|
})
|
|
522
432
|
|
|
523
|
-
/**
|
|
524
|
-
* One schedule on the wire. The budget crosses as \`durationMs\` because that is
|
|
525
|
-
* what every other call here takes; the engine's own duration also accepts
|
|
526
|
-
* \`'30m'\`, which nothing on this side ever writes.
|
|
527
|
-
*/
|
|
528
|
-
const serializeSchedule = (schedule: VirtualUserScheduleRecord) => ({
|
|
529
|
-
persona: schedule.persona,
|
|
530
|
-
enabled: schedule.enabled,
|
|
531
|
-
disposition: schedule.disposition,
|
|
532
|
-
goals: schedule.goals,
|
|
533
|
-
budget: schedule.budget
|
|
534
|
-
? {
|
|
535
|
-
steps: schedule.budget.steps,
|
|
536
|
-
mutations: schedule.budget.mutations,
|
|
537
|
-
durationMs:
|
|
538
|
-
typeof schedule.budget.duration === 'number'
|
|
539
|
-
? schedule.budget.duration
|
|
540
|
-
: undefined,
|
|
541
|
-
}
|
|
542
|
-
: null,
|
|
543
|
-
minIntervalMs: schedule.minIntervalMs,
|
|
544
|
-
maxIntervalMs: schedule.maxIntervalMs,
|
|
545
|
-
nextRunAt: schedule.nextRunAt.toISOString(),
|
|
546
|
-
lastRunId: schedule.lastRunId,
|
|
547
|
-
lastRunAt: schedule.lastRunAt ? schedule.lastRunAt.toISOString() : null,
|
|
548
|
-
})
|
|
549
|
-
|
|
550
433
|
export const setVirtualUserSchedule = pikkuFunc({
|
|
551
434
|
tags: ['pikku'],
|
|
552
435
|
title: 'Put a Virtual User on a Clock',
|
|
@@ -560,48 +443,12 @@ export const setVirtualUserSchedule = pikkuFunc({
|
|
|
560
443
|
input: SetVirtualUserScheduleInput,
|
|
561
444
|
output: SetVirtualUserScheduleOutput,
|
|
562
445
|
func: async ({ virtualUserScheduleStore }, input) => {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
)
|
|
568
|
-
}
|
|
569
|
-
const persona = personaConfigs[
|
|
570
|
-
input.persona as keyof typeof personaConfigs
|
|
571
|
-
] as { runnable: boolean } | undefined
|
|
572
|
-
if (!persona) {
|
|
573
|
-
throw new Error(
|
|
574
|
-
\`Unknown persona "\${input.persona}" — declare it with definePersonas()\`
|
|
575
|
-
)
|
|
576
|
-
}
|
|
577
|
-
// The same rule \`runVirtualUser\` enforces, applied at the point the row is
|
|
578
|
-
// written rather than every hour afterwards: an acted-upon persona has no
|
|
579
|
-
// session, so a cadence for one is a tick that can only ever fail to start.
|
|
580
|
-
if (!persona.runnable) {
|
|
581
|
-
throw new Error(
|
|
582
|
-
\`Persona "\${input.persona}" is declared as acted upon, never run\`
|
|
583
|
-
)
|
|
584
|
-
}
|
|
585
|
-
const schedule = await virtualUserScheduleStore.set({
|
|
586
|
-
persona: input.persona,
|
|
587
|
-
enabled: input.enabled,
|
|
588
|
-
disposition: input.disposition as VirtualUserDisposition | undefined,
|
|
589
|
-
goals: input.goals,
|
|
590
|
-
budget:
|
|
591
|
-
input.budget === undefined
|
|
592
|
-
? undefined
|
|
593
|
-
: input.budget === null
|
|
594
|
-
? null
|
|
595
|
-
: {
|
|
596
|
-
steps: input.budget.steps,
|
|
597
|
-
mutations: input.budget.mutations,
|
|
598
|
-
duration: input.budget.durationMs,
|
|
599
|
-
},
|
|
600
|
-
minIntervalMs: input.minIntervalMs,
|
|
601
|
-
maxIntervalMs: input.maxIntervalMs,
|
|
602
|
-
nextRunAt: input.nextRunAt ? new Date(input.nextRunAt) : undefined,
|
|
446
|
+
const schedule = await writeVirtualUserSchedule({
|
|
447
|
+
store: virtualUserScheduleStore,
|
|
448
|
+
personas: personaConfigs,
|
|
449
|
+
...input,
|
|
603
450
|
})
|
|
604
|
-
return
|
|
451
|
+
return serializeVirtualUserSchedule(schedule, personaConfigs)
|
|
605
452
|
},
|
|
606
453
|
})
|
|
607
454
|
|
|
@@ -612,17 +459,50 @@ export const listVirtualUserSchedules = pikkuFunc({
|
|
|
612
459
|
'Which personas are on a clock, how often they run, and when each is next due.',
|
|
613
460
|
expose: true,
|
|
614
461
|
scopes: ['virtualUser:read'],
|
|
615
|
-
input: null,
|
|
616
462
|
output: ListVirtualUserSchedulesOutput,
|
|
617
463
|
func: async ({ virtualUserScheduleStore }) => {
|
|
618
464
|
if (!virtualUserScheduleStore) {
|
|
619
465
|
return { schedules: [] }
|
|
620
466
|
}
|
|
621
467
|
const schedules = await virtualUserScheduleStore.list()
|
|
622
|
-
return {
|
|
468
|
+
return {
|
|
469
|
+
schedules: schedules.map((schedule) =>
|
|
470
|
+
serializeVirtualUserSchedule(schedule, personaConfigs)
|
|
471
|
+
),
|
|
472
|
+
}
|
|
623
473
|
},
|
|
624
474
|
})
|
|
625
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
|
+
|
|
626
506
|
/**
|
|
627
507
|
* Acts on whichever personas are due, once.
|
|
628
508
|
*
|
|
@@ -635,13 +515,21 @@ export const listVirtualUserSchedules = pikkuFunc({
|
|
|
635
515
|
* wireScheduler({
|
|
636
516
|
* name: 'virtualUsers',
|
|
637
517
|
* schedule: '0 * * * *',
|
|
518
|
+
* middleware: [virtualUserPlatformSession],
|
|
638
519
|
* func: tickVirtualUserSchedules,
|
|
639
520
|
* })
|
|
640
521
|
* \`\`\`
|
|
641
522
|
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
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.
|
|
645
533
|
*/
|
|
646
534
|
export const tickVirtualUserSchedules = pikkuSessionlessFunc<void, void>({
|
|
647
535
|
tags: ['pikku'],
|
|
@@ -650,50 +538,26 @@ export const tickVirtualUserSchedules = pikkuSessionlessFunc<void, void>({
|
|
|
650
538
|
if (!virtualUserScheduleStore || !virtualUserRunStore) {
|
|
651
539
|
return
|
|
652
540
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
? schedule.budget.duration
|
|
668
|
-
: undefined,
|
|
669
|
-
}
|
|
670
|
-
: undefined,
|
|
671
|
-
}),
|
|
672
|
-
})
|
|
673
|
-
// Logged rather than returned: the caller is a cron, and a run this started
|
|
674
|
-
// is otherwise the only trace that a persona is still out there working.
|
|
675
|
-
for (const { persona, runId } of result.dispatched) {
|
|
676
|
-
logger.info(\`Virtual user \${persona} started run \${runId} on schedule\`)
|
|
677
|
-
}
|
|
678
|
-
for (const runId of result.reaped) {
|
|
679
|
-
logger.warn(
|
|
680
|
-
\`Virtual user run \${runId} was abandoned — marked failed so its persona can run again\`
|
|
681
|
-
)
|
|
682
|
-
}
|
|
683
|
-
for (const { persona, reason } of result.skipped) {
|
|
684
|
-
logger.info(\`Virtual user \${persona} skipped this tick: \${reason}\`)
|
|
685
|
-
}
|
|
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
|
+
)
|
|
686
555
|
},
|
|
687
556
|
})
|
|
688
557
|
|
|
689
558
|
/**
|
|
690
559
|
* The run itself. Not exposed: it is dispatched by \`runVirtualUser\` and has no
|
|
691
560
|
* caller of its own.
|
|
692
|
-
*
|
|
693
|
-
* Everything it needs is derived through \`metaService\` and the generated
|
|
694
|
-
* personas — the same public surface any consumer has. Nothing reaches into
|
|
695
|
-
* pikku's internals, because an app could not, and a feature built on what only
|
|
696
|
-
* the framework can see would not be this feature.
|
|
697
561
|
*/
|
|
698
562
|
export const executeVirtualUserRun = pikkuSessionlessFunc({
|
|
699
563
|
tags: ['pikku'],
|
|
@@ -701,116 +565,18 @@ export const executeVirtualUserRun = pikkuSessionlessFunc({
|
|
|
701
565
|
output: ExecuteVirtualUserRunOutput,
|
|
702
566
|
func: async (
|
|
703
567
|
{ virtualUserRunStore, metaService, agentRunner, variables, logger },
|
|
704
|
-
|
|
705
|
-
) =>
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const apiUrl = await variables.get(API_URL_VARIABLE)
|
|
718
|
-
if (!apiUrl) {
|
|
719
|
-
throw new Error(
|
|
720
|
-
\`\${API_URL_VARIABLE} is not set — a virtual user has no address to sign in at.\`
|
|
721
|
-
)
|
|
722
|
-
}
|
|
723
|
-
const secret = await variables.get(SECRET_VARIABLE)
|
|
724
|
-
if (!secret) {
|
|
725
|
-
throw new Error(
|
|
726
|
-
\`\${SECRET_VARIABLE} is not set — actor sign-in is disabled, so there is nobody for the virtual user to be.\`
|
|
727
|
-
)
|
|
728
|
-
}
|
|
729
|
-
const model = await variables.get(MODEL_VARIABLE)
|
|
730
|
-
if (!model) {
|
|
731
|
-
throw new Error(\`\${MODEL_VARIABLE} is not set — no model to think with.\`)
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const functionsMeta = await metaService.getFunctionsMeta()
|
|
735
|
-
// Only the schemas the catalogue can actually refer to, so a large app
|
|
736
|
-
// does not pull every schema it has ever generated into one run.
|
|
737
|
-
const schemaNames = [
|
|
738
|
-
...new Set(
|
|
739
|
-
Object.values(functionsMeta).flatMap((meta: any) =>
|
|
740
|
-
[meta.inputSchemaName, meta.outputSchemaName].filter(
|
|
741
|
-
(name: unknown): name is string => !!name
|
|
742
|
-
)
|
|
743
|
-
)
|
|
744
|
-
),
|
|
745
|
-
]
|
|
746
|
-
const schemas = (await metaService.getSchemas(schemaNames)) as SchemaMap
|
|
747
|
-
|
|
748
|
-
const persona = personaConfigs[personaId as keyof typeof personaConfigs]
|
|
749
|
-
if (!persona) {
|
|
750
|
-
throw new Error(\`Persona "\${personaId}" is no longer declared\`)
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
const { catalogue, intents, agents } = prepareVirtualUserRun({
|
|
754
|
-
persona,
|
|
755
|
-
functionsMeta,
|
|
756
|
-
schemas,
|
|
757
|
-
workflowsMeta: await metaService.getWorkflowMeta(),
|
|
758
|
-
systemRoles: await metaService.getSystemRolesMeta(),
|
|
759
|
-
agentsMeta: await metaService.getAgentsMeta(),
|
|
760
|
-
})
|
|
761
|
-
|
|
762
|
-
const signedIn = createPersonas({ apiUrl, secret, model })
|
|
763
|
-
const target = signedIn[personaId as keyof typeof signedIn]
|
|
764
|
-
if (!target) {
|
|
765
|
-
throw new Error(\`Persona "\${personaId}" cannot sign in\`)
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
const result = await runVirtualUserEngine({
|
|
769
|
-
persona,
|
|
770
|
-
personaId,
|
|
771
|
-
disposition: disposition as VirtualUserDisposition,
|
|
772
|
-
catalogue,
|
|
773
|
-
intents,
|
|
774
|
-
goals,
|
|
775
|
-
memory,
|
|
776
|
-
seed,
|
|
777
|
-
agents,
|
|
778
|
-
target: personaVirtualUserTarget(target, {
|
|
779
|
-
model,
|
|
780
|
-
agents: agents.map((agent) => agent.name),
|
|
781
|
-
}),
|
|
782
|
-
// \`AgentRunnerService.run\` IS the engine's \`ActorLLM\` — same params,
|
|
783
|
-
// same result — so a virtual user thinks through the same runner every
|
|
784
|
-
// agent in the app does, provider quirks and all.
|
|
785
|
-
llm: (params) => agentRunner.run(params),
|
|
786
|
-
model,
|
|
787
|
-
budget: {
|
|
788
|
-
steps: budget?.steps,
|
|
789
|
-
mutations: budget?.mutations,
|
|
790
|
-
duration: budget?.durationMs,
|
|
791
|
-
},
|
|
792
|
-
})
|
|
793
|
-
|
|
794
|
-
await virtualUserRunStore.complete(runId, {
|
|
795
|
-
findings: result.findings,
|
|
796
|
-
tally: result.tally,
|
|
797
|
-
memory: result.memory,
|
|
798
|
-
stoppedBy: result.stoppedBy ?? null,
|
|
799
|
-
intents: result.intents,
|
|
800
|
-
steps: result.steps,
|
|
801
|
-
})
|
|
802
|
-
|
|
803
|
-
return { findings: result.findings.length }
|
|
804
|
-
} catch (error) {
|
|
805
|
-
// A crashed run and a run that found nothing are different states, and the
|
|
806
|
-
// record is the only place that distinction survives — leaving it at
|
|
807
|
-
// 'running' forever is what \`fail\` exists to prevent.
|
|
808
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
809
|
-
logger.error(\`Virtual user run \${runId} (\${personaId}) failed: \${message}\`)
|
|
810
|
-
await virtualUserRunStore.fail(runId, message)
|
|
811
|
-
throw error
|
|
812
|
-
}
|
|
813
|
-
},
|
|
568
|
+
input
|
|
569
|
+
) =>
|
|
570
|
+
executeRun({
|
|
571
|
+
runStore: virtualUserRunStore,
|
|
572
|
+
metaService,
|
|
573
|
+
agentRunner,
|
|
574
|
+
variables,
|
|
575
|
+
logger,
|
|
576
|
+
personas: personaConfigs,
|
|
577
|
+
createPersonas,
|
|
578
|
+
...input,
|
|
579
|
+
}),
|
|
814
580
|
})
|
|
815
581
|
`;
|
|
816
582
|
return { schemas, functions };
|