@pikku/core 0.12.141 → 0.12.143

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 (251) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/errors/errors.d.ts +1 -1
  3. package/dist/errors/errors.js +1 -1
  4. package/dist/function/function-meta.types.d.ts +6 -6
  5. package/dist/function/functions.types.d.ts +2 -2
  6. package/dist/services/audit-service.d.ts +4 -4
  7. package/dist/services/feature-flag-service.d.ts +3 -3
  8. package/dist/services/{http-personas.d.ts → http-virtual-users.d.ts} +25 -47
  9. package/dist/services/{http-personas.js → http-virtual-users.js} +29 -53
  10. package/dist/services/in-memory-queue-service.d.ts +8 -1
  11. package/dist/services/in-memory-queue-service.js +67 -0
  12. package/dist/services/incoming-webhook-service.d.ts +2 -0
  13. package/dist/services/incoming-webhook-service.js +4 -0
  14. package/dist/services/index.d.ts +2 -2
  15. package/dist/services/index.js +1 -1
  16. package/dist/services/meta-service.d.ts +6 -6
  17. package/dist/services/meta-service.js +8 -8
  18. package/dist/{wirings/workflow → services}/scenario-cookie-jar.d.ts +2 -0
  19. package/dist/{wirings/workflow → services}/scenario-cookie-jar.js +18 -14
  20. package/dist/services/scheduler-service.d.ts +3 -0
  21. package/dist/services/scheduler-service.js +4 -0
  22. package/dist/services/system-role-guard.d.ts +1 -1
  23. package/dist/services/virtual-user-sign-in.d.ts +52 -0
  24. package/dist/services/virtual-user-sign-in.js +80 -0
  25. package/dist/services/virtual-user-statement.d.ts +31 -0
  26. package/dist/services/virtual-user-statement.js +24 -0
  27. package/dist/services/{personas-service.d.ts → virtual-users-service.d.ts} +25 -23
  28. package/dist/services/{personas-service.js → virtual-users-service.js} +3 -3
  29. package/dist/types/core.types.d.ts +11 -11
  30. package/dist/wirings/queue/index.d.ts +1 -1
  31. package/dist/wirings/queue/queue.types.d.ts +21 -0
  32. package/dist/wirings/queue/signed-queue-service.d.ts +4 -1
  33. package/dist/wirings/queue/signed-queue-service.js +3 -0
  34. package/dist/wirings/role/define-system-role.d.ts +1 -1
  35. package/dist/wirings/role/define-system-role.js +1 -1
  36. package/dist/wirings/scheduler/index.d.ts +1 -1
  37. package/dist/wirings/scheduler/scheduler.types.d.ts +12 -0
  38. package/dist/wirings/{actor-flow/actor-flow.types.d.ts → virtual-user/conversation/conversation.types.d.ts} +5 -5
  39. package/dist/wirings/virtual-user/conversation/index.d.ts +2 -0
  40. package/dist/wirings/virtual-user/conversation/run-conversation.d.ts +36 -0
  41. package/dist/wirings/{actor-flow → virtual-user/conversation}/run-conversation.js +30 -30
  42. package/dist/wirings/virtual-user/define-virtual-users.d.ts +32 -0
  43. package/dist/wirings/{persona/define-personas.js → virtual-user/define-virtual-users.js} +9 -9
  44. package/dist/wirings/virtual-user/index.d.ts +30 -7
  45. package/dist/wirings/virtual-user/index.js +23 -4
  46. package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +8 -8
  47. package/dist/wirings/virtual-user/prepare-virtual-user-run.js +9 -9
  48. package/dist/wirings/virtual-user/run-virtual-user.d.ts +6 -6
  49. package/dist/wirings/virtual-user/run-virtual-user.js +10 -10
  50. package/dist/wirings/{persona/validate-personas.d.ts → virtual-user/validate-virtual-users.d.ts} +11 -11
  51. package/dist/wirings/{persona/validate-personas.js → virtual-user/validate-virtual-users.js} +24 -24
  52. package/dist/wirings/virtual-user/virtual-user-agents.d.ts +7 -7
  53. package/dist/wirings/virtual-user/virtual-user-agents.js +2 -2
  54. package/dist/wirings/{persona/persona-app-scopes.d.ts → virtual-user/virtual-user-app-scopes.d.ts} +10 -10
  55. package/dist/wirings/{persona/persona-app-scopes.js → virtual-user/virtual-user-app-scopes.js} +12 -12
  56. package/dist/wirings/virtual-user/virtual-user-catalogue.d.ts +3 -3
  57. package/dist/wirings/virtual-user/virtual-user-catalogue.js +2 -2
  58. package/dist/wirings/{persona/persona.types.d.ts → virtual-user/virtual-user-declaration.types.d.ts} +24 -24
  59. package/dist/wirings/virtual-user/virtual-user-derive.js +13 -7
  60. package/dist/wirings/virtual-user/virtual-user-dispositions.d.ts +1 -1
  61. package/dist/wirings/{persona/persona-email.d.ts → virtual-user/virtual-user-email.d.ts} +11 -11
  62. package/dist/wirings/{persona/persona-email.js → virtual-user/virtual-user-email.js} +19 -19
  63. package/dist/wirings/{persona/persona-environments.d.ts → virtual-user/virtual-user-environments.d.ts} +10 -10
  64. package/dist/wirings/{persona/persona-environments.js → virtual-user/virtual-user-environments.js} +21 -21
  65. package/dist/wirings/virtual-user/virtual-user-intents.d.ts +3 -3
  66. package/dist/wirings/virtual-user/virtual-user-intents.js +3 -3
  67. package/dist/wirings/{persona/persona-mailbox.d.ts → virtual-user/virtual-user-mailbox.d.ts} +4 -4
  68. package/dist/wirings/virtual-user/virtual-user-run-store.d.ts +4 -4
  69. package/dist/wirings/virtual-user/virtual-user-scaffold.d.ts +42 -61
  70. package/dist/wirings/virtual-user/virtual-user-scaffold.js +69 -117
  71. package/dist/wirings/virtual-user/virtual-user-schedule-store.d.ts +15 -15
  72. package/dist/wirings/virtual-user/virtual-user-schedule.d.ts +6 -6
  73. package/dist/wirings/virtual-user/virtual-user-schedule.js +11 -11
  74. package/dist/wirings/virtual-user/virtual-user-target.d.ts +6 -6
  75. package/dist/wirings/virtual-user/virtual-user-target.js +5 -5
  76. package/dist/wirings/virtual-user/virtual-user.types.d.ts +11 -11
  77. package/dist/wirings/virtual-user/virtual-user.types.js +1 -1
  78. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +9 -9
  79. package/dist/wirings/workflow/pikku-scenario-service.d.ts +28 -28
  80. package/dist/wirings/workflow/pikku-scenario-service.js +77 -77
  81. package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -2
  82. package/dist/wirings/workflow/pikku-workflow-service.js +4 -4
  83. package/dist/wirings/workflow/scenario-prose.d.ts +8 -8
  84. package/dist/wirings/workflow/scenario-prose.js +12 -8
  85. package/dist/wirings/workflow/scenario-run.types.d.ts +13 -13
  86. package/dist/wirings/workflow/scenario-step.types.d.ts +23 -23
  87. package/dist/wirings/workflow/workflow.types.d.ts +2 -2
  88. package/knowledge/decisions/index.md +0 -2
  89. package/knowledge/decisions/internals/a-credential-name-declared-twice-must-agree-on-schema-and-type.md +29 -0
  90. package/knowledge/decisions/internals/a-grant-can-name-a-scope-declared-without-its-addon.md +29 -0
  91. package/knowledge/decisions/internals/a-latency-budget-is-asserted-by-its-cause.md +32 -0
  92. package/knowledge/decisions/internals/a-scheduled-task-writes-its-caller-session-before-it-runs.md +22 -0
  93. package/knowledge/decisions/internals/a-secret-set-in-memory-shadows-the-variable-of-the-same-name.md +28 -0
  94. package/knowledge/decisions/internals/a-virtual-user-acts-through-invokeraw-so-a-refusal-is-data.md +32 -0
  95. package/knowledge/decisions/internals/a-virtual-user-cadence-is-a-row-not-a-timer.md +12 -12
  96. package/knowledge/decisions/internals/a-virtual-user-converse-signs-in-before-its-first-turn.md +24 -0
  97. package/knowledge/decisions/internals/a-virtual-user-email-is-computed-with-the-run-id-as-a-sub-address.md +34 -0
  98. package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-but-it-needs-a-trigger.md +2 -2
  99. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +3 -0
  100. package/knowledge/decisions/internals/an-agent-is-offered-under-its-registry-key-and-only-within-the-virtual-users-scopes.md +27 -0
  101. package/knowledge/decisions/internals/an-oauth2-credential-implies-an-optional-app-secret.md +26 -0
  102. package/knowledge/decisions/internals/analytics-identity-resolvers-run-in-declared-order.md +22 -0
  103. package/knowledge/decisions/internals/feature-flag-declarations-refuse-an-empty-anyof-and-conflicting-scopes.md +22 -0
  104. package/knowledge/decisions/internals/feature-flags-fail-open-and-never-enforce-access.md +28 -0
  105. package/knowledge/decisions/internals/flag-sync-rewrites-only-the-declared-text.md +24 -0
  106. package/knowledge/decisions/internals/in-a-scenario-a-4xx-is-data-not-an-exception.md +3 -3
  107. package/knowledge/decisions/internals/index.md +47 -31
  108. package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +10 -10
  109. package/knowledge/decisions/internals/role-names-cannot-contain-the-scope-separator.md +30 -0
  110. package/knowledge/decisions/internals/scenario-agent-calls-sign-in-on-401-only.md +23 -19
  111. package/knowledge/decisions/internals/scenario-steps-return-drained-response-records.md +2 -2
  112. package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +12 -12
  113. package/knowledge/decisions/internals/the-core-scope-validator-refuses-separator-wildcard-and-empty-segments.md +22 -0
  114. package/knowledge/decisions/internals/the-pikku-user-id-resolves-in-a-fixed-order.md +24 -0
  115. package/knowledge/decisions/internals/{the-actor-prompt-says-json-because-of-json-object-mode.md → the-virtual-user-prompt-says-json-because-of-json-object-mode.md} +4 -4
  116. package/knowledge/decisions/internals/{the-persona-runtime-is-exported-from-the-persona-entry-point.md → the-virtual-user-runtime-is-exported-from-the-virtual-user-entry-point.md} +8 -8
  117. package/knowledge/decisions/internals/validate-checks-virtual-users-through-a-shared-module.md +43 -0
  118. package/knowledge/decisions/internals/{actor-flow-drives-the-target-through-a-transport-seam.md → virtual-user-conversation-drives-the-target-through-a-transport-seam.md} +6 -6
  119. package/knowledge/decisions/internals/virtual-user-conversation-verdicts-are-llm-self-evaluations.md +25 -0
  120. package/knowledge/decisions/internals/virtual-user-conversations-seed-a-hidden-kickoff-message.md +23 -0
  121. package/knowledge/decisions/internals/virtual-users-are-declared-in-one-definevirtualusers-call-per-codebase.md +30 -0
  122. package/knowledge/decisions/internals/workflow-core-never-imports-a-browser-driver.md +5 -5
  123. package/knowledge/decisions/internals/workflow-run-capabilities-are-extensions-not-subclasses.md +2 -2
  124. package/knowledge/decisions/internals/workflow-scenario-assertions-never-retry-and-record-one-step.md +4 -4
  125. package/knowledge/decisions/internals/workflow-scenario-hooks-are-a-scenario-only-affordance.md +1 -1
  126. package/knowledge/decisions/internals/workflow-scenario-prose-is-rendered-from-typed-calls-not-parsed-from-english.md +2 -2
  127. package/knowledge/decisions/internals/workflow-scenario-step-targets-are-string-literals-for-the-inspector.md +1 -1
  128. package/knowledge/decisions/internals/workflow-suspended-runs-keep-their-in-process-context.md +1 -1
  129. package/knowledge/decisions/security/a-bearer-token-checked-against-a-secret-reads-it-per-request.md +26 -0
  130. package/knowledge/decisions/security/a-call-without-a-session-raises-missing-session-not-forbidden.md +20 -0
  131. package/knowledge/decisions/security/a-caller-cookie-wins-over-the-jar-for-the-same-name.md +23 -0
  132. package/knowledge/decisions/security/a-channel-lifecycle-acts-as-the-socket-upgrade-session.md +16 -0
  133. package/knowledge/decisions/security/a-channel-requires-a-session-unless-it-says-auth-false.md +26 -0
  134. package/knowledge/decisions/security/a-credential-read-path-follows-its-declared-type-not-absence.md +32 -0
  135. package/knowledge/decisions/security/a-credential-type-is-a-literal-the-inspector-checks-at-declaration.md +27 -0
  136. package/knowledge/decisions/security/a-first-party-cookie-is-written-only-after-its-purposes-consent.md +33 -0
  137. package/knowledge/decisions/security/a-readonly-session-reaches-only-readonly-functions.md +25 -0
  138. package/knowledge/decisions/security/a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md +10 -5
  139. package/knowledge/decisions/security/a-scheduled-virtual-user-run-needs-no-admin-at-run-time.md +27 -0
  140. package/knowledge/decisions/security/a-scoped-credential-view-is-limited-to-declared-names.md +26 -0
  141. package/knowledge/decisions/security/a-scoped-secret-view-reads-only-declared-keys-and-never-writes.md +25 -0
  142. package/knowledge/decisions/security/a-secret-is-sent-only-to-the-hosts-it-declares.md +29 -0
  143. package/knowledge/decisions/security/a-step-can-name-an-identity-no-virtual-user-holds-through-per-call-headers.md +25 -0
  144. package/knowledge/decisions/security/a-stopped-run-by-id-skips-the-ownership-check.md +24 -0
  145. package/knowledge/decisions/security/a-vault-secret-redacts-on-serialization-and-throws-on-coercion.md +29 -0
  146. package/knowledge/decisions/security/a-virtual-user-catalogue-entry-is-filtered-by-scope-and-approval-is-denied-by-default.md +30 -0
  147. package/knowledge/decisions/security/a-virtual-user-environment-fails-closed.md +38 -0
  148. package/knowledge/decisions/security/a-virtual-user-never-runs-against-production.md +23 -0
  149. package/knowledge/decisions/security/a-virtual-user-reads-its-roles-from-the-apps-own-rpc-before-better-auth.md +27 -0
  150. package/knowledge/decisions/security/a-virtual-user-run-treats-an-unresolved-environment-as-production.md +41 -0
  151. package/knowledge/decisions/security/a-virtual-user-scopes-are-expanded-from-its-roles-once-at-run-preparation.md +25 -0
  152. package/knowledge/decisions/security/a-virtual-user-whose-roles-drift-from-the-declaration-refuses-to-run.md +32 -0
  153. package/knowledge/decisions/security/addon-screen-scopes-hide-and-do-not-enforce.md +20 -0
  154. package/knowledge/decisions/security/an-addon-header-can-only-narrow-the-session.md +26 -0
  155. package/knowledge/decisions/security/an-addon-instance-reads-only-the-secrets-and-credentials-it-is-named.md +33 -0
  156. package/knowledge/decisions/security/an-addon-may-reach-only-itself-and-the-addons-it-uses.md +16 -0
  157. package/knowledge/decisions/security/an-api-key-is-jwt-decoded-into-the-session.md +26 -0
  158. package/knowledge/decisions/security/an-external-virtual-user-caller-presents-one-signed-statement.md +38 -0
  159. package/knowledge/decisions/security/an-inbound-webhook-source-dispatches-only-verified-requests.md +31 -0
  160. package/knowledge/decisions/security/an-mcp-call-with-no-http-request-stays-anonymous.md +18 -0
  161. package/knowledge/decisions/security/an-omitted-user-id-addresses-the-platform-credential.md +32 -0
  162. package/knowledge/decisions/security/analytics-identity-is-stamped-from-the-session-not-the-event.md +24 -0
  163. package/knowledge/decisions/security/app-access-is-a-scope-tree-synthesised-from-virtual-users.md +26 -0
  164. package/knowledge/decisions/security/approval-policy-decides-who-answers-a-tool-call.md +27 -0
  165. package/knowledge/decisions/security/auth-false-on-a-session-function-warns-and-still-requires-one.md +25 -0
  166. package/knowledge/decisions/security/bearer-token-hashes-are-unpeppered-sha256.md +18 -0
  167. package/knowledge/decisions/security/consent-is-read-from-a-cookie-and-a-recognised-denial-counts-as-refusal.md +29 -0
  168. package/knowledge/decisions/security/cors-defaults-to-any-origin-and-refuses-the-wildcard-with-credentials.md +22 -0
  169. package/knowledge/decisions/security/credential-backends-share-one-conformance-suite-for-per-user-isolation.md +29 -0
  170. package/knowledge/decisions/security/each-stage-has-one-virtual-user-key-pair.md +27 -0
  171. package/knowledge/decisions/security/hmac-verification-fails-closed-on-empty-inputs.md +25 -0
  172. package/knowledge/decisions/security/index.md +88 -24
  173. package/knowledge/decisions/security/local-content-refuses-private-buckets-under-the-public-segment.md +21 -0
  174. package/knowledge/decisions/security/mail-is-allowlisted-before-a-virtual-user-run-sees-it.md +38 -0
  175. package/knowledge/decisions/security/mcp-resources-tools-and-prompts-take-sessionless-functions.md +18 -0
  176. package/knowledge/decisions/security/origin-locked-routes-match-the-origin-exactly.md +22 -0
  177. package/knowledge/decisions/security/oss-auth-works-without-pikku-studio-cloud.md +26 -0
  178. package/knowledge/decisions/security/outbound-webhooks-use-an-allowlist-when-configured.md +24 -0
  179. package/knowledge/decisions/security/pikku-carries-virtual-user-scopes-as-data-and-the-app-grants-them.md +35 -0
  180. package/knowledge/decisions/security/scoped-content-access-is-a-root-folder-with-longest-prefix-grants.md +31 -0
  181. package/knowledge/decisions/security/services-without-secrets-get-a-throwing-secrets-accessor.md +22 -0
  182. package/knowledge/decisions/security/shared-guards-refuse-to-mutate-or-shadow-a-system-role.md +34 -0
  183. package/knowledge/decisions/security/signed-uploads-are-verified-by-the-same-check-as-signed-reads.md +26 -0
  184. package/knowledge/decisions/security/system-role-scopes-must-be-declared-with-define-scope.md +24 -0
  185. package/knowledge/decisions/security/system-roles-are-additive-and-immutable.md +16 -0
  186. package/knowledge/decisions/security/the-channel-rpc-does-not-send-the-callers-session.md +21 -0
  187. package/knowledge/decisions/security/the-remote-jobs-inbox-fails-closed-without-a-secret.md +24 -0
  188. package/knowledge/decisions/security/the-remote-rpc-gate-fails-closed-and-binds-the-token-to-its-function.md +34 -0
  189. package/knowledge/decisions/security/the-workflow-status-stream-rechecks-ownership-on-every-poll.md +16 -0
  190. package/knowledge/decisions/security/two-credentials-on-one-key-must-share-a-read-mode.md +29 -0
  191. package/knowledge/decisions/security/verified-bearer-claims-are-pass-through-data-that-core-never-fills.md +23 -0
  192. package/knowledge/decisions/security/virtual-user-catalogue-excludes-the-machinery-that-runs-virtual-users.md +25 -0
  193. package/knowledge/decisions/security/{actor-flow-missing-approval-decisions-default-to-denied.md → virtual-user-conversation-missing-approval-decisions-default-to-denied.md} +7 -7
  194. package/knowledge/decisions/security/virtual-user-run-records-are-gated-behind-a-read-scope.md +18 -0
  195. package/knowledge/decisions/security/virtual-user-sign-in-does-not-exist-in-production.md +25 -0
  196. package/knowledge/decisions/security/virtual-user-sign-in-has-one-door.md +42 -0
  197. package/knowledge/decisions/security/{actor-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md → virtual-user-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md} +10 -8
  198. package/knowledge/decisions/security/virtual-user-sign-in-only-works-for-virtual-user-flagged-users.md +29 -0
  199. package/knowledge/decisions/security/workflow-helper-functions-are-sessionless-by-declaration.md +18 -0
  200. package/knowledge/decisions/security/{workflow-scenario-sessions-are-isolated-per-actor-and-per-scenario.md → workflow-scenario-sessions-are-isolated-per-virtual-user-and-per-scenario.md} +6 -6
  201. package/knowledge/decisions/security/workflow-scenario-steps-are-never-network-invocable.md +5 -5
  202. package/knowledge/decisions/security/{workflow-actor-steps-always-use-the-real-transport.md → workflow-virtual-user-steps-always-use-the-real-transport.md} +11 -11
  203. package/knowledge/decisions/security/writing-a-virtual-user-schedule-is-gated-by-its-own-scope.md +18 -0
  204. package/knowledge/index.md +0 -2
  205. package/knowledge/questions/a-bearer-jwt-with-no-identity-becomes-a-session.md +24 -0
  206. package/knowledge/questions/dev-quick-login-as-impersonation.md +25 -0
  207. package/knowledge/questions/does-a-delayed-rpc-run-as-the-session-it-was-scheduled-with.md +26 -0
  208. package/knowledge/questions/does-a-user-id-with-a-colon-read-another-users-credentials.md +27 -0
  209. package/knowledge/questions/does-a-wirecli-command-need-a-session-by-default.md +26 -0
  210. package/knowledge/questions/does-an-adversarial-in-scope-success-count-as-a-finding.md +26 -0
  211. package/knowledge/questions/does-approve-step-need-the-run-owners-session-or-the-gates-approvers.md +34 -0
  212. package/knowledge/questions/does-cors-origin-true-reflect-any-origin-with-credentials.md +20 -0
  213. package/knowledge/questions/does-skip-user-session-do-anything-on-a-channel.md +24 -0
  214. package/knowledge/questions/does-the-store-session-match-the-other-two.md +19 -0
  215. package/knowledge/questions/index.md +25 -2
  216. package/knowledge/questions/is-the-agent-score-worker-reachable-without-a-session.md +21 -0
  217. package/knowledge/questions/live-scheduled-tests-may-write.md +19 -0
  218. package/knowledge/questions/provision-virtual-users-has-no-caller-in-this-repo.md +23 -0
  219. package/knowledge/questions/remote-secret-refuses-requests-without-a-remote-token-on-every-path.md +32 -0
  220. package/knowledge/questions/should-a-middleware-set-session-persist-to-the-store.md +25 -0
  221. package/knowledge/questions/should-a-scoped-secret-view-throw-from-has-secret-for-a-disallowed-key.md +23 -0
  222. package/knowledge/questions/should-a-wrong-bearer-token-be-anonymous-or-refused.md +35 -0
  223. package/knowledge/questions/should-an-empty-webhook-signing-secret-send-unsigned.md +23 -0
  224. package/knowledge/questions/should-http-virtual-user-sessions-reset-between-scenarios-in-one-run.md +27 -0
  225. package/knowledge/questions/should-the-cookie-jar-scope-cookies-to-the-api-origin.md +26 -0
  226. package/knowledge/questions/should-voice-input-fetch-audio-urls-without-a-host-allowlist.md +20 -0
  227. package/knowledge/questions/staging-impersonation-rules.md +27 -0
  228. package/knowledge/questions/virtual-user-keys-on-cloud-stages-are-pikku-studios-to-build.md +33 -0
  229. package/knowledge/questions/virtual-user-statement-replay-is-per-process.md +20 -0
  230. package/knowledge/questions/what-an-impersonated-session-carries.md +22 -0
  231. package/package.json +2 -3
  232. package/src/public-surface.json +38 -47
  233. package/dist/services/persona-actor-secret.d.ts +0 -38
  234. package/dist/services/persona-actor-secret.js +0 -39
  235. package/dist/services/persona-sign-in.d.ts +0 -117
  236. package/dist/services/persona-sign-in.js +0 -154
  237. package/dist/wirings/actor-flow/index.d.ts +0 -2
  238. package/dist/wirings/actor-flow/run-conversation.d.ts +0 -36
  239. package/dist/wirings/persona/define-personas.d.ts +0 -32
  240. package/dist/wirings/persona/index.d.ts +0 -23
  241. package/dist/wirings/persona/index.js +0 -19
  242. package/knowledge/decisions/internals/actor-flow-conversations-seed-a-hidden-kickoff-message.md +0 -23
  243. package/knowledge/decisions/internals/actor-flow-verdicts-are-llm-self-evaluations.md +0 -25
  244. package/knowledge/decisions/internals/validate-checks-personas-through-a-shared-module.md +0 -43
  245. package/knowledge/decisions/security/actor-sign-in-only-works-for-actor-flagged-users.md +0 -31
  246. package/knowledge/decisions/security/an-actor-credential-is-derived-per-persona.md +0 -42
  247. package/knowledge/decisions/security/pikku-carries-actor-scopes-as-data-and-the-app-grants-them.md +0 -26
  248. /package/dist/wirings/{actor-flow/actor-flow.types.js → virtual-user/conversation/conversation.types.js} +0 -0
  249. /package/dist/wirings/{actor-flow → virtual-user/conversation}/index.js +0 -0
  250. /package/dist/wirings/{persona/persona.types.js → virtual-user/virtual-user-declaration.types.js} +0 -0
  251. /package/dist/wirings/{persona/persona-mailbox.js → virtual-user/virtual-user-mailbox.js} +0 -0
