@pikku/core 0.12.139 → 0.12.142

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 (253) hide show
  1. package/CHANGELOG.md +47 -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/lifecycle.d.ts +6 -0
  7. package/dist/lifecycle.js +58 -0
  8. package/dist/services/audit-service.d.ts +4 -4
  9. package/dist/services/feature-flag-service.d.ts +3 -3
  10. package/dist/services/{http-personas.d.ts → http-virtual-users.d.ts} +25 -47
  11. package/dist/services/{http-personas.js → http-virtual-users.js} +29 -53
  12. package/dist/services/index.d.ts +2 -2
  13. package/dist/services/index.js +1 -1
  14. package/dist/services/meta-service.d.ts +6 -6
  15. package/dist/services/meta-service.js +8 -8
  16. package/dist/{wirings/workflow → services}/scenario-cookie-jar.d.ts +2 -0
  17. package/dist/{wirings/workflow → services}/scenario-cookie-jar.js +18 -14
  18. package/dist/services/scoped-secret-service.d.ts +1 -0
  19. package/dist/services/scoped-secret-service.js +3 -0
  20. package/dist/services/secret-service.d.ts +6 -0
  21. package/dist/services/system-role-guard.d.ts +1 -1
  22. package/dist/services/typed-secret-service.d.ts +1 -0
  23. package/dist/services/typed-secret-service.js +7 -0
  24. package/dist/services/virtual-user-sign-in.d.ts +52 -0
  25. package/dist/services/virtual-user-sign-in.js +80 -0
  26. package/dist/services/virtual-user-statement.d.ts +31 -0
  27. package/dist/services/virtual-user-statement.js +24 -0
  28. package/dist/services/{personas-service.d.ts → virtual-users-service.d.ts} +25 -23
  29. package/dist/services/{personas-service.js → virtual-users-service.js} +3 -3
  30. package/dist/testing/service-tests/secret-service-tests.js +9 -0
  31. package/dist/types/core.types.d.ts +11 -11
  32. package/dist/utils.d.ts +2 -5
  33. package/dist/utils.js +1 -38
  34. package/dist/wirings/cli/cli-runner.js +24 -9
  35. package/dist/wirings/http/http-runner.js +4 -0
  36. package/dist/wirings/http/http.types.d.ts +1 -0
  37. package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
  38. package/dist/wirings/http/pikku-fetch-http-response.js +12 -0
  39. package/dist/wirings/http/web-request.js +7 -0
  40. package/dist/wirings/role/define-system-role.d.ts +1 -1
  41. package/dist/wirings/role/define-system-role.js +1 -1
  42. package/dist/wirings/{actor-flow/actor-flow.types.d.ts → virtual-user/conversation/conversation.types.d.ts} +5 -5
  43. package/dist/wirings/virtual-user/conversation/index.d.ts +2 -0
  44. package/dist/wirings/virtual-user/conversation/run-conversation.d.ts +36 -0
  45. package/dist/wirings/{actor-flow → virtual-user/conversation}/run-conversation.js +30 -30
  46. package/dist/wirings/virtual-user/define-virtual-users.d.ts +32 -0
  47. package/dist/wirings/{persona/define-personas.js → virtual-user/define-virtual-users.js} +9 -9
  48. package/dist/wirings/virtual-user/index.d.ts +30 -7
  49. package/dist/wirings/virtual-user/index.js +23 -4
  50. package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +8 -8
  51. package/dist/wirings/virtual-user/prepare-virtual-user-run.js +9 -9
  52. package/dist/wirings/virtual-user/run-virtual-user.d.ts +6 -6
  53. package/dist/wirings/virtual-user/run-virtual-user.js +10 -10
  54. package/dist/wirings/{persona/validate-personas.d.ts → virtual-user/validate-virtual-users.d.ts} +11 -11
  55. package/dist/wirings/{persona/validate-personas.js → virtual-user/validate-virtual-users.js} +24 -24
  56. package/dist/wirings/virtual-user/virtual-user-agents.d.ts +7 -7
  57. package/dist/wirings/virtual-user/virtual-user-agents.js +2 -2
  58. package/dist/wirings/{persona/persona-app-scopes.d.ts → virtual-user/virtual-user-app-scopes.d.ts} +10 -10
  59. package/dist/wirings/{persona/persona-app-scopes.js → virtual-user/virtual-user-app-scopes.js} +12 -12
  60. package/dist/wirings/virtual-user/virtual-user-catalogue.d.ts +3 -3
  61. package/dist/wirings/virtual-user/virtual-user-catalogue.js +2 -2
  62. package/dist/wirings/{persona/persona.types.d.ts → virtual-user/virtual-user-declaration.types.d.ts} +24 -24
  63. package/dist/wirings/virtual-user/virtual-user-derive.js +13 -7
  64. package/dist/wirings/virtual-user/virtual-user-dispositions.d.ts +1 -1
  65. package/dist/wirings/{persona/persona-email.d.ts → virtual-user/virtual-user-email.d.ts} +11 -11
  66. package/dist/wirings/{persona/persona-email.js → virtual-user/virtual-user-email.js} +19 -19
  67. package/dist/wirings/{persona/persona-environments.d.ts → virtual-user/virtual-user-environments.d.ts} +10 -10
  68. package/dist/wirings/{persona/persona-environments.js → virtual-user/virtual-user-environments.js} +21 -21
  69. package/dist/wirings/virtual-user/virtual-user-intents.d.ts +3 -3
  70. package/dist/wirings/virtual-user/virtual-user-intents.js +3 -3
  71. package/dist/wirings/{persona/persona-mailbox.d.ts → virtual-user/virtual-user-mailbox.d.ts} +4 -4
  72. package/dist/wirings/virtual-user/virtual-user-run-store.d.ts +4 -4
  73. package/dist/wirings/virtual-user/virtual-user-scaffold.d.ts +42 -61
  74. package/dist/wirings/virtual-user/virtual-user-scaffold.js +69 -117
  75. package/dist/wirings/virtual-user/virtual-user-schedule-store.d.ts +15 -15
  76. package/dist/wirings/virtual-user/virtual-user-schedule.d.ts +6 -6
  77. package/dist/wirings/virtual-user/virtual-user-schedule.js +11 -11
  78. package/dist/wirings/virtual-user/virtual-user-target.d.ts +6 -6
  79. package/dist/wirings/virtual-user/virtual-user-target.js +5 -5
  80. package/dist/wirings/virtual-user/virtual-user.types.d.ts +11 -11
  81. package/dist/wirings/virtual-user/virtual-user.types.js +1 -1
  82. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +9 -9
  83. package/dist/wirings/workflow/pikku-scenario-service.d.ts +28 -28
  84. package/dist/wirings/workflow/pikku-scenario-service.js +77 -77
  85. package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -2
  86. package/dist/wirings/workflow/pikku-workflow-service.js +4 -4
  87. package/dist/wirings/workflow/scenario-prose.d.ts +8 -8
  88. package/dist/wirings/workflow/scenario-prose.js +12 -8
  89. package/dist/wirings/workflow/scenario-run.types.d.ts +13 -13
  90. package/dist/wirings/workflow/scenario-step.types.d.ts +23 -23
  91. package/dist/wirings/workflow/workflow.types.d.ts +2 -2
  92. package/knowledge/decisions/index.md +0 -2
  93. package/knowledge/decisions/internals/a-credential-name-declared-twice-must-agree-on-schema-and-type.md +29 -0
  94. package/knowledge/decisions/internals/a-grant-can-name-a-scope-declared-without-its-addon.md +29 -0
  95. package/knowledge/decisions/internals/a-scheduled-task-writes-its-caller-session-before-it-runs.md +22 -0
  96. package/knowledge/decisions/internals/a-secret-set-in-memory-shadows-the-variable-of-the-same-name.md +28 -0
  97. package/knowledge/decisions/internals/a-virtual-user-acts-through-invokeraw-so-a-refusal-is-data.md +32 -0
  98. package/knowledge/decisions/internals/a-virtual-user-cadence-is-a-row-not-a-timer.md +12 -12
  99. package/knowledge/decisions/internals/a-virtual-user-converse-signs-in-before-its-first-turn.md +24 -0
  100. package/knowledge/decisions/internals/a-virtual-user-email-is-computed-with-the-run-id-as-a-sub-address.md +34 -0
  101. package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-but-it-needs-a-trigger.md +2 -2
  102. package/knowledge/decisions/internals/an-agent-is-offered-under-its-registry-key-and-only-within-the-virtual-users-scopes.md +27 -0
  103. package/knowledge/decisions/internals/an-oauth2-credential-implies-an-optional-app-secret.md +26 -0
  104. package/knowledge/decisions/internals/analytics-identity-resolvers-run-in-declared-order.md +22 -0
  105. package/knowledge/decisions/internals/feature-flag-declarations-refuse-an-empty-anyof-and-conflicting-scopes.md +22 -0
  106. package/knowledge/decisions/internals/feature-flags-fail-open-and-never-enforce-access.md +28 -0
  107. package/knowledge/decisions/internals/flag-sync-rewrites-only-the-declared-text.md +24 -0
  108. package/knowledge/decisions/internals/in-a-scenario-a-4xx-is-data-not-an-exception.md +3 -3
  109. package/knowledge/decisions/internals/index.md +46 -31
  110. package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +10 -10
  111. package/knowledge/decisions/internals/role-names-cannot-contain-the-scope-separator.md +30 -0
  112. package/knowledge/decisions/internals/scenario-agent-calls-sign-in-on-401-only.md +23 -19
  113. package/knowledge/decisions/internals/scenario-steps-return-drained-response-records.md +2 -2
  114. package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +12 -12
  115. package/knowledge/decisions/internals/the-core-scope-validator-refuses-separator-wildcard-and-empty-segments.md +22 -0
  116. package/knowledge/decisions/internals/the-pikku-user-id-resolves-in-a-fixed-order.md +24 -0
  117. 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
  118. 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
  119. package/knowledge/decisions/internals/validate-checks-virtual-users-through-a-shared-module.md +43 -0
  120. 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
  121. package/knowledge/decisions/internals/virtual-user-conversation-verdicts-are-llm-self-evaluations.md +25 -0
  122. package/knowledge/decisions/internals/virtual-user-conversations-seed-a-hidden-kickoff-message.md +23 -0
  123. package/knowledge/decisions/internals/virtual-users-are-declared-in-one-definevirtualusers-call-per-codebase.md +30 -0
  124. package/knowledge/decisions/internals/workflow-core-never-imports-a-browser-driver.md +5 -5
  125. package/knowledge/decisions/internals/workflow-run-capabilities-are-extensions-not-subclasses.md +2 -2
  126. package/knowledge/decisions/internals/workflow-scenario-assertions-never-retry-and-record-one-step.md +4 -4
  127. package/knowledge/decisions/internals/workflow-scenario-hooks-are-a-scenario-only-affordance.md +1 -1
  128. package/knowledge/decisions/internals/workflow-scenario-prose-is-rendered-from-typed-calls-not-parsed-from-english.md +2 -2
  129. package/knowledge/decisions/internals/workflow-scenario-step-targets-are-string-literals-for-the-inspector.md +1 -1
  130. package/knowledge/decisions/internals/workflow-suspended-runs-keep-their-in-process-context.md +1 -1
  131. package/knowledge/decisions/security/a-bearer-token-checked-against-a-secret-reads-it-per-request.md +26 -0
  132. package/knowledge/decisions/security/a-call-without-a-session-raises-missing-session-not-forbidden.md +20 -0
  133. package/knowledge/decisions/security/a-caller-cookie-wins-over-the-jar-for-the-same-name.md +23 -0
  134. package/knowledge/decisions/security/a-channel-lifecycle-acts-as-the-socket-upgrade-session.md +16 -0
  135. package/knowledge/decisions/security/a-channel-requires-a-session-unless-it-says-auth-false.md +26 -0
  136. package/knowledge/decisions/security/a-credential-read-path-follows-its-declared-type-not-absence.md +32 -0
  137. package/knowledge/decisions/security/a-credential-type-is-a-literal-the-inspector-checks-at-declaration.md +27 -0
  138. package/knowledge/decisions/security/a-first-party-cookie-is-written-only-after-its-purposes-consent.md +33 -0
  139. package/knowledge/decisions/security/a-readonly-session-reaches-only-readonly-functions.md +25 -0
  140. package/knowledge/decisions/security/a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md +10 -5
  141. package/knowledge/decisions/security/a-scheduled-virtual-user-run-needs-no-admin-at-run-time.md +27 -0
  142. package/knowledge/decisions/security/a-scoped-credential-view-is-limited-to-declared-names.md +26 -0
  143. package/knowledge/decisions/security/a-scoped-secret-view-reads-only-declared-keys-and-never-writes.md +25 -0
  144. package/knowledge/decisions/security/a-secret-is-sent-only-to-the-hosts-it-declares.md +29 -0
  145. package/knowledge/decisions/security/a-step-can-name-an-identity-no-virtual-user-holds-through-per-call-headers.md +25 -0
  146. package/knowledge/decisions/security/a-stopped-run-by-id-skips-the-ownership-check.md +24 -0
  147. package/knowledge/decisions/security/a-vault-secret-redacts-on-serialization-and-throws-on-coercion.md +29 -0
  148. package/knowledge/decisions/security/a-virtual-user-catalogue-entry-is-filtered-by-scope-and-approval-is-denied-by-default.md +30 -0
  149. package/knowledge/decisions/security/a-virtual-user-environment-fails-closed.md +38 -0
  150. package/knowledge/decisions/security/a-virtual-user-never-runs-against-production.md +23 -0
  151. package/knowledge/decisions/security/a-virtual-user-reads-its-roles-from-the-apps-own-rpc-before-better-auth.md +27 -0
  152. package/knowledge/decisions/security/a-virtual-user-run-treats-an-unresolved-environment-as-production.md +41 -0
  153. package/knowledge/decisions/security/a-virtual-user-scopes-are-expanded-from-its-roles-once-at-run-preparation.md +25 -0
  154. package/knowledge/decisions/security/a-virtual-user-whose-roles-drift-from-the-declaration-refuses-to-run.md +32 -0
  155. package/knowledge/decisions/security/addon-screen-scopes-hide-and-do-not-enforce.md +20 -0
  156. package/knowledge/decisions/security/an-addon-header-can-only-narrow-the-session.md +26 -0
  157. package/knowledge/decisions/security/an-addon-instance-reads-only-the-secrets-and-credentials-it-is-named.md +33 -0
  158. package/knowledge/decisions/security/an-addon-may-reach-only-itself-and-the-addons-it-uses.md +16 -0
  159. package/knowledge/decisions/security/an-api-key-is-jwt-decoded-into-the-session.md +26 -0
  160. package/knowledge/decisions/security/an-external-virtual-user-caller-presents-one-signed-statement.md +38 -0
  161. package/knowledge/decisions/security/an-inbound-webhook-source-dispatches-only-verified-requests.md +31 -0
  162. package/knowledge/decisions/security/an-mcp-call-with-no-http-request-stays-anonymous.md +18 -0
  163. package/knowledge/decisions/security/an-omitted-user-id-addresses-the-platform-credential.md +32 -0
  164. package/knowledge/decisions/security/analytics-identity-is-stamped-from-the-session-not-the-event.md +24 -0
  165. package/knowledge/decisions/security/app-access-is-a-scope-tree-synthesised-from-virtual-users.md +26 -0
  166. package/knowledge/decisions/security/approval-policy-decides-who-answers-a-tool-call.md +27 -0
  167. package/knowledge/decisions/security/auth-false-on-a-session-function-warns-and-still-requires-one.md +25 -0
  168. package/knowledge/decisions/security/bearer-token-hashes-are-unpeppered-sha256.md +18 -0
  169. package/knowledge/decisions/security/consent-is-read-from-a-cookie-and-a-recognised-denial-counts-as-refusal.md +29 -0
  170. package/knowledge/decisions/security/cors-defaults-to-any-origin-and-refuses-the-wildcard-with-credentials.md +22 -0
  171. package/knowledge/decisions/security/credential-backends-share-one-conformance-suite-for-per-user-isolation.md +29 -0
  172. package/knowledge/decisions/security/each-stage-has-one-virtual-user-key-pair.md +27 -0
  173. package/knowledge/decisions/security/hmac-verification-fails-closed-on-empty-inputs.md +25 -0
  174. package/knowledge/decisions/security/index.md +88 -24
  175. package/knowledge/decisions/security/local-content-refuses-private-buckets-under-the-public-segment.md +21 -0
  176. package/knowledge/decisions/security/mail-is-allowlisted-before-a-virtual-user-run-sees-it.md +38 -0
  177. package/knowledge/decisions/security/mcp-resources-tools-and-prompts-take-sessionless-functions.md +18 -0
  178. package/knowledge/decisions/security/origin-locked-routes-match-the-origin-exactly.md +22 -0
  179. package/knowledge/decisions/security/oss-auth-works-without-pikku-studio-cloud.md +26 -0
  180. package/knowledge/decisions/security/outbound-webhooks-use-an-allowlist-when-configured.md +24 -0
  181. package/knowledge/decisions/security/pikku-carries-virtual-user-scopes-as-data-and-the-app-grants-them.md +35 -0
  182. package/knowledge/decisions/security/scoped-content-access-is-a-root-folder-with-longest-prefix-grants.md +31 -0
  183. package/knowledge/decisions/security/services-without-secrets-get-a-throwing-secrets-accessor.md +22 -0
  184. package/knowledge/decisions/security/shared-guards-refuse-to-mutate-or-shadow-a-system-role.md +34 -0
  185. package/knowledge/decisions/security/signed-uploads-are-verified-by-the-same-check-as-signed-reads.md +26 -0
  186. package/knowledge/decisions/security/system-role-scopes-must-be-declared-with-define-scope.md +24 -0
  187. package/knowledge/decisions/security/system-roles-are-additive-and-immutable.md +16 -0
  188. package/knowledge/decisions/security/the-channel-rpc-does-not-send-the-callers-session.md +21 -0
  189. package/knowledge/decisions/security/the-remote-jobs-inbox-fails-closed-without-a-secret.md +24 -0
  190. package/knowledge/decisions/security/the-remote-rpc-gate-fails-closed-and-binds-the-token-to-its-function.md +34 -0
  191. package/knowledge/decisions/security/the-workflow-status-stream-rechecks-ownership-on-every-poll.md +16 -0
  192. package/knowledge/decisions/security/two-credentials-on-one-key-must-share-a-read-mode.md +29 -0
  193. package/knowledge/decisions/security/verified-bearer-claims-are-pass-through-data-that-core-never-fills.md +23 -0
  194. package/knowledge/decisions/security/virtual-user-catalogue-excludes-the-machinery-that-runs-virtual-users.md +25 -0
  195. 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
  196. package/knowledge/decisions/security/virtual-user-run-records-are-gated-behind-a-read-scope.md +18 -0
  197. package/knowledge/decisions/security/virtual-user-sign-in-does-not-exist-in-production.md +25 -0
  198. package/knowledge/decisions/security/virtual-user-sign-in-has-one-door.md +42 -0
  199. 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
  200. package/knowledge/decisions/security/virtual-user-sign-in-only-works-for-virtual-user-flagged-users.md +29 -0
  201. package/knowledge/decisions/security/workflow-helper-functions-are-sessionless-by-declaration.md +18 -0
  202. 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
  203. package/knowledge/decisions/security/workflow-scenario-steps-are-never-network-invocable.md +5 -5
  204. 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
  205. package/knowledge/decisions/security/writing-a-virtual-user-schedule-is-gated-by-its-own-scope.md +18 -0
  206. package/knowledge/index.md +0 -2
  207. package/knowledge/questions/a-bearer-jwt-with-no-identity-becomes-a-session.md +24 -0
  208. package/knowledge/questions/dev-quick-login-as-impersonation.md +25 -0
  209. package/knowledge/questions/does-a-delayed-rpc-run-as-the-session-it-was-scheduled-with.md +26 -0
  210. package/knowledge/questions/does-a-user-id-with-a-colon-read-another-users-credentials.md +27 -0
  211. package/knowledge/questions/does-a-wirecli-command-need-a-session-by-default.md +26 -0
  212. package/knowledge/questions/does-an-adversarial-in-scope-success-count-as-a-finding.md +26 -0
  213. package/knowledge/questions/does-approve-step-need-the-run-owners-session-or-the-gates-approvers.md +34 -0
  214. package/knowledge/questions/does-cors-origin-true-reflect-any-origin-with-credentials.md +20 -0
  215. package/knowledge/questions/does-skip-user-session-do-anything-on-a-channel.md +24 -0
  216. package/knowledge/questions/does-the-store-session-match-the-other-two.md +19 -0
  217. package/knowledge/questions/index.md +25 -2
  218. package/knowledge/questions/is-the-agent-score-worker-reachable-without-a-session.md +21 -0
  219. package/knowledge/questions/live-scheduled-tests-may-write.md +19 -0
  220. package/knowledge/questions/provision-virtual-users-has-no-caller-in-this-repo.md +23 -0
  221. package/knowledge/questions/remote-secret-refuses-requests-without-a-remote-token-on-every-path.md +32 -0
  222. package/knowledge/questions/should-a-middleware-set-session-persist-to-the-store.md +25 -0
  223. package/knowledge/questions/should-a-scoped-secret-view-throw-from-has-secret-for-a-disallowed-key.md +23 -0
  224. package/knowledge/questions/should-a-wrong-bearer-token-be-anonymous-or-refused.md +35 -0
  225. package/knowledge/questions/should-an-empty-webhook-signing-secret-send-unsigned.md +23 -0
  226. package/knowledge/questions/should-http-virtual-user-sessions-reset-between-scenarios-in-one-run.md +27 -0
  227. package/knowledge/questions/should-the-cookie-jar-scope-cookies-to-the-api-origin.md +26 -0
  228. package/knowledge/questions/should-voice-input-fetch-audio-urls-without-a-host-allowlist.md +20 -0
  229. package/knowledge/questions/staging-impersonation-rules.md +27 -0
  230. package/knowledge/questions/virtual-user-keys-on-cloud-stages-are-pikku-studios-to-build.md +34 -0
  231. package/knowledge/questions/virtual-user-statement-replay-is-per-process.md +20 -0
  232. package/knowledge/questions/what-an-impersonated-session-carries.md +22 -0
  233. package/package.json +3 -3
  234. package/src/public-surface.json +43 -47
  235. package/dist/services/persona-actor-secret.d.ts +0 -38
  236. package/dist/services/persona-actor-secret.js +0 -39
  237. package/dist/services/persona-sign-in.d.ts +0 -117
  238. package/dist/services/persona-sign-in.js +0 -154
  239. package/dist/wirings/actor-flow/index.d.ts +0 -2
  240. package/dist/wirings/actor-flow/run-conversation.d.ts +0 -36
  241. package/dist/wirings/persona/define-personas.d.ts +0 -32
  242. package/dist/wirings/persona/index.d.ts +0 -23
  243. package/dist/wirings/persona/index.js +0 -19
  244. package/knowledge/decisions/internals/actor-flow-conversations-seed-a-hidden-kickoff-message.md +0 -23
  245. package/knowledge/decisions/internals/actor-flow-verdicts-are-llm-self-evaluations.md +0 -25
  246. package/knowledge/decisions/internals/validate-checks-personas-through-a-shared-module.md +0 -43
  247. package/knowledge/decisions/security/actor-sign-in-only-works-for-actor-flagged-users.md +0 -31
  248. package/knowledge/decisions/security/an-actor-credential-is-derived-per-persona.md +0 -42
  249. package/knowledge/decisions/security/pikku-carries-actor-scopes-as-data-and-the-app-grants-them.md +0 -26
  250. /package/dist/wirings/{actor-flow/actor-flow.types.js → virtual-user/conversation/conversation.types.js} +0 -0
  251. /package/dist/wirings/{actor-flow → virtual-user/conversation}/index.js +0 -0
  252. /package/dist/wirings/{persona/persona.types.js → virtual-user/virtual-user-declaration.types.js} +0 -0
  253. /package/dist/wirings/{persona/persona-mailbox.js → virtual-user/virtual-user-mailbox.js} +0 -0