@@ -3,7 +3,7 @@ import type { ScenarioArtifact } from './scenario-run.types.js';
3
3
  * Scenario steps: named, typed units of scenario behaviour.
4
4
  *
5
5
  * A step's body is an ordinary pikku function, so it may drive a browser, call
6
- * an RPC as its actor, or run a workflow. `given` and `when` differ only in the
6
+ * an RPC as its virtual user, or run a workflow. `given` and `when` differ only in the
7
7
  * prose the reporter renders; `then` also changes what runs, because its
8
8
  * bindings are witnesses — every declared surface is observed and the
9
9
  * observations must agree. See {@link ScenarioSurfaceResolution}.
@@ -20,7 +20,7 @@ export type ScenarioStepPhase = 'given' | 'when' | 'then';
20
20
  /**
21
21
  * Who acts in a step.
22
22
  *
23
- * `persona` is a person driving the system through one of its surfaces — the
23
+ * `virtualUser` is a person driving the system through one of its surfaces — the
24
24
  * ordinary `pikkuScenarioStep`. `platform` is the app acting on itself ("the
25
25
  * platform has expired the trial"), and `addon` is a third-party system acting
26
26
  * on it ("Stripe's webhook arrives"), contributed by the addon that wraps that
@@ -31,9 +31,9 @@ export type ScenarioStepPhase = 'given' | 'when' | 'then';
31
31
  * trusting a string literal — and so an addon package exports only addon steps,
32
32
  * which is checkable.
33
33
  */