@@ -0,0 +1,26 @@
1
+ ---
2
+ type: decision
3
+ title: App access is a scope tree synthesised from the virtual users that name an app
4
+ description: buildAppScopeDefinition creates one app:<name> scope per app any virtual user names, so which frontend a person may sign into is a runtime-revocable grant rather than a column per app
5
+ tags: core, virtual-users, scopes, access
6
+ ---
7
+
8
+ # App access is a scope tree synthesised from the virtual users that name an app
9
+
10
+ `packages/core/src/wirings/virtual-user/virtual-user-app-scopes.ts` builds an `app` scope
11
+ tree from the virtual users. `declaredApps` returns the app names used by virtual users,
12
+ sorted and without duplicates (`virtual-user-app-scopes.ts:23-30`).
13
+ `buildAppScopeDefinition` creates one child scope per app, with the origin
14
+ `generated` (`virtual-user-app-scopes.ts:60-74`). It returns `null` when no virtual user
15
+ names an app (`virtual-user-app-scopes.ts:55-58`).
16
+
17
+ The doc comment gives the reasons. Which frontend a person may use is the same
18
+ kind of question as a scope: an admin can grant and revoke it at runtime, it
19
+ resolves at the session boundary, and a restricted API key can decline to inherit
20
+ it (`virtual-user-app-scopes.ts:35-41`). The tree is synthesised, not written by hand,
21
+ because a hand-written copy could drift from the virtual users. The comment says that
22
+ failure "is one `pikku scopes prune` away from silently revoking sign-in"
23
+ (`virtual-user-app-scopes.ts:45-47`).
24
+
25
+ **What this rules out:** a separate per-app access column, and a hand-maintained
26
+ list of apps that has to match the virtual users.
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: decision
3
+ title: The approval policy decides who answers a tool call
4
+ description: The conversation approvals option picks either a fixed answer or the virtual user's model for each pending tool call, and a call with no answer is denied
5
+ tags: core, virtual-users, approvals, security
6
+ ---
7
+
8
+ # The approval policy decides who answers a tool call
9
+
10
+ `ConversationApprovalPolicy` in
11
+ `packages/core/src/wirings/virtual-user/conversation/conversation.types.ts:1` has
12
+ three values, and `approvals` is an optional field on `ConverseOptions` (`:9`).
13
+
14
+ `decideApprovals` in
15
+ `packages/core/src/wirings/virtual-user/conversation/run-conversation.ts:133-181`
16
+ applies it. The default is `in-virtual-user` (`:138`). `always` approves every
17
+ pending call and `never` denies every one (`:139-144`). `in-virtual-user` sends the
18
+ pending calls to the virtual user's model with `tools: []` and `toolChoice: 'none'`
19
+ (`:153-169`), and the model's decisions are what get applied.
20
+
21
+ Every pending call must get a decision. The code maps the answers by `toolCallId`
22
+ and a call with no answer is denied (`:176-180`). The missing-decision rule has
23
+ its own note,
24
+ [missing approval decisions default to denied](virtual-user-conversation-missing-approval-decisions-default-to-denied.md).
25
+
26
+ **What this rules out:** reading a missing answer as consent, and an
27
+ `in-virtual-user` run approving a call the virtual user's model did not name.
@@ -0,0 +1,25 @@
1
+ ---
2
+ type: decision
3
+ title: auth false on a session-required function warns and still requires a session
4
+ description: Writing auth: false on a function that is not sessionless does not open it; the runner logs a warning and still throws MissingSessionError without a session
5
+ tags: core, auth, function-runner
6
+ ---
7
+
8
+ # auth false on a session-required function warns and still requires a session
9
+
10
+ In `runPikkuFunc` (`packages/core/src/function/function-runner.ts`), a function
11
+ that is not `sessionless` is session-required. If the wiring or the function
12
+ config says `auth: false`, the runner logs
13
+ "requires a session but auth was explicitly disabled — use pikkuSessionlessFunc
14
+ instead." and then still throws `MissingSessionError` when no session is present.
15
+
16
+ The test "should warn when auth is disabled for a session-required function but
17
+ still enforce session" in `function-runner.test.ts` states the intent. The
18
+ warning points the author at the function kind that actually means "no session",
19
+ which is `pikkuSessionlessFunc`. The same flag on a sessionless function is the
20
+ opposite declaration and is covered by
21
+ [A public function says so with auth: false](a-public-function-says-so-with-auth-false.md).
22
+
23
+ **What this rules out:** treating `auth: false` on a `pikkuFunc` as making it
24
+ public. Making a function public means using `pikkuSessionlessFunc`, and a
25
+ session-required function that disables auth still fails closed.
@@ -0,0 +1,18 @@
1
+ ---
2
+ type: decision
3
+ title: Bearer token hashes are unpeppered SHA-256
4
+ description: hashToken stores bearer tokens as plain SHA-256 hex because they are high-entropy random values; a low-entropy secret is not a hashed column
5
+ tags: core, classification, security
6
+ ---
7
+
8
+ # Bearer token hashes are unpeppered SHA-256
9
+
10
+ `hashToken` in `packages/core/src/classification/column-form.ts:21-32` returns the
11
+ lowercase hex SHA-256 of its input through WebCrypto. The doc comment at `:8-15`
12
+ gives the reason: bearer tokens are high-entropy random values with no candidate
13
+ set to search, so a pepper would add a key to manage and a rotation story while
14
+ buying nothing. The same comment says a column that holds a low-entropy secret is
15
+ not a `hashed` column and needs a password KDF instead.
16
+
17
+ **What this rules out:** adding a pepper or key to token hashes, and using this
18
+ hash for passwords or other low-entropy values.
@@ -0,0 +1,29 @@
1
+ ---
2
+ type: decision
3
+ title: Consent is read from a cookie, and a recognised denial value counts as a refusal
4
+ description: cookieAnalyticsIdentity grants a consent purpose only when its cookie is present and not one of a fixed set of denial values, and resolves no identity off a non-HTTP wire
5
+ tags: core, analytics, consent, cookies
6
+ ---
7
+
8
+ # Consent is read from a cookie, and a recognised denial value counts as a refusal
9
+
10
+ `packages/core/src/analytics/cookie-analytics-identity.ts` reads consent and
11
+ vendor identifiers from first-party cookies. The doc comment says a purpose is
12
+ true "only when the cookie is present and is not a recognised denial", so a
13
+ consent tool that writes a refusal rather than deleting the cookie is read as a
14
+ refusal (`cookie-analytics-identity.ts:7-11`).
15
+
16
+ The denial values are `0`, `false`, `denied`, `deny` and `no`
17
+ (`cookie-analytics-identity.ts:15`). The comparison is case-insensitive
18
+ (`cookie-analytics-identity.ts:49`).
19
+
20
+ A purpose whose cookie is absent is left out of the consent record entirely
21
+ (`cookie-analytics-identity.ts:47-48`). Since `mintCookie` requires `=== true`
22
+ (`mint-cookie.ts:34`), an absent cookie is a refusal there.
23
+
24
+ A wire with no HTTP request resolves nothing (`cookie-analytics-identity.ts:34-35`).
25
+ The doc comment says a cron task or queue worker has no browser, so "any vendor
26
+ id they produced would be invented" (`cookie-analytics-identity.ts:24-25`).
27
+
28
+ **What this rules out:** treating any present cookie value as consent, and
29
+ inventing a vendor identity for a non-browser caller.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: decision
3
+ title: CORS defaults to any origin and refuses the wildcard when credentials are on
4
+ description: The cors middleware's origin defaults to *, and a resolved wildcard origin combined with credentials throws when the middleware is built
5
+ tags: core, middleware, cors, security
6
+ ---
7
+
8
+ # CORS defaults to any origin and refuses the wildcard when credentials are on
9
+
10
+ In `packages/core/src/middleware/cors.ts`, `origin` defaults to `'*'` (`:29`) and
11
+ `credentials` defaults to `false` (`:33`). The factory throws when the resolved
12
+ origin is `'*'` and credentials are on (`:36-40`). Because the check runs on the
13
+ resolved value, the default origin with `credentials: true` throws too.
14
+
15
+ A `true` origin reflects the request's own origin, or `*` when none is sent
16
+ (`:55-56`). An array origin answers with the request origin only when it is in the
17
+ list (`:57-61`). Any other request origin is left out of the response, with a debug
18
+ log (`:62-65`).
19
+
20
+ **What this rules out:** a wildcard origin together with credentials.
21
+ `questions/does-cors-origin-true-reflect-any-origin-with-credentials.md` records
22
+ the `true` case, which this check does not refuse.
@@ -0,0 +1,29 @@
1
+ ---
2
+ type: decision
3
+ title: Credential backends are checked by one conformance suite that proves per-user isolation
4
+ description: defineCredentialServiceTests asserts that one user's credential is invisible to another and that KEK rotation re-wraps stored values, for any backend that supplies a credential service
5
+ tags: core, credentials, testing, isolation
6
+ ---
7
+
8
+ # Credential backends are checked by one conformance suite that proves per-user isolation
9
+
10
+ `defineCredentialServiceTests` in `packages/core/src/testing/service-tests/credential-service-tests.ts`
11
+ is the shared conformance suite for `credentialService`. Its doc comment says it
12
+ "runs only when a backend supplies one" (`credential-service-tests.ts:6`), so a
13
+ backend without a credential service is not checked by it.
14
+
15
+ Per-user isolation is asserted directly. A value set for `user-1` is read back
16
+ only for `user-1`, and `user-2` gets its own value (`credential-service-tests.ts:53-62`).
17
+ A delete for one user leaves the other user's credential in place
18
+ (`credential-service-tests.ts:64-72`). `getAll` for one user returns that user's
19
+ credentials and omits another user's (`credential-service-tests.ts:85-95`), and
20
+ returns `{}` for an unknown user (`credential-service-tests.ts:97-101`).
21
+
22
+ KEK rotation is asserted too. After rotation to a new key version, a credential
23
+ written under the old key is still readable, and the new key alone can read it
24
+ (`credential-service-tests.ts:103-133`). Rotation without a `previousKey` throws
25
+ (`credential-service-tests.ts:135-141`).
26
+
27
+ **What this rules out:** a credential backend that returns one user's stored value
28
+ to another user, or that rotates keys without re-wrapping existing credentials,
29
+ while still passing the suite.
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: decision
3
+ title: Each stage has one virtual user key pair, and the app holds only the public half
4
+ description: The deployer holds the private key, the app reads the public key from an unreserved variable, and users never see either
5
+ tags: services
6
+ ---
7
+
8
+ # Each stage has one virtual user key pair, and the app holds only the public half
9
+
10
+ | Environment | Private key held by | Public key reaches the app as |
11
+ | --- | --- | --- |
12
+ | `pikku dev` | Generated on first start into `<runtimeDir>/virtual-user/signing.key` (mode 0600, a `.gitignore` of `*` beside it); the CLI reads it there | `process.env.VIRTUAL_USER_VERIFY_KEY`, set before services are built |
13
+ | Pikku Studio Cloud stage | Pikku Studio Cloud's backend, one pair per stage | the stage variable `VIRTUAL_USER_VERIFY_KEY`, set by Pikku Studio Cloud |
14
+ | Open source deploy | The deployer's machine, as `VIRTUAL_USER_SIGNING_KEY` in the environment of whoever runs the CLI | the variable `VIRTUAL_USER_VERIFY_KEY`, set by the deployer |
15
+
16
+ The app reads it with `variables.get('VIRTUAL_USER_VERIFY_KEY')`: the SPKI DER, base64
17
+ on one line (a PEM is also accepted). A stage without it registers no statement
18
+ endpoint, and the CLI's error for that says which variable to set.
19
+
20
+ **One pair per stage** so a statement for one stage verifies nowhere else; the
21
+ audience check is a second line, not the first. **Unreserved names**: neither starts
22
+ with `PIKKU_` or `PIKKUSTUDIO_`, which a platform may refuse to let an app or a deployer
23
+ set. **Users never manage keys**: `pikku dev` makes them and a deploy supplies them.
24
+
25
+ **What this rules out:** one key shared across stages, a private key stored in the
26
+ app's variables or secrets, and a key a person has to generate or paste to run
27
+ scenarios locally.
@@ -0,0 +1,25 @@
1
+ ---
2
+ type: decision
3
+ title: HMAC verification fails closed on empty or malformed inputs
4
+ description: verifyHmacSignature returns false for an empty secret, a missing signature, an unknown algorithm, an empty key, a malformed hex signature or an empty signature
5
+ tags: core, security, webhooks
6
+ ---
7
+
8
+ # HMAC verification fails closed on empty or malformed inputs
9
+
10
+ `verifyHmacSignature` in `packages/core/src/utils/hmac.ts:142-161` returns `false`
11
+ in each of these cases:
12
+
13
+ - an empty secret or a missing signature (`:150`);
14
+ - an algorithm with no entry in `HMAC_HASH` (`:153`);
15
+ - a secret that decodes to an empty key (`:155`);
16
+ - a hex encoding whose signature does not match `^(?:[0-9a-fA-F]{2})+$` (`:156-158`);
17
+ - a signature that decodes to zero bytes (`:161`).
18
+
19
+ The key is imported with WebCrypto `importKey` (`:162-166`). The doc comment at
20
+ `:139-140` says the comparison is `crypto.subtle.verify('HMAC', ...)`, which runs
21
+ in the platform's constant-time implementation. `timingSafeStringEqual`
22
+ (`:52-63`) is a separate helper for opaque tokens and is not used for HMACs.
23
+
24
+ **What this rules out:** treating an empty secret, or a signature that does not
25
+ decode, as a match.
@@ -9,63 +9,127 @@ description: Who may do what in core — gates, defaults, and the failures they
9
9
  A rule about who may do what, and which way it fails when it is unsure.
10
10
 
11
11
  <!-- pikku:knowledge-index -->
12
-
12
+ - [A bearer token checked against a secret is read per request, and an unreadable secret means no session](a-bearer-token-checked-against-a-secret-reads-it-per-request.md) — The secretId form of authBearer reads the secret on every request; a missing or failing read leaves the request without a session rather than raising an error
13
+ - [A call without a session raises MissingSessionError, and a missing scope raises MissingScopeError](a-call-without-a-session-raises-missing-session-not-forbidden.md) — The function runner separates the no-session refusal from the scope and permission refusals, so a caller can tell a missing login from a denied grant
14
+ - [A cookie the caller sets wins over the jar's copy of the same name](a-caller-cookie-wins-over-the-jar-for-the-same-name.md) — createCookieJar sends the caller's cookie for a name and drops the jar's copy of that name, so one request never carries the same cookie twice
15
+ - [A channel lifecycle acts as the session the socket upgrade established](a-channel-lifecycle-acts-as-the-socket-upgrade-session.md) — onConnect and onDisconnect run with the userSession captured during the socket upgrade, both in the wire they receive and as the session service passed to runPikkuFunc
16
+ - [A channel requires a session unless it says auth: false](a-channel-requires-a-session-unless-it-says-auth-false.md) — The channel-level requirement is auth !== false, and a message handler's own auth, when it is set, decides for that message in either direction
17
+ - [A credential's read path follows its declared type, never a missing value](a-credential-read-path-follows-its-declared-type-not-absence.md) — The credential wire service reads a wire credential only from the user's store and a singleton only from the deployment, so an absent user value never falls back to the deployment's token
18
+ - [A credential's wire or singleton type is a literal the inspector checks at declaration](a-credential-type-is-a-literal-the-inspector-checks-at-declaration.md) — defineCredential is a no-op read by AST, so its type must be a literal 'wire' or 'singleton' and the inspector refuses any other value
13
19
  - [A dropped audit write is always logged](a-dropped-audit-write-is-always-logged.md) — The no-op audit service falls back to the singleton logger when the wire carries none, so an unconfigured audit call is never silent
20
+ - [A first-party tracking cookie is written only after its purposes are consented](a-first-party-cookie-is-written-only-after-its-purposes-consent.md) — mintCookie writes a new cookie only when every purpose it requires is true in the consent record, and writes nothing on a wire with no HTTP response
14
21
  - [A function never receives the secret service](a-function-never-receives-the-secret-service.md) — Every function-, permission- and auth-facing services type is bounded by SecretlessServices, so reaching for `secrets` in a function body is a type error rather than a lint
15
22
  - [A graph run starts at an entry node the graph declared](a-graph-run-starts-at-an-entry-node-the-graph-declared.md) — startNode may only name a node in meta.entryNodeIds, and no generated route offers it, because otherwise a caller picks which half of the graph to skip
16
23
  - [A permission gets a wire it cannot reply on](a-permission-gets-a-wire-it-cannot-reply-on.md) — The permission wire is typed with Out = never so a permission cannot send on the channel; that narrowing is not a subtype, so the call site asserts
24
+ - [A public function says so with auth: false](a-public-function-says-so-with-auth-false.md) — An exposed sessionless function that writes auth: false is public on purpose and PKU574 stays quiet; one that leaves auth out still warns
25
+ - [A readonly session reaches only functions declared readonly](a-readonly-session-reaches-only-readonly-functions.md) — A session flagged readonly is refused by every function not declared readonly, checked straight after the session is required and before scopes
26
+ - [A scaffold flag says a surface exists, not who may call it](a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md) — scaffold.<feature> is boolean | { path } — the auth field is gone, because the function runner already enforces each generated function's own auth, wiring, scopes and addon
27
+ - [A scheduled virtual-user run needs no admin at run time](a-scheduled-virtual-user-run-needs-no-admin-at-run-time.md) — An admin may start a virtual-user run, but the run itself never depends on an admin being signed in or on an admin's credential
28
+ - [A scoped credential service is limited to the names its package declared](a-scoped-credential-view-is-limited-to-declared-names.md) — ScopedCredentialService refuses any credential name outside the package's declared set, filters getAll to that set, and refuses to enumerate users
29
+ - [A scoped secret view reads only its declared keys and never writes](a-scoped-secret-view-reads-only-declared-keys-and-never-writes.md) — ScopedSecretService checks every read against an allowed-key set and refuses all writes, unlike the credential service that stays writable
30
+ - [A secret may only be sent to the hosts its declaration names](a-secret-is-sent-only-to-the-hosts-it-declares.md) — assertSecretAllowedForHost refuses a host outside a secret's allowedHosts, and refuses any host at all for a secret with no allowedHosts when the project requires them
31
+ - [A scenario step can name an identity no virtual user holds, through per-call headers](a-step-can-name-an-identity-no-virtual-user-holds-through-per-call-headers.md) — ScenarioInvokeOptions.headers lets a step send an impersonation or shim header on one call, and a virtual-user run never gets that option
17
32
  - [A step runs the function the workflow dispatched it with](a-step-runs-the-function-the-workflow-dispatched-it-with.md) — StepState records the step's function name so the worker can reject a queue message naming a different one, because the step executes under the run owner's identity
33
+ - [Stopping an agent run by its id skips the ownership check; interruptAgent is the checked path](a-stopped-run-by-id-skips-the-ownership-check.md) — signalRunInterrupt is exported and performs no authorisation, while interruptAgent asserts the run's resource owner before it signals
34
+ - [A vault secret redacts when serialized and throws when coerced to a string](a-vault-secret-redacts-on-serialization-and-throws-on-coercion.md) — SecretValue serializes to a redacted marker so logs stay honest without crashing, but string coercion throws, and reveal() is the only way to read the value
35
+ - [Virtual users are offered calls by declared scope, and approval-gated calls are denied by default](a-virtual-user-catalogue-entry-is-filtered-by-scope-and-approval-is-denied-by-default.md) — The catalogue type filters by the declared scopes of each RPC rather than by permission functions, and the catalogue filter drops approval-required entries unless the caller opts in
36
+ - [A virtual user's sign-in is refused unless the environment is known and allowed](a-virtual-user-environment-fails-closed.md) — Virtual user provisioning, sync and sign-in refuse when PIKKU_ENV is unresolved, unknown, production without the accountable disposition, or outside the virtual user's environments, and production environments are omitted by default
18
37
  - [A virtual user is never offered a scenario, platform or addon step](a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md) — Being able to invoke "the webhook arrives" lets the user manufacture the outcome it exists to discover, which invalidates every finding downstream
19
- - [A workflow run is read and approved by its owner](a-workflow-run-is-read-and-approved-by-its-owner.md) — A run started through a session records that user and only that user may read it or answer its approval gates; a run with no recorded owner has no ownership to enforce
20
- - [An actor's missing approval decision defaults to denied](actor-flow-missing-approval-decisions-default-to-denied.md) — Every pending tool call gets an explicit decision; an id the persona LLM omitted is denied, so a dropped field can never read as consent
21
- - [Actor sign-in is proven by Set-Cookie, not a non-empty jar](actor-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md) — HttpScenarioActor tracks its own signedIn flag and requires the sign-in response itself to set a cookie, because a populated jar proves nothing
22
- - [Actor sign-in only works for actor-flagged users](actor-sign-in-only-works-for-actor-flagged-users.md) — An actor credential mints sessions for user rows flagged actor and nothing else, so holding one never impersonates a real user
23
- - [An actor credential is derived per persona](an-actor-credential-is-derived-per-persona.md) — What a caller presents to the actor endpoint is HKDF-derived from the root secret and the address it signs in as, so one credential opens one persona
38
+ - [A virtual user never runs against production, whatever its disposition](a-virtual-user-never-runs-against-production.md) — startVirtualUserRun and an enabling writeVirtualUserSchedule both refuse with a 403 in production, including for the accountable disposition; an unresolved environment counts as production
39
+ - [A virtual user reads its roles from the app's own RPC before better-auth's user.role](a-virtual-user-reads-its-roles-from-the-apps-own-rpc-before-better-auth.md) — sessionRoles asks getMyScopes first, then get-session, and returns null rather than an empty list when the stage reports no roles
40
+ - [A virtual-user run treats an unresolved PIKKU_ENV as production](a-virtual-user-run-treats-an-unresolved-environment-as-production.md) — The virtual-user scaffold decides "production" from the configured PIKKU_ENV environment, counts an unresolved one as production, and falls back to NODE_ENV only when no environments are configured
41
+ - [A virtual user's scopes are expanded from its roles once, at run preparation](a-virtual-user-scopes-are-expanded-from-its-roles-once-at-run-preparation.md) — A virtual user's scope set is computed from its declared roles against the defineSystemRole definitions when the run is prepared, and that set is the oracle for scope drift
42
+ - [A virtual user whose granted roles drift from its declaration refuses to run](a-virtual-user-whose-roles-drift-from-the-declaration-refuses-to-run.md) — verifyVirtualUserRoles compares declared and granted roles in both directions before a run, and roleMismatchMessage produces the refusal text; a provider login is never runnable
43
+ - [A workflow run is read by its owner, and answered by whoever its gate declares](a-workflow-run-is-read-and-approved-by-its-owner.md) — A run started through a session records that user and only that user may read it; who may answer an approval gate is the gate's own declaration, and a run with no recorded owner has no ownership to enforce
24
44
  - [Addon auth and tags only tighten, and resolve where the function runs](addon-auth-and-tags-only-tighten.md) — wireAddon auth and tags are applied in runPikkuFunc like scopes, but auth:false is ignored and tags resolve against the consuming app's tag groups rather than the addon package's
25
45
  - [Addon auth and tag gates apply wherever the function runs, including inside the addon](addon-config-gates-apply-only-at-the-namespaced-rpc-boundary.md) — wireAddon's auth and tags moved from the namespaced RPC boundary into runPikkuFunc, so they also apply to direct wirings and to bare intra-addon calls
26
46
  - [Addon scopes are resolved where the function runs](addon-scopes-are-resolved-where-the-function-runs.md) — wireAddon scopes are merged inside runPikkuFunc rather than at namespace resolution, because most wirings reach an addon function without ever resolving a namespace
27
- - [wireAddon expose selects the RPC surface, and a list may widen it](wire-addon-expose-selects-the-rpc-surface.md) — The consuming app decides which addon functions rpc.exposed reaches per instance — unset keeps the addon's own expose, false closes the instance, a list names exactly what is callable — and the deploy analyzer applies the same rule
28
- - [Only a Symbol-branded framework result can request tool approval](ai-agent-approval-forwarding-requires-a-symbol-brand.md) — Approval markers are trusted from the APPROVAL_REQUIRED Symbol on a forwardsApproval tool, never from a JSON key an LLM could emit
29
- - [Credential requests are trusted only when Symbol-branded](ai-agent-credential-requests-are-symbol-branded.md) — The string key is a wire field; the Symbol is the capability, and only core can mint it
30
- - [An agent requires a session only when auth is true, but always enforces scopes and permissions](ai-agent-gate-requires-a-session-only-when-auth-is-true.md) — Agents follow pikkuSessionlessFunc semantics so crons and queue workers can run them; scopes are an AND gate checked before any permission I/O
31
- - [An agent ownership failure never echoes the resource it refused](ai-agent-ownership-failures-never-echo-the-resource.md) — assertResourceOwner throws a bare ForbiddenError so the error cannot be used as an existence oracle, at the cost of thinner debugging output
32
- - [Resuming a suspended agent run re-runs the agent's authorization gate](ai-agent-resume-re-runs-the-authorization-gate.md) — Ownership of the run is not enough — a grant revoked while the run was suspended must block the approval
33
- - [Agent thread ownership fails closed when there is no principal](ai-agent-sessionless-deployments-have-no-thread-ownership.md) — A sessionless caller gets an ephemeral owner and reaches no stored thread, rather than reaching all of them
34
- - [An agent thread key is always prefixed with the trusted principal](ai-agent-thread-ownership-composes-the-session-principal.md) — Ownership keys are composed as principal:resourceId, so a client id can sub-divide its own boundary but never widen it
35
- - [Agent tool permission filtering reads the live function config, not the metadata](ai-agent-tool-filtering-reads-the-live-function-config.md) — The pikkuAuth brand survives only on live permission objects, so a metadata-driven check would silently admit every gated tool
47
+ - [Addon screen scopes hide a screen and do not enforce access](addon-screen-scopes-hide-and-do-not-enforce.md) — The scopes on a defineScreens entry state what a viewer must hold to see the screen; the addon's own functions are the access check
48
+ - [Only a Symbol-branded framework result can request tool approval](agent-approval-forwarding-requires-a-symbol-brand.md) — Approval markers are trusted from the APPROVAL_REQUIRED Symbol on a forwardsApproval tool, never from a JSON key an LLM could emit
49
+ - [Credential requests are trusted only when Symbol-branded](agent-credential-requests-are-symbol-branded.md) — The string key is a wire field; the Symbol is the capability, and only core can mint it
50
+ - [An agent requires a session only when auth is true, but always enforces scopes and permissions](agent-gate-requires-a-session-only-when-auth-is-true.md) — Agents follow pikkuSessionlessFunc semantics so crons and queue workers can run them; scopes are an AND gate checked before any permission I/O
51
+ - [An agent ownership failure never echoes the resource it refused](agent-ownership-failures-never-echo-the-resource.md) — assertResourceOwner throws a bare ForbiddenError so the error cannot be used as an existence oracle, at the cost of thinner debugging output
52
+ - [Resuming a suspended agent run re-runs the agent's authorization gate](agent-resume-re-runs-the-authorization-gate.md) — Ownership of the run is not enough — a grant revoked while the run was suspended must block the approval
53
+ - [Agent thread ownership fails closed when there is no principal](agent-sessionless-deployments-have-no-thread-ownership.md) — A sessionless caller gets an ephemeral owner and reaches no stored thread, rather than reaching all of them
54
+ - [An agent thread key is always prefixed with the trusted principal](agent-thread-ownership-composes-the-session-principal.md) — Ownership keys are composed as principal:resourceId, so a client id can sub-divide its own boundary but never widen it
55
+ - [Agent tool permission filtering reads the live function config, not the metadata](agent-tool-filtering-reads-the-live-function-config.md) — The pikkuAuth brand survives only on live permission objects, so a metadata-driven check would silently admit every gated tool
56
+ - [An addon header can only narrow the session, and an unknown addon is refused](an-addon-header-can-only-narrow-the-session.md) — addon-scope-cap intersects the session's scopes with the named addon's role, so naming an addon never widens access, and a name the host does not know is a 403
57
+ - [An addon instance reads only the secrets and credentials it is named](an-addon-instance-reads-only-the-secrets-and-credentials-it-is-named.md) — An addon's secret and credential views are its declared names plus the consuming app's grants and override keys; the global flags opt out of scoping
58
+ - [An addon may reach only itself and the addons it declares in uses](an-addon-may-reach-only-itself-and-the-addons-it-uses.md) — An RPC call that arrives tagged with an addon header is refused unless the target is the caller or one of the caller's declared uses, and an unknown addon name is refused outright
36
59
  - [An agent approval is claimed before the tool runs](an-agent-approval-is-claimed-before-the-tool-runs.md) — resolveApproval is a compare-and-swap returning whether this caller won, because the read that precedes it is not a claim and ten concurrent approvals would otherwise mean ten refunds
60
+ - [An API key is JWT-decoded into the session, from a header or a query parameter](an-api-key-is-jwt-decoded-into-the-session.md) — authAPIKey reads x-api-key or the apiKey query parameter, decodes it as a JWT, and sets the session only when no session exists yet
61
+ - [An approval answer outlives the run it answered](an-approval-answer-outlives-the-run-it-answered.md) — Run state holds a decision only while the gate is open, so the settled answer carries decidedBy/decidedAt into the step result and every attempt is written to the audit sink, which has no foreign key to the run
37
62
  - [An empty owners constraint matches nothing](an-empty-owners-constraint-matches-nothing.md) — owners is an authorization boundary, so every storage backend must treat [] as no rows rather than no filter
63
+ - [An exposed function with no gate is reported at codegen, not at boot](an-exposed-ungated-function-is-a-codegen-warning.md) — The check runs in the inspector where function meta and every wireAddon declaration are both in hand, because neither source alone can tell a gated function from an ungated one
64
+ - [An external virtual user caller presents one signed statement](an-external-virtual-user-caller-presents-one-signed-statement.md) — Callers outside the app sign an EdDSA statement naming the virtual user; the app verifies it against a public key and accepts each statement once
65
+ - [An inbound webhook source dispatches only requests verified against its credential](an-inbound-webhook-source-dispatches-only-verified-requests.md) — A webhook source with verify refuses a body-carrying request that fails its check, fails closed on an empty secret, and does not dispatch events from a body-less request
66
+ - [An MCP call with no HTTP request stays anonymous by transport design](an-mcp-call-with-no-http-request-stays-anonymous.md) — MCP calls that arrive without an HTTP request, such as stdio, reach the function with no session because auth middleware reads the session only from the request
38
67
  - [An MCP refusal is decided before dispatch, not raised from the tool](an-mcp-refusal-is-decided-before-dispatch.md) — mcpTargetRequiresSession lets the transport answer 401 with an OAuth challenge, because the MCP response streams and the status is gone by the time the runner refuses
39
68
  - [An OAuth2 credential is read by its account row, not by its provider name](an-oauth2-credential-is-read-by-its-account-row.md) — better-auth 1.7.5 selects an account by row id under a strict body schema, so BetterAuthCredentialService resolves the row through the internal adapter before asking for a token
40
- - [An exposed function with no gate is reported at codegen, not at boot](an-exposed-ungated-function-is-a-codegen-warning.md) — The check runs in the inspector where function meta and every wireAddon declaration are both in hand, because neither source alone can tell a gated function from an ungated one
69
+ - [An omitted userId on CredentialService addresses the platform credential](an-omitted-user-id-addresses-the-platform-credential.md) — The CredentialService contract reads and writes the platform-level credential whenever a per-credential call omits userId, and the better-auth implementation defaults its platform owner to pikku-platform
41
70
  - [An upload is counted as it arrives, not buffered and then measured](an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md) — Reading the whole body before checking its size hands an unauthenticated caller a way to spend the server's memory
71
+ - [Analytics identity is stamped from the session and resolvers, never from the event body](analytics-identity-is-stamped-from-the-session-not-the-event.md) — Each analytics record takes userId and orgId from the wire session, pikkuUserId from the wire, and the rest from the identity resolvers, not from the event payload
72
+ - [App access is a scope tree synthesised from the virtual users that name an app](app-access-is-a-scope-tree-synthesised-from-virtual-users.md) — buildAppScopeDefinition creates one app:<name> scope per app any virtual user names, so which frontend a person may sign into is a runtime-revocable grant rather than a column per app
73
+ - [The approval policy decides who answers a tool call](approval-policy-decides-who-answers-a-tool-call.md) — The conversation approvals option picks either a fixed answer or the virtual user's model for each pending tool call, and a call with no answer is denied
74
+ - [auth false on a session-required function warns and still requires a session](auth-false-on-a-session-function-warns-and-still-requires-one.md) — Writing auth: false on a function that is not sessionless does not open it; the runner logs a warning and still throws MissingSessionError without a session
75
+ - [Bearer token hashes are unpeppered SHA-256](bearer-token-hashes-are-unpeppered-sha256.md) — hashToken stores bearer tokens as plain SHA-256 hex because they are high-entropy random values; a low-entropy secret is not a hashed column
76
+ - [Consent is read from a cookie, and a recognised denial value counts as a refusal](consent-is-read-from-a-cookie-and-a-recognised-denial-counts-as-refusal.md) — cookieAnalyticsIdentity grants a consent purpose only when its cookie is present and not one of a fixed set of denial values, and resolves no identity off a non-HTTP wire
42
77
  - [The console addon's privileged functions gate themselves](console-addon-privileged-functions-gate-themselves.md) — Thread listing is owner-scoped unless the caller holds admin, and addon installation requires an admin session, rather than trusting the host to register a global permission
43
78
  - [Core's SSRF guard matches host literals because edge runtimes have no DNS](core-safe-fetch-blocks-ssrf-by-host-literal-not-dns.md) — safeFetch rejects internal address literals and re-validates every redirect hop; it cannot stop DNS rebinding
44
79
  - [Core secrets are encrypted with a per-secret DEK wrapped by a KEK](core-secrets-use-a-per-secret-dek-wrapped-by-a-kek.md) — Envelope encryption keeps ciphertext untouched during key rotation, at the cost of storing two blobs per secret
80
+ - [CORS defaults to any origin and refuses the wildcard when credentials are on](cors-defaults-to-any-origin-and-refuses-the-wildcard-with-credentials.md) — The cors middleware's origin defaults to *, and a resolved wildcard origin combined with credentials throws when the middleware is built
81
+ - [Credential backends are checked by one conformance suite that proves per-user isolation](credential-backends-share-one-conformance-suite-for-per-user-isolation.md) — defineCredentialServiceTests asserts that one user's credential is invisible to another and that KEK rotation re-wraps stored values, for any backend that supplies a credential service
82
+ - [Each stage has one virtual user key pair, and the app holds only the public half](each-stage-has-one-virtual-user-key-pair.md) — The deployer holds the private key, the app reads the public key from an unreserved variable, and users never see either
45
83
  - [Gateway handlers run through the function runner gate](gateway-handlers-run-through-the-function-runner-gate.md) — A gateway's handler is registered as a real pikku function and invoked via runPikkuFunc, because calling it directly skips auth, scopes and permissions
46
84
  - [Gateway middleware sessions must be bridged onto the wire](gateway-middleware-sessions-must-be-bridged-onto-the-wire.md) — Gateway middleware calling wire.setSession writes to a session service the handler's invocation never reads, so the session is copied onto wire.session before the gate runs
47
85
  - [Global permissions and function permissions are independent gates](global-permissions-and-function-permissions-are-independent-gates.md) — Globals AND together and can only narrow access; a function's own group ORs internally and is never satisfied by a global
86
+ - [HMAC verification fails closed on empty or malformed inputs](hmac-verification-fails-closed-on-empty-inputs.md) — verifyHmacSignature returns false for an empty secret, a missing signature, an unknown algorithm, an empty key, a malformed hex signature or an empty signature
48
87
  - [HTTP error detail is withheld from clients in production](http-error-detail-is-withheld-from-clients-in-production.md) — 5xx bodies carry only a trace id in production; exposeErrors can widen that in development but never in production
49
88
  - [HTTP request bodies are bounded before they are buffered](http-request-bodies-are-bounded-before-they-are-buffered.md) — Content-Length is rejected up front and the stream is measured as it arrives, because that header is optional and attacker-controlled
89
+ - [Local content refuses private buckets under the public segment and paths outside the root](local-content-refuses-private-buckets-under-the-public-segment.md) — The local content path resolver returns no target for a private bucket whose first segment is the public URL segment, for NUL bytes, and for any path resolving outside its visibility root
90
+ - [Mail reaches a virtual user run only after sender and link-origin allowlisting](mail-is-allowlisted-before-a-virtual-user-run-sees-it.md) — applyMailboxAllowlist keeps only mail from listed senders and drops links to unlisted origins; the engine ships no mailbox, so every implementation must call this helper
50
91
  - [MCP internal error details are double-gated on production](mcp-internal-error-details-are-double-gated-on-production.md) — exposeErrors is checked again against isProduction() at throw time so an explicit true cannot leak stack traces from a production build
92
+ - [MCP resources, tools and prompts take sessionless functions, because an MCP client is not a logged-in user](mcp-resources-tools-and-prompts-take-sessionless-functions.md) — The MCP wiring types default each func to CorePikkuFunctionSessionless, so an MCP entry point is declared without a session type; whether a session is required is decided per function by the runner
93
+ - [Origin-locked routes accept only an exact origin match and refuse a missing one](origin-locked-routes-match-the-origin-exactly.md) — requireOrigin rejects a request whose Origin, or Referer when Origin is absent, is not the host's own origin or a configured origin, compared by equality
94
+ - [Open-source auth works without Pikku Studio Cloud](oss-auth-works-without-pikku-studio-cloud.md) — Every virtual-user sign-in path works in an app that runs no Pikku Studio Cloud code; the signing key is the project's own, never a cloud-issued credential
95
+ - [Outbound webhooks use an allowlist when one is configured, and block private hosts otherwise](outbound-webhooks-use-an-allowlist-when-configured.md) — The webhook delivery reads allowedHosts from config and passes it to safeFetch; without an allowlist safeFetch refuses private hosts, and an allowlist replaces that check
51
96
  - [Passphrases are stretched, key material is expanded](passphrases-are-stretched-key-material-is-expanded.md) — PBKDF2 guards low-entropy human input; HKDF derives from high-entropy secrets, and the two are never swapped
52
97
  - [Auth filtering requires live permission functions, never their metadata](permission-auth-filtering-requires-live-permission-functions.md) — checkAuthPermissions collects pikkuAuth-branded predicates off the real config; passing metadata would let every gated tool through
53
- - [Pikku carries actor scopes as data and the app grants them](pikku-carries-actor-scopes-as-data-and-the-app-grants-them.md) — scopes and roles on a ScenarioActorConfig are transported, never applied — the app's own seed reads them back and performs the grant
98
+ - [Core carries virtual user roles as data and provisioning grants them](pikku-carries-virtual-user-scopes-as-data-and-the-app-grants-them.md) — roles on a resolved virtual user are transported by core, never applied there — provisionVirtualUsers grants them in the running stage, through the scope service the app wired
54
99
  - [Queue job identities are signed at enqueue](queue-job-identities-are-signed-at-enqueue.md) — A job's pikkuUserId is an HMAC claim bound to the queue and payload; an unverifiable claim is dropped, never trusted
55
100
  - [Queue jobs carry the producer's pikku user id](queue-jobs-carry-the-producers-pikku-user-id.md) — A job's pikkuUserId is trusted as identity by the worker, so enqueue rights are effectively act-as-user rights
56
101
  - [Remote addon tokens are client credentials, not mesh trust](remote-addon-tokens-are-client-credentials-not-mesh-trust.md) — wireRemoteAddon authenticates as a client to a hosted library and fails closed on an empty token; it never uses PIKKU_REMOTE_SECRET
57
- - [A scaffold flag says a surface exists, not who may call it](a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md) — scaffold.<feature> is boolean | { path }; the auth field is gone because the function runner already enforces each generated function's own auth, wiring, scopes and addon
58
- - [A scaffolded surface is authenticated unless the config opts out in writing](scaffold-features-are-authenticated-unless-opted-out.md) — SUPERSEDED; retains why the legacy 'auth' | 'no-auth' strings are refused rather than coerced
102
+ - [A scaffolded surface is authenticated unless the config opts out in writing](scaffold-features-are-authenticated-unless-opted-out.md) — scaffold.<feature> became boolean | { auth, path }, where true means authenticated — going public requires typing { auth: false }, and the legacy 'auth' | 'no-auth' strings are refused rather than coerced
59
103
  - [Scenario-step functions are never externally invocable over RPC](scenario-step-functions-are-never-externally-invocable.md) — rpcExposed requires expose and rejects scenarioStep, so test steps stay reachable only from inside a scenario run
60
104
  - [Scope resolution happens at the session boundary and scope sync never deletes](scope-resolution-happens-at-the-session-boundary-and-sync-never-deletes.md) — ScopeService is called when a session is built, never by the function runner, and syncScopes only ever adds — revoking is an explicit operation
61
- - [A public function says so with auth: false](a-public-function-says-so-with-auth-false.md) — An exposed sessionless function that writes auth: false is public on purpose and PKU574 stays quiet; one that leaves auth out still warns
105
+ - [Scoped content access is confined to a root folder, with longest-prefix grants outside it](scoped-content-access-is-a-root-folder-with-longest-prefix-grants.md) — ScopedContentService keeps an addon inside its root folder, lets it reach the wider store only through read or write grants matched by longest prefix, and refuses signURL and copyFile
62
106
  - [Self-authentication is declared, not detected](self-authentication-is-declared-not-detected.md) — A function that authorizes callers in its own body says so with selfAuthenticated; codegen never tries to infer it
107
+ - [Contexts that must not hold secrets get a services bag whose secrets accessor throws](services-without-secrets-get-a-throwing-secrets-accessor.md) — withoutSecrets replaces the secrets entry with a non-enumerable getter that throws, so code outside the permitted contexts cannot reach the secret service
108
+ - [Shared guards refuse to mutate or shadow a system role, in every store](shared-guards-refuse-to-mutate-or-shadow-a-system-role.md) — system-role-guard holds the guards every ScopeService must call before changing a role, and roleLockReason gives the console a reason to show for a locked role
63
109
  - [Signed content URLs bind the request path and verify fail-closed](signed-content-urls-bind-the-request-path.md) — A signature that only carries timestamps authorizes every asset, and a verifier with no key must refuse rather than allow
110
+ - [Signed uploads are verified by the same check as signed reads](signed-uploads-are-verified-by-the-same-check-as-signed-reads.md) — The local content handler refuses an upload whose presigned URL does not verify, using the same exported verifier that signed reads use
111
+ - [System role scopes must be declared with defineScope, or the build fails](system-role-scopes-must-be-declared-with-define-scope.md) — A system role may grant only scope ids that defineScope declares, and the bare wildcard is refused; both are critical inspector diagnostics, so a role cannot grant a scope no function checks
112
+ - [System roles are never deleted by a deploy, but their scope set follows the declaration, and the admin API cannot change them](system-roles-are-additive-and-immutable.md) — syncSystemRoles keeps a role row whose declaration has gone (marked declared false) but replaces a declared role's scope set; deleteRole and setRoleScopes refuse system roles
113
+ - [The channel RPC does not put the caller's session on the wire](the-channel-rpc-does-not-send-the-callers-session.md) — ChannelDeploymentService.invoke accepts a session only to satisfy the DeploymentService shape and never sends it, so the peer runs the capability as itself
114
+ - [The remote jobs inbox rejects every caller when its secret is unset](the-remote-jobs-inbox-fails-closed-without-a-secret.md) — remoteJobsSecret refuses all requests when the dispatch secret variable is empty, and compares the header in constant time when it is set
115
+ - [The remote RPC gate fails closed and binds a token to one function](the-remote-rpc-gate-fails-closed-and-binds-the-token-to-its-function.md) — pikkuRemoteAuthMiddleware refuses a remote-RPC request unless the mesh token is audience pikku-remote, names the requested function, and carries a session that decrypts under the mesh secret
116
+ - [The workflow status stream re-checks run ownership on every poll and closes when access is lost](the-workflow-status-stream-rechecks-ownership-on-every-poll.md) — streamWorkflowRunStatus asserts the run owner on each poll rather than once when the stream opens, and closes the channel when that check throws
117
+ - [Two credentials resolving to one key must agree on whether they are per-user or singleton](two-credentials-on-one-key-must-share-a-read-mode.md) — buildCredentialResolutions throws when two credentials resolve to the same stored key but would be read in different modes, so one cannot silently serve the other's value
118
+ - [Verified bearer claims are pass-through data that core never fills](verified-bearer-claims-are-pass-through-data-that-core-never-fills.md) — PikkuHTTP.authInfo is a typed slot for claims a host has already verified; core declares it and no core source assigns it from request headers
119
+ - [The virtual user catalogue excludes any RPC scoped under virtualUser](virtual-user-catalogue-excludes-the-machinery-that-runs-virtual-users.md) — deriveCatalogue drops every function whose scope begins with virtualUser, so a virtual user holding those scopes cannot start runs, read findings or set schedules through the catalogue
120
+ - [A virtual user's missing approval decision defaults to denied](virtual-user-conversation-missing-approval-decisions-default-to-denied.md) — Every pending tool call gets an explicit decision; an id the virtual user LLM omitted is denied, so a dropped field can never read as consent
121
+ - [Virtual user run records are privileged, and reads are gated behind their own scope](virtual-user-run-records-are-gated-behind-a-read-scope.md) — The generated read RPCs carry virtualUser:read while starting a run carries virtualUser:run, because adversarial findings are working exploits with live ids
122
+ - [Virtual user sign-in endpoints do not exist in production](virtual-user-sign-in-does-not-exist-in-production.md) — The plugin registers the statement, in-process and switcher endpoints only when the sign-in gate is open at construction, and creating a row is dev-only
123
+ - [Virtual user sign-in has one door, reached two ways](virtual-user-sign-in-has-one-door.md) — Only the app's virtual user sign-in handler creates a virtual user session; runs reach it in-process and everything else reaches it with a signed statement
124
+ - [Virtual user sign-in is proven by Set-Cookie, not a non-empty jar](virtual-user-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md) — HttpVirtualUser tracks its own signedIn flag and both VirtualUserSignIn implementations require the sign-in response itself to set a cookie, because a populated jar proves nothing
125
+ - [Virtual user sign-in only works for virtual-user-flagged users](virtual-user-sign-in-only-works-for-virtual-user-flagged-users.md) — A virtual user statement mints sessions for user rows flagged virtual user and nothing else, so holding one never impersonates a real user
64
126
  - [Webhook bodies are signed before they are enqueued](webhook-bodies-are-signed-before-they-are-enqueued.md) — QueueWebhookService computes the HMAC at enqueue time so the signing key never travels in the queue payload
65
- - [A scenario actor step always goes over the real transport and never through internal dispatch](workflow-actor-steps-always-use-the-real-transport.md) — Internal dispatch would bypass auth middleware and permissions, turning a scenario into a green health check that proves nothing
127
+ - [wireAddon expose selects the RPC surface, and a list may widen it](wire-addon-expose-selects-the-rpc-surface.md) — The consuming app decides which addon functions rpc.exposed reaches per instance — unset keeps the addon's own expose, false closes the instance, a list names exactly what is callable — and the deploy analyzer applies the same rule
66
128
  - [An approval decision is stored raw and validated on replay, and an invalid one closes the gate rather than failing the run](workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md) — The schema only exists inside the workflow body, and letting an external payload fail the run would let any caller kill a workflow
129
+ - [Workflow helper functions are registered sessionless by declaration](workflow-helper-functions-are-sessionless-by-declaration.md) — Queue-worker helpers and the workflow sleeper get meta with sessionless true, so the runner requires a session for them only where their own auth flag says so
67
130
  - [A queued workflow step rehydrates its session from the persisted run wire](workflow-queued-steps-rehydrate-their-session-from-the-run-wire.md) — The queue job payload is just `{ runId }`, so without threading `pikkuUserId` an authed step sees no session and throws
68
- - [Scenario sessions are isolated per actor and reset between scenarios](workflow-scenario-sessions-are-isolated-per-actor-and-per-scenario.md) — One jar per actor keeps two personas from sharing a session; a browser reset keeps one scenario from leaving the next signed in as somebody else
69
- - [A scenario step is never registered as a callable RPC and never dispatched on the queue](workflow-scenario-steps-are-never-network-invocable.md) — A step drives a browser and holds an actor's session, so exposing it as an RPC would put that reach on the network
70
-
131
+ - [Scenario sessions are isolated per virtual user and reset between scenarios](workflow-scenario-sessions-are-isolated-per-virtual-user-and-per-scenario.md) — One jar per virtual user keeps two virtual users from sharing a session; a browser reset keeps one scenario from leaving the next signed in as somebody else
132
+ - [A scenario step is never registered as a callable RPC and never dispatched on the queue](workflow-scenario-steps-are-never-network-invocable.md) — A step drives a browser and holds a virtual user's session, so exposing it as an RPC would put that reach on the network
133
+ - [A scenario virtual user step always goes over the real transport and never through internal dispatch](workflow-virtual-user-steps-always-use-the-real-transport.md) — Internal dispatch would bypass auth middleware and permissions, turning a scenario into a green health check that proves nothing
134
+ - [Writing a virtual user schedule is gated by its own scope, separate from running one](writing-a-virtual-user-schedule-is-gated-by-its-own-scope.md) — setVirtualUserSchedule carries virtualUser:schedule, because a schedule write spends money on every future tick while a run spends it once with a caller watching
71
135
  <!-- /pikku:knowledge-index -->
@@ -0,0 +1,21 @@
1
+ ---
2
+ type: decision
3
+ title: Local content refuses private buckets under the public segment and paths outside the root
4
+ description: The local content path resolver returns no target for a private bucket whose first segment is the public URL segment, for NUL bytes, and for any path resolving outside its visibility root
5
+ tags: core, content, security
6
+ ---
7
+
8
+ # Local content refuses private buckets under the public segment and paths outside the root
9
+
10
+ The path resolver in `packages/core/src/services/local-content.ts:88-104` returns
11
+ `null` (no target) in four cases:
12
+
13
+ - the visibility is not one of the known visibilities (`:92`);
14
+ - the bucket or key contains a NUL byte (`:93`);
15
+ - the visibility is `private` and the bucket's first path segment is
16
+ `PUBLIC_URL_SEGMENT` (`:94-96`);
17
+ - the scoped path or the final target resolves outside the visibility root
18
+ (`:98-102`). `inside` accepts only the root itself or a path under `root + sep`.
19
+
20
+ **What this rules out:** reaching private content through the public URL segment,
21
+ and a bucket or key with `..` or an absolute component escaping the root.
@@ -0,0 +1,38 @@
1
+ ---
2
+ type: decision
3
+ title: Mail reaches a virtual user run only after sender and link-origin allowlisting
4
+ description: applyMailboxAllowlist keeps only mail from listed senders and drops links to unlisted origins; the engine ships no mailbox, so every implementation must call this helper
5
+ tags: core, virtual-users, email, prompt-injection
6
+ ---
7
+
8
+ # Mail reaches a virtual user run only after sender and link-origin allowlisting
9
+
10
+ `packages/core/src/wirings/virtual-user/virtual-user-mailbox.ts` defines the `VirtualUserMailbox`
11
+ interface and the allowlist helpers.
12
+
13
+ The interface doc says implementations "**must** apply `MailboxAllowlist` before
14
+ returning anything", and calls this the primary defence against prompt injection,
15
+ because a virtual user's address can be reached by anyone who learns it
16
+ (`virtual-user-mailbox.ts:40-44`). The engine ships no mailbox: capturing mail is
17
+ platform-specific, so pikku "declares the interface and ships no default"
18
+ (`virtual-user-mailbox.ts:36-38`).
19
+
20
+ `applyMailboxAllowlist` filters by sender and then trims each email's links
21
+ (`virtual-user-mailbox.ts:150-156`). The doc comment says implementations should call
22
+ it rather than filter themselves (`virtual-user-mailbox.ts:146-148`).
23
+
24
+ - Sender: an entry with `@` must match the whole address. A bare domain must
25
+ equal the part after the last `@` (`virtual-user-mailbox.ts:91-101`). The comment
26
+ says a bare domain never matches as a substring (`virtual-user-mailbox.ts:97-98`).
27
+ - Links: with no `linkOrigins`, every link is kept (`virtual-user-mailbox.ts:122-124`).
28
+ Otherwise a link is kept only when its origin is listed, and a link that does not
29
+ parse is dropped (`virtual-user-mailbox.ts:134-140`).
30
+ - Links are filtered rather than the whole email rejected, because a legitimate
31
+ email may carry an unrelated unsubscribe link (`virtual-user-mailbox.ts:109-116`).
32
+
33
+ The raw `body` is optional and is not meant to reach a model context
34
+ (`virtual-user-mailbox.ts:25-29`).
35
+
36
+ **What this rules out:** a virtual user run acting on mail from an unlisted sender, and
37
+ following a link that leaves the origins the stage allowed. Nothing in this file
38
+ checks that an implementation actually calls the helper.
@@ -0,0 +1,18 @@
1
+ ---
2
+ type: decision
3
+ title: MCP resources, tools and prompts take sessionless functions, because an MCP client is not a logged-in user
4
+ description: The MCP wiring types default each func to CorePikkuFunctionSessionless, so an MCP entry point is declared without a session type; whether a session is required is decided per function by the runner
5
+ tags: core, mcp, auth, sessionless
6
+ ---
7
+
8
+ # MCP resources, tools and prompts take sessionless functions, because an MCP client is not a logged-in user
9
+
10
+ `packages/core/src/wirings/mcp/mcp.types.ts` types the three MCP wirings the same way. Each one's `func` defaults to `CorePikkuFunctionSessionless`:
11
+
12
+ - `CoreMCPResource`: default at line 92, and the `func` field is commented "It is sessionless: an MCP client is not a logged-in user" at line 113.
13
+ - `CoreMCPTool`: default at line 123, and the same comment at line 138.
14
+ - `CoreMCPPrompt`: default at line 150, and the same comment at line 163.
15
+
16
+ So an MCP entry point is declared without a session-carrying function type. The types do not say whether a particular MCP call needs a session. The per-function declaration does that: `mcpTargetRequiresSession`, described in [An MCP refusal is decided before dispatch, not raised from the tool](an-mcp-refusal-is-decided-before-dispatch.md), reads a `pikkuFunc` as always needing a session, and a sessionless function as needing one only where it sets `auth: true`.
17
+
18
+ **What this rules out:** typing an MCP wiring's `func` as a session-carrying function, which would make the MCP surface look as if it always has a logged-in user behind it.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: decision
3
+ title: Origin-locked routes accept only an exact origin match and refuse a missing one
4
+ description: requireOrigin rejects a request whose Origin, or Referer when Origin is absent, is not the host's own origin or a configured origin, compared by equality
5
+ tags: core, middleware, security
6
+ ---
7
+
8
+ # Origin-locked routes accept only an exact origin match and refuse a missing one
9
+
10
+ `isAllowedOrigin` in `packages/core/src/middleware/require-origin.ts:25-33`
11
+ returns false when the request has no origin (`:30`). It then accepts only an
12
+ equal host origin (`:31`) or an equal configured origin after `toOrigin`
13
+ normalisation (`:32`). There is no suffix or prefix match.
14
+
15
+ The middleware takes `Origin` first and falls back to `Referer` (`:59-61`),
16
+ derives the host origin from `host` and `x-forwarded-proto`, which defaults to
17
+ `https` (`:63-65`), and throws `InvalidOriginError` when the check fails
18
+ (`:72-75`). The doc comment at `:36-42` says this is not CORS: it rejects before
19
+ the function runs, and a missing origin is rejected on purpose.
20
+
21
+ **What this rules out:** matching origins by suffix, and accepting a request that
22
+ carries neither `Origin` nor `Referer`.
@@ -0,0 +1,26 @@
1
+ ---
2
+ type: decision
3
+ title: Open-source auth works without Pikku Studio Cloud
4
+ description: Every virtual-user sign-in path works in an app that runs no Pikku Studio Cloud code; the signing key is the project's own, never a cloud-issued credential
5
+ tags: services, auth
6
+ ---
7
+
8
+ # Open-source auth works without Pikku Studio Cloud
9
+
10
+ Virtual users and staged impersonation must all work in an open-source app with
11
+ no Pikku Studio Cloud service and no cloud-issued token. The cloud may add its
12
+ own way in, but nothing in core, the CLI or the better-auth service may require
13
+ it.
14
+
15
+ The code holds to this. A virtual user signs in either in-process
16
+ (`inProcessSignIn`, no credential at all) or with a signed statement
17
+ (`statementSignIn`), both in `packages/core/src/services/virtual-user-sign-in.ts`.
18
+ The statement is signed with an Ed25519 key the project owns:
19
+ `packages/cli/src/utils/virtual-user-signer.ts` generates it for `pikku dev`, or
20
+ reads `VIRTUAL_USER_SIGNING_KEY` from whoever deploys the stage. The app is given
21
+ only the public half, as `VIRTUAL_USER_VERIFY_KEY`
22
+ (`packages/services/better-auth/src/virtual-user-plugin.ts`).
23
+
24
+ **What this rules out:** any design where Pikku Studio Cloud is the issuer of the
25
+ credential a virtual user presents. A signer that only exists in the cloud cannot
26
+ be the basis for an open-source feature.