34
- export type ScenarioStepKind = 'persona' | 'platform' | 'addon';
34
+ export type ScenarioStepKind = 'virtualUser' | 'platform' | 'addon';
35
35
  /**
36
- * How an actor drives the system for one step.
36
+ * How a virtual user drives the system for one step.
37
37
  *
38
38
  * A step declares one implementation per surface it supports, and the runner
39
39
  * picks between them — so the same ladder can run through a real browser, over
@@ -87,10 +87,10 @@ export type ScenarioSurfaceResolution = {
87
87
  */
88
88
  export interface ScenarioStepOptions {
89
89
  /**
90
- * The actor this step runs as. Required for a step that declares `actor: true`
90
+ * The virtual user this step runs as. Required for a step that declares `virtualUser: true`
91
91
  * or a `browser` binding; the runner refuses to dispatch one without it.
92
92
  */
93
- actor?: unknown;
93
+ virtualUser?: unknown;
94
94
  /** Overrides the step's own `description` for this call site only. */
95
95
  description?: string;
96
96
  /** Defaults to 0 for steps — a failed assertion must not be retried. */
@@ -110,7 +110,7 @@ export interface ScenarioEnvironment {
110
110
  /**
111
111
  * The `scenarioStep` wire, present on every scenario step invocation.
112
112
  *
113
- * The persona a step runs as is not here: it arrives as `wire.actor`, injected
113
+ * The virtual user a step runs as is not here: it arrives as `wire.virtualUser`, injected
114
114
  * by the runner and typed as present exactly on the bindings that declared they
115
115
  * need one. Keeping it off this interface is what makes that possible — a wire
116
116
  * member can be required per binding, a property of one cannot.
@@ -178,10 +178,10 @@ export interface ScenarioScreenshotOptions {
178
178
  fullPage?: boolean;
179
179
  }
180
180
  /**
181
- * One actor's browser session, handed to a step as `wire.browser`.
181
+ * One virtual user's browser session, handed to a step as `wire.browser`.
182
182
  *
183
- * Every actor gets their own, so a two-actor scenario drives two sessions and
184
- * `screenshot` files each shot under the actor it belongs to.
183
+ * Every virtual user gets their own, so a two-virtual-user scenario drives two sessions and
184
+ * `screenshot` files each shot under the virtual user it belongs to.
185
185
  *
186
186
  * Only what every driver can honour is declared here. A driver package adds
187
187
  * the rest by declaration-merging onto this interface — `@pikku/playwright`
@@ -189,21 +189,21 @@ export interface ScenarioScreenshotOptions {
189
189
  * structural surface keeps working whichever driver runs it.
190
190
  */
191
191
  export interface PikkuBrowserWire {
192
- /** The actor whose browser context this is */
193
- readonly actor: string;
192
+ /** The virtual user whose browser context this is */
193
+ readonly virtualUser: string;
194
194
  goto(url: string): Promise<void>;
195
195
  screenshot(name?: string, options?: ScenarioScreenshotOptions): Promise<Uint8Array>;
196
196
  }
197
197
  /**
198
- * What one actor's window looked like at the moment a scenario failed.
198
+ * What one virtual user's window looked like at the moment a scenario failed.
199
199
  *
200
200
  * A browser step fails with a selector timeout that says nothing about *why*
201
201
  * the page never rendered. The answer is almost always in the page's own
202
202
  * errors, which the driver has been collecting all along.
203
203
  */
204
204
  export interface ScenarioBrowserFailure {
205
- /** The actor whose window this is. */
206
- actor: string;
205
+ /** The virtual user whose window this is. */
206
+ virtualUser: string;
207
207
  /** Where the window was pointed, when the driver can still report it. */
208
208
  url?: string;
209
209
  /** Path the screenshot was written to; absent when none could be taken. */
@@ -222,10 +222,10 @@ export interface ScenarioBrowserFailure {
222
222
  * one that simply offers no isolation and no diagnostics.
223
223
  */
224
224
  export interface ScenarioBrowserProvider {
225
- /** Resolve — creating on first use — the browser session for an actor. */
226
- sessionFor(actorName: string): Promise<PikkuBrowserWire>;
225
+ /** Resolve — creating on first use — the browser session for a virtual user. */
226
+ sessionFor(virtualUserName: string): Promise<PikkuBrowserWire>;
227
227
  /**
228
- * Discard every actor's per-scenario state — cookies, storage, open pages —
228
+ * Discard every virtual user's per-scenario state — cookies, storage, open pages —
229
229
  * while keeping the browser itself. Called between scenarios, so one
230
230
  * scenario cannot leave the next signed in as somebody else.
231
231
  */
@@ -247,23 +247,23 @@ export interface ScenarioBrowserProvider {
247
247
  */
248
248
  endScenario?(outcome: 'passed' | 'failed'): void;
249
249
  /**
250
- * When this actor's recording started, as epoch milliseconds.
250
+ * When this virtual user's recording started, as epoch milliseconds.
251
251
  *
252
252
  * The seam that keeps the video clock out of `@pikku/core`: a driver knows
253
253
  * when it opened the context it passed `recordVideo` to, and the runner turns
254
- * that into a per-step offset. Absent for an actor with no window open, and
254
+ * that into a per-step offset. Absent for a virtual user with no window open, and
255
255
  * for a run recording nothing — both of which leave the step's offset off.
256
256
  */
257
- videoStartedAt?(actorName: string): number | undefined;
257
+ videoStartedAt?(virtualUserName: string): number | undefined;
258
258
  /**
259
- * Mark a browser step starting in this actor's recording, answering where it
259
+ * Mark a browser step starting in this virtual user's recording, answering where it
260
260
  * falls in the finished video (ms).
261
261
  *
262
262
  * Preferred over `videoStartedAt` when present: a driver that edits its
263
263
  * footage afterwards — holding each step's screen still, say — is the only
264
264
  * one that knows how far that moves the step. Undefined when nothing records.
265
265
  */
266
- markVideoStep?(actorName: string): number | undefined;
266
+ markVideoStep?(virtualUserName: string): number | undefined;
267
267
  /**
268
268
  * Snapshot every open window for a failed scenario. `label` identifies the
269
269
  * scenario in artifact filenames. Never throws: a failure to capture must
@@ -193,7 +193,7 @@ export type WorkflowsMeta = Record<string, CommonWireMeta & {
193
193
  */
194
194
  asserts?: boolean;
195
195
  skip?: string;
196
- actors?: string[];
196
+ virtualUsers?: string[];
197
197
  }>;
198
198
  export interface WorkflowRuntimeMeta {
199
199
  name: string;
@@ -201,7 +201,7 @@ export interface WorkflowRuntimeMeta {
201
201
  source: 'dsl' | 'complex' | 'graph' | 'scenario';
202
202
  description?: string;
203
203
  tags?: string[];
204
- actors?: string[];
204
+ virtualUsers?: string[];
205
205
  nodes?: Record<string, any>;
206
206
  entryNodeIds?: string[];
207
207
  graphHash?: string;
@@ -14,8 +14,6 @@ here first — several of these exist because the obvious simplification was tri
14
14
  and broke something.
15
15
 
16
16
  <!-- pikku:knowledge-index -->
17
-
18
17
  - [internals](internals/index.md) — a rule about how it works under the hood, and why
19
18
  - [security](security/index.md) — a rule about who may do what
20
-
21
19
  <!-- /pikku:knowledge-index -->
@@ -0,0 +1,29 @@
1
+ ---
2
+ type: decision
3
+ title: A credential name declared twice must agree on its schema and its type
4
+ description: validateAndBuildCredentialDefinitionsMeta throws when two declarations of one credential name differ in schema or in type, and otherwise keeps the first declaration
5
+ tags: core, credentials, validation
6
+ ---
7
+
8
+ # A credential name declared twice must agree on its schema and its type
9
+
10
+ `packages/core/src/wirings/credential/validate-credential-definitions.ts` builds
11
+ the credential metadata, keyed by name.
12
+
13
+ When a name repeats, the schema is compared first, but only if both declarations
14
+ resolve to a schema reference (`validate-credential-definitions.ts:19-23`). Two
15
+ references differ when their variable name or source file differs
16
+ (`validate-credential-definitions.ts:24-27`). A difference throws, and the message
17
+ says "Credentials sharing a name must use the same schema"
18
+ (`validate-credential-definitions.ts:28-33`).
19
+
20
+ The `type` field is then compared (`validate-credential-definitions.ts:38-45`).
21
+ The type is `'singleton' | 'wire'` (`credential.types.ts:26`). A different type
22
+ throws with the message "Credentials sharing a name must use the same type"
23
+ (`validate-credential-definitions.ts:43`).
24
+
25
+ When both checks pass, the repeat is skipped and the first declaration stays
26
+ (`validate-credential-definitions.ts:47`).
27
+
28
+ **What this rules out:** two addons declaring the same credential name with
29
+ different schemas or read modes, and a silent choice between them.
@@ -0,0 +1,29 @@
1
+ ---
2
+ type: decision
3
+ title: declareScopes lets an app grant a scope whose addon it does not wire
4
+ description: defineScope is a no-op read by the CLI, and declareScopes declares scope ids so a grant is valid without wiring the addon that owns the scope tree
5
+ tags: core, scopes, addons, codegen
6
+ ---
7
+
8
+ # declareScopes lets an app grant a scope whose addon it does not wire
9
+
10
+ `packages/core/src/wirings/scope/define-scope.ts` has two declaration functions,
11
+ both with empty bodies.
12
+
13
+ `defineScope` is a no-op that "tree-shakes away". The CLI reads the call by AST and
14
+ generates a `ScopeId` union, so a function that names an undeclared scope fails to
15
+ build (`define-scope.ts:3-6`, `33`).
16
+
17
+ `declareScopes` takes a list of ids (`define-scope.ts:53`). Its doc comment says it
18
+ exists so that an app can grant a scope, in a system role or a function's
19
+ `scopes`, whose tree belongs to an addon the app has not wired
20
+ (`define-scope.ts:36-42`). Each id declares that node and its ancestors
21
+ (`define-scope.ts:44`). When the addon is wired, its own description wins, and the
22
+ two calls "never conflict, unlike a second `defineScope` for the same root"
23
+ (`define-scope.ts:44-46`).
24
+
25
+ The example is `declareScopes(['pikku:console'])` (`define-scope.ts:49-51`).
26
+
27
+ **What this rules out:** declaring the same scope root with `defineScope` in two
28
+ places, which the doc says conflicts, and making a grant valid only by wiring the
29
+ addon that owns it.
@@ -0,0 +1,32 @@
1
+ ---
2
+ type: decision
3
+ title: A latency budget is asserted by its cause, not by the clock
4
+ description: The per-request key derivation and remote RPC round-trip tests count WebCrypto calls and check the algorithm instead of timing them, because a millisecond budget fails under CI load while the regression it guards is a choice of KDF
5
+ tags: core, testing, crypto
6
+ ---
7
+
8
+ # A latency budget is asserted by its cause, not by the clock
9
+
10
+ Two tests guarded the per-request crypto path with wall-clock budgets:
11
+ `encryptWithKeyMaterial` + `decryptWithKeyMaterial` under 5ms a pair, and a
12
+ remote RPC round trip (`buildRemoteHeaders` → `pikkuRemoteAuthMiddleware`)
13
+ under 20ms. Both went red on loaded CI runners (8.6ms, 151ms) with nothing
14
+ wrong, blocking unrelated PRs (#2112), after #1175 had already loosened them.
15
+
16
+ What the budgets protected is a single decision: the per-request path expands
17
+ key material with HKDF, never with the PBKDF2 passphrase KDF `encryptJSON`
18
+ uses (600k iterations, ~170ms). The remote session used to go through
19
+ `encryptJSON`, which made every hop pay two password derivations.
20
+
21
+ **So the tests spy on `crypto.subtle` with `t.mock.method` and assert the
22
+ calls**: exactly one `importKey` and one `deriveKey` per encrypt and per
23
+ decrypt, both named `HKDF`, and no `deriveBits`. Switching the KDF to PBKDF2,
24
+ adding a second derivation, or routing the remote session back through
25
+ `encryptJSON`/`decryptJSON` each fails them — checked by hand when they were
26
+ written. None of that depends on how busy the machine is.
27
+
28
+ **What this rules out:** a looser constant kept as a fallback, or a ratio
29
+ against a measurement when the regression is a structural choice. A ratio
30
+ ([[a-wall-clock-threshold-is-a-load-test-in-disguise]]) is the right tool when
31
+ the property really is "fewer of an expensive thing" and nothing countable is
32
+ exposed; when the cause is observable, assert the cause.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: decision
3
+ title: A scheduled task writes its caller's session to the session store before it runs
4
+ description: runScheduledTask stores the session it is passed, or nothing, before it looks up the task, so the run uses only a session its caller supplied
5
+ tags: core, scheduler, sessions
6
+ ---
7
+
8
+ # A scheduled task writes its caller's session to the session store before it runs
9
+
10
+ `runScheduledTask` in `packages/core/src/wirings/scheduler/scheduler-runner.ts:70-87`
11
+ takes an optional `session`. It builds a `PikkuSessionService` on the session store
12
+ (`:84`), and when a session was passed it calls `userSession.set(session)` (`:85-87`).
13
+ Nothing else in the function sets a session. The task and its meta are looked up
14
+ afterwards (`:89-96`), so a missing task still leaves the session written.
15
+
16
+ The in-repo caller of this path, `packages/schedule/src/in-memory-scheduler-service.ts:163`,
17
+ passes only `{ name }`, so a cron run has no session.
18
+
19
+ **What this rules out:** a scheduled run taking a session from anywhere other than
20
+ its caller, and any default system session being set by this function.
21
+ `questions/does-a-delayed-rpc-run-as-the-session-it-was-scheduled-with.md` records a
22
+ related gap in the delayed-RPC path.
@@ -0,0 +1,28 @@
1
+ ---
2
+ type: decision
3
+ title: A secret set in memory shadows the variable of the same name
4
+ description: LocalSecretService reads its in-memory map before the variables service, stores a SecretValue unwrapped, and treats an empty string as absent
5
+ tags: core, secrets, local-dev
6
+ ---
7
+
8
+ # A secret set in memory shadows the variable of the same name
9
+
10
+ `packages/core/src/services/local-secrets.ts` keeps an in-memory map of secrets.
11
+ `getSecret` checks that map first (`local-secrets.ts:25-29`). Only when the key is
12
+ missing or empty does it fall back to the variables service
13
+ (`local-secrets.ts:31-34`), and it throws if neither has the key
14
+ (`local-secrets.ts:35`). `hasSecret` follows the same order (`local-secrets.ts:49-52`).
15
+
16
+ Values are parsed as JSON when possible and kept as the raw string otherwise
17
+ (`local-secrets.ts:13-19`).
18
+
19
+ `setSecret` unwraps a `SecretValue` before storing it (`local-secrets.ts:38-45`).
20
+ The comment gives the reason: storing the wrapper "would serialize it to
21
+ '[secret]'" (`local-secrets.ts:39`).
22
+
23
+ `deleteSecret` removes only the in-memory entry (`local-secrets.ts:56-58`), so a
24
+ variable of the same name becomes visible again after a delete.
25
+
26
+ **What this rules out:** a secret written through `setSecret` being replaced by
27
+ the value of a variable with the same name, and storing a redacted wrapper in
28
+ place of the secret.
@@ -0,0 +1,32 @@
1
+ ---
2
+ type: decision
3
+ title: A virtual user acts through invokeRaw, so a refusal is data it can act on
4
+ description: virtualUserTarget calls virtualUser.invokeRaw, which signs the virtual user in when needed, retries once after a 401, and returns the status instead of throwing
5
+ tags: core, virtual-users
6
+ ---
7
+
8
+ # A virtual user acts through invokeRaw, so a refusal is data it can act on
9
+
10
+ `virtualUserTarget` in `packages/core/src/wirings/virtual-user/virtual-user-target.ts`
11
+ is "the only place the engine meets the network" (`virtual-user-target.ts:14`). It
12
+ drives a signed-in virtual user. The doc comment says the virtual user keeps its
13
+ cookies, re-logs in when a session expires mid-run, and reports status as data
14
+ rather than throwing (`virtual-user-target.ts:14-17`).
15
+
16
+ The call goes through `virtualUser.invokeRaw(rpcName, args)` (`virtual-user-target.ts:27`).
17
+ The comment gives the reason: "to a virtual user a 403 is not an error, it is the
18
+ system saying no, which is information it should be allowed to act on"
19
+ (`virtual-user-target.ts:19-22`).
20
+
21
+ In `HttpVirtualUser.invokeRaw`, the virtual user signs in first if it is not signed
22
+ in (`packages/core/src/services/http-virtual-users.ts:117-119`). On a 401 it signs
23
+ in again and retries once (`http-virtual-users.ts:121-126`). The method returns the
24
+ response for every status.
25
+
26
+ The virtual user's target passes no per-call options, so it cannot add a header
27
+ (`virtual-user-target.ts:27`). That is covered in
28
+ [a scenario step can name an identity no virtual user holds, through per-call headers](../security/a-step-can-name-an-identity-no-virtual-user-holds-through-per-call-headers.md).
29
+
30
+ **What this rules out:** a virtual user calling `invoke`, which throws on error, and
31
+ a virtual user being given per-call headers. For why `invokeRaw` returns status rather
32
+ than throwing, see [in a scenario a 4xx is data, not an exception](in-a-scenario-a-4xx-is-data-not-an-exception.md).
@@ -1,29 +1,29 @@
1
1
  ---
2
2
  type: decision
3
3
  title: A virtual user cadence is a row, not a timer
4
- description: How often a persona runs is stored as a due time per persona and acted on by a tick the project schedules — pikku never starts a timer, and a run never reschedules itself
4
+ description: How often a virtual user runs is stored as a due time per virtual user and acted on by a tick the project schedules — pikku never starts a timer, and a run never reschedules itself
5
5
  tags: virtual-user, storage, scheduling
6
6
  ---
7
7
 
8
8
  # A virtual user cadence is a row, not a timer
9
9
 
10
- A run has a budget; a persona has a cadence. The two get confused because both
10
+ A run has a budget; a virtual user has a cadence. The two get confused because both
11
11
  answer "how often", and neither answers the other's question: a budget caps one
12
12
  outing, and raising it only buys a more tired user. What tells you about a
13
13
  product is the same person coming back over a fortnight.
14
14
 
15
- That cadence is one row per persona in `virtualUserSchedule`, holding
15
+ That cadence is one row per virtual user in `virtualUserSchedule`, holding
16
16
  `nextRunAt`. `tickVirtualUserSchedules` acts on whichever rows are due. There is
17
17
  no timer, no interval, and no in-memory loop.
18
18
 
19
19
  **Not a timer**, because a process holding the next run in its own heap forgets
20
- it on the next deploy, and the persona silently stops — with nothing anywhere
20
+ it on the next deploy, and the virtual user silently stops — with nothing anywhere
21
21
  saying it used to run. The row survives restarts, and any instance can act on
22
22
  it.
23
23
 
24
24
  **Not reschedule-on-completion**, which is the tempting shape: finish a run,
25
25
  draw a delay, schedule the next. It has exactly one failure mode and it is
26
- fatal — a crash between the two ends the persona forever, and the evidence is an
26
+ fatal — a crash between the two ends the virtual user forever, and the evidence is an
27
27
  absence. A due time written down before the run starts cannot be lost by the run
28
28
  failing.
29
29
 
@@ -36,8 +36,8 @@ may not even run schedulers. One line in the project turns it on:
36
36
  wireScheduler({ name: 'virtualUsers', schedule: '0 * * * *', func: tickVirtualUserSchedules })
37
37
  ```
38
38
 
39
- Tick resolution bounds how *late* a due persona is, never how often it runs — a
40
- persona due at 09:07 under an hourly tick starts at 10:00. Running the tick more
39
+ Tick resolution bounds how *late* a due virtual user is, never how often it runs — a
40
+ virtual user due at 09:07 under an hourly tick starts at 10:00. Running the tick more
41
41
  often costs one indexed query and changes no cadence.
42
42
 
43
43
  Three rules make a tick safe to run at any resolution, from any number of
@@ -46,14 +46,14 @@ instances:
46
46
  - **The due time is written before the run is dispatched**, so a tick that dies
47
47
  halfway cannot leave the row due for the next one to pick up again. A dispatch
48
48
  that throws therefore waits a full interval, which is the right way round: a
49
- persona failing to start should not be retried every minute for a week.
50
- - **A persona whose previous run is still `running` is skipped, not queued.**
49
+ virtual user failing to start should not be retried every minute for a week.
50
+ - **A virtual user whose previous run is still `running` is skipped, not queued.**
51
51
  Two copies of the same user acting at once is not a heavier test, it is a
52
52
  different one, and every finding it produces is unreproducible.
53
- - **A run still `running` after `STALE_RUN_AFTER_MS` is failed and the persona
53
+ - **A run still `running` after `STALE_RUN_AFTER_MS` is failed and the virtual user
54
54
  runs again.** This is where the stranded-record cost of
55
55
  [a virtual user run not being a workflow](a-virtual-user-run-is-not-a-workflow-but-it-needs-a-trigger.md)
56
- gets paid: without it, one restart mid-run would block that persona's schedule
56
+ gets paid: without it, one restart mid-run would block that virtual user's schedule
57
57
  permanently.
58
58
 
59
59
  The interval is a range (`minIntervalMs`, `maxIntervalMs`), drawn per run. A
@@ -63,4 +63,4 @@ cron neighbourhood; a real one does not keep an appointment.
63
63
  **What this rules out:** a `setTimeout` or interval anywhere in the run path;
64
64
  the engine scheduling its own next run; a scaffolded scheduled task; a queue
65
65
  holding the next run; and a cadence that lives only in a config file, which
66
- cannot record when the persona last actually went.
66
+ cannot record when the virtual user last actually went.
@@ -0,0 +1,24 @@
1
+ ---
2
+ type: decision
3
+ title: A virtual user's converse signs in before its first turn
4
+ description: HttpVirtualUser.converse calls login() before a run when it holds no session, because a public agent route never triggers the 401 retry that would sign the virtual user in
5
+ tags: services, virtual-users, agents
6
+ ---
7
+
8
+ # A virtual user's converse signs in before its first turn
9
+
10
+ `HttpVirtualUser.converse` (`packages/core/src/services/http-virtual-users.ts`)
11
+ calls `this.login()` when `signedIn` is false, before it starts the run. Only
12
+ then does it send the agent request.
13
+
14
+ The code comment gives the reason. A public agent route never returns 401, so
15
+ the 401 retry in `sendAgent` never runs and would never sign the virtual user
16
+ in. An agent thread is also minted under a fresh anonymous id per request when
17
+ no one owns it, so turn one could succeed and turn two be refused as somebody
18
+ else's. A virtual user is a real account with real credentials, so conversing as
19
+ nobody is not the intent.
20
+
21
+ **What this rules out:** relying on the 401 retry for `converse`, and running a
22
+ virtual user's conversation unauthenticated when it was meant to sign in.
23
+ Compare [the 401-only retry](scenario-agent-calls-sign-in-on-401-only.md), which is
24
+ how the other agent calls behave.
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: decision
3
+ title: A virtual user's email is computed from its id, with the run id as a sub-address
4
+ description: virtualUserEmail derives the address from the virtual user id and a real domain, appends the run id so concurrent runs get separate inboxes and accounts, and refuses colliding addresses
5
+ tags: core, virtual-users, email, runs
6
+ ---
7
+
8
+ # A virtual user's email is computed from its id, with the run id as a sub-address
9
+
10
+ `packages/core/src/wirings/virtual-user/virtual-user-email.ts` computes the address a virtual user
11
+ reads mail at. Nothing is declared by hand.
12
+
13
+ The label is the virtual user id, lower-cased, with runs of non-alphanumerics turned
14
+ into `-` and leading or trailing dashes removed (`virtual-user-email.ts:19-30`). A
15
+ virtual user id with no letters or digits left throws (`virtual-user-email.ts:24-28`).
16
+
17
+ The domain must contain a dot and no `@` (`virtual-user-email.ts:49-54`). The error
18
+ asks for "a domain the stage actually receives mail on" (`virtual-user-email.ts:52`).
19
+
20
+ With a `runId`, the address is `label+runIdLabel@domain` (`virtual-user-email.ts:55-56`).
21
+ The doc comment says that without the suffix, two concurrent runs share "an inbox
22
+ _and_ a user row, and run B reads run A's magic link" (`virtual-user-email.ts:35-37`).
23
+ Without a `runId`, the bare form is returned (`virtual-user-email.ts:40-41`).
24
+
25
+ `virtualUserEmails` throws when two virtual users compute the same address
26
+ (`virtual-user-email.ts:76-82`). The comment says they "would share a user row and read
27
+ each other's email" (`virtual-user-email.ts:79-80`).
28
+
29
+ All of one virtual user's logins share one address (`virtual-user-email.ts:62-66`). The
30
+ comment gives the reason: better-auth keeps `email` on the `user` and defaults
31
+ `allowDifferentEmails` to false.
32
+
33
+ **What this rules out:** two concurrent runs sharing an inbox, and two virtual users
34
+ mapping to one account.
@@ -30,7 +30,7 @@ unit: there is no in-process promise to leave running, and `executeVirtualUserRu
30
30
  — sessionless, unexposed, wired to nothing — is not a function any unit can be
31
31
  reached at. The RPC resolves to nothing, the rejection is swallowed by the
32
32
  `catch` that exists to stop it taking the process down, and the run parks at
33
- `running` with zero steps and no error anywhere. That is what a fabric stage did.
33
+ `running` with zero steps and no error anywhere. That is what a cloud stage did.
34
34
 
35
35
  So the queue is not bought for durability. It is bought because **a trigger is
36
36
  what makes a function deployable**: `wireQueueWorker` puts `executeVirtualUserRun`
@@ -56,7 +56,7 @@ wants the same exploration.
56
56
 
57
57
  Where that rule is actually applied is
58
58
  [the schedule tick](a-virtual-user-cadence-is-a-row-not-a-timer.md), which has
59
- to: a record stuck at `running` would otherwise block its persona's cadence
59
+ to: a record stuck at `running` would otherwise block its virtual user's cadence
60
60
  forever.
61
61
 
62
62
  **What this rules out:** dispatching the run through `startWorkflow`; awaiting
@@ -44,3 +44,6 @@ and quiet rather than to delete it.
44
44
 
45
45
  Related: [[an-unref-d-timer-cannot-be-awaited-under-node-test]], the other
46
46
  source of nondeterminism found in the same pass.
47
+
48
+ Where the cause is observable, prefer asserting it outright:
49
+ [[a-latency-budget-is-asserted-by-its-cause]].
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: decision
3
+ title: A reachable agent is offered by its registry key, filtered by the virtual user's scopes
4
+ description: reachableAgents offers an agent to a virtual user under the key it is registered with, and only when the virtual user holds the agent's declared scopes; the server still enforces access
5
+ tags: core, virtual-users, agents, scopes
6
+ ---
7
+
8
+ # A reachable agent is offered by its registry key, filtered by the virtual user's scopes
9
+
10
+ `packages/core/src/wirings/virtual-user/virtual-user-agents.ts` decides which agents
11
+ a virtual user is told about.
12
+
13
+ An agent with no declared scopes is offered to everybody (`virtual-user-agents.ts:54-55`).
14
+ The doc comment says undeclared is not the same as denied (`virtual-user-agents.ts:45-46`).
15
+ Otherwise the agent is offered only when the virtual user's scopes satisfy the agent's
16
+ scopes (`virtual-user-agents.ts:57`, `hasScopes`).
17
+
18
+ The name offered is the registry key, not the agent's display name. The code uses
19
+ `name: id` (`virtual-user-agents.ts:59-60`), and the doc comment says the display
20
+ name "is frequently something else entirely", and offering it makes the run die
21
+ on a 500 (`virtual-user-agents.ts:30-37`).
22
+
23
+ The doc comment says this filter narrows only what is offered. The server decides
24
+ who may talk to which agent (`virtual-user-agents.ts:39-44`).
25
+
26
+ **What this rules out:** offering an agent name the server cannot resolve, and
27
+ treating the offer list as the access check.
@@ -0,0 +1,26 @@
1
+ ---
2
+ type: decision
3
+ title: An OAuth2 credential implies its app client secret, optional and overridable by declaration
4
+ description: deriveOAuth2AppSecrets creates the app client id and secret for each OAuth2 credential, marked optional, unless the project already declares that secret id
5
+ tags: core, secrets, credentials, oauth2
6
+ ---
7
+
8
+ # An OAuth2 credential implies its app client secret, optional and overridable by declaration
9
+
10
+ `packages/core/src/wirings/secret/derive-oauth2-app-secrets.ts` adds the app's
11
+ client id and secret for each OAuth2 credential a project declares. The doc comment
12
+ says the pair has the same shape every time, so writing it by hand "bought nothing"
13
+ and was often skipped (`derive-oauth2-app-secrets.ts:7-12`).
14
+
15
+ - A secret id that is already declared is skipped, so an explicit declaration
16
+ wins and keeps its own description or `docsUrl` (`derive-oauth2-app-secrets.ts:25`,
17
+ `31`, and the comment at `derive-oauth2-app-secrets.ts:14-15`).
18
+ - The derived secret has `optional: true` (`derive-oauth2-app-secrets.ts:38-39`).
19
+ The comment says an addon that also authenticates by API key "must still deploy
20
+ without an OAuth app configured" (`derive-oauth2-app-secrets.ts:17-19`).
21
+ - The OAuth2 settings (token secret, URLs, scopes, PKCE, extra params) are copied
22
+ from the credential (`derive-oauth2-app-secrets.ts:41-49`).
23
+
24
+ **What this rules out:** requiring a deployment to configure an OAuth app for a
25
+ credential that is not used, and a derived secret overriding one the author
26
+ declared.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: decision
3
+ title: Analytics identity resolvers run in declared order so consent is read before a cookie is minted
4
+ description: composeAnalyticsIdentity threads each resolver's output into the next, so a cookie-minting resolver sees the consent that an earlier resolver read
5
+ tags: core, analytics, consent
6
+ ---
7
+
8
+ # Analytics identity resolvers run in declared order so consent is read before a cookie is minted
9
+
10
+ `composeAnalyticsIdentity` in `packages/core/src/analytics/compose-analytics-identity.ts:22-35`
11
+ runs the resolvers in the order given. Each one receives the accumulated result of
12
+ the ones before it (`:26`, `:29`), and its output is merged over that result (`:31-33`).
13
+
14
+ The doc comment at `:8-15` states the ordering rule: a resolver that mints a cookie
15
+ may do so only once consent has been read, so the consent reader has to come first.
16
+ A later resolver wins a key it sets (`:17-18`).
17
+
18
+ The cookie-side rule is in `a-first-party-cookie-is-written-only-after-its-purposes-consent.md`.
19
+ That note does not record the order in which the resolvers are composed.
20
+
21
+ **What this rules out:** running a cookie-minting resolver ahead of the consent
22
+ reader, and merging independent resolver results so that no resolver sees another's output.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: decision
3
+ title: Feature flag declarations refuse an empty anyOf and conflicting scopes
4
+ description: validateAndBuildFeatureFlagDefinitionsMeta throws on an anyOf that is an empty list, and on a repeated flag name whose scopes differ between declarations
5
+ tags: core, flags, validation
6
+ ---
7
+
8
+ # Feature flag declarations refuse an empty anyOf and conflicting scopes
9
+
10
+ `validateAndBuildFeatureFlagDefinitionsMeta` in
11
+ `packages/core/src/wirings/flag/validate-flag-definitions.ts:70-98` checks each
12
+ declaration in two ways.
13
+
14
+ An `anyOf` that is an empty array throws (`:78-84`). The message says an empty
15
+ list satisfies nobody, and that a pure switch should omit `anyOf`.
16
+
17
+ When a flag name is declared twice, the two `anyOf` lists are compared as sorted
18
+ JSON (`:86-90`). A difference throws (`:91-97`), and the message says flags sharing
19
+ a name must name the same scopes.
20
+
21
+ **What this rules out:** a flag that no session can satisfy, and two declarations
22
+ of one flag that disagree about who may see it.
@@ -0,0 +1,28 @@
1
+ ---
2
+ type: decision
3
+ title: Feature flags fail open on availability and never enforce access
4
+ description: A flag with no config row is available by default, anyOf decides only the advisory capable half, and the runner's assertFeatureAvailable checks availability alone
5
+ tags: core, flags, authorization
6
+ ---
7
+
8
+ # Feature flags fail open on availability and never enforce access
9
+
10
+ `resolveFlag` in `packages/core/src/wirings/flag/resolve-flag.ts:54-95`
11
+ separates two halves. `capable` is true when `anyOf` is absent, and otherwise
12
+ comes from `hasAnyScope` on the session's scopes (`:68-69`). If the flag has no
13
+ config row, the result is `available: true` (`:72-74`). The doc comment at `:54-59`
14
+ says this fail-open is safe only because `capable` comes from the session and
15
+ `scopes:` is not changed by a flag.
16
+
17
+ `FlagState` in `packages/core/src/wirings/flag/flag.types.ts:97-104` says the same
18
+ thing: `available` is the only half the runner enforces, and `capable` is advisory
19
+ and "protects nothing". `flag.types.ts:14-18` says `anyOf` is an OR over scopes,
20
+ deliberately the opposite of `scopes`, which is an AND.
21
+
22
+ `assertFeatureAvailable` in `packages/core/src/wirings/flag/assert-feature-available.ts`
23
+ calls `resolveFlag` and throws only when `available` is false (`:28-30`). It does
24
+ nothing when no feature-flag source is registered (`:24-26`). The doc comment at
25
+ `:10-12` says enforcing `capable` here would make a second authorization path.
26
+
27
+ **What this rules out:** using a flag as an access check, and a store outage
28
+ turning every flagged feature off